commit
a9f31d188d
21 changed files with 1516 additions and 385 deletions
|
|
@ -1,5 +1,20 @@
|
|||
# Changelog hassio-plejd Home Assistant Plejd addon
|
||||
|
||||
## 0.8.0-dev
|
||||
|
||||
**BREAKING - READ BELOW FIRST**
|
||||
|
||||
Release 0.8 will break ALL EXISTING DEVICES. Unique mqtt id:s will change, meaning HA will create new devices. Scenes will be added as scenes not as switches.
|
||||
|
||||
Recommendations to minimize impact
|
||||
|
||||
- Optionally install MQTT explorer to bulk-delete discovered devices. If so - start MQTT explorer, connect, restart Plejd addon and then delete from MQTT explorer
|
||||
- Shut down Plejd addon, disable autostart
|
||||
- Reboot HA
|
||||
- Go to Configuration => Integration => MQTT. Go to entities and after that devices and remove all Plejd devices (should be listed as unavailable)
|
||||
- Upgrade addon to latest version and start
|
||||
- All devices should now be back. With luck they will have the same HA id:s as before so most things should work. Room assignments, icons, etc will be gone though.
|
||||
|
||||
## [0.7.1](https://github.com/icanos/hassio-plejd/tree/0.7.1) (2021-03-25)
|
||||
|
||||
[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.7.0...0.7.1)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
const fs = require('fs');
|
||||
|
||||
class Configuration {
|
||||
/** @type {import('types/Configuration').Options} */
|
||||
static _options = null;
|
||||
/** @type {import('types/Configuration').AddonInfo} */
|
||||
static _addonInfo = null;
|
||||
|
||||
/** @returns Options */
|
||||
static getOptions() {
|
||||
if (!Configuration._options) {
|
||||
Configuration._hydrateCache();
|
||||
|
|
@ -11,6 +14,7 @@ class Configuration {
|
|||
return Configuration._options;
|
||||
}
|
||||
|
||||
/** @returns AddonInfo */
|
||||
static getAddonInfo() {
|
||||
if (!Configuration._addonInfo) {
|
||||
Configuration._hydrateCache();
|
||||
|
|
@ -20,10 +24,10 @@ class Configuration {
|
|||
|
||||
static _hydrateCache() {
|
||||
const rawData = fs.readFileSync('/data/options.json');
|
||||
const config = JSON.parse(rawData);
|
||||
const config = JSON.parse(rawData.toString());
|
||||
|
||||
const defaultRawData = fs.readFileSync('/plejd/config.json');
|
||||
const defaultConfig = JSON.parse(defaultRawData);
|
||||
const defaultConfig = JSON.parse(defaultRawData.toString());
|
||||
|
||||
Configuration._options = { ...defaultConfig.options, ...config };
|
||||
Configuration._addonInfo = {
|
||||
|
|
|
|||
|
|
@ -2,154 +2,195 @@ const Logger = require('./Logger');
|
|||
|
||||
const logger = Logger.getLogger('device-registry');
|
||||
class DeviceRegistry {
|
||||
apiSite;
|
||||
/** @type {string} */
|
||||
cryptoKey = null;
|
||||
|
||||
deviceIdsByRoom = {};
|
||||
deviceIdsBySerial = {};
|
||||
/** @private @type {Object.<string, import('types/ApiSite').Device>} */
|
||||
devices = {};
|
||||
/** @private @type {Object.<string, string[]>} */
|
||||
outputDeviceUniqueIdsByRoomId = {};
|
||||
/** @private @type {Object.<number, string>} */
|
||||
outputUniqueIdByBleOutputAddress = {};
|
||||
/** @private @type {Object.<number, string>} */
|
||||
sceneUniqueIdByBleOutputAddress = {};
|
||||
|
||||
/** @private @type {import('./types/ApiSite').ApiSite} */
|
||||
apiSite;
|
||||
|
||||
// Dictionaries of [id]: device per type
|
||||
plejdDevices = {};
|
||||
roomDevices = {};
|
||||
/** @private @type {import('types/DeviceRegistry').OutputDevices} */
|
||||
outputDevices = {};
|
||||
/** @private @type {import('types/DeviceRegistry').OutputDevices} */
|
||||
sceneDevices = {};
|
||||
|
||||
get allDevices() {
|
||||
return [
|
||||
...Object.values(this.plejdDevices),
|
||||
...Object.values(this.roomDevices),
|
||||
...Object.values(this.sceneDevices),
|
||||
];
|
||||
/** @param device {import('./types/ApiSite').Device} */
|
||||
addPhysicalDevice(device) {
|
||||
this.devices[device.deviceId] = device;
|
||||
}
|
||||
|
||||
addPlejdDevice(device) {
|
||||
const added = {
|
||||
...this.plejdDevices[device.id],
|
||||
...device,
|
||||
};
|
||||
/** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */
|
||||
addOutputDevice(outputDevice) {
|
||||
if (outputDevice.hiddenFromIntegrations || outputDevice.hiddenFromRoomList) {
|
||||
logger.verbose(`Device ${outputDevice.name} is hidden and will not be included.
|
||||
Hidden from room list: ${outputDevice.hiddenFromRoomList}
|
||||
Hidden from integrations: ${outputDevice.hiddenFromIntegrations}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.plejdDevices = {
|
||||
...this.plejdDevices,
|
||||
[added.id]: added,
|
||||
this.outputDevices = {
|
||||
...this.outputDevices,
|
||||
[outputDevice.uniqueId]: outputDevice,
|
||||
};
|
||||
|
||||
this.deviceIdsBySerial[added.serialNumber] = added.id;
|
||||
|
||||
logger.verbose(
|
||||
`Added/updated device: ${JSON.stringify(added)}. ${
|
||||
Object.keys(this.plejdDevices).length
|
||||
} plejd devices in total.`,
|
||||
`Added/updated output device: ${JSON.stringify(outputDevice)}. ${
|
||||
Object.keys(this.outputDevices).length
|
||||
} output devices in total.`,
|
||||
);
|
||||
|
||||
if (added.roomId) {
|
||||
if (!this.deviceIdsByRoom[added.roomId]) {
|
||||
this.deviceIdsByRoom[added.roomId] = [];
|
||||
}
|
||||
const room = this.deviceIdsByRoom[added.roomId];
|
||||
if (!room.includes(added.id)) {
|
||||
this.deviceIdsByRoom[added.roomId] = [...room, added.id];
|
||||
}
|
||||
this.outputUniqueIdByBleOutputAddress[outputDevice.bleOutputAddress] = outputDevice.uniqueId;
|
||||
|
||||
if (!this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId]) {
|
||||
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId] = [];
|
||||
}
|
||||
if (
|
||||
outputDevice.roomId !== outputDevice.uniqueId
|
||||
&& !this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId)
|
||||
) {
|
||||
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].push(outputDevice.uniqueId);
|
||||
logger.verbose(
|
||||
`Added device to room ${added.roomId}: ${JSON.stringify(
|
||||
this.deviceIdsByRoom[added.roomId],
|
||||
`Added device to room ${outputDevice.roomId}: ${JSON.stringify(
|
||||
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
addRoomDevice(device) {
|
||||
const added = {
|
||||
...this.roomDevices[device.id],
|
||||
...device,
|
||||
};
|
||||
this.roomDevices = {
|
||||
...this.roomDevices,
|
||||
[added.id]: added,
|
||||
};
|
||||
|
||||
logger.verbose(
|
||||
`Added/updated room device: ${JSON.stringify(added)}. ${
|
||||
Object.keys(this.roomDevices).length
|
||||
} room devices total.`,
|
||||
);
|
||||
return added;
|
||||
}
|
||||
|
||||
/** @param scene {import('types/DeviceRegistry').OutputDevice} */
|
||||
addScene(scene) {
|
||||
const added = {
|
||||
...this.sceneDevices[scene.id],
|
||||
...scene,
|
||||
};
|
||||
this.sceneDevices = {
|
||||
...this.sceneDevices,
|
||||
[added.id]: added,
|
||||
[scene.uniqueId]: scene,
|
||||
};
|
||||
this.sceneUniqueIdByBleOutputAddress[scene.bleOutputAddress] = scene.uniqueId;
|
||||
|
||||
logger.verbose(
|
||||
`Added/updated scene: ${JSON.stringify(added)}. ${
|
||||
`Added/updated scene: ${JSON.stringify(scene)}. ${
|
||||
Object.keys(this.sceneDevices).length
|
||||
} scenes in total.`,
|
||||
);
|
||||
return added;
|
||||
}
|
||||
|
||||
clearPlejdDevices() {
|
||||
this.plejdDevices = {};
|
||||
this.deviceIdsByRoom = {};
|
||||
this.deviceIdsBySerial = {};
|
||||
}
|
||||
|
||||
clearRoomDevices() {
|
||||
this.roomDevices = {};
|
||||
this.devices = {};
|
||||
this.outputDevices = {};
|
||||
this.outputDeviceUniqueIdsByRoomId = {};
|
||||
this.outputUniqueIdByBleOutputAddress = {};
|
||||
}
|
||||
|
||||
clearSceneDevices() {
|
||||
this.sceneDevices = {};
|
||||
this.sceneUniqueIdByBleOutputAddress = {};
|
||||
}
|
||||
|
||||
getDevice(deviceId) {
|
||||
return this.plejdDevices[deviceId] || this.roomDevices[deviceId];
|
||||
/**
|
||||
* @returns {import('./types/DeviceRegistry').OutputDevice[]}
|
||||
*/
|
||||
getAllOutputDevices() {
|
||||
return Object.values(this.outputDevices);
|
||||
}
|
||||
|
||||
getDeviceIdsByRoom(roomId) {
|
||||
return this.deviceIdsByRoom[roomId];
|
||||
/**
|
||||
* @returns {import('./types/DeviceRegistry').OutputDevice[]}
|
||||
*/
|
||||
getAllSceneDevices() {
|
||||
return Object.values(this.sceneDevices);
|
||||
}
|
||||
|
||||
getDeviceBySerialNumber(serialNumber) {
|
||||
return this.getDevice(this.deviceIdsBySerial[serialNumber]);
|
||||
/** @returns {import('./types/ApiSite').ApiSite} */
|
||||
getApiSite() {
|
||||
return this.apiSite;
|
||||
}
|
||||
|
||||
getDeviceName(deviceId) {
|
||||
return (this.plejdDevices[deviceId] || {}).name;
|
||||
/**
|
||||
* @param {string} uniqueOutputId
|
||||
*/
|
||||
getOutputDevice(uniqueOutputId) {
|
||||
return this.outputDevices[uniqueOutputId];
|
||||
}
|
||||
|
||||
getScene(sceneId) {
|
||||
return this.sceneDevices[sceneId];
|
||||
/** @returns {import('./types/DeviceRegistry').OutputDevice} */
|
||||
getOutputDeviceByBleOutputAddress(bleOutputAddress) {
|
||||
return this.outputDevices[this.outputUniqueIdByBleOutputAddress[bleOutputAddress]];
|
||||
}
|
||||
|
||||
getSceneName(sceneId) {
|
||||
return (this.sceneDevices[sceneId] || {}).name;
|
||||
/** @returns {string[]} */
|
||||
getOutputDeviceIdsByRoomId(roomId) {
|
||||
return this.outputDeviceUniqueIdsByRoomId[roomId];
|
||||
}
|
||||
|
||||
getState(deviceId) {
|
||||
const device = this.getDevice(deviceId) || {};
|
||||
if (device.dimmable) {
|
||||
return {
|
||||
state: device.state,
|
||||
dim: device.dim,
|
||||
};
|
||||
getOutputDeviceName(uniqueOutputId) {
|
||||
return (this.outputDevices[uniqueOutputId] || {}).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string } deviceId The physical device serial number
|
||||
* @return {import('./types/ApiSite').Device}
|
||||
*/
|
||||
getPhysicalDevice(deviceId) {
|
||||
return this.devices[deviceId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sceneUniqueId
|
||||
*/
|
||||
getScene(sceneUniqueId) {
|
||||
return this.sceneDevices[sceneUniqueId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} sceneBleAddress
|
||||
*/
|
||||
getSceneByBleAddress(sceneBleAddress) {
|
||||
const sceneUniqueId = this.sceneUniqueIdByBleOutputAddress[sceneBleAddress];
|
||||
if (!sceneUniqueId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
state: device.state,
|
||||
};
|
||||
return this.sceneDevices[sceneUniqueId];
|
||||
}
|
||||
|
||||
setApiSite(siteDetails) {
|
||||
this.apiSite = siteDetails;
|
||||
/**
|
||||
* @param {string} sceneUniqueId
|
||||
*/
|
||||
getSceneName(sceneUniqueId) {
|
||||
return (this.sceneDevices[sceneUniqueId] || {}).name;
|
||||
}
|
||||
|
||||
setState(deviceId, state, dim) {
|
||||
const device = this.getDevice(deviceId) || this.addPlejdDevice({ id: deviceId });
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getUniqueOutputId(deviceId, outputIndex) {
|
||||
return `${deviceId}_${outputIndex}`;
|
||||
}
|
||||
|
||||
/** @param apiSite {import('./types/ApiSite').ApiSite} */
|
||||
setApiSite(apiSite) {
|
||||
this.apiSite = apiSite;
|
||||
this.cryptoKey = apiSite.plejdMesh.cryptoKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} uniqueOutputId
|
||||
* @param {boolean} state
|
||||
* @param {number?} [dim]
|
||||
*/
|
||||
setOutputState(uniqueOutputId, state, dim) {
|
||||
const device = this.getOutputDevice(uniqueOutputId);
|
||||
if (!device) {
|
||||
logger.warn(
|
||||
`Trying to set state for ${uniqueOutputId} which is not in the list of known outputs.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
device.state = state;
|
||||
if (dim && device.dimmable) {
|
||||
device.dim = dim;
|
||||
|
|
|
|||
|
|
@ -8,18 +8,36 @@ const startTopics = ['hass/status', 'homeassistant/status'];
|
|||
|
||||
const logger = Logger.getLogger('plejd-mqtt');
|
||||
|
||||
// #region discovery
|
||||
|
||||
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',
|
||||
COMMAND: 'set',
|
||||
};
|
||||
|
||||
const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) => `${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`;
|
||||
|
||||
const getTopicName = (
|
||||
/** @type { string } */ uniqueId,
|
||||
/** @type { import('./types/Mqtt').MqttType } */ mqttDeviceType,
|
||||
/** @type { import('./types/Mqtt').TopicType } */ topicType,
|
||||
) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`;
|
||||
|
||||
const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trigger`;
|
||||
const getSceneEventTopic = (/** @type {string} */ sceneId) => `${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`;
|
||||
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
|
||||
const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`;
|
||||
const getConfigPath = (plug) => `${getPath(plug)}/config`;
|
||||
const getStateTopic = (plug) => `${getPath(plug)}/state`;
|
||||
const getAvailabilityTopic = (plug) => `${getPath(plug)}/availability`;
|
||||
const getCommandTopic = (plug) => `${getPath(plug)}/set`;
|
||||
const getSceneEventTopic = () => 'plejd/event/scene';
|
||||
|
||||
const decodeTopicRegexp = new RegExp(
|
||||
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/,
|
||||
|
|
@ -33,41 +51,63 @@ const decodeTopic = (topic) => {
|
|||
return matches.groups;
|
||||
};
|
||||
|
||||
const getDiscoveryPayload = (device) => ({
|
||||
schema: 'json',
|
||||
const getOutputDeviceDiscoveryPayload = (
|
||||
/** @type {import('./types/DeviceRegistry').OutputDevice} */ device,
|
||||
) => ({
|
||||
name: device.name,
|
||||
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
|
||||
state_topic: getStateTopic(device),
|
||||
command_topic: getCommandTopic(device),
|
||||
availability_topic: getAvailabilityTopic(device),
|
||||
unique_id: device.uniqueId,
|
||||
'~': getBaseTopic(device.uniqueId, device.type),
|
||||
state_topic: `~/${TOPIC_TYPES.STATE}`,
|
||||
command_topic: `~/${TOPIC_TYPES.COMMAND}`,
|
||||
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
||||
optimistic: false,
|
||||
brightness: `${device.dimmable}`,
|
||||
qos: 1,
|
||||
retain: true,
|
||||
device: {
|
||||
identifiers: `${device.serialNumber}_${device.id}`,
|
||||
identifiers: `${device.deviceId}`,
|
||||
manufacturer: 'Plejd',
|
||||
model: device.typeName,
|
||||
name: device.name,
|
||||
sw_version: device.version,
|
||||
},
|
||||
...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}),
|
||||
});
|
||||
|
||||
const getSwitchPayload = (device) => ({
|
||||
name: device.name,
|
||||
state_topic: getStateTopic(device),
|
||||
command_topic: getCommandTopic(device),
|
||||
optimistic: false,
|
||||
const getSceneDiscoveryPayload = (
|
||||
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
|
||||
) => ({
|
||||
name: sceneDevice.name,
|
||||
unique_id: sceneDevice.uniqueId,
|
||||
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
|
||||
command_topic: `~/${TOPIC_TYPES.COMMAND}`,
|
||||
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
||||
payload_on: 'ON',
|
||||
qos: 1,
|
||||
retain: false,
|
||||
});
|
||||
|
||||
const getSceneDeviceTriggerhDiscoveryPayload = (
|
||||
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
|
||||
) => ({
|
||||
automation_type: 'trigger',
|
||||
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION),
|
||||
qos: 1,
|
||||
topic: `~/${TOPIC_TYPES.STATE}`,
|
||||
type: 'scene',
|
||||
subtype: 'trigger',
|
||||
device: {
|
||||
identifiers: `${device.serialNumber}_${device.id}`,
|
||||
identifiers: `${sceneDevice.uniqueId}`,
|
||||
manufacturer: 'Plejd',
|
||||
model: device.typeName,
|
||||
name: device.name,
|
||||
sw_version: device.version,
|
||||
model: sceneDevice.typeName,
|
||||
name: sceneDevice.name,
|
||||
},
|
||||
});
|
||||
|
||||
// #endregion
|
||||
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
|
||||
const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' };
|
||||
|
||||
class MqttClient extends EventEmitter {
|
||||
/** @type {import('DeviceRegistry')} */
|
||||
deviceRegistry;
|
||||
|
||||
static EVENTS = {
|
||||
|
|
@ -75,6 +115,9 @@ class MqttClient extends EventEmitter {
|
|||
stateChanged: 'stateChanged',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import("DeviceRegistry")} deviceRegistry
|
||||
*/
|
||||
constructor(deviceRegistry) {
|
||||
super();
|
||||
|
||||
|
|
@ -86,8 +129,11 @@ class MqttClient extends EventEmitter {
|
|||
logger.info('Initializing MQTT connection for Plejd addon');
|
||||
|
||||
this.client = mqtt.connect(this.config.mqttBroker, {
|
||||
username: this.config.mqttUsername,
|
||||
clientId: `hassio-plejd_${Math.random().toString(16).substr(2, 8)}`,
|
||||
password: this.config.mqttPassword,
|
||||
protocolVersion: 4, // v5 not supported by HassIO Mosquitto
|
||||
queueQoSZero: true,
|
||||
username: this.config.mqttUsername,
|
||||
});
|
||||
|
||||
this.client.on('error', (err) => {
|
||||
|
|
@ -97,13 +143,22 @@ class MqttClient extends EventEmitter {
|
|||
this.client.on('connect', () => {
|
||||
logger.info('Connected to MQTT.');
|
||||
|
||||
this.client.subscribe(startTopics, (err) => {
|
||||
if (err) {
|
||||
logger.error('Unable to subscribe to status topics', err);
|
||||
}
|
||||
this.client.subscribe(
|
||||
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);
|
||||
});
|
||||
this.emit(MqttClient.EVENTS.connected);
|
||||
},
|
||||
);
|
||||
|
||||
this.client.subscribe(getSubscribePath(), (err) => {
|
||||
if (err) {
|
||||
|
|
@ -123,27 +178,24 @@ class MqttClient extends EventEmitter {
|
|||
logger.info('Home Assistant has started. lets do discovery.');
|
||||
this.emit(MqttClient.EVENTS.connected);
|
||||
} else {
|
||||
logger.verbose(`Received mqtt message on ${topic}`);
|
||||
const decodedTopic = decodeTopic(topic);
|
||||
if (decodedTopic) {
|
||||
let device = this.deviceRegistry.getDevice(decodedTopic.id);
|
||||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||
let device;
|
||||
|
||||
if (decodedTopic.type === MQTT_TYPES.SCENE) {
|
||||
logger.verbose(`Getting scene ${decodedTopic.id} from registry`);
|
||||
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;
|
||||
|
||||
if (
|
||||
!isJsonMessage
|
||||
&& messageString === 'ON'
|
||||
&& this.deviceRegistry.getScene(decodedTopic.id)
|
||||
) {
|
||||
// Guess that id that got state command without dim value belongs to Scene, not Device
|
||||
// This guess could very well be wrong depending on the installation...
|
||||
logger.warn(
|
||||
`Device id ${decodedTopic.id} belongs to both scene and device, guessing Scene is what should be set to ON. `
|
||||
+ 'OFF commands still sent to device.',
|
||||
);
|
||||
device = this.deviceRegistry.getScene(decodedTopic.id);
|
||||
}
|
||||
const deviceName = device ? device.name : '';
|
||||
|
||||
switch (decodedTopic.command) {
|
||||
|
|
@ -195,35 +247,121 @@ class MqttClient extends EventEmitter {
|
|||
}
|
||||
|
||||
disconnect(callback) {
|
||||
this.deviceRegistry.allDevices.forEach((device) => {
|
||||
this.client.publish(getAvailabilityTopic(device), 'offline');
|
||||
logger.info('Mqtt disconnect requested. Setting all devices as unavailable in HA...');
|
||||
this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => {
|
||||
const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
||||
this.client.publish(
|
||||
getTopicName(outputDevice.uniqueId, mqttType, 'availability'),
|
||||
AVAILABLILITY.OFFLINE,
|
||||
{
|
||||
retain: true,
|
||||
qos: 1,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
|
||||
allSceneDevices.forEach((sceneDevice) => {
|
||||
this.client.publish(
|
||||
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
||||
AVAILABLILITY.OFFLINE,
|
||||
{
|
||||
retain: true,
|
||||
qos: 1,
|
||||
},
|
||||
);
|
||||
});
|
||||
this.client.end(callback);
|
||||
}
|
||||
|
||||
sendDiscoveryToHomeAssistant() {
|
||||
logger.debug(`Sending discovery of ${this.deviceRegistry.allDevices.length} device(s).`);
|
||||
const allOutputDevices = this.deviceRegistry.getAllOutputDevices();
|
||||
logger.info(`Sending discovery for ${allOutputDevices.length} Plejd output devices`);
|
||||
allOutputDevices.forEach((outputDevice) => {
|
||||
logger.debug(`Sending discovery for ${outputDevice.name}`);
|
||||
|
||||
this.deviceRegistry.allDevices.forEach((device) => {
|
||||
logger.debug(`Sending discovery for ${device.name}`);
|
||||
|
||||
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
||||
const configPayload = getOutputDeviceDiscoveryPayload(outputDevice);
|
||||
logger.info(
|
||||
`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`,
|
||||
`Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`,
|
||||
);
|
||||
|
||||
this.client.publish(getConfigPath(device), JSON.stringify(payload));
|
||||
const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
||||
this.client.publish(
|
||||
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.CONFIG),
|
||||
JSON.stringify(configPayload),
|
||||
{
|
||||
retain: true,
|
||||
qos: 1,
|
||||
},
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.client.publish(getAvailabilityTopic(device), 'online');
|
||||
this.client.publish(
|
||||
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
||||
AVAILABLILITY.ONLINE,
|
||||
{
|
||||
retain: true,
|
||||
qos: 1,
|
||||
},
|
||||
);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
|
||||
logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`);
|
||||
allSceneDevices.forEach((sceneDevice) => {
|
||||
logger.debug(`Sending discovery for ${sceneDevice.name}`);
|
||||
|
||||
const sceneConfigPayload = getSceneDiscoveryPayload(sceneDevice);
|
||||
logger.info(
|
||||
`Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`,
|
||||
);
|
||||
|
||||
this.client.publish(
|
||||
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.CONFIG),
|
||||
JSON.stringify(sceneConfigPayload),
|
||||
{
|
||||
retain: true,
|
||||
qos: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const sceneTriggerConfigPayload = getSceneDeviceTriggerhDiscoveryPayload(sceneDevice);
|
||||
|
||||
this.client.publish(
|
||||
getTopicName(
|
||||
getTriggerUniqueId(sceneDevice.uniqueId),
|
||||
MQTT_TYPES.DEVICE_AUTOMATION,
|
||||
TOPIC_TYPES.CONFIG,
|
||||
),
|
||||
JSON.stringify(sceneTriggerConfigPayload),
|
||||
{
|
||||
retain: true,
|
||||
qos: 1,
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
this.client.publish(
|
||||
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
||||
AVAILABLILITY.ONLINE,
|
||||
{
|
||||
retain: true,
|
||||
qos: 1,
|
||||
},
|
||||
);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
updateState(deviceId, data) {
|
||||
const device = this.deviceRegistry.getDevice(deviceId);
|
||||
/**
|
||||
* @param {string} uniqueOutputId
|
||||
* @param {{ state: boolean; brightness?: number; }} data
|
||||
*/
|
||||
updateOutputState(uniqueOutputId, data) {
|
||||
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
|
||||
|
||||
if (!device) {
|
||||
logger.warn(`Unknown device id ${deviceId} - not handled by us.`);
|
||||
logger.warn(`Unknown output id ${uniqueOutputId} - not handled by us.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -235,29 +373,40 @@ class MqttClient extends EventEmitter {
|
|||
let payload = null;
|
||||
|
||||
if (device.type === 'switch') {
|
||||
payload = data.state === 1 ? 'ON' : 'OFF';
|
||||
payload = getMqttStateString(data.state);
|
||||
} else {
|
||||
if (device.dimmable) {
|
||||
payload = {
|
||||
state: data.state === 1 ? 'ON' : 'OFF',
|
||||
state: getMqttStateString(data.state),
|
||||
brightness: data.brightness,
|
||||
};
|
||||
} else {
|
||||
payload = {
|
||||
state: data.state === 1 ? 'ON' : 'OFF',
|
||||
state: getMqttStateString(data.state),
|
||||
};
|
||||
}
|
||||
|
||||
payload = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
this.client.publish(getStateTopic(device), payload);
|
||||
this.client.publish(getAvailabilityTopic(device), 'online');
|
||||
const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
||||
this.client.publish(getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.STATE), payload, {
|
||||
retain: true,
|
||||
qos: 1,
|
||||
});
|
||||
// this.client.publish(
|
||||
// getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
||||
// AVAILABLILITY.ONLINE,
|
||||
// { retain: true, qos: 1 },
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sceneId
|
||||
*/
|
||||
sceneTriggered(sceneId) {
|
||||
logger.verbose(`Scene triggered: ${sceneId}`);
|
||||
this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId }));
|
||||
this.client.publish(getSceneEventTopic(sceneId), '', { qos: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,57 +65,61 @@ class PlejdAddon extends EventEmitter {
|
|||
});
|
||||
|
||||
// subscribe to changes from HA
|
||||
this.mqttClient.on(MqttClient.EVENTS.stateChanged, (device, command) => {
|
||||
try {
|
||||
const deviceId = device.id;
|
||||
this.mqttClient.on(
|
||||
MqttClient.EVENTS.stateChanged,
|
||||
/** @param device {import('./types/DeviceRegistry').OutputDevice} */
|
||||
(device, command) => {
|
||||
try {
|
||||
const { uniqueId } = device;
|
||||
|
||||
if (device.typeName === 'Scene') {
|
||||
// we're triggering a scene, lets do that and jump out.
|
||||
// since scenes aren't "real" devices.
|
||||
this.sceneManager.executeScene(device.id);
|
||||
return;
|
||||
if (device.typeName === 'Scene') {
|
||||
// we're triggering a scene, lets do that and jump out.
|
||||
// since scenes aren't "real" devices.
|
||||
this.sceneManager.executeScene(uniqueId);
|
||||
return;
|
||||
}
|
||||
|
||||
let state = false;
|
||||
let commandObj = {};
|
||||
|
||||
if (typeof command === 'string') {
|
||||
// switch command
|
||||
state = command === 'ON';
|
||||
commandObj = {
|
||||
state,
|
||||
};
|
||||
|
||||
// since the switch doesn't get any updates on whether it's on or not,
|
||||
// we fake this by directly send the updateState back to HA in order for
|
||||
// it to change state.
|
||||
this.mqttClient.updateOutputState(uniqueId, {
|
||||
state,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
state = command.state === 'ON';
|
||||
commandObj = command;
|
||||
}
|
||||
|
||||
if (state) {
|
||||
this.plejdDeviceCommunication.turnOn(uniqueId, commandObj);
|
||||
} else {
|
||||
this.plejdDeviceCommunication.turnOff(uniqueId, commandObj);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error in MqttClient.stateChanged callback', err);
|
||||
}
|
||||
|
||||
let state = 'OFF';
|
||||
let commandObj = {};
|
||||
|
||||
if (typeof command === 'string') {
|
||||
// switch command
|
||||
state = command;
|
||||
commandObj = {
|
||||
state,
|
||||
};
|
||||
|
||||
// since the switch doesn't get any updates on whether it's on or not,
|
||||
// we fake this by directly send the updateState back to HA in order for
|
||||
// it to change state.
|
||||
this.mqttClient.updateState(deviceId, {
|
||||
state: state === 'ON' ? 1 : 0,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
state = command.state;
|
||||
commandObj = command;
|
||||
}
|
||||
|
||||
if (state === 'ON') {
|
||||
this.plejdDeviceCommunication.turnOn(deviceId, commandObj);
|
||||
} else {
|
||||
this.plejdDeviceCommunication.turnOff(deviceId, commandObj);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error in MqttClient.stateChanged callback', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.mqttClient.init();
|
||||
|
||||
// subscribe to changes from Plejd
|
||||
this.plejdDeviceCommunication.on(
|
||||
PlejdDeviceCommunication.EVENTS.stateChanged,
|
||||
(deviceId, command) => {
|
||||
(uniqueOutputId, command) => {
|
||||
try {
|
||||
this.mqttClient.updateState(deviceId, command);
|
||||
this.mqttClient.updateOutputState(uniqueOutputId, command);
|
||||
} catch (err) {
|
||||
logger.error('Error in PlejdService.stateChanged callback', err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,33 @@ const API_LOGIN_URL = 'login';
|
|||
const API_SITE_LIST_URL = 'functions/getSiteList';
|
||||
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
||||
|
||||
const TRAITS = {
|
||||
NO_LOAD: 0,
|
||||
NON_DIMMABLE: 9,
|
||||
DIMMABLE: 11,
|
||||
};
|
||||
|
||||
const logger = Logger.getLogger('plejd-api');
|
||||
|
||||
class PlejdApi {
|
||||
/** @private @type {import('types/Configuration').Options} */
|
||||
config;
|
||||
|
||||
/** @private @type {import('DeviceRegistry')} */
|
||||
deviceRegistry;
|
||||
|
||||
/** @private @type {string} */
|
||||
sessionToken;
|
||||
|
||||
/** @private @type {string} */
|
||||
siteId;
|
||||
|
||||
/** @private @type {import('types/ApiSite').ApiSite} */
|
||||
siteDetails;
|
||||
|
||||
/**
|
||||
* @param {import("./DeviceRegistry")} deviceRegistry
|
||||
*/
|
||||
constructor(deviceRegistry) {
|
||||
this.config = Configuration.getOptions();
|
||||
this.deviceRegistry = deviceRegistry;
|
||||
|
|
@ -58,19 +76,19 @@ class PlejdApi {
|
|||
}
|
||||
}
|
||||
}
|
||||
this.deviceRegistry.setApiSite(this.siteDetails);
|
||||
this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey;
|
||||
|
||||
this.deviceRegistry.setApiSite(this.siteDetails);
|
||||
this.getDevices();
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('types/ApiSite').CachedSite>} */
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async getCachedCopy() {
|
||||
logger.info('Getting cached api response from disk');
|
||||
|
||||
try {
|
||||
const rawData = await fs.promises.readFile('/data/cachedApiResponse.json');
|
||||
const cachedCopy = JSON.parse(rawData);
|
||||
const cachedCopy = JSON.parse(rawData.toString());
|
||||
|
||||
return cachedCopy;
|
||||
} catch (err) {
|
||||
|
|
@ -82,12 +100,14 @@ class PlejdApi {
|
|||
async saveCachedCopy() {
|
||||
logger.info('Saving cached copy');
|
||||
try {
|
||||
const rawData = JSON.stringify({
|
||||
/** @type {import('types/ApiSite').CachedSite} */
|
||||
const cachedSite = {
|
||||
siteId: this.siteId,
|
||||
siteDetails: this.siteDetails,
|
||||
sessionToken: this.sessionToken,
|
||||
dtCache: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
const rawData = JSON.stringify(cachedSite);
|
||||
await fs.promises.writeFile('/data/cachedApiResponse.json', rawData);
|
||||
} catch (err) {
|
||||
logger.error('Failed to save cache of api response', err);
|
||||
|
|
@ -194,6 +214,14 @@ class PlejdApi {
|
|||
getDevices() {
|
||||
logger.info('Getting devices from site details response...');
|
||||
|
||||
if (this.siteDetails.gateways && this.siteDetails.gateways.length) {
|
||||
this.siteDetails.gateways.forEach((gwy) => {
|
||||
logger.info(`Plejd gateway '${gwy.title}' found on site`);
|
||||
});
|
||||
} else {
|
||||
logger.info('No Plejd gateway found on site');
|
||||
}
|
||||
|
||||
this._getPlejdDevices();
|
||||
this._getRoomDevices();
|
||||
this._getSceneDevices();
|
||||
|
|
@ -216,8 +244,11 @@ class PlejdApi {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_getDeviceType(hardwareId) {
|
||||
switch (parseInt(hardwareId, 10)) {
|
||||
_getDeviceType(plejdDevice) {
|
||||
// Type name is also sometimes available in device.hardware.name
|
||||
// (maybe only when GWY-01 is present?)
|
||||
|
||||
switch (parseInt(plejdDevice.hardwareId, 10)) {
|
||||
case 1:
|
||||
case 11:
|
||||
return { name: 'DIM-01', type: 'light', dimmable: true };
|
||||
|
|
@ -259,69 +290,115 @@ class PlejdApi {
|
|||
case 20:
|
||||
return { name: 'SPR-01', type: 'switch', dimmable: false };
|
||||
default:
|
||||
throw new Error(`Unknown device type with id ${hardwareId}`);
|
||||
throw new Error(`Unknown device type with id ${plejdDevice.hardwareId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plejd API properties parsed
|
||||
*
|
||||
* * `devices` - physical Plejd devices, duplicated for devices with multiple outputs
|
||||
* devices: [{deviceId, title, objectId, ...}, {...}]
|
||||
* * `deviceAddress` - BLE address of each physical device
|
||||
* deviceAddress: {[deviceId]: bleDeviceAddress}
|
||||
* * `outputSettings` - lots of info about load settings, also links devices to output index
|
||||
* outputSettings: [{deviceId, output, deviceParseId, ...}] //deviceParseId === objectId above
|
||||
* * `outputAddress`: BLE address of [0] main output and [n] other output (loads)
|
||||
* outputAddress: {[deviceId]: {[output]: bleDeviceAddress}}
|
||||
* * `inputSettings` - detailed settings for inputs (buttons, RTR-01, ...), scenes triggered, ...
|
||||
* inputSettings: [{deviceId, input, ...}] //deviceParseId === objectId above
|
||||
* * `inputAddress` - Links inputs to what BLE device they control, or 255 for unassigned/scene
|
||||
* inputAddress: {[deviceId]: {[input]: bleDeviceAddress}}
|
||||
*/
|
||||
_getPlejdDevices() {
|
||||
this.deviceRegistry.clearPlejdDevices();
|
||||
|
||||
this.siteDetails.devices.forEach((device) => {
|
||||
const { deviceId } = device;
|
||||
this.deviceRegistry.addPhysicalDevice(device);
|
||||
|
||||
const settings = this.siteDetails.outputSettings.find(
|
||||
const outputSettings = this.siteDetails.outputSettings.find(
|
||||
(x) => x.deviceParseId === device.objectId,
|
||||
);
|
||||
|
||||
let deviceNum = this.siteDetails.deviceAddress[deviceId];
|
||||
if (!outputSettings) {
|
||||
logger.verbose(
|
||||
`No outputSettings found for ${device.title} (${device.deviceId}), assuming output 0`,
|
||||
);
|
||||
}
|
||||
const deviceOutput = outputSettings ? outputSettings.output : 0;
|
||||
const outputAddress = this.siteDetails.outputAddress[device.deviceId];
|
||||
|
||||
if (settings) {
|
||||
const outputs = this.siteDetails.outputAddress[deviceId];
|
||||
deviceNum = outputs[settings.output];
|
||||
if (outputAddress) {
|
||||
const bleOutputAddress = outputAddress[deviceOutput];
|
||||
|
||||
if (device.traits === TRAITS.NO_LOAD) {
|
||||
logger.warn(
|
||||
`Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`,
|
||||
);
|
||||
} else {
|
||||
const uniqueOutputId = this.deviceRegistry.getUniqueOutputId(
|
||||
device.deviceId,
|
||||
deviceOutput,
|
||||
);
|
||||
|
||||
const plejdDevice = this.siteDetails.plejdDevices.find(
|
||||
(x) => x.deviceId === device.deviceId,
|
||||
);
|
||||
|
||||
const dimmable = device.traits === TRAITS.DIMMABLE;
|
||||
// dimmable = settings.dimCurve !== 'NonDimmable';
|
||||
|
||||
const { name: typeName, type: deviceType } = this._getDeviceType(plejdDevice);
|
||||
let loadType = deviceType;
|
||||
if (device.outputType === 'RELAY') {
|
||||
loadType = 'switch';
|
||||
} else if (device.outputType === 'LIGHT') {
|
||||
loadType = 'light';
|
||||
}
|
||||
|
||||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||
const outputDevice = {
|
||||
bleOutputAddress,
|
||||
deviceId: device.deviceId,
|
||||
dimmable,
|
||||
hiddenFromRoomList: device.hiddenFromRoomList,
|
||||
hiddenFromIntegrations: device.hiddenFromIntegrations,
|
||||
name: device.title,
|
||||
output: deviceOutput,
|
||||
roomId: device.roomId,
|
||||
state: undefined,
|
||||
type: loadType,
|
||||
typeName,
|
||||
version: plejdDevice.firmware.version,
|
||||
uniqueId: uniqueOutputId,
|
||||
};
|
||||
|
||||
this.deviceRegistry.addOutputDevice(outputDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// check if device is dimmable
|
||||
const plejdDevice = this.siteDetails.plejdDevices.find((x) => x.deviceId === deviceId);
|
||||
const deviceType = this._getDeviceType(plejdDevice.hardwareId);
|
||||
const { name, type } = deviceType;
|
||||
let { dimmable } = deviceType;
|
||||
// What should we do with inputs?!
|
||||
// if (outputDevice.typeName === 'WPH-01') {
|
||||
// // WPH-01 is special, it has two buttons which needs to be
|
||||
// // registered separately.
|
||||
// const inputs = this.siteDetails.inputAddress[deviceId];
|
||||
// const first = inputs[0];
|
||||
// const second = inputs[1];
|
||||
|
||||
if (settings) {
|
||||
dimmable = settings.dimCurve !== 'NonDimmable';
|
||||
}
|
||||
// this.deviceRegistry.addPlejdDevice({
|
||||
// ...outputDevice,
|
||||
// id: first,
|
||||
// name: `${device.title} left`,
|
||||
// });
|
||||
|
||||
const newDevice = {
|
||||
id: deviceNum,
|
||||
name: device.title,
|
||||
type,
|
||||
typeName: name,
|
||||
dimmable,
|
||||
roomId: device.roomId,
|
||||
version: plejdDevice.firmware.version,
|
||||
serialNumber: plejdDevice.deviceId,
|
||||
};
|
||||
|
||||
if (newDevice.typeName === 'WPH-01') {
|
||||
// WPH-01 is special, it has two buttons which needs to be
|
||||
// registered separately.
|
||||
const inputs = this.siteDetails.inputAddress[deviceId];
|
||||
const first = inputs[0];
|
||||
const second = inputs[1];
|
||||
|
||||
this.deviceRegistry.addPlejdDevice({
|
||||
...newDevice,
|
||||
id: first,
|
||||
name: `${device.title} left`,
|
||||
});
|
||||
|
||||
this.deviceRegistry.addPlejdDevice({
|
||||
...newDevice,
|
||||
id: second,
|
||||
name: `${device.title} right`,
|
||||
});
|
||||
} else {
|
||||
this.deviceRegistry.addPlejdDevice(newDevice);
|
||||
}
|
||||
// this.deviceRegistry.addPlejdDevice({
|
||||
// ...outputDevice,
|
||||
// id: second,
|
||||
// name: `${device.title} right`,
|
||||
// });
|
||||
// } else {
|
||||
// this.deviceRegistry.addPlejdDevice(outputDevice);
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -332,39 +409,57 @@ class PlejdApi {
|
|||
const { roomId } = room;
|
||||
const roomAddress = this.siteDetails.roomAddress[roomId];
|
||||
|
||||
const deviceIdsByRoom = this.deviceRegistry.getDeviceIdsByRoom(roomId);
|
||||
const deviceIdsByRoom = this.deviceRegistry.getOutputDeviceIdsByRoomId(roomId);
|
||||
|
||||
const dimmable = deviceIdsByRoom
|
||||
&& deviceIdsByRoom.some((deviceId) => this.deviceRegistry.getDevice(deviceId).dimmable);
|
||||
&& deviceIdsByRoom.some(
|
||||
(deviceId) => this.deviceRegistry.getOutputDevice(deviceId).dimmable,
|
||||
);
|
||||
|
||||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||
const newDevice = {
|
||||
id: roomAddress,
|
||||
bleOutputAddress: roomAddress,
|
||||
deviceId: null,
|
||||
dimmable,
|
||||
hiddenFromRoomList: false,
|
||||
hiddenFromIntegrations: false,
|
||||
name: room.title,
|
||||
output: undefined,
|
||||
roomId,
|
||||
state: undefined,
|
||||
type: 'light',
|
||||
typeName: 'Room',
|
||||
dimmable,
|
||||
uniqueId: roomId,
|
||||
version: undefined,
|
||||
};
|
||||
|
||||
this.deviceRegistry.addRoomDevice(newDevice);
|
||||
this.deviceRegistry.addOutputDevice(newDevice);
|
||||
});
|
||||
logger.debug('includeRoomsAsLights done.');
|
||||
}
|
||||
}
|
||||
|
||||
_getSceneDevices() {
|
||||
this.deviceRegistry.clearSceneDevices();
|
||||
// add scenes as switches
|
||||
const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||
|
||||
scenes.forEach((scene) => {
|
||||
const sceneNum = this.siteDetails.sceneIndex[scene.sceneId];
|
||||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||
const newScene = {
|
||||
id: sceneNum,
|
||||
name: scene.title,
|
||||
type: 'switch',
|
||||
typeName: 'Scene',
|
||||
bleOutputAddress: sceneNum,
|
||||
deviceId: undefined,
|
||||
dimmable: false,
|
||||
version: '1.0',
|
||||
serialNumber: scene.objectId,
|
||||
hiddenFromSceneList: scene.hiddenFromSceneList,
|
||||
name: scene.title,
|
||||
output: undefined,
|
||||
roomId: undefined,
|
||||
state: false,
|
||||
type: 'scene',
|
||||
typeName: 'Scene',
|
||||
version: undefined,
|
||||
uniqueId: scene.sceneId,
|
||||
};
|
||||
|
||||
this.deviceRegistry.addScene(newScene);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const dbus = require('dbus-next');
|
||||
const crypto = require('crypto');
|
||||
const xor = require('buffer-xor');
|
||||
const EventEmitter = require('events');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
const Configuration = require('./Configuration');
|
||||
const constants = require('./constants');
|
||||
|
|
@ -49,6 +49,8 @@ class PlejBLEHandler extends EventEmitter {
|
|||
connectedDevice = null;
|
||||
consecutiveWriteFails;
|
||||
consecutiveReconnectAttempts = 0;
|
||||
/** @type {import('./DeviceRegistry')} */
|
||||
deviceRegistry;
|
||||
discoveryTimeout = null;
|
||||
plejdService = null;
|
||||
pingRef = null;
|
||||
|
|
@ -152,21 +154,26 @@ class PlejBLEHandler extends EventEmitter {
|
|||
logger.info('BLE init done, waiting for devices.');
|
||||
}
|
||||
|
||||
async sendCommand(command, deviceId, data) {
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {number} bleOutputAddress
|
||||
* @param {number} data
|
||||
*/
|
||||
async sendCommand(command, bleOutputAddress, data) {
|
||||
let payload;
|
||||
let brightnessVal;
|
||||
switch (command) {
|
||||
case COMMANDS.TURN_ON:
|
||||
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '01');
|
||||
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '01');
|
||||
break;
|
||||
case COMMANDS.TURN_OFF:
|
||||
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '00');
|
||||
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '00');
|
||||
break;
|
||||
case COMMANDS.DIM:
|
||||
// eslint-disable-next-line no-bitwise
|
||||
brightnessVal = (data << 8) | data;
|
||||
payload = this._createHexPayload(
|
||||
deviceId,
|
||||
bleOutputAddress,
|
||||
BLE_CMD_DIM2_CHANGE,
|
||||
`01${brightnessVal.toString(16).padStart(4, '0')}`,
|
||||
);
|
||||
|
|
@ -194,9 +201,9 @@ class PlejBLEHandler extends EventEmitter {
|
|||
plejd.instance = device;
|
||||
|
||||
const segments = plejd.path.split('/');
|
||||
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
|
||||
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
|
||||
plejd.device = this.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath);
|
||||
let plejdSerialNumber = segments[segments.length - 1].replace('dev_', '');
|
||||
plejdSerialNumber = plejdSerialNumber.replace(/_/g, '');
|
||||
plejd.device = this.deviceRegistry.getPhysicalDevice(plejdSerialNumber);
|
||||
|
||||
if (plejd.device) {
|
||||
logger.debug(
|
||||
|
|
@ -204,7 +211,7 @@ class PlejBLEHandler extends EventEmitter {
|
|||
);
|
||||
this.bleDevices.push(plejd);
|
||||
} else {
|
||||
logger.warn(`Device registry does not contain device with serial ${fixedPlejdPath}`);
|
||||
logger.warn(`Device registry does not contain device with serial ${plejdSerialNumber}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed inspecting ${path}. `, err);
|
||||
|
|
@ -796,7 +803,7 @@ class PlejBLEHandler extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const deviceId = decoded.readUInt8(0);
|
||||
const bleOutputAddress = decoded.readUInt8(0);
|
||||
// Bytes 2-3 is Command/Request
|
||||
const cmd = decoded.readUInt16BE(3);
|
||||
|
||||
|
|
@ -810,38 +817,52 @@ class PlejBLEHandler extends EventEmitter {
|
|||
logger.silly(`Dim: ${dim.toString(16)}, full precision: ${dimFull.toString(16)}`);
|
||||
}
|
||||
|
||||
const deviceName = this.deviceRegistry.getDeviceName(deviceId);
|
||||
const device = this.deviceRegistry.getOutputDeviceByBleOutputAddress(bleOutputAddress);
|
||||
const deviceName = device ? device.name : 'Unknown';
|
||||
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 ${deviceId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`,
|
||||
`Decoded: Device ${outputUniqueId} (BLE address ${bleOutputAddress}), cmd ${cmd.toString(
|
||||
16,
|
||||
)}, state ${state}, dim ${dim}`,
|
||||
);
|
||||
}
|
||||
|
||||
let command;
|
||||
let data = {};
|
||||
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
|
||||
logger.debug(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`);
|
||||
logger.debug(
|
||||
`${deviceName} (${outputUniqueId}) got state+dim update. S: ${state}, D: ${dim}`,
|
||||
);
|
||||
|
||||
command = COMMANDS.DIM;
|
||||
data = { state, dim };
|
||||
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data);
|
||||
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
|
||||
} else if (cmd === BLE_CMD_STATE_CHANGE) {
|
||||
logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`);
|
||||
logger.debug(`${deviceName} (${outputUniqueId}) got state update. S: ${state}`);
|
||||
command = state ? COMMANDS.TURN_ON : COMMANDS.TURN_OFF;
|
||||
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data);
|
||||
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
|
||||
} else if (cmd === BLE_CMD_SCENE_TRIG) {
|
||||
const sceneId = state;
|
||||
const sceneName = this.deviceRegistry.getSceneName(sceneId);
|
||||
const sceneBleAddress = state;
|
||||
const scene = this.deviceRegistry.getSceneByBleAddress(sceneBleAddress);
|
||||
|
||||
if (!scene) {
|
||||
logger.warn(
|
||||
`Scene with BLE address ${sceneBleAddress} could not be found, can't process message`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`,
|
||||
`${scene.name} (${sceneBleAddress}) scene triggered (device id ${outputUniqueId}).`,
|
||||
);
|
||||
|
||||
command = COMMANDS.TRIGGER_SCENE;
|
||||
data = { sceneId };
|
||||
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data);
|
||||
data = { sceneId: scene.uniqueId };
|
||||
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
|
||||
} else if (cmd === BLE_CMD_TIME_UPDATE) {
|
||||
const now = new Date();
|
||||
// Guess Plejd timezone based on HA time zone
|
||||
|
|
@ -851,7 +872,7 @@ class PlejBLEHandler extends EventEmitter {
|
|||
const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000;
|
||||
const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000);
|
||||
if (
|
||||
deviceId !== BLE_BROADCAST_DEVICE_ID
|
||||
bleOutputAddress !== BLE_BROADCAST_DEVICE_ID
|
||||
|| Logger.shouldLog('verbose')
|
||||
|| Math.abs(diffSeconds) > 60
|
||||
) {
|
||||
|
|
@ -863,7 +884,7 @@ class PlejBLEHandler extends EventEmitter {
|
|||
logger.warn(
|
||||
`Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`,
|
||||
);
|
||||
if (this.connectedDevice && deviceId === this.connectedDevice.id) {
|
||||
if (this.connectedDevice && bleOutputAddress === this.connectedDevice.id) {
|
||||
// Requested time sync by us
|
||||
const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess;
|
||||
logger.info(`Setting time to ${now.toString()}`);
|
||||
|
|
@ -874,14 +895,14 @@ class PlejBLEHandler extends EventEmitter {
|
|||
(pl) => pl.writeInt32LE(Math.trunc(newLocalTimestamp), 5),
|
||||
);
|
||||
try {
|
||||
this.write(payload);
|
||||
this._write(payload);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
'Failed writing new time to Plejd. Will try again in one hour or at restart.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (deviceId !== BLE_BROADCAST_DEVICE_ID) {
|
||||
} else if (bleOutputAddress !== BLE_BROADCAST_DEVICE_ID) {
|
||||
logger.info('Got time response. Plejd clock time in sync with Home Assistant time');
|
||||
}
|
||||
}
|
||||
|
|
@ -889,19 +910,19 @@ class PlejBLEHandler extends EventEmitter {
|
|||
logger.verbose(
|
||||
`Command ${cmd.toString(16)} unknown. ${decoded.toString(
|
||||
'hex',
|
||||
)}. Device ${deviceName} (${deviceId})`,
|
||||
)}. Device ${deviceName} (${bleOutputAddress}: ${outputUniqueId})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_createHexPayload(
|
||||
deviceId,
|
||||
bleOutputAddress,
|
||||
command,
|
||||
hexDataString,
|
||||
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
|
||||
) {
|
||||
return this._createPayload(
|
||||
deviceId,
|
||||
bleOutputAddress,
|
||||
command,
|
||||
5 + Math.ceil(hexDataString.length / 2),
|
||||
(payload) => payload.write(hexDataString, 5, 'hex'),
|
||||
|
|
@ -911,14 +932,14 @@ class PlejBLEHandler extends EventEmitter {
|
|||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_createPayload(
|
||||
deviceId,
|
||||
bleOutputAddress,
|
||||
command,
|
||||
bufferLength,
|
||||
payloadBufferAddDataFunc,
|
||||
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
|
||||
) {
|
||||
const payload = Buffer.alloc(bufferLength);
|
||||
payload.writeUInt8(deviceId);
|
||||
payload.writeUInt8(bleOutputAddress);
|
||||
payload.writeUInt16BE(requestResponseCommand, 1);
|
||||
payload.writeUInt16BE(command, 3);
|
||||
payloadBufferAddDataFunc(payload);
|
||||
|
|
@ -945,12 +966,12 @@ class PlejBLEHandler extends EventEmitter {
|
|||
|
||||
let ct = cipher.update(buf).toString('hex');
|
||||
ct += cipher.final().toString('hex');
|
||||
ct = Buffer.from(ct, 'hex');
|
||||
const ctBuf = Buffer.from(ct, 'hex');
|
||||
|
||||
let output = '';
|
||||
for (let i = 0, { length } = data; i < length; i++) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
output += String.fromCharCode(data[i] ^ ct[i % 16]);
|
||||
output += String.fromCharCode(data[i] ^ ctBuf[i % 16]);
|
||||
}
|
||||
|
||||
return Buffer.from(output, 'ascii');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const EventEmitter = require('events');
|
||||
const { EventEmitter } = require('events');
|
||||
const Configuration = require('./Configuration');
|
||||
const constants = require('./constants');
|
||||
const Logger = require('./Logger');
|
||||
|
|
@ -12,10 +12,13 @@ const MAX_RETRY_COUNT = 10; // Could be made a setting
|
|||
|
||||
class PlejdDeviceCommunication extends EventEmitter {
|
||||
bleConnected;
|
||||
bleDeviceTransitionTimers = {};
|
||||
bleOutputTransitionTimers = {};
|
||||
plejdBleHandler;
|
||||
config;
|
||||
/** @type {import('./DeviceRegistry')} */
|
||||
deviceRegistry;
|
||||
// eslint-disable-next-line max-len
|
||||
/** @type {{uniqueOutputId: string, command: string, data: any, shouldRetry: boolean, retryCount?: number}[]} */
|
||||
writeQueue = [];
|
||||
writeQueueRef = null;
|
||||
|
||||
|
|
@ -34,7 +37,7 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
}
|
||||
|
||||
cleanup() {
|
||||
Object.values(this.bleDeviceTransitionTimers).forEach((t) => clearTimeout(t));
|
||||
Object.values(this.bleOutputTransitionTimers).forEach((t) => clearTimeout(t));
|
||||
this.plejdBleHandler.cleanup();
|
||||
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived);
|
||||
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected);
|
||||
|
|
@ -46,7 +49,10 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
this.cleanup();
|
||||
this.bleConnected = false;
|
||||
// eslint-disable-next-line max-len
|
||||
this.plejdBleHandler.on(PlejBLEHandler.EVENTS.commandReceived, (deviceId, command, data) => this._bleCommandReceived(deviceId, command, data));
|
||||
this.plejdBleHandler.on(
|
||||
PlejBLEHandler.EVENTS.commandReceived,
|
||||
(uniqueOutputId, command, data) => this._bleCommandReceived(uniqueOutputId, command, data),
|
||||
);
|
||||
|
||||
this.plejdBleHandler.on(PlejBLEHandler.EVENTS.connected, () => {
|
||||
logger.info('Bluetooth connected. Plejd BLE up and running!');
|
||||
|
|
@ -70,42 +76,42 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
turnOn(deviceId, command) {
|
||||
const deviceName = this.deviceRegistry.getDeviceName(deviceId);
|
||||
turnOn(uniqueOutputId, command) {
|
||||
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
|
||||
logger.info(
|
||||
`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${
|
||||
`Plejd got turn on command for ${deviceName} (${uniqueOutputId}), brightness ${
|
||||
command.brightness
|
||||
}${command.transition ? `, transition: ${command.transition}` : ''}`,
|
||||
);
|
||||
this._transitionTo(uniqueOutputId, command.brightness, command.transition, deviceName);
|
||||
}
|
||||
|
||||
turnOff(uniqueOutputId, command) {
|
||||
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
|
||||
logger.info(
|
||||
`Plejd got turn off command for ${deviceName} (${uniqueOutputId})${
|
||||
command.transition ? `, transition: ${command.transition}` : ''
|
||||
}`,
|
||||
);
|
||||
this._transitionTo(deviceId, command.brightness, command.transition, deviceName);
|
||||
this._transitionTo(uniqueOutputId, 0, command.transition, deviceName);
|
||||
}
|
||||
|
||||
turnOff(deviceId, command) {
|
||||
const deviceName = this.deviceRegistry.getDeviceName(deviceId);
|
||||
logger.info(
|
||||
`Plejd got turn off command for ${deviceName} (${deviceId})${
|
||||
command.transition ? `, transition: ${command.transition}` : ''
|
||||
}`,
|
||||
);
|
||||
this._transitionTo(deviceId, 0, command.transition, deviceName);
|
||||
}
|
||||
|
||||
_bleCommandReceived(deviceId, command, data) {
|
||||
_bleCommandReceived(uniqueOutputId, command, data) {
|
||||
try {
|
||||
if (command === COMMANDS.DIM) {
|
||||
this.deviceRegistry.setState(deviceId, data.state, data.dim);
|
||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
|
||||
state: data.state,
|
||||
this.deviceRegistry.setOutputState(uniqueOutputId, data.state, data.dim);
|
||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
|
||||
state: !!data.state,
|
||||
brightness: data.dim,
|
||||
});
|
||||
} else if (command === COMMANDS.TURN_ON) {
|
||||
this.deviceRegistry.setState(deviceId, 1);
|
||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
|
||||
this.deviceRegistry.setOutputState(uniqueOutputId, true);
|
||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
|
||||
state: 1,
|
||||
});
|
||||
} else if (command === COMMANDS.TURN_OFF) {
|
||||
this.deviceRegistry.setState(deviceId, 0);
|
||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
|
||||
this.deviceRegistry.setOutputState(uniqueOutputId, false);
|
||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
|
||||
state: 0,
|
||||
});
|
||||
} else if (command === COMMANDS.TRIGGER_SCENE) {
|
||||
|
|
@ -118,18 +124,18 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
_clearDeviceTransitionTimer(deviceId) {
|
||||
if (this.bleDeviceTransitionTimers[deviceId]) {
|
||||
clearInterval(this.bleDeviceTransitionTimers[deviceId]);
|
||||
_clearDeviceTransitionTimer(uniqueOutputId) {
|
||||
if (this.bleOutputTransitionTimers[uniqueOutputId]) {
|
||||
clearInterval(this.bleOutputTransitionTimers[uniqueOutputId]);
|
||||
}
|
||||
}
|
||||
|
||||
_transitionTo(deviceId, targetBrightness, transition, deviceName) {
|
||||
const device = this.deviceRegistry.getDevice(deviceId);
|
||||
_transitionTo(uniqueOutputId, targetBrightness, transition, deviceName) {
|
||||
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
|
||||
const initialBrightness = device ? device.state && device.dim : null;
|
||||
this._clearDeviceTransitionTimer(deviceId);
|
||||
this._clearDeviceTransitionTimer(uniqueOutputId);
|
||||
|
||||
const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable;
|
||||
const isDimmable = this.deviceRegistry.getOutputDevice(uniqueOutputId).dimmable;
|
||||
|
||||
if (
|
||||
transition > 1
|
||||
|
|
@ -164,7 +170,7 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
|
||||
let nSteps = 0;
|
||||
|
||||
this.bleDeviceTransitionTimers[deviceId] = setInterval(() => {
|
||||
this.bleOutputTransitionTimers[uniqueOutputId] = setInterval(() => {
|
||||
const tElapsedMs = new Date().getTime() - dtStart.getTime();
|
||||
let tElapsed = tElapsedMs / 1000;
|
||||
|
||||
|
|
@ -178,20 +184,20 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
|
||||
if (tElapsed === transition) {
|
||||
nSteps++;
|
||||
this._clearDeviceTransitionTimer(deviceId);
|
||||
this._clearDeviceTransitionTimer(uniqueOutputId);
|
||||
newBrightness = targetBrightness;
|
||||
logger.debug(
|
||||
`Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
|
||||
`Queueing finalize ${deviceName} (${uniqueOutputId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
|
||||
tElapsedMs / (nSteps || 1)
|
||||
} ms.`,
|
||||
);
|
||||
this._setBrightness(deviceId, newBrightness, true, deviceName);
|
||||
this._setBrightness(uniqueOutputId, newBrightness, true, deviceName);
|
||||
} else {
|
||||
nSteps++;
|
||||
logger.verbose(
|
||||
`Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
|
||||
`Queueing dim transition for ${deviceName} (${uniqueOutputId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
|
||||
);
|
||||
this._setBrightness(deviceId, newBrightness, false, deviceName);
|
||||
this._setBrightness(uniqueOutputId, newBrightness, false, deviceName);
|
||||
}
|
||||
}, transitionInterval);
|
||||
} else {
|
||||
|
|
@ -200,34 +206,34 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
`Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`,
|
||||
);
|
||||
}
|
||||
this._setBrightness(deviceId, targetBrightness, true, deviceName);
|
||||
this._setBrightness(uniqueOutputId, targetBrightness, true, deviceName);
|
||||
}
|
||||
}
|
||||
|
||||
_setBrightness(deviceId, brightness, shouldRetry, deviceName) {
|
||||
_setBrightness(unqiueOutputId, brightness, shouldRetry, deviceName) {
|
||||
if (!brightness && brightness !== 0) {
|
||||
logger.debug(
|
||||
`Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`,
|
||||
`Queueing turn on ${deviceName} (${unqiueOutputId}). No brightness specified, setting DIM to previous.`,
|
||||
);
|
||||
this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_ON, null, shouldRetry);
|
||||
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_ON, null, shouldRetry);
|
||||
} else if (brightness <= 0) {
|
||||
logger.debug(`Queueing turn off ${deviceId}`);
|
||||
this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_OFF, null, shouldRetry);
|
||||
logger.debug(`Queueing turn off ${unqiueOutputId}`);
|
||||
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_OFF, null, shouldRetry);
|
||||
} else {
|
||||
if (brightness > 255) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
brightness = 255;
|
||||
}
|
||||
|
||||
logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`);
|
||||
logger.debug(`Queueing ${unqiueOutputId} set brightness to ${brightness}`);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
this._appendCommandToWriteQueue(deviceId, COMMANDS.DIM, brightness, shouldRetry);
|
||||
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.DIM, brightness, shouldRetry);
|
||||
}
|
||||
}
|
||||
|
||||
_appendCommandToWriteQueue(deviceId, command, data, shouldRetry) {
|
||||
_appendCommandToWriteQueue(uniqueOutputId, command, data, shouldRetry) {
|
||||
this.writeQueue.unshift({
|
||||
deviceId,
|
||||
uniqueOutputId,
|
||||
command,
|
||||
data,
|
||||
shouldRetry,
|
||||
|
|
@ -249,28 +255,29 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
const queueItem = this.writeQueue.pop();
|
||||
const deviceName = this.deviceRegistry.getDeviceName(queueItem.deviceId);
|
||||
const device = this.deviceRegistry.getOutputDevice(queueItem.uniqueOutputId);
|
||||
|
||||
logger.debug(
|
||||
`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${
|
||||
`Write queue: Processing ${device.name} (${queueItem.uniqueOutputId}). Command ${
|
||||
queueItem.command
|
||||
}${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${
|
||||
this.writeQueue.length
|
||||
}`,
|
||||
);
|
||||
|
||||
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) {
|
||||
if (this.writeQueue.some((item) => item.uniqueOutputId === queueItem.uniqueOutputId)) {
|
||||
logger.verbose(
|
||||
`Skipping ${deviceName} (${queueItem.deviceId}) `
|
||||
`Skipping ${device.name} (${queueItem.uniqueOutputId}) `
|
||||
+ `${queueItem.command} due to more recent command in queue.`,
|
||||
);
|
||||
// Skip commands if new ones exist for the same deviceId
|
||||
// Skip commands if new ones exist for the same uniqueOutputId
|
||||
// still process all messages in order
|
||||
} else {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
try {
|
||||
await this.plejdBleHandler.sendCommand(
|
||||
queueItem.command,
|
||||
queueItem.deviceId,
|
||||
device.bleOutputAddress,
|
||||
queueItem.data,
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
@ -281,7 +288,7 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
this.writeQueue.push(queueItem); // Add back to top of queue to be processed next;
|
||||
} else {
|
||||
logger.error(
|
||||
`Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.deviceId}). Command ${queueItem.command} failed.`,
|
||||
`Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${device.name} (${queueItem.uniqueOutputId}). Command ${queueItem.command} failed.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,6 +189,11 @@ The code in this project follows the [Airbnb JavaScript guide](https://github.co
|
|||
|
||||
For a nice developer experience it is very convenient to have `eslint` and `prettier` installed in your favorite editor (such as VS Code) and use the "format on save" option (or invoke formatting by Alt+Shift+F in VS Code). Any code issues should appear in the problems window inside the editor, as well as when running the command above.
|
||||
|
||||
For partial type hinting you can run
|
||||
|
||||
- `npm install --global typings`
|
||||
- `typings install`
|
||||
|
||||
When contributing, please do so by forking the repo and then using pull requests towards the dev branch.
|
||||
|
||||
### Logs
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
const SceneStep = require('./SceneStep');
|
||||
|
||||
class Scene {
|
||||
constructor(idx, scene, steps) {
|
||||
/**
|
||||
* @param {import('./DeviceRegistry')} deviceRegistry
|
||||
* @param {number} idx
|
||||
* @param {import("./types/ApiSite").Scene} scene
|
||||
*/
|
||||
constructor(deviceRegistry, idx, scene) {
|
||||
this.id = idx;
|
||||
this.title = scene.title;
|
||||
this.sceneId = scene.sceneId;
|
||||
|
||||
const sceneSteps = steps.filter((x) => x.sceneId === scene.sceneId);
|
||||
this.steps = [];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const step of sceneSteps) {
|
||||
this.steps.push(new SceneStep(step));
|
||||
}
|
||||
this.steps = deviceRegistry
|
||||
.getApiSite()
|
||||
.sceneSteps.filter((step) => step.sceneId === scene.sceneId)
|
||||
.map((step) => new SceneStep(step));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,45 +3,52 @@ const Scene = require('./Scene');
|
|||
|
||||
const logger = Logger.getLogger('scene-manager');
|
||||
class SceneManager {
|
||||
/** @private @type {import('./DeviceRegistry')} */
|
||||
deviceRegistry;
|
||||
plejdBle;
|
||||
/** @private @type {import('./PlejdDeviceCommunication')} */
|
||||
plejdDeviceCommunication;
|
||||
/** @private @type {Object.<string,Scene>} */
|
||||
scenes;
|
||||
|
||||
constructor(deviceRegistry, plejdBle) {
|
||||
constructor(deviceRegistry, plejdDeviceCommunication) {
|
||||
this.deviceRegistry = deviceRegistry;
|
||||
this.plejdBle = plejdBle;
|
||||
this.plejdDeviceCommunication = plejdDeviceCommunication;
|
||||
this.scenes = {};
|
||||
}
|
||||
|
||||
init() {
|
||||
const scenes = this.deviceRegistry.apiSite.scenes.filter(
|
||||
(x) => x.hiddenFromSceneList === false,
|
||||
);
|
||||
const scenes = this.deviceRegistry
|
||||
.getApiSite()
|
||||
.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||
|
||||
this.scenes = {};
|
||||
scenes.forEach((scene) => {
|
||||
const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId];
|
||||
this.scenes[idx] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps);
|
||||
const sceneBleAddress = this.deviceRegistry.getApiSite().sceneIndex[scene.sceneId];
|
||||
this.scenes[scene.sceneId] = new Scene(this.deviceRegistry, sceneBleAddress, scene);
|
||||
});
|
||||
}
|
||||
|
||||
executeScene(sceneId) {
|
||||
const scene = this.scenes[sceneId];
|
||||
/**
|
||||
* @param {string} sceneUniqueId
|
||||
*/
|
||||
executeScene(sceneUniqueId) {
|
||||
const scene = this.scenes[sceneUniqueId];
|
||||
if (!scene) {
|
||||
logger.info(`Scene with id ${sceneId} not found`);
|
||||
logger.info(`Scene with id ${sceneUniqueId} not found`);
|
||||
logger.verbose(`Scenes: ${JSON.stringify(this.scenes, null, 2)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
scene.steps.forEach((step) => {
|
||||
const device = this.deviceRegistry.getDeviceBySerialNumber(step.deviceId);
|
||||
const uniqueId = this.deviceRegistry.getUniqueOutputId(step.deviceId, step.output);
|
||||
const device = this.deviceRegistry.getOutputDevice(uniqueId);
|
||||
if (device) {
|
||||
if (device.dimmable && step.state) {
|
||||
this.plejdBle.turnOn(device.id, { brightness: step.brightness });
|
||||
this.plejdDeviceCommunication.turnOn(uniqueId, { brightness: step.brightness });
|
||||
} else if (!device.dimmable && step.state) {
|
||||
this.plejdBle.turnOn(device.id, {});
|
||||
this.plejdDeviceCommunication.turnOn(uniqueId, {});
|
||||
} else if (!step.state) {
|
||||
this.plejdBle.turnOff(device.id, {});
|
||||
this.plejdDeviceCommunication.turnOff(uniqueId, {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
class SceneStep {
|
||||
/**
|
||||
* @param {import("./types/ApiSite").SceneStep} step
|
||||
*/
|
||||
constructor(step) {
|
||||
this.sceneId = step.sceneId;
|
||||
this.deviceId = step.deviceId;
|
||||
this.output = step.output;
|
||||
this.state = step.state === 'On' ? 1 : 0;
|
||||
this.brightness = step.value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Plejd",
|
||||
"version": "0.7.1",
|
||||
"version": "0.8.0-dev",
|
||||
"slug": "plejd",
|
||||
"description": "Adds support for the Swedish home automation devices from Plejd.",
|
||||
"url": "https://github.com/icanos/hassio-plejd/",
|
||||
|
|
|
|||
10
plejd/jsconfig.json
Normal file
10
plejd/jsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"checkJs": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6"
|
||||
},
|
||||
"exclude": ["node_modules", "rootfs"]
|
||||
}
|
||||
|
|
@ -3,17 +3,17 @@
|
|||
"@abandonware/bluetooth-hci-socket": "~0.5.3-7",
|
||||
"axios": "~0.21.1",
|
||||
"buffer-xor": "~2.0.2",
|
||||
"dbus-next": "~0.9.1",
|
||||
"dbus-next": "~0.9.2",
|
||||
"fs": "0.0.1-security",
|
||||
"jspack": "~0.0.4",
|
||||
"mqtt": "~3.0.0",
|
||||
"mqtt": "~4.2.6",
|
||||
"winston": "~3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "~10.1.0",
|
||||
"eslint": "~7.18.0",
|
||||
"eslint": "~7.23.0",
|
||||
"eslint-config-airbnb": "~18.2.1",
|
||||
"eslint-config-prettier": "~7.2.0",
|
||||
"eslint-config-prettier": "~8.1.0",
|
||||
"eslint-plugin-import": "~2.22.1",
|
||||
"eslint-plugin-prettier": "~3.3.1",
|
||||
"prettier": "~2.2.1"
|
||||
|
|
|
|||
655
plejd/types/ApiSite.d.ts
vendored
Normal file
655
plejd/types/ApiSite.d.ts
vendored
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
/* eslint-disable camelcase */
|
||||
/* eslint-disable no-use-before-define */
|
||||
|
||||
export interface CachedSite {
|
||||
siteId: string;
|
||||
siteDetails: ApiSite;
|
||||
sessionToken: string;
|
||||
dtCache: string;
|
||||
}
|
||||
|
||||
export interface ApiSite {
|
||||
site: SiteDetailsSite;
|
||||
plejdMesh: PlejdMesh;
|
||||
rooms: Room[];
|
||||
scenes: Scene[];
|
||||
devices: Device[];
|
||||
plejdDevices: PlejdDevice[];
|
||||
gateways: Gateway[];
|
||||
resourceSets: ResourceSet[];
|
||||
timeEvents: TimeEvent[];
|
||||
sceneSteps: SceneStep[];
|
||||
astroEvents: AstroEvent[];
|
||||
inputSettings: InputSetting[];
|
||||
outputSettings: OutputSetting[];
|
||||
stateTimers: StateTimers;
|
||||
sitePermission: SitePermission;
|
||||
inputAddress: { [key: string]: { [key: string]: number } };
|
||||
outputAddress: { [key: string]: OutputAddress };
|
||||
deviceAddress: { [key: string]: number };
|
||||
outputGroups: { [key: string]: OutputGroup };
|
||||
roomAddress: { [key: string]: number };
|
||||
sceneIndex: { [key: string]: number };
|
||||
images: Images;
|
||||
deviceLimit: number;
|
||||
}
|
||||
|
||||
export interface AstroEvent {
|
||||
dirtyDevices?: any[];
|
||||
dirtyRemovedDevices?: any[];
|
||||
deviceId: string;
|
||||
siteId: string;
|
||||
sceneId: string;
|
||||
fadeTime: number;
|
||||
activated: boolean;
|
||||
astroEventId: string;
|
||||
index: number;
|
||||
sunriseOffset: number;
|
||||
sunsetOffset: number;
|
||||
pauseStart: string;
|
||||
pauseEnd: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dirtyRemove?: boolean;
|
||||
ACL: AstroEventACL;
|
||||
targetDevices: AstroEventTargetDevice[];
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export interface AstroEventACL {}
|
||||
|
||||
export enum AstroEventType {
|
||||
Object = 'Object',
|
||||
}
|
||||
|
||||
export interface AstroEventTargetDevice {
|
||||
deviceId: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
deviceId: string;
|
||||
siteId: string;
|
||||
roomId: string;
|
||||
title: string;
|
||||
traits: number;
|
||||
hardware?: Hardware;
|
||||
hiddenFromRoomList: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
outputType: OutputType;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: DeviceClassName;
|
||||
hiddenFromIntegrations?: boolean;
|
||||
}
|
||||
|
||||
export enum DeviceClassName {
|
||||
Device = 'Device',
|
||||
}
|
||||
|
||||
export interface Hardware {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
name: Name;
|
||||
hardwareId: string;
|
||||
minSupportedFirmware: PlejdMeshClass;
|
||||
latestFirmware: PlejdMeshClass;
|
||||
brand: Brand;
|
||||
type: Type;
|
||||
image: Image;
|
||||
requiredAccountType: RequiredAccountType[];
|
||||
numberOfDevices: number;
|
||||
predefinedLoad: PredefinedLoad;
|
||||
supportedFirmware: PredefinedLoad;
|
||||
ACL: AstroEventACL;
|
||||
objectId: HardwareObjectID;
|
||||
__type: AstroEventType;
|
||||
className: HardwareClassName;
|
||||
}
|
||||
|
||||
export enum Brand {
|
||||
PlejdLight = 'Plejd Light',
|
||||
}
|
||||
|
||||
export enum HardwareClassName {
|
||||
Hardware = 'Hardware',
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
__type: ImageType;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export enum ImageType {
|
||||
File = 'File',
|
||||
}
|
||||
|
||||
export interface PlejdMeshClass {
|
||||
__type: InstallerType;
|
||||
className: SiteClassName;
|
||||
objectId: ObjectID;
|
||||
}
|
||||
|
||||
export enum InstallerType {
|
||||
Pointer = 'Pointer',
|
||||
}
|
||||
|
||||
export enum SiteClassName {
|
||||
DimCurve = 'DimCurve',
|
||||
Firmware = 'Firmware',
|
||||
PlejdMesh = 'PlejdMesh',
|
||||
Site = 'Site',
|
||||
User = '_User',
|
||||
UserProfile = 'UserProfile',
|
||||
}
|
||||
|
||||
export enum ObjectID {
|
||||
BBBJO2Cufm = 'BBBJO2cufm',
|
||||
D4Dw87Hq21 = 'D4DW87HQ21',
|
||||
FCrrS1NJHH = 'FCrrS1nJHH',
|
||||
GX1W4P06QS = 'gX1W4p06QS',
|
||||
Ndlvzgh4Df = 'ndlvzgh4df',
|
||||
UHoKQLuXqZ = 'uHoKQLuXqZ',
|
||||
VfHiawBPA8 = 'vfHiawBPA8',
|
||||
WgAFPloWjK = 'wgAfPloWjK',
|
||||
YkyNDotBNa = 'YkyNDotBNa',
|
||||
}
|
||||
|
||||
export enum Name {
|
||||
Ctr01 = 'CTR-01',
|
||||
Dim01 = 'DIM-01',
|
||||
}
|
||||
|
||||
export enum HardwareObjectID {
|
||||
R3Gfd6ACAu = 'R3gfd6ACAu',
|
||||
XjslOltgvi = 'xjslOltgvi',
|
||||
}
|
||||
|
||||
export interface PredefinedLoad {
|
||||
__type: SupportedFirmwareType;
|
||||
className: PredefinedLoadClassName;
|
||||
}
|
||||
|
||||
export enum SupportedFirmwareType {
|
||||
Relation = 'Relation',
|
||||
}
|
||||
|
||||
export enum PredefinedLoadClassName {
|
||||
DimCurve = 'DimCurve',
|
||||
Firmware = 'Firmware',
|
||||
PredefinedLoad = 'PredefinedLoad',
|
||||
}
|
||||
|
||||
export enum RequiredAccountType {
|
||||
Installer = 'installer',
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
Controller = 'Controller',
|
||||
LEDDimmer = 'LED Dimmer',
|
||||
}
|
||||
|
||||
export enum OutputType {
|
||||
Light = 'LIGHT',
|
||||
Relay = 'RELAY',
|
||||
}
|
||||
|
||||
export interface Gateway {
|
||||
title: string;
|
||||
deviceId: string;
|
||||
siteId: string;
|
||||
hardwareId: string;
|
||||
installer: ObjectID;
|
||||
firmware: number;
|
||||
firmwareObject: Firmware;
|
||||
dirtyInstall: boolean;
|
||||
dirtyUpdate: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
factoryKey: string;
|
||||
resourceSetId: string;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export interface Firmware {
|
||||
notes: Notes;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
data: Image;
|
||||
metaData: Image;
|
||||
version: Version;
|
||||
buildTime: number;
|
||||
firmwareApi: string;
|
||||
ACL: AstroEventACL;
|
||||
objectId: FirmwareObjectObjectID;
|
||||
__type: AstroEventType;
|
||||
className: SiteClassName;
|
||||
}
|
||||
|
||||
export enum Notes {
|
||||
Ctr01 = 'CTR-01',
|
||||
Ctr20ReleaseCandidate1 = 'Ctr 2.0 Release candidate 1',
|
||||
Dim20ReleaseCandidate1 = 'Dim 2.0 Release candidate 1',
|
||||
Dim221ReleaseCandidate = 'Dim 2.2.1 Release Candidate',
|
||||
GWY10ReleaseCandidate = 'GWY 1.0 Release Candidate',
|
||||
}
|
||||
|
||||
export enum FirmwareObjectObjectID {
|
||||
BBBJO2Cufm = 'BBBJO2cufm',
|
||||
E6YxfREDuF = 'E6yxfREDuF',
|
||||
JYSZ0EvyCU = 'JYSZ0EvyCU',
|
||||
Ndlvzgh4Df = 'ndlvzgh4df',
|
||||
RlglTfVHDe = 'rlglTfVHDe',
|
||||
}
|
||||
|
||||
export enum Version {
|
||||
The12 = '1.2',
|
||||
The20 = '2.0',
|
||||
The221 = '2.2.1',
|
||||
The304 = '3.0.4',
|
||||
}
|
||||
|
||||
export interface Images {
|
||||
'2afc6c6e-7a26-466a-b8ec-febbca90f5f7': string;
|
||||
}
|
||||
|
||||
export interface InputSetting {
|
||||
deviceId: string;
|
||||
input: number;
|
||||
siteId: string;
|
||||
dimSpeed: number;
|
||||
buttonType: ButtonType;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: InputSettingClassName;
|
||||
doubleClick?: string;
|
||||
singleClick?: null;
|
||||
doubleSidedDirectionButton?: boolean;
|
||||
}
|
||||
|
||||
export enum ButtonType {
|
||||
PushButton = 'PushButton',
|
||||
RotateMesh = 'RotateMesh',
|
||||
Scene = 'Scene',
|
||||
}
|
||||
|
||||
export enum InputSettingClassName {
|
||||
PlejdDeviceInputSetting = 'PlejdDeviceInputSetting',
|
||||
}
|
||||
|
||||
export interface OutputAddress {
|
||||
'0': number;
|
||||
}
|
||||
|
||||
export interface OutputGroup {
|
||||
'0': number[];
|
||||
}
|
||||
|
||||
export interface OutputSetting {
|
||||
deviceId: string;
|
||||
output: number;
|
||||
deviceParseId: string;
|
||||
siteId: string;
|
||||
predefinedLoad: OutputSettingPredefinedLoad;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dimMin: number;
|
||||
dimMax: number;
|
||||
dimStart: number;
|
||||
outputStartTime: number;
|
||||
outputSpeed: number;
|
||||
bootState: BootState;
|
||||
dimCurve: DimCurve;
|
||||
curveLogarithm: number;
|
||||
curveSinusCompensation: number;
|
||||
curveRectification: boolean;
|
||||
output_0_10V_Mode?: Output0_10_VMode;
|
||||
zeroCrossing?: Output0_10_VMode;
|
||||
minimumRelayOffTime?: number;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: OutputSettingClassName;
|
||||
ledCurrent?: number;
|
||||
ledVoltage?: number;
|
||||
relayConfig?: Output0_10_VMode;
|
||||
}
|
||||
|
||||
export enum BootState {
|
||||
UseLast = 'UseLast',
|
||||
}
|
||||
|
||||
export enum OutputSettingClassName {
|
||||
PlejdDeviceOutputSetting = 'PlejdDeviceOutputSetting',
|
||||
}
|
||||
|
||||
export enum DimCurve {
|
||||
LinearLogarithmicSlidingProportion = 'LinearLogarithmicSlidingProportion',
|
||||
NonDimmable = 'NonDimmable',
|
||||
}
|
||||
|
||||
export enum Output0_10_VMode {
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export interface OutputSettingPredefinedLoad {
|
||||
updatedAt: Date;
|
||||
createdAt: Date;
|
||||
loadType: string;
|
||||
predefinedLoadData: string;
|
||||
defaultDimCurve: PlejdMeshClass;
|
||||
description_en?: DescriptionEn;
|
||||
title_en?: TitleEn;
|
||||
title_sv?: TitleSv;
|
||||
description_sv?: DescriptionSv;
|
||||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
allowedDimCurves: PredefinedLoad;
|
||||
ACL: PredefinedLoadACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: PredefinedLoadClassName;
|
||||
supportMessage?: SupportMessage;
|
||||
filters?: Filters;
|
||||
}
|
||||
|
||||
export interface PredefinedLoadACL {
|
||||
'*': Empty;
|
||||
}
|
||||
|
||||
export interface Empty {
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export enum DescriptionEn {
|
||||
OnOff = 'On / Off',
|
||||
OnlySwitchingOffOn = 'Only switching off/on',
|
||||
The230VDimmableLEDLightSourceMax100VA = '230V dimmable LED light source - Max 100VA',
|
||||
The230VIncandescentHalogenElectronicTransformatorMax300W = '230V Incandescent / Halogen, Electronic transformator - Max 300W',
|
||||
WithoutRelay = 'Without relay',
|
||||
}
|
||||
|
||||
export enum DescriptionSv {
|
||||
EndastBrytningAVPå = 'Endast brytning av/på',
|
||||
ReläbrytningAVPå = 'Reläbrytning av/på',
|
||||
The230VDimbarLEDLjuskällaMax100VA = '230V dimbar LED ljuskälla - Max 100VA',
|
||||
The230VDimbarLEDLjuskällaMax200VA = '230V dimbar LED ljuskälla - Max 200VA',
|
||||
The230VHalogenGlödljusElektroniskTransformatorMax300W = '230V Halogen / Glödljus, Elektronisk transformator - Max 300W',
|
||||
UtanReläbrytning = 'Utan reläbrytning',
|
||||
}
|
||||
|
||||
export interface Filters {
|
||||
allowedCountriesFilter: AllowedCountriesFilter;
|
||||
}
|
||||
|
||||
export interface AllowedCountriesFilter {
|
||||
countryCodes: CountryCode[];
|
||||
}
|
||||
|
||||
export enum CountryCode {
|
||||
Fi = 'FI',
|
||||
No = 'NO',
|
||||
SE = 'SE',
|
||||
}
|
||||
|
||||
export enum SupportMessage {
|
||||
PredefinedLoadNonDimmableSupportMessageHTML = 'PredefinedLoadNonDimmableSupportMessageHTML',
|
||||
}
|
||||
|
||||
export enum TitleEn {
|
||||
IncandescentHalogen = 'Incandescent / Halogen',
|
||||
LEDTrailingEdgeCommon = 'LED Trailing Edge (Common)',
|
||||
LeadingEdge = 'Leading edge',
|
||||
NonDimmableLEDLightSourceMax200VA = 'Non-dimmable LED light source (Max 200VA)',
|
||||
RelayOnly = 'Relay only',
|
||||
The010V = '0-10V',
|
||||
}
|
||||
|
||||
export enum TitleSv {
|
||||
EjDimbarLEDLjuskällaMax200VA = 'Ej dimbar LED-ljuskälla (Max 200VA)',
|
||||
HalogenGlödljus = 'Halogen / Glödljus',
|
||||
LEDBakkantVanligast = 'LED Bakkant (Vanligast)',
|
||||
LEDFramkant = 'LED Framkant',
|
||||
Reläfunktion = 'Reläfunktion',
|
||||
The010V = '0-10V',
|
||||
}
|
||||
|
||||
export interface PlejdDevice {
|
||||
deviceId: string;
|
||||
installer: PlejdMeshClass;
|
||||
dirtyInstall: boolean;
|
||||
dirtyUpdate: boolean;
|
||||
dirtyClock: boolean;
|
||||
hardwareId: string;
|
||||
faceplateId: string;
|
||||
firmware: Firmware;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
coordinates: Coordinates;
|
||||
dirtySettings: boolean;
|
||||
diagnostics: string;
|
||||
siteId: string;
|
||||
predefinedLoad: OutputSettingPredefinedLoad;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: PlejdDeviceClassName;
|
||||
}
|
||||
|
||||
export enum PlejdDeviceClassName {
|
||||
PlejdDevice = 'PlejdDevice',
|
||||
}
|
||||
|
||||
export interface Coordinates {
|
||||
__type: CoordinatesType;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export enum CoordinatesType {
|
||||
GeoPoint = 'GeoPoint',
|
||||
}
|
||||
|
||||
export interface PlejdMesh {
|
||||
siteId: string;
|
||||
plejdMeshId: string;
|
||||
meshKey: string;
|
||||
cryptoKey: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
site: PlejdMeshClass;
|
||||
ACL: AstroEventACL;
|
||||
objectId: ObjectID;
|
||||
__type: AstroEventType;
|
||||
className: SiteClassName;
|
||||
}
|
||||
|
||||
export interface ResourceSet {
|
||||
scopes: string[];
|
||||
remoteAccessUsers: string[];
|
||||
name: string;
|
||||
type: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export interface Room {
|
||||
siteId: string;
|
||||
roomId: string;
|
||||
title: string;
|
||||
category: string;
|
||||
imageHash: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: RoomClassName;
|
||||
}
|
||||
|
||||
export enum RoomClassName {
|
||||
Room = 'Room',
|
||||
}
|
||||
|
||||
export interface SceneStep {
|
||||
sceneId: string;
|
||||
siteId: string;
|
||||
deviceId: string;
|
||||
state: State;
|
||||
value: number;
|
||||
output: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: SceneStepClassName;
|
||||
dirty?: boolean;
|
||||
dirtyRemoved?: boolean;
|
||||
}
|
||||
|
||||
export enum SceneStepClassName {
|
||||
SceneStep = 'SceneStep',
|
||||
}
|
||||
|
||||
export enum State {
|
||||
Off = 'Off',
|
||||
On = 'On',
|
||||
}
|
||||
|
||||
export interface Scene {
|
||||
title: string;
|
||||
sceneId: string;
|
||||
siteId: string;
|
||||
hiddenFromSceneList: boolean;
|
||||
settings: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: ButtonType;
|
||||
}
|
||||
|
||||
export interface SiteDetailsSite {
|
||||
installers: ObjectID[];
|
||||
title: string;
|
||||
siteId: string;
|
||||
version: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
plejdMesh: PlejdMeshClass;
|
||||
coordinates: Coordinates;
|
||||
astroTable: AstroTable;
|
||||
deviceAstroTable: DeviceAstroTable;
|
||||
zipCode: string;
|
||||
city: string;
|
||||
country: string;
|
||||
previousOwners: string[];
|
||||
ACL: AstroEventACL;
|
||||
objectId: ObjectID;
|
||||
__type: AstroEventType;
|
||||
className: SiteClassName;
|
||||
}
|
||||
|
||||
export interface AstroTable {
|
||||
sunrise: string[];
|
||||
sunset: string[];
|
||||
}
|
||||
|
||||
export interface DeviceAstroTable {
|
||||
sunrise: number[];
|
||||
sunset: number[];
|
||||
}
|
||||
|
||||
export interface SitePermission {
|
||||
siteId: string;
|
||||
userId: ObjectID;
|
||||
user: User;
|
||||
isOwner: boolean;
|
||||
isInstaller: boolean;
|
||||
isUser: boolean;
|
||||
site: SiteDetailsSite;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
ACL: AstroEventACL;
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
profileName: string;
|
||||
isInstaller: boolean;
|
||||
email: string;
|
||||
locale: string;
|
||||
username: string;
|
||||
emailVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
profile: PlejdMeshClass;
|
||||
_failed_login_count: number;
|
||||
hasIntegration: boolean;
|
||||
ACL: UserACL;
|
||||
objectId: ObjectID;
|
||||
__type: AstroEventType;
|
||||
className: SiteClassName;
|
||||
}
|
||||
|
||||
export interface UserACL {
|
||||
gX1W4p06QS: GX1W4P06QS;
|
||||
}
|
||||
|
||||
export interface GX1W4P06QS {
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
}
|
||||
|
||||
export interface StateTimers {
|
||||
SafetyTimer: any[];
|
||||
}
|
||||
|
||||
export interface TimeEvent {
|
||||
dirtyDevices?: any[];
|
||||
dirtyRemovedDevices?: any[];
|
||||
scheduledDays: number[];
|
||||
deviceId: string;
|
||||
siteId: string;
|
||||
sceneId: string;
|
||||
fadeTime: number;
|
||||
activated: boolean;
|
||||
timeEventId: string;
|
||||
startTimeIndex: number;
|
||||
endTimeIndex: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dirtyRemove?: boolean;
|
||||
ACL: AstroEventACL;
|
||||
targetDevices: TimeEventTargetDevice[];
|
||||
objectId: string;
|
||||
__type: AstroEventType;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export interface TimeEventTargetDevice {
|
||||
deviceId: string;
|
||||
startTimeIndex: number;
|
||||
endTimeIndex: number;
|
||||
}
|
||||
50
plejd/types/Configuration.d.ts
vendored
Normal file
50
plejd/types/Configuration.d.ts
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/* eslint-disable no-use-before-define */
|
||||
|
||||
export interface AddonInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
url: string;
|
||||
arch: string[];
|
||||
startup: string;
|
||||
boot: string;
|
||||
host_network: boolean;
|
||||
host_dbus: boolean;
|
||||
apparmor: boolean;
|
||||
}
|
||||
|
||||
export interface Configuration extends AddonInfo {
|
||||
options: Options;
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
site: string;
|
||||
username: string;
|
||||
password: string;
|
||||
mqttBroker: string;
|
||||
mqttUsername: string;
|
||||
mqttPassword: string;
|
||||
includeRoomsAsLights: boolean;
|
||||
preferCachedApiResponse: boolean;
|
||||
updatePlejdClock: boolean;
|
||||
logLevel: string;
|
||||
connectionTimeout: number;
|
||||
writeQueueWaitTime: number;
|
||||
}
|
||||
|
||||
export interface Schema {
|
||||
site: string;
|
||||
username: string;
|
||||
password: string;
|
||||
mqttBroker: string;
|
||||
mqttUsername: string;
|
||||
mqttPassword: string;
|
||||
includeRoomsAsLights: string;
|
||||
preferCachedApiResponse: string;
|
||||
updatePlejdClock: string;
|
||||
logLevel: string;
|
||||
connectionTimeout: string;
|
||||
writeQueueWaitTime: string;
|
||||
}
|
||||
21
plejd/types/DeviceRegistry.d.ts
vendored
Normal file
21
plejd/types/DeviceRegistry.d.ts
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable no-use-before-define */
|
||||
|
||||
export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice };
|
||||
|
||||
export interface OutputDevice {
|
||||
bleOutputAddress: number;
|
||||
deviceId: string;
|
||||
dim?: number;
|
||||
dimmable: boolean;
|
||||
hiddenFromRoomList?: boolean;
|
||||
hiddenFromIntegrations?: boolean;
|
||||
hiddenFromSceneList?: boolean;
|
||||
name: string;
|
||||
output: number;
|
||||
roomId: string;
|
||||
state: boolean | undefined;
|
||||
type: string;
|
||||
typeName: string;
|
||||
version: string;
|
||||
uniqueId: string;
|
||||
}
|
||||
25
plejd/types/Mqtt.d.ts
vendored
Normal file
25
plejd/types/Mqtt.d.ts
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* eslint-disable no-use-before-define */
|
||||
|
||||
export type TopicType = 'config' | 'state' | 'availability' | 'set';
|
||||
export type TOPIC_TYPES = { [key: string]: TopicType };
|
||||
|
||||
export type MqttType = 'light' | 'scene' | 'switch' | 'device_automation';
|
||||
export type MQTT_TYPES = { [key: string]: MqttType };
|
||||
|
||||
export interface OutputDevice {
|
||||
bleOutputAddress: number;
|
||||
deviceId: string;
|
||||
dim?: number;
|
||||
dimmable: boolean;
|
||||
hiddenFromRoomList?: boolean;
|
||||
hiddenFromIntegrations?: boolean;
|
||||
hiddenFromSceneList?: boolean;
|
||||
name: string;
|
||||
output: number;
|
||||
roomId: string;
|
||||
state: boolean | undefined;
|
||||
type: string;
|
||||
typeName: string;
|
||||
version: string;
|
||||
uniqueId: string;
|
||||
}
|
||||
11
plejd/types/PlejdApi.d.ts
vendored
Normal file
11
plejd/types/PlejdApi.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable no-use-before-define */
|
||||
|
||||
import { ApiSite } from './ApiSite.d.ts';
|
||||
|
||||
export type PlejdApi = {
|
||||
config: any;
|
||||
deviceRegistry: any;
|
||||
sessionToken: string;
|
||||
siteId: string;
|
||||
siteDetails: ApiSite;
|
||||
};
|
||||
5
plejd/typings.json
Normal file
5
plejd/typings.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"globalDependencies": {
|
||||
"node": "registry:dt/node#7.0.0+20170322231424"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue