Merge pull request #179 from icanos/feature/id-refactor
Refactor unique id handling throughout the addon
This commit is contained in:
commit
34cb7b36d1
21 changed files with 1516 additions and 385 deletions
|
|
@ -1,5 +1,20 @@
|
||||||
# Changelog hassio-plejd Home Assistant Plejd addon
|
# 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)
|
## [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)
|
[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.7.0...0.7.1)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
class Configuration {
|
class Configuration {
|
||||||
|
/** @type {import('types/Configuration').Options} */
|
||||||
static _options = null;
|
static _options = null;
|
||||||
|
/** @type {import('types/Configuration').AddonInfo} */
|
||||||
static _addonInfo = null;
|
static _addonInfo = null;
|
||||||
|
|
||||||
|
/** @returns Options */
|
||||||
static getOptions() {
|
static getOptions() {
|
||||||
if (!Configuration._options) {
|
if (!Configuration._options) {
|
||||||
Configuration._hydrateCache();
|
Configuration._hydrateCache();
|
||||||
|
|
@ -11,6 +14,7 @@ class Configuration {
|
||||||
return Configuration._options;
|
return Configuration._options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @returns AddonInfo */
|
||||||
static getAddonInfo() {
|
static getAddonInfo() {
|
||||||
if (!Configuration._addonInfo) {
|
if (!Configuration._addonInfo) {
|
||||||
Configuration._hydrateCache();
|
Configuration._hydrateCache();
|
||||||
|
|
@ -20,10 +24,10 @@ class Configuration {
|
||||||
|
|
||||||
static _hydrateCache() {
|
static _hydrateCache() {
|
||||||
const rawData = fs.readFileSync('/data/options.json');
|
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 defaultRawData = fs.readFileSync('/plejd/config.json');
|
||||||
const defaultConfig = JSON.parse(defaultRawData);
|
const defaultConfig = JSON.parse(defaultRawData.toString());
|
||||||
|
|
||||||
Configuration._options = { ...defaultConfig.options, ...config };
|
Configuration._options = { ...defaultConfig.options, ...config };
|
||||||
Configuration._addonInfo = {
|
Configuration._addonInfo = {
|
||||||
|
|
|
||||||
|
|
@ -2,154 +2,195 @@ const Logger = require('./Logger');
|
||||||
|
|
||||||
const logger = Logger.getLogger('device-registry');
|
const logger = Logger.getLogger('device-registry');
|
||||||
class DeviceRegistry {
|
class DeviceRegistry {
|
||||||
apiSite;
|
/** @type {string} */
|
||||||
cryptoKey = null;
|
cryptoKey = null;
|
||||||
|
|
||||||
deviceIdsByRoom = {};
|
/** @private @type {Object.<string, import('types/ApiSite').Device>} */
|
||||||
deviceIdsBySerial = {};
|
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
|
// Dictionaries of [id]: device per type
|
||||||
plejdDevices = {};
|
/** @private @type {import('types/DeviceRegistry').OutputDevices} */
|
||||||
roomDevices = {};
|
outputDevices = {};
|
||||||
|
/** @private @type {import('types/DeviceRegistry').OutputDevices} */
|
||||||
sceneDevices = {};
|
sceneDevices = {};
|
||||||
|
|
||||||
get allDevices() {
|
/** @param device {import('./types/ApiSite').Device} */
|
||||||
return [
|
addPhysicalDevice(device) {
|
||||||
...Object.values(this.plejdDevices),
|
this.devices[device.deviceId] = device;
|
||||||
...Object.values(this.roomDevices),
|
|
||||||
...Object.values(this.sceneDevices),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlejdDevice(device) {
|
/** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */
|
||||||
const added = {
|
addOutputDevice(outputDevice) {
|
||||||
...this.plejdDevices[device.id],
|
if (outputDevice.hiddenFromIntegrations || outputDevice.hiddenFromRoomList) {
|
||||||
...device,
|
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.outputDevices = {
|
||||||
...this.plejdDevices,
|
...this.outputDevices,
|
||||||
[added.id]: added,
|
[outputDevice.uniqueId]: outputDevice,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.deviceIdsBySerial[added.serialNumber] = added.id;
|
|
||||||
|
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
`Added/updated device: ${JSON.stringify(added)}. ${
|
`Added/updated output device: ${JSON.stringify(outputDevice)}. ${
|
||||||
Object.keys(this.plejdDevices).length
|
Object.keys(this.outputDevices).length
|
||||||
} plejd devices in total.`,
|
} output devices in total.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (added.roomId) {
|
this.outputUniqueIdByBleOutputAddress[outputDevice.bleOutputAddress] = outputDevice.uniqueId;
|
||||||
if (!this.deviceIdsByRoom[added.roomId]) {
|
|
||||||
this.deviceIdsByRoom[added.roomId] = [];
|
if (!this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId]) {
|
||||||
}
|
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId] = [];
|
||||||
const room = this.deviceIdsByRoom[added.roomId];
|
}
|
||||||
if (!room.includes(added.id)) {
|
if (
|
||||||
this.deviceIdsByRoom[added.roomId] = [...room, added.id];
|
outputDevice.roomId !== outputDevice.uniqueId
|
||||||
}
|
&& !this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId)
|
||||||
|
) {
|
||||||
|
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].push(outputDevice.uniqueId);
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
`Added device to room ${added.roomId}: ${JSON.stringify(
|
`Added device to room ${outputDevice.roomId}: ${JSON.stringify(
|
||||||
this.deviceIdsByRoom[added.roomId],
|
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) {
|
addScene(scene) {
|
||||||
const added = {
|
|
||||||
...this.sceneDevices[scene.id],
|
|
||||||
...scene,
|
|
||||||
};
|
|
||||||
this.sceneDevices = {
|
this.sceneDevices = {
|
||||||
...this.sceneDevices,
|
...this.sceneDevices,
|
||||||
[added.id]: added,
|
[scene.uniqueId]: scene,
|
||||||
};
|
};
|
||||||
|
this.sceneUniqueIdByBleOutputAddress[scene.bleOutputAddress] = scene.uniqueId;
|
||||||
|
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
`Added/updated scene: ${JSON.stringify(added)}. ${
|
`Added/updated scene: ${JSON.stringify(scene)}. ${
|
||||||
Object.keys(this.sceneDevices).length
|
Object.keys(this.sceneDevices).length
|
||||||
} scenes in total.`,
|
} scenes in total.`,
|
||||||
);
|
);
|
||||||
return added;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPlejdDevices() {
|
clearPlejdDevices() {
|
||||||
this.plejdDevices = {};
|
this.devices = {};
|
||||||
this.deviceIdsByRoom = {};
|
this.outputDevices = {};
|
||||||
this.deviceIdsBySerial = {};
|
this.outputDeviceUniqueIdsByRoomId = {};
|
||||||
}
|
this.outputUniqueIdByBleOutputAddress = {};
|
||||||
|
|
||||||
clearRoomDevices() {
|
|
||||||
this.roomDevices = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSceneDevices() {
|
clearSceneDevices() {
|
||||||
this.sceneDevices = {};
|
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) {
|
/** @returns {import('./types/ApiSite').ApiSite} */
|
||||||
return this.getDevice(this.deviceIdsBySerial[serialNumber]);
|
getApiSite() {
|
||||||
|
return this.apiSite;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDeviceName(deviceId) {
|
/**
|
||||||
return (this.plejdDevices[deviceId] || {}).name;
|
* @param {string} uniqueOutputId
|
||||||
|
*/
|
||||||
|
getOutputDevice(uniqueOutputId) {
|
||||||
|
return this.outputDevices[uniqueOutputId];
|
||||||
}
|
}
|
||||||
|
|
||||||
getScene(sceneId) {
|
/** @returns {import('./types/DeviceRegistry').OutputDevice} */
|
||||||
return this.sceneDevices[sceneId];
|
getOutputDeviceByBleOutputAddress(bleOutputAddress) {
|
||||||
|
return this.outputDevices[this.outputUniqueIdByBleOutputAddress[bleOutputAddress]];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSceneName(sceneId) {
|
/** @returns {string[]} */
|
||||||
return (this.sceneDevices[sceneId] || {}).name;
|
getOutputDeviceIdsByRoomId(roomId) {
|
||||||
|
return this.outputDeviceUniqueIdsByRoomId[roomId];
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(deviceId) {
|
getOutputDeviceName(uniqueOutputId) {
|
||||||
const device = this.getDevice(deviceId) || {};
|
return (this.outputDevices[uniqueOutputId] || {}).name;
|
||||||
if (device.dimmable) {
|
}
|
||||||
return {
|
|
||||||
state: device.state,
|
/**
|
||||||
dim: device.dim,
|
* @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 {
|
return this.sceneDevices[sceneUniqueId];
|
||||||
state: device.state,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setApiSite(siteDetails) {
|
/**
|
||||||
this.apiSite = siteDetails;
|
* @param {string} sceneUniqueId
|
||||||
|
*/
|
||||||
|
getSceneName(sceneUniqueId) {
|
||||||
|
return (this.sceneDevices[sceneUniqueId] || {}).name;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(deviceId, state, dim) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
const device = this.getDevice(deviceId) || this.addPlejdDevice({ id: deviceId });
|
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;
|
device.state = state;
|
||||||
if (dim && device.dimmable) {
|
if (dim && device.dimmable) {
|
||||||
device.dim = dim;
|
device.dim = dim;
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,36 @@ const startTopics = ['hass/status', 'homeassistant/status'];
|
||||||
|
|
||||||
const logger = Logger.getLogger('plejd-mqtt');
|
const logger = Logger.getLogger('plejd-mqtt');
|
||||||
|
|
||||||
// #region discovery
|
|
||||||
|
|
||||||
const discoveryPrefix = 'homeassistant';
|
const discoveryPrefix = 'homeassistant';
|
||||||
const nodeId = 'plejd';
|
const nodeId = 'plejd';
|
||||||
|
|
||||||
|
/** @type {import('./types/Mqtt').MQTT_TYPES} */
|
||||||
|
const MQTT_TYPES = {
|
||||||
|
LIGHT: 'light',
|
||||||
|
SCENE: 'scene',
|
||||||
|
SWITCH: 'switch',
|
||||||
|
DEVICE_AUTOMATION: 'device_automation',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {import('./types/Mqtt').TOPIC_TYPES} */
|
||||||
|
const TOPIC_TYPES = {
|
||||||
|
CONFIG: 'config',
|
||||||
|
STATE: 'state',
|
||||||
|
AVAILABILITY: 'availability',
|
||||||
|
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 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(
|
const decodeTopicRegexp = new RegExp(
|
||||||
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/,
|
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/,
|
||||||
|
|
@ -33,41 +51,63 @@ const decodeTopic = (topic) => {
|
||||||
return matches.groups;
|
return matches.groups;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDiscoveryPayload = (device) => ({
|
const getOutputDeviceDiscoveryPayload = (
|
||||||
schema: 'json',
|
/** @type {import('./types/DeviceRegistry').OutputDevice} */ device,
|
||||||
|
) => ({
|
||||||
name: device.name,
|
name: device.name,
|
||||||
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
|
unique_id: device.uniqueId,
|
||||||
state_topic: getStateTopic(device),
|
'~': getBaseTopic(device.uniqueId, device.type),
|
||||||
command_topic: getCommandTopic(device),
|
state_topic: `~/${TOPIC_TYPES.STATE}`,
|
||||||
availability_topic: getAvailabilityTopic(device),
|
command_topic: `~/${TOPIC_TYPES.COMMAND}`,
|
||||||
|
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
||||||
optimistic: false,
|
optimistic: false,
|
||||||
brightness: `${device.dimmable}`,
|
qos: 1,
|
||||||
|
retain: true,
|
||||||
device: {
|
device: {
|
||||||
identifiers: `${device.serialNumber}_${device.id}`,
|
identifiers: `${device.deviceId}`,
|
||||||
manufacturer: 'Plejd',
|
manufacturer: 'Plejd',
|
||||||
model: device.typeName,
|
model: device.typeName,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
sw_version: device.version,
|
sw_version: device.version,
|
||||||
},
|
},
|
||||||
|
...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getSwitchPayload = (device) => ({
|
const getSceneDiscoveryPayload = (
|
||||||
name: device.name,
|
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
|
||||||
state_topic: getStateTopic(device),
|
) => ({
|
||||||
command_topic: getCommandTopic(device),
|
name: sceneDevice.name,
|
||||||
optimistic: false,
|
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: {
|
device: {
|
||||||
identifiers: `${device.serialNumber}_${device.id}`,
|
identifiers: `${sceneDevice.uniqueId}`,
|
||||||
manufacturer: 'Plejd',
|
manufacturer: 'Plejd',
|
||||||
model: device.typeName,
|
model: sceneDevice.typeName,
|
||||||
name: device.name,
|
name: sceneDevice.name,
|
||||||
sw_version: device.version,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// #endregion
|
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
|
||||||
|
const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' };
|
||||||
|
|
||||||
class MqttClient extends EventEmitter {
|
class MqttClient extends EventEmitter {
|
||||||
|
/** @type {import('DeviceRegistry')} */
|
||||||
deviceRegistry;
|
deviceRegistry;
|
||||||
|
|
||||||
static EVENTS = {
|
static EVENTS = {
|
||||||
|
|
@ -75,6 +115,9 @@ class MqttClient extends EventEmitter {
|
||||||
stateChanged: 'stateChanged',
|
stateChanged: 'stateChanged',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("DeviceRegistry")} deviceRegistry
|
||||||
|
*/
|
||||||
constructor(deviceRegistry) {
|
constructor(deviceRegistry) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
@ -86,8 +129,11 @@ 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, {
|
||||||
username: this.config.mqttUsername,
|
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
|
||||||
|
queueQoSZero: true,
|
||||||
|
username: this.config.mqttUsername,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('error', (err) => {
|
this.client.on('error', (err) => {
|
||||||
|
|
@ -97,13 +143,22 @@ 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(startTopics, (err) => {
|
this.client.subscribe(
|
||||||
if (err) {
|
startTopics,
|
||||||
logger.error('Unable to subscribe to status topics', err);
|
// 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) => {
|
this.client.subscribe(getSubscribePath(), (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
@ -123,27 +178,24 @@ class MqttClient extends EventEmitter {
|
||||||
logger.info('Home Assistant has started. lets do discovery.');
|
logger.info('Home Assistant has started. lets do discovery.');
|
||||||
this.emit(MqttClient.EVENTS.connected);
|
this.emit(MqttClient.EVENTS.connected);
|
||||||
} else {
|
} else {
|
||||||
|
logger.verbose(`Received mqtt message on ${topic}`);
|
||||||
const decodedTopic = decodeTopic(topic);
|
const decodedTopic = decodeTopic(topic);
|
||||||
if (decodedTopic) {
|
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 messageString = message.toString();
|
||||||
const isJsonMessage = messageString.startsWith('{');
|
const isJsonMessage = messageString.startsWith('{');
|
||||||
const command = isJsonMessage ? JSON.parse(messageString) : messageString;
|
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 : '';
|
const deviceName = device ? device.name : '';
|
||||||
|
|
||||||
switch (decodedTopic.command) {
|
switch (decodedTopic.command) {
|
||||||
|
|
@ -195,35 +247,121 @@ class MqttClient extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect(callback) {
|
disconnect(callback) {
|
||||||
this.deviceRegistry.allDevices.forEach((device) => {
|
logger.info('Mqtt disconnect requested. Setting all devices as unavailable in HA...');
|
||||||
this.client.publish(getAvailabilityTopic(device), 'offline');
|
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);
|
this.client.end(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDiscoveryToHomeAssistant() {
|
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) => {
|
const configPayload = getOutputDeviceDiscoveryPayload(outputDevice);
|
||||||
logger.debug(`Sending discovery for ${device.name}`);
|
|
||||||
|
|
||||||
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
|
||||||
logger.info(
|
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(() => {
|
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);
|
}, 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) {
|
if (!device) {
|
||||||
logger.warn(`Unknown device id ${deviceId} - not handled by us.`);
|
logger.warn(`Unknown output id ${uniqueOutputId} - not handled by us.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,29 +373,40 @@ class MqttClient extends EventEmitter {
|
||||||
let payload = null;
|
let payload = null;
|
||||||
|
|
||||||
if (device.type === 'switch') {
|
if (device.type === 'switch') {
|
||||||
payload = data.state === 1 ? 'ON' : 'OFF';
|
payload = getMqttStateString(data.state);
|
||||||
} else {
|
} else {
|
||||||
if (device.dimmable) {
|
if (device.dimmable) {
|
||||||
payload = {
|
payload = {
|
||||||
state: data.state === 1 ? 'ON' : 'OFF',
|
state: getMqttStateString(data.state),
|
||||||
brightness: data.brightness,
|
brightness: data.brightness,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
payload = {
|
payload = {
|
||||||
state: data.state === 1 ? 'ON' : 'OFF',
|
state: getMqttStateString(data.state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = JSON.stringify(payload);
|
payload = JSON.stringify(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.publish(getStateTopic(device), payload);
|
const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
||||||
this.client.publish(getAvailabilityTopic(device), 'online');
|
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) {
|
sceneTriggered(sceneId) {
|
||||||
logger.verbose(`Scene triggered: ${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
|
// subscribe to changes from HA
|
||||||
this.mqttClient.on(MqttClient.EVENTS.stateChanged, (device, command) => {
|
this.mqttClient.on(
|
||||||
try {
|
MqttClient.EVENTS.stateChanged,
|
||||||
const deviceId = device.id;
|
/** @param device {import('./types/DeviceRegistry').OutputDevice} */
|
||||||
|
(device, command) => {
|
||||||
|
try {
|
||||||
|
const { uniqueId } = device;
|
||||||
|
|
||||||
if (device.typeName === 'Scene') {
|
if (device.typeName === 'Scene') {
|
||||||
// we're triggering a scene, lets do that and jump out.
|
// we're triggering a scene, lets do that and jump out.
|
||||||
// since scenes aren't "real" devices.
|
// since scenes aren't "real" devices.
|
||||||
this.sceneManager.executeScene(device.id);
|
this.sceneManager.executeScene(uniqueId);
|
||||||
return;
|
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();
|
this.mqttClient.init();
|
||||||
|
|
||||||
// subscribe to changes from Plejd
|
// subscribe to changes from Plejd
|
||||||
this.plejdDeviceCommunication.on(
|
this.plejdDeviceCommunication.on(
|
||||||
PlejdDeviceCommunication.EVENTS.stateChanged,
|
PlejdDeviceCommunication.EVENTS.stateChanged,
|
||||||
(deviceId, command) => {
|
(uniqueOutputId, command) => {
|
||||||
try {
|
try {
|
||||||
this.mqttClient.updateState(deviceId, command);
|
this.mqttClient.updateOutputState(uniqueOutputId, command);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error in PlejdService.stateChanged callback', 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_LIST_URL = 'functions/getSiteList';
|
||||||
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
||||||
|
|
||||||
|
const TRAITS = {
|
||||||
|
NO_LOAD: 0,
|
||||||
|
NON_DIMMABLE: 9,
|
||||||
|
DIMMABLE: 11,
|
||||||
|
};
|
||||||
|
|
||||||
const logger = Logger.getLogger('plejd-api');
|
const logger = Logger.getLogger('plejd-api');
|
||||||
|
|
||||||
class PlejdApi {
|
class PlejdApi {
|
||||||
|
/** @private @type {import('types/Configuration').Options} */
|
||||||
config;
|
config;
|
||||||
|
|
||||||
|
/** @private @type {import('DeviceRegistry')} */
|
||||||
deviceRegistry;
|
deviceRegistry;
|
||||||
|
|
||||||
|
/** @private @type {string} */
|
||||||
sessionToken;
|
sessionToken;
|
||||||
|
|
||||||
|
/** @private @type {string} */
|
||||||
siteId;
|
siteId;
|
||||||
|
|
||||||
|
/** @private @type {import('types/ApiSite').ApiSite} */
|
||||||
siteDetails;
|
siteDetails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("./DeviceRegistry")} deviceRegistry
|
||||||
|
*/
|
||||||
constructor(deviceRegistry) {
|
constructor(deviceRegistry) {
|
||||||
this.config = Configuration.getOptions();
|
this.config = Configuration.getOptions();
|
||||||
this.deviceRegistry = deviceRegistry;
|
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();
|
this.getDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<import('types/ApiSite').CachedSite>} */
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
async getCachedCopy() {
|
async getCachedCopy() {
|
||||||
logger.info('Getting cached api response from disk');
|
logger.info('Getting cached api response from disk');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawData = await fs.promises.readFile('/data/cachedApiResponse.json');
|
const rawData = await fs.promises.readFile('/data/cachedApiResponse.json');
|
||||||
const cachedCopy = JSON.parse(rawData);
|
const cachedCopy = JSON.parse(rawData.toString());
|
||||||
|
|
||||||
return cachedCopy;
|
return cachedCopy;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -82,12 +100,14 @@ class PlejdApi {
|
||||||
async saveCachedCopy() {
|
async saveCachedCopy() {
|
||||||
logger.info('Saving cached copy');
|
logger.info('Saving cached copy');
|
||||||
try {
|
try {
|
||||||
const rawData = JSON.stringify({
|
/** @type {import('types/ApiSite').CachedSite} */
|
||||||
|
const cachedSite = {
|
||||||
siteId: this.siteId,
|
siteId: this.siteId,
|
||||||
siteDetails: this.siteDetails,
|
siteDetails: this.siteDetails,
|
||||||
sessionToken: this.sessionToken,
|
sessionToken: this.sessionToken,
|
||||||
dtCache: new Date().toISOString(),
|
dtCache: new Date().toISOString(),
|
||||||
});
|
};
|
||||||
|
const rawData = JSON.stringify(cachedSite);
|
||||||
await fs.promises.writeFile('/data/cachedApiResponse.json', rawData);
|
await fs.promises.writeFile('/data/cachedApiResponse.json', rawData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to save cache of api response', err);
|
logger.error('Failed to save cache of api response', err);
|
||||||
|
|
@ -194,6 +214,14 @@ class PlejdApi {
|
||||||
getDevices() {
|
getDevices() {
|
||||||
logger.info('Getting devices from site details response...');
|
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._getPlejdDevices();
|
||||||
this._getRoomDevices();
|
this._getRoomDevices();
|
||||||
this._getSceneDevices();
|
this._getSceneDevices();
|
||||||
|
|
@ -216,8 +244,11 @@ class PlejdApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_getDeviceType(hardwareId) {
|
_getDeviceType(plejdDevice) {
|
||||||
switch (parseInt(hardwareId, 10)) {
|
// 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 1:
|
||||||
case 11:
|
case 11:
|
||||||
return { name: 'DIM-01', type: 'light', dimmable: true };
|
return { name: 'DIM-01', type: 'light', dimmable: true };
|
||||||
|
|
@ -259,69 +290,115 @@ class PlejdApi {
|
||||||
case 20:
|
case 20:
|
||||||
return { name: 'SPR-01', type: 'switch', dimmable: false };
|
return { name: 'SPR-01', type: 'switch', dimmable: false };
|
||||||
default:
|
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() {
|
_getPlejdDevices() {
|
||||||
this.deviceRegistry.clearPlejdDevices();
|
this.deviceRegistry.clearPlejdDevices();
|
||||||
|
|
||||||
this.siteDetails.devices.forEach((device) => {
|
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,
|
(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) {
|
if (outputAddress) {
|
||||||
const outputs = this.siteDetails.outputAddress[deviceId];
|
const bleOutputAddress = outputAddress[deviceOutput];
|
||||||
deviceNum = outputs[settings.output];
|
|
||||||
|
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
|
// What should we do with inputs?!
|
||||||
const plejdDevice = this.siteDetails.plejdDevices.find((x) => x.deviceId === deviceId);
|
// if (outputDevice.typeName === 'WPH-01') {
|
||||||
const deviceType = this._getDeviceType(plejdDevice.hardwareId);
|
// // WPH-01 is special, it has two buttons which needs to be
|
||||||
const { name, type } = deviceType;
|
// // registered separately.
|
||||||
let { dimmable } = deviceType;
|
// const inputs = this.siteDetails.inputAddress[deviceId];
|
||||||
|
// const first = inputs[0];
|
||||||
|
// const second = inputs[1];
|
||||||
|
|
||||||
if (settings) {
|
// this.deviceRegistry.addPlejdDevice({
|
||||||
dimmable = settings.dimCurve !== 'NonDimmable';
|
// ...outputDevice,
|
||||||
}
|
// id: first,
|
||||||
|
// name: `${device.title} left`,
|
||||||
|
// });
|
||||||
|
|
||||||
const newDevice = {
|
// this.deviceRegistry.addPlejdDevice({
|
||||||
id: deviceNum,
|
// ...outputDevice,
|
||||||
name: device.title,
|
// id: second,
|
||||||
type,
|
// name: `${device.title} right`,
|
||||||
typeName: name,
|
// });
|
||||||
dimmable,
|
// } else {
|
||||||
roomId: device.roomId,
|
// this.deviceRegistry.addPlejdDevice(outputDevice);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,39 +409,57 @@ class PlejdApi {
|
||||||
const { roomId } = room;
|
const { roomId } = room;
|
||||||
const roomAddress = this.siteDetails.roomAddress[roomId];
|
const roomAddress = this.siteDetails.roomAddress[roomId];
|
||||||
|
|
||||||
const deviceIdsByRoom = this.deviceRegistry.getDeviceIdsByRoom(roomId);
|
const deviceIdsByRoom = this.deviceRegistry.getOutputDeviceIdsByRoomId(roomId);
|
||||||
|
|
||||||
const dimmable = deviceIdsByRoom
|
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 = {
|
const newDevice = {
|
||||||
id: roomAddress,
|
bleOutputAddress: roomAddress,
|
||||||
|
deviceId: null,
|
||||||
|
dimmable,
|
||||||
|
hiddenFromRoomList: false,
|
||||||
|
hiddenFromIntegrations: false,
|
||||||
name: room.title,
|
name: room.title,
|
||||||
|
output: undefined,
|
||||||
|
roomId,
|
||||||
|
state: undefined,
|
||||||
type: 'light',
|
type: 'light',
|
||||||
typeName: 'Room',
|
typeName: 'Room',
|
||||||
dimmable,
|
uniqueId: roomId,
|
||||||
|
version: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.deviceRegistry.addRoomDevice(newDevice);
|
this.deviceRegistry.addOutputDevice(newDevice);
|
||||||
});
|
});
|
||||||
logger.debug('includeRoomsAsLights done.');
|
logger.debug('includeRoomsAsLights done.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getSceneDevices() {
|
_getSceneDevices() {
|
||||||
|
this.deviceRegistry.clearSceneDevices();
|
||||||
// add scenes as switches
|
// add scenes as switches
|
||||||
const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false);
|
const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||||
|
|
||||||
scenes.forEach((scene) => {
|
scenes.forEach((scene) => {
|
||||||
const sceneNum = this.siteDetails.sceneIndex[scene.sceneId];
|
const sceneNum = this.siteDetails.sceneIndex[scene.sceneId];
|
||||||
|
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||||
const newScene = {
|
const newScene = {
|
||||||
id: sceneNum,
|
bleOutputAddress: sceneNum,
|
||||||
name: scene.title,
|
deviceId: undefined,
|
||||||
type: 'switch',
|
|
||||||
typeName: 'Scene',
|
|
||||||
dimmable: false,
|
dimmable: false,
|
||||||
version: '1.0',
|
hiddenFromSceneList: scene.hiddenFromSceneList,
|
||||||
serialNumber: scene.objectId,
|
name: scene.title,
|
||||||
|
output: undefined,
|
||||||
|
roomId: undefined,
|
||||||
|
state: false,
|
||||||
|
type: 'scene',
|
||||||
|
typeName: 'Scene',
|
||||||
|
version: undefined,
|
||||||
|
uniqueId: scene.sceneId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.deviceRegistry.addScene(newScene);
|
this.deviceRegistry.addScene(newScene);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const dbus = require('dbus-next');
|
const dbus = require('dbus-next');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const xor = require('buffer-xor');
|
const xor = require('buffer-xor');
|
||||||
const EventEmitter = require('events');
|
const { EventEmitter } = require('events');
|
||||||
|
|
||||||
const Configuration = require('./Configuration');
|
const Configuration = require('./Configuration');
|
||||||
const constants = require('./constants');
|
const constants = require('./constants');
|
||||||
|
|
@ -49,6 +49,8 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
connectedDevice = null;
|
connectedDevice = null;
|
||||||
consecutiveWriteFails;
|
consecutiveWriteFails;
|
||||||
consecutiveReconnectAttempts = 0;
|
consecutiveReconnectAttempts = 0;
|
||||||
|
/** @type {import('./DeviceRegistry')} */
|
||||||
|
deviceRegistry;
|
||||||
discoveryTimeout = null;
|
discoveryTimeout = null;
|
||||||
plejdService = null;
|
plejdService = null;
|
||||||
pingRef = null;
|
pingRef = null;
|
||||||
|
|
@ -152,21 +154,26 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
logger.info('BLE init done, waiting for devices.');
|
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 payload;
|
||||||
let brightnessVal;
|
let brightnessVal;
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case COMMANDS.TURN_ON:
|
case COMMANDS.TURN_ON:
|
||||||
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '01');
|
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '01');
|
||||||
break;
|
break;
|
||||||
case COMMANDS.TURN_OFF:
|
case COMMANDS.TURN_OFF:
|
||||||
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '00');
|
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '00');
|
||||||
break;
|
break;
|
||||||
case COMMANDS.DIM:
|
case COMMANDS.DIM:
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
brightnessVal = (data << 8) | data;
|
brightnessVal = (data << 8) | data;
|
||||||
payload = this._createHexPayload(
|
payload = this._createHexPayload(
|
||||||
deviceId,
|
bleOutputAddress,
|
||||||
BLE_CMD_DIM2_CHANGE,
|
BLE_CMD_DIM2_CHANGE,
|
||||||
`01${brightnessVal.toString(16).padStart(4, '0')}`,
|
`01${brightnessVal.toString(16).padStart(4, '0')}`,
|
||||||
);
|
);
|
||||||
|
|
@ -194,9 +201,9 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
plejd.instance = device;
|
plejd.instance = device;
|
||||||
|
|
||||||
const segments = plejd.path.split('/');
|
const segments = plejd.path.split('/');
|
||||||
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
|
let plejdSerialNumber = segments[segments.length - 1].replace('dev_', '');
|
||||||
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
|
plejdSerialNumber = plejdSerialNumber.replace(/_/g, '');
|
||||||
plejd.device = this.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath);
|
plejd.device = this.deviceRegistry.getPhysicalDevice(plejdSerialNumber);
|
||||||
|
|
||||||
if (plejd.device) {
|
if (plejd.device) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
@ -204,7 +211,7 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
);
|
);
|
||||||
this.bleDevices.push(plejd);
|
this.bleDevices.push(plejd);
|
||||||
} else {
|
} 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) {
|
} catch (err) {
|
||||||
logger.error(`Failed inspecting ${path}. `, err);
|
logger.error(`Failed inspecting ${path}. `, err);
|
||||||
|
|
@ -796,7 +803,7 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceId = decoded.readUInt8(0);
|
const bleOutputAddress = decoded.readUInt8(0);
|
||||||
// Bytes 2-3 is Command/Request
|
// Bytes 2-3 is Command/Request
|
||||||
const cmd = decoded.readUInt16BE(3);
|
const cmd = decoded.readUInt16BE(3);
|
||||||
|
|
||||||
|
|
@ -810,38 +817,52 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
logger.silly(`Dim: ${dim.toString(16)}, full precision: ${dimFull.toString(16)}`);
|
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')) {
|
if (Logger.shouldLog('verbose')) {
|
||||||
// decoded.toString() could potentially be expensive
|
// decoded.toString() could potentially be expensive
|
||||||
logger.verbose(`Raw event received: ${decoded.toString('hex')}`);
|
logger.verbose(`Raw event received: ${decoded.toString('hex')}`);
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
`Decoded: Device ${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 command;
|
||||||
let data = {};
|
let data = {};
|
||||||
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
|
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;
|
command = COMMANDS.DIM;
|
||||||
data = { state, 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) {
|
} 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;
|
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) {
|
} else if (cmd === BLE_CMD_SCENE_TRIG) {
|
||||||
const sceneId = state;
|
const sceneBleAddress = state;
|
||||||
const sceneName = this.deviceRegistry.getSceneName(sceneId);
|
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(
|
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;
|
command = COMMANDS.TRIGGER_SCENE;
|
||||||
data = { sceneId };
|
data = { sceneId: scene.uniqueId };
|
||||||
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data);
|
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
|
||||||
} else if (cmd === BLE_CMD_TIME_UPDATE) {
|
} else if (cmd === BLE_CMD_TIME_UPDATE) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
// Guess Plejd timezone based on HA time zone
|
// Guess Plejd timezone based on HA time zone
|
||||||
|
|
@ -851,7 +872,7 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000;
|
const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000;
|
||||||
const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000);
|
const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000);
|
||||||
if (
|
if (
|
||||||
deviceId !== BLE_BROADCAST_DEVICE_ID
|
bleOutputAddress !== BLE_BROADCAST_DEVICE_ID
|
||||||
|| Logger.shouldLog('verbose')
|
|| Logger.shouldLog('verbose')
|
||||||
|| Math.abs(diffSeconds) > 60
|
|| Math.abs(diffSeconds) > 60
|
||||||
) {
|
) {
|
||||||
|
|
@ -863,7 +884,7 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`,
|
`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
|
// Requested time sync by us
|
||||||
const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess;
|
const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess;
|
||||||
logger.info(`Setting time to ${now.toString()}`);
|
logger.info(`Setting time to ${now.toString()}`);
|
||||||
|
|
@ -874,14 +895,14 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
(pl) => pl.writeInt32LE(Math.trunc(newLocalTimestamp), 5),
|
(pl) => pl.writeInt32LE(Math.trunc(newLocalTimestamp), 5),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
this.write(payload);
|
this._write(payload);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Failed writing new time to Plejd. Will try again in one hour or at restart.',
|
'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');
|
logger.info('Got time response. Plejd clock time in sync with Home Assistant time');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -889,19 +910,19 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
`Command ${cmd.toString(16)} unknown. ${decoded.toString(
|
`Command ${cmd.toString(16)} unknown. ${decoded.toString(
|
||||||
'hex',
|
'hex',
|
||||||
)}. Device ${deviceName} (${deviceId})`,
|
)}. Device ${deviceName} (${bleOutputAddress}: ${outputUniqueId})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_createHexPayload(
|
_createHexPayload(
|
||||||
deviceId,
|
bleOutputAddress,
|
||||||
command,
|
command,
|
||||||
hexDataString,
|
hexDataString,
|
||||||
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
|
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
|
||||||
) {
|
) {
|
||||||
return this._createPayload(
|
return this._createPayload(
|
||||||
deviceId,
|
bleOutputAddress,
|
||||||
command,
|
command,
|
||||||
5 + Math.ceil(hexDataString.length / 2),
|
5 + Math.ceil(hexDataString.length / 2),
|
||||||
(payload) => payload.write(hexDataString, 5, 'hex'),
|
(payload) => payload.write(hexDataString, 5, 'hex'),
|
||||||
|
|
@ -911,14 +932,14 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_createPayload(
|
_createPayload(
|
||||||
deviceId,
|
bleOutputAddress,
|
||||||
command,
|
command,
|
||||||
bufferLength,
|
bufferLength,
|
||||||
payloadBufferAddDataFunc,
|
payloadBufferAddDataFunc,
|
||||||
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
|
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
|
||||||
) {
|
) {
|
||||||
const payload = Buffer.alloc(bufferLength);
|
const payload = Buffer.alloc(bufferLength);
|
||||||
payload.writeUInt8(deviceId);
|
payload.writeUInt8(bleOutputAddress);
|
||||||
payload.writeUInt16BE(requestResponseCommand, 1);
|
payload.writeUInt16BE(requestResponseCommand, 1);
|
||||||
payload.writeUInt16BE(command, 3);
|
payload.writeUInt16BE(command, 3);
|
||||||
payloadBufferAddDataFunc(payload);
|
payloadBufferAddDataFunc(payload);
|
||||||
|
|
@ -945,12 +966,12 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
|
|
||||||
let ct = cipher.update(buf).toString('hex');
|
let ct = cipher.update(buf).toString('hex');
|
||||||
ct += cipher.final().toString('hex');
|
ct += cipher.final().toString('hex');
|
||||||
ct = Buffer.from(ct, 'hex');
|
const ctBuf = Buffer.from(ct, 'hex');
|
||||||
|
|
||||||
let output = '';
|
let output = '';
|
||||||
for (let i = 0, { length } = data; i < length; i++) {
|
for (let i = 0, { length } = data; i < length; i++) {
|
||||||
// eslint-disable-next-line no-bitwise
|
// 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');
|
return Buffer.from(output, 'ascii');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const EventEmitter = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const Configuration = require('./Configuration');
|
const Configuration = require('./Configuration');
|
||||||
const constants = require('./constants');
|
const constants = require('./constants');
|
||||||
const Logger = require('./Logger');
|
const Logger = require('./Logger');
|
||||||
|
|
@ -12,10 +12,13 @@ const MAX_RETRY_COUNT = 10; // Could be made a setting
|
||||||
|
|
||||||
class PlejdDeviceCommunication extends EventEmitter {
|
class PlejdDeviceCommunication extends EventEmitter {
|
||||||
bleConnected;
|
bleConnected;
|
||||||
bleDeviceTransitionTimers = {};
|
bleOutputTransitionTimers = {};
|
||||||
plejdBleHandler;
|
plejdBleHandler;
|
||||||
config;
|
config;
|
||||||
|
/** @type {import('./DeviceRegistry')} */
|
||||||
deviceRegistry;
|
deviceRegistry;
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
/** @type {{uniqueOutputId: string, command: string, data: any, shouldRetry: boolean, retryCount?: number}[]} */
|
||||||
writeQueue = [];
|
writeQueue = [];
|
||||||
writeQueueRef = null;
|
writeQueueRef = null;
|
||||||
|
|
||||||
|
|
@ -34,7 +37,7 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
Object.values(this.bleDeviceTransitionTimers).forEach((t) => clearTimeout(t));
|
Object.values(this.bleOutputTransitionTimers).forEach((t) => clearTimeout(t));
|
||||||
this.plejdBleHandler.cleanup();
|
this.plejdBleHandler.cleanup();
|
||||||
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived);
|
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived);
|
||||||
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected);
|
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected);
|
||||||
|
|
@ -46,7 +49,10 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
this.bleConnected = false;
|
this.bleConnected = false;
|
||||||
// eslint-disable-next-line max-len
|
// 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, () => {
|
this.plejdBleHandler.on(PlejBLEHandler.EVENTS.connected, () => {
|
||||||
logger.info('Bluetooth connected. Plejd BLE up and running!');
|
logger.info('Bluetooth connected. Plejd BLE up and running!');
|
||||||
|
|
@ -70,42 +76,42 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
turnOn(deviceId, command) {
|
turnOn(uniqueOutputId, command) {
|
||||||
const deviceName = this.deviceRegistry.getDeviceName(deviceId);
|
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
|
||||||
logger.info(
|
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}` : ''
|
command.transition ? `, transition: ${command.transition}` : ''
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
this._transitionTo(deviceId, command.brightness, command.transition, deviceName);
|
this._transitionTo(uniqueOutputId, 0, command.transition, deviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
turnOff(deviceId, command) {
|
_bleCommandReceived(uniqueOutputId, command, data) {
|
||||||
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) {
|
|
||||||
try {
|
try {
|
||||||
if (command === COMMANDS.DIM) {
|
if (command === COMMANDS.DIM) {
|
||||||
this.deviceRegistry.setState(deviceId, data.state, data.dim);
|
this.deviceRegistry.setOutputState(uniqueOutputId, data.state, data.dim);
|
||||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
|
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
|
||||||
state: data.state,
|
state: !!data.state,
|
||||||
brightness: data.dim,
|
brightness: data.dim,
|
||||||
});
|
});
|
||||||
} else if (command === COMMANDS.TURN_ON) {
|
} else if (command === COMMANDS.TURN_ON) {
|
||||||
this.deviceRegistry.setState(deviceId, 1);
|
this.deviceRegistry.setOutputState(uniqueOutputId, true);
|
||||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
|
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
|
||||||
state: 1,
|
state: 1,
|
||||||
});
|
});
|
||||||
} else if (command === COMMANDS.TURN_OFF) {
|
} else if (command === COMMANDS.TURN_OFF) {
|
||||||
this.deviceRegistry.setState(deviceId, 0);
|
this.deviceRegistry.setOutputState(uniqueOutputId, false);
|
||||||
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
|
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
|
||||||
state: 0,
|
state: 0,
|
||||||
});
|
});
|
||||||
} else if (command === COMMANDS.TRIGGER_SCENE) {
|
} else if (command === COMMANDS.TRIGGER_SCENE) {
|
||||||
|
|
@ -118,18 +124,18 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearDeviceTransitionTimer(deviceId) {
|
_clearDeviceTransitionTimer(uniqueOutputId) {
|
||||||
if (this.bleDeviceTransitionTimers[deviceId]) {
|
if (this.bleOutputTransitionTimers[uniqueOutputId]) {
|
||||||
clearInterval(this.bleDeviceTransitionTimers[deviceId]);
|
clearInterval(this.bleOutputTransitionTimers[uniqueOutputId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_transitionTo(deviceId, targetBrightness, transition, deviceName) {
|
_transitionTo(uniqueOutputId, targetBrightness, transition, deviceName) {
|
||||||
const device = this.deviceRegistry.getDevice(deviceId);
|
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
|
||||||
const initialBrightness = device ? device.state && device.dim : null;
|
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 (
|
if (
|
||||||
transition > 1
|
transition > 1
|
||||||
|
|
@ -164,7 +170,7 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
|
|
||||||
let nSteps = 0;
|
let nSteps = 0;
|
||||||
|
|
||||||
this.bleDeviceTransitionTimers[deviceId] = setInterval(() => {
|
this.bleOutputTransitionTimers[uniqueOutputId] = setInterval(() => {
|
||||||
const tElapsedMs = new Date().getTime() - dtStart.getTime();
|
const tElapsedMs = new Date().getTime() - dtStart.getTime();
|
||||||
let tElapsed = tElapsedMs / 1000;
|
let tElapsed = tElapsedMs / 1000;
|
||||||
|
|
||||||
|
|
@ -178,20 +184,20 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
|
|
||||||
if (tElapsed === transition) {
|
if (tElapsed === transition) {
|
||||||
nSteps++;
|
nSteps++;
|
||||||
this._clearDeviceTransitionTimer(deviceId);
|
this._clearDeviceTransitionTimer(uniqueOutputId);
|
||||||
newBrightness = targetBrightness;
|
newBrightness = targetBrightness;
|
||||||
logger.debug(
|
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)
|
tElapsedMs / (nSteps || 1)
|
||||||
} ms.`,
|
} ms.`,
|
||||||
);
|
);
|
||||||
this._setBrightness(deviceId, newBrightness, true, deviceName);
|
this._setBrightness(uniqueOutputId, newBrightness, true, deviceName);
|
||||||
} else {
|
} else {
|
||||||
nSteps++;
|
nSteps++;
|
||||||
logger.verbose(
|
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);
|
}, transitionInterval);
|
||||||
} else {
|
} 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}`,
|
`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) {
|
if (!brightness && brightness !== 0) {
|
||||||
logger.debug(
|
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) {
|
} else if (brightness <= 0) {
|
||||||
logger.debug(`Queueing turn off ${deviceId}`);
|
logger.debug(`Queueing turn off ${unqiueOutputId}`);
|
||||||
this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_OFF, null, shouldRetry);
|
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_OFF, null, shouldRetry);
|
||||||
} else {
|
} else {
|
||||||
if (brightness > 255) {
|
if (brightness > 255) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
brightness = 255;
|
brightness = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`);
|
logger.debug(`Queueing ${unqiueOutputId} set brightness to ${brightness}`);
|
||||||
// eslint-disable-next-line no-bitwise
|
// 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({
|
this.writeQueue.unshift({
|
||||||
deviceId,
|
uniqueOutputId,
|
||||||
command,
|
command,
|
||||||
data,
|
data,
|
||||||
shouldRetry,
|
shouldRetry,
|
||||||
|
|
@ -249,28 +255,29 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const queueItem = this.writeQueue.pop();
|
const queueItem = this.writeQueue.pop();
|
||||||
const deviceName = this.deviceRegistry.getDeviceName(queueItem.deviceId);
|
const device = this.deviceRegistry.getOutputDevice(queueItem.uniqueOutputId);
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${
|
`Write queue: Processing ${device.name} (${queueItem.uniqueOutputId}). Command ${
|
||||||
queueItem.command
|
queueItem.command
|
||||||
}${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${
|
}${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${
|
||||||
this.writeQueue.length
|
this.writeQueue.length
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) {
|
if (this.writeQueue.some((item) => item.uniqueOutputId === queueItem.uniqueOutputId)) {
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
`Skipping ${deviceName} (${queueItem.deviceId}) `
|
`Skipping ${device.name} (${queueItem.uniqueOutputId}) `
|
||||||
+ `${queueItem.command} due to more recent command in queue.`,
|
+ `${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
|
// still process all messages in order
|
||||||
} else {
|
} else {
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
try {
|
try {
|
||||||
await this.plejdBleHandler.sendCommand(
|
await this.plejdBleHandler.sendCommand(
|
||||||
queueItem.command,
|
queueItem.command,
|
||||||
queueItem.deviceId,
|
device.bleOutputAddress,
|
||||||
queueItem.data,
|
queueItem.data,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -281,7 +288,7 @@ class PlejdDeviceCommunication extends EventEmitter {
|
||||||
this.writeQueue.push(queueItem); // Add back to top of queue to be processed next;
|
this.writeQueue.push(queueItem); // Add back to top of queue to be processed next;
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
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;
|
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 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.
|
When contributing, please do so by forking the repo and then using pull requests towards the dev branch.
|
||||||
|
|
||||||
### Logs
|
### Logs
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
const SceneStep = require('./SceneStep');
|
const SceneStep = require('./SceneStep');
|
||||||
|
|
||||||
class Scene {
|
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.id = idx;
|
||||||
this.title = scene.title;
|
this.title = scene.title;
|
||||||
this.sceneId = scene.sceneId;
|
this.sceneId = scene.sceneId;
|
||||||
|
|
||||||
const sceneSteps = steps.filter((x) => x.sceneId === scene.sceneId);
|
this.steps = deviceRegistry
|
||||||
this.steps = [];
|
.getApiSite()
|
||||||
|
.sceneSteps.filter((step) => step.sceneId === scene.sceneId)
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
.map((step) => new SceneStep(step));
|
||||||
for (const step of sceneSteps) {
|
|
||||||
this.steps.push(new SceneStep(step));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,45 +3,52 @@ const Scene = require('./Scene');
|
||||||
|
|
||||||
const logger = Logger.getLogger('scene-manager');
|
const logger = Logger.getLogger('scene-manager');
|
||||||
class SceneManager {
|
class SceneManager {
|
||||||
|
/** @private @type {import('./DeviceRegistry')} */
|
||||||
deviceRegistry;
|
deviceRegistry;
|
||||||
plejdBle;
|
/** @private @type {import('./PlejdDeviceCommunication')} */
|
||||||
|
plejdDeviceCommunication;
|
||||||
|
/** @private @type {Object.<string,Scene>} */
|
||||||
scenes;
|
scenes;
|
||||||
|
|
||||||
constructor(deviceRegistry, plejdBle) {
|
constructor(deviceRegistry, plejdDeviceCommunication) {
|
||||||
this.deviceRegistry = deviceRegistry;
|
this.deviceRegistry = deviceRegistry;
|
||||||
this.plejdBle = plejdBle;
|
this.plejdDeviceCommunication = plejdDeviceCommunication;
|
||||||
this.scenes = {};
|
this.scenes = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const scenes = this.deviceRegistry.apiSite.scenes.filter(
|
const scenes = this.deviceRegistry
|
||||||
(x) => x.hiddenFromSceneList === false,
|
.getApiSite()
|
||||||
);
|
.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||||
|
|
||||||
this.scenes = {};
|
this.scenes = {};
|
||||||
scenes.forEach((scene) => {
|
scenes.forEach((scene) => {
|
||||||
const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId];
|
const sceneBleAddress = this.deviceRegistry.getApiSite().sceneIndex[scene.sceneId];
|
||||||
this.scenes[idx] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps);
|
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) {
|
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)}`);
|
logger.verbose(`Scenes: ${JSON.stringify(this.scenes, null, 2)}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.steps.forEach((step) => {
|
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) {
|
||||||
if (device.dimmable && step.state) {
|
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) {
|
} else if (!device.dimmable && step.state) {
|
||||||
this.plejdBle.turnOn(device.id, {});
|
this.plejdDeviceCommunication.turnOn(uniqueId, {});
|
||||||
} else if (!step.state) {
|
} else if (!step.state) {
|
||||||
this.plejdBle.turnOff(device.id, {});
|
this.plejdDeviceCommunication.turnOff(uniqueId, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
class SceneStep {
|
class SceneStep {
|
||||||
|
/**
|
||||||
|
* @param {import("./types/ApiSite").SceneStep} step
|
||||||
|
*/
|
||||||
constructor(step) {
|
constructor(step) {
|
||||||
this.sceneId = step.sceneId;
|
this.sceneId = step.sceneId;
|
||||||
this.deviceId = step.deviceId;
|
this.deviceId = step.deviceId;
|
||||||
|
this.output = step.output;
|
||||||
this.state = step.state === 'On' ? 1 : 0;
|
this.state = step.state === 'On' ? 1 : 0;
|
||||||
this.brightness = step.value;
|
this.brightness = step.value;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Plejd",
|
"name": "Plejd",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0-dev",
|
||||||
"slug": "plejd",
|
"slug": "plejd",
|
||||||
"description": "Adds support for the Swedish home automation devices from Plejd.",
|
"description": "Adds support for the Swedish home automation devices from Plejd.",
|
||||||
"url": "https://github.com/icanos/hassio-plejd/",
|
"url": "https://github.com/icanos/hassio-plejd/",
|
||||||
|
|
|
||||||
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",
|
"@abandonware/bluetooth-hci-socket": "~0.5.3-7",
|
||||||
"axios": "~0.21.1",
|
"axios": "~0.21.1",
|
||||||
"buffer-xor": "~2.0.2",
|
"buffer-xor": "~2.0.2",
|
||||||
"dbus-next": "~0.9.1",
|
"dbus-next": "~0.9.2",
|
||||||
"fs": "0.0.1-security",
|
"fs": "0.0.1-security",
|
||||||
"jspack": "~0.0.4",
|
"jspack": "~0.0.4",
|
||||||
"mqtt": "~3.0.0",
|
"mqtt": "~4.2.6",
|
||||||
"winston": "~3.3.3"
|
"winston": "~3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "~10.1.0",
|
"babel-eslint": "~10.1.0",
|
||||||
"eslint": "~7.18.0",
|
"eslint": "~7.23.0",
|
||||||
"eslint-config-airbnb": "~18.2.1",
|
"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-import": "~2.22.1",
|
||||||
"eslint-plugin-prettier": "~3.3.1",
|
"eslint-plugin-prettier": "~3.3.1",
|
||||||
"prettier": "~2.2.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