Merge pull request #1 from icanos/develop

Merge develop
This commit is contained in:
faanskit 2021-05-05 12:23:40 +02:00 committed by GitHub
commit a9f31d188d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1516 additions and 385 deletions

View file

@ -1,5 +1,20 @@
# Changelog hassio-plejd Home Assistant Plejd addon
## 0.8.0-dev
**BREAKING - READ BELOW FIRST**
Release 0.8 will break ALL EXISTING DEVICES. Unique mqtt id:s will change, meaning HA will create new devices. Scenes will be added as scenes not as switches.
Recommendations to minimize impact
- Optionally install MQTT explorer to bulk-delete discovered devices. If so - start MQTT explorer, connect, restart Plejd addon and then delete from MQTT explorer
- Shut down Plejd addon, disable autostart
- Reboot HA
- Go to Configuration => Integration => MQTT. Go to entities and after that devices and remove all Plejd devices (should be listed as unavailable)
- Upgrade addon to latest version and start
- All devices should now be back. With luck they will have the same HA id:s as before so most things should work. Room assignments, icons, etc will be gone though.
## [0.7.1](https://github.com/icanos/hassio-plejd/tree/0.7.1) (2021-03-25)
[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.7.0...0.7.1)

View file

@ -1,9 +1,12 @@
const fs = require('fs');
class Configuration {
/** @type {import('types/Configuration').Options} */
static _options = null;
/** @type {import('types/Configuration').AddonInfo} */
static _addonInfo = null;
/** @returns Options */
static getOptions() {
if (!Configuration._options) {
Configuration._hydrateCache();
@ -11,6 +14,7 @@ class Configuration {
return Configuration._options;
}
/** @returns AddonInfo */
static getAddonInfo() {
if (!Configuration._addonInfo) {
Configuration._hydrateCache();
@ -20,10 +24,10 @@ class Configuration {
static _hydrateCache() {
const rawData = fs.readFileSync('/data/options.json');
const config = JSON.parse(rawData);
const config = JSON.parse(rawData.toString());
const defaultRawData = fs.readFileSync('/plejd/config.json');
const defaultConfig = JSON.parse(defaultRawData);
const defaultConfig = JSON.parse(defaultRawData.toString());
Configuration._options = { ...defaultConfig.options, ...config };
Configuration._addonInfo = {

View file

@ -2,154 +2,195 @@ const Logger = require('./Logger');
const logger = Logger.getLogger('device-registry');
class DeviceRegistry {
apiSite;
/** @type {string} */
cryptoKey = null;
deviceIdsByRoom = {};
deviceIdsBySerial = {};
/** @private @type {Object.<string, import('types/ApiSite').Device>} */
devices = {};
/** @private @type {Object.<string, string[]>} */
outputDeviceUniqueIdsByRoomId = {};
/** @private @type {Object.<number, string>} */
outputUniqueIdByBleOutputAddress = {};
/** @private @type {Object.<number, string>} */
sceneUniqueIdByBleOutputAddress = {};
/** @private @type {import('./types/ApiSite').ApiSite} */
apiSite;
// Dictionaries of [id]: device per type
plejdDevices = {};
roomDevices = {};
/** @private @type {import('types/DeviceRegistry').OutputDevices} */
outputDevices = {};
/** @private @type {import('types/DeviceRegistry').OutputDevices} */
sceneDevices = {};
get allDevices() {
return [
...Object.values(this.plejdDevices),
...Object.values(this.roomDevices),
...Object.values(this.sceneDevices),
];
/** @param device {import('./types/ApiSite').Device} */
addPhysicalDevice(device) {
this.devices[device.deviceId] = device;
}
addPlejdDevice(device) {
const added = {
...this.plejdDevices[device.id],
...device,
};
/** @param outputDevice {import('types/DeviceRegistry').OutputDevice} */
addOutputDevice(outputDevice) {
if (outputDevice.hiddenFromIntegrations || outputDevice.hiddenFromRoomList) {
logger.verbose(`Device ${outputDevice.name} is hidden and will not be included.
Hidden from room list: ${outputDevice.hiddenFromRoomList}
Hidden from integrations: ${outputDevice.hiddenFromIntegrations}`);
return;
}
this.plejdDevices = {
...this.plejdDevices,
[added.id]: added,
this.outputDevices = {
...this.outputDevices,
[outputDevice.uniqueId]: outputDevice,
};
this.deviceIdsBySerial[added.serialNumber] = added.id;
logger.verbose(
`Added/updated device: ${JSON.stringify(added)}. ${
Object.keys(this.plejdDevices).length
} plejd devices in total.`,
`Added/updated output device: ${JSON.stringify(outputDevice)}. ${
Object.keys(this.outputDevices).length
} output devices in total.`,
);
if (added.roomId) {
if (!this.deviceIdsByRoom[added.roomId]) {
this.deviceIdsByRoom[added.roomId] = [];
}
const room = this.deviceIdsByRoom[added.roomId];
if (!room.includes(added.id)) {
this.deviceIdsByRoom[added.roomId] = [...room, added.id];
}
this.outputUniqueIdByBleOutputAddress[outputDevice.bleOutputAddress] = outputDevice.uniqueId;
if (!this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId]) {
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId] = [];
}
if (
outputDevice.roomId !== outputDevice.uniqueId
&& !this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId)
) {
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].push(outputDevice.uniqueId);
logger.verbose(
`Added device to room ${added.roomId}: ${JSON.stringify(
this.deviceIdsByRoom[added.roomId],
`Added device to room ${outputDevice.roomId}: ${JSON.stringify(
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId],
)}`,
);
}
return added;
}
addRoomDevice(device) {
const added = {
...this.roomDevices[device.id],
...device,
};
this.roomDevices = {
...this.roomDevices,
[added.id]: added,
};
logger.verbose(
`Added/updated room device: ${JSON.stringify(added)}. ${
Object.keys(this.roomDevices).length
} room devices total.`,
);
return added;
}
/** @param scene {import('types/DeviceRegistry').OutputDevice} */
addScene(scene) {
const added = {
...this.sceneDevices[scene.id],
...scene,
};
this.sceneDevices = {
...this.sceneDevices,
[added.id]: added,
[scene.uniqueId]: scene,
};
this.sceneUniqueIdByBleOutputAddress[scene.bleOutputAddress] = scene.uniqueId;
logger.verbose(
`Added/updated scene: ${JSON.stringify(added)}. ${
`Added/updated scene: ${JSON.stringify(scene)}. ${
Object.keys(this.sceneDevices).length
} scenes in total.`,
);
return added;
}
clearPlejdDevices() {
this.plejdDevices = {};
this.deviceIdsByRoom = {};
this.deviceIdsBySerial = {};
}
clearRoomDevices() {
this.roomDevices = {};
this.devices = {};
this.outputDevices = {};
this.outputDeviceUniqueIdsByRoomId = {};
this.outputUniqueIdByBleOutputAddress = {};
}
clearSceneDevices() {
this.sceneDevices = {};
this.sceneUniqueIdByBleOutputAddress = {};
}
getDevice(deviceId) {
return this.plejdDevices[deviceId] || this.roomDevices[deviceId];
/**
* @returns {import('./types/DeviceRegistry').OutputDevice[]}
*/
getAllOutputDevices() {
return Object.values(this.outputDevices);
}
getDeviceIdsByRoom(roomId) {
return this.deviceIdsByRoom[roomId];
/**
* @returns {import('./types/DeviceRegistry').OutputDevice[]}
*/
getAllSceneDevices() {
return Object.values(this.sceneDevices);
}
getDeviceBySerialNumber(serialNumber) {
return this.getDevice(this.deviceIdsBySerial[serialNumber]);
/** @returns {import('./types/ApiSite').ApiSite} */
getApiSite() {
return this.apiSite;
}
getDeviceName(deviceId) {
return (this.plejdDevices[deviceId] || {}).name;
/**
* @param {string} uniqueOutputId
*/
getOutputDevice(uniqueOutputId) {
return this.outputDevices[uniqueOutputId];
}
getScene(sceneId) {
return this.sceneDevices[sceneId];
/** @returns {import('./types/DeviceRegistry').OutputDevice} */
getOutputDeviceByBleOutputAddress(bleOutputAddress) {
return this.outputDevices[this.outputUniqueIdByBleOutputAddress[bleOutputAddress]];
}
getSceneName(sceneId) {
return (this.sceneDevices[sceneId] || {}).name;
/** @returns {string[]} */
getOutputDeviceIdsByRoomId(roomId) {
return this.outputDeviceUniqueIdsByRoomId[roomId];
}
getState(deviceId) {
const device = this.getDevice(deviceId) || {};
if (device.dimmable) {
return {
state: device.state,
dim: device.dim,
};
getOutputDeviceName(uniqueOutputId) {
return (this.outputDevices[uniqueOutputId] || {}).name;
}
/**
* @param {string } deviceId The physical device serial number
* @return {import('./types/ApiSite').Device}
*/
getPhysicalDevice(deviceId) {
return this.devices[deviceId];
}
/**
* @param {string} sceneUniqueId
*/
getScene(sceneUniqueId) {
return this.sceneDevices[sceneUniqueId];
}
/**
* @param {number} sceneBleAddress
*/
getSceneByBleAddress(sceneBleAddress) {
const sceneUniqueId = this.sceneUniqueIdByBleOutputAddress[sceneBleAddress];
if (!sceneUniqueId) {
return null;
}
return {
state: device.state,
};
return this.sceneDevices[sceneUniqueId];
}
setApiSite(siteDetails) {
this.apiSite = siteDetails;
/**
* @param {string} sceneUniqueId
*/
getSceneName(sceneUniqueId) {
return (this.sceneDevices[sceneUniqueId] || {}).name;
}
setState(deviceId, state, dim) {
const device = this.getDevice(deviceId) || this.addPlejdDevice({ id: deviceId });
// eslint-disable-next-line class-methods-use-this
getUniqueOutputId(deviceId, outputIndex) {
return `${deviceId}_${outputIndex}`;
}
/** @param apiSite {import('./types/ApiSite').ApiSite} */
setApiSite(apiSite) {
this.apiSite = apiSite;
this.cryptoKey = apiSite.plejdMesh.cryptoKey;
}
/**
* @param {string} uniqueOutputId
* @param {boolean} state
* @param {number?} [dim]
*/
setOutputState(uniqueOutputId, state, dim) {
const device = this.getOutputDevice(uniqueOutputId);
if (!device) {
logger.warn(
`Trying to set state for ${uniqueOutputId} which is not in the list of known outputs.`,
);
return;
}
device.state = state;
if (dim && device.dimmable) {
device.dim = dim;

View file

@ -8,18 +8,36 @@ const startTopics = ['hass/status', 'homeassistant/status'];
const logger = Logger.getLogger('plejd-mqtt');
// #region discovery
const discoveryPrefix = 'homeassistant';
const nodeId = 'plejd';
/** @type {import('./types/Mqtt').MQTT_TYPES} */
const MQTT_TYPES = {
LIGHT: 'light',
SCENE: 'scene',
SWITCH: 'switch',
DEVICE_AUTOMATION: 'device_automation',
};
/** @type {import('./types/Mqtt').TOPIC_TYPES} */
const TOPIC_TYPES = {
CONFIG: 'config',
STATE: 'state',
AVAILABILITY: 'availability',
COMMAND: 'set',
};
const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) => `${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`;
const getTopicName = (
/** @type { string } */ uniqueId,
/** @type { import('./types/Mqtt').MqttType } */ mqttDeviceType,
/** @type { import('./types/Mqtt').TopicType } */ topicType,
) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`;
const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trigger`;
const getSceneEventTopic = (/** @type {string} */ sceneId) => `${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`;
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`;
const getConfigPath = (plug) => `${getPath(plug)}/config`;
const getStateTopic = (plug) => `${getPath(plug)}/state`;
const getAvailabilityTopic = (plug) => `${getPath(plug)}/availability`;
const getCommandTopic = (plug) => `${getPath(plug)}/set`;
const getSceneEventTopic = () => 'plejd/event/scene';
const decodeTopicRegexp = new RegExp(
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/,
@ -33,41 +51,63 @@ const decodeTopic = (topic) => {
return matches.groups;
};
const getDiscoveryPayload = (device) => ({
schema: 'json',
const getOutputDeviceDiscoveryPayload = (
/** @type {import('./types/DeviceRegistry').OutputDevice} */ device,
) => ({
name: device.name,
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
state_topic: getStateTopic(device),
command_topic: getCommandTopic(device),
availability_topic: getAvailabilityTopic(device),
unique_id: device.uniqueId,
'~': getBaseTopic(device.uniqueId, device.type),
state_topic: `~/${TOPIC_TYPES.STATE}`,
command_topic: `~/${TOPIC_TYPES.COMMAND}`,
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
optimistic: false,
brightness: `${device.dimmable}`,
qos: 1,
retain: true,
device: {
identifiers: `${device.serialNumber}_${device.id}`,
identifiers: `${device.deviceId}`,
manufacturer: 'Plejd',
model: device.typeName,
name: device.name,
sw_version: device.version,
},
...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}),
});
const getSwitchPayload = (device) => ({
name: device.name,
state_topic: getStateTopic(device),
command_topic: getCommandTopic(device),
optimistic: false,
const getSceneDiscoveryPayload = (
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
) => ({
name: sceneDevice.name,
unique_id: sceneDevice.uniqueId,
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
command_topic: `~/${TOPIC_TYPES.COMMAND}`,
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
payload_on: 'ON',
qos: 1,
retain: false,
});
const getSceneDeviceTriggerhDiscoveryPayload = (
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
) => ({
automation_type: 'trigger',
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION),
qos: 1,
topic: `~/${TOPIC_TYPES.STATE}`,
type: 'scene',
subtype: 'trigger',
device: {
identifiers: `${device.serialNumber}_${device.id}`,
identifiers: `${sceneDevice.uniqueId}`,
manufacturer: 'Plejd',
model: device.typeName,
name: device.name,
sw_version: device.version,
model: sceneDevice.typeName,
name: sceneDevice.name,
},
});
// #endregion
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' };
class MqttClient extends EventEmitter {
/** @type {import('DeviceRegistry')} */
deviceRegistry;
static EVENTS = {
@ -75,6 +115,9 @@ class MqttClient extends EventEmitter {
stateChanged: 'stateChanged',
};
/**
* @param {import("DeviceRegistry")} deviceRegistry
*/
constructor(deviceRegistry) {
super();
@ -86,8 +129,11 @@ class MqttClient extends EventEmitter {
logger.info('Initializing MQTT connection for Plejd addon');
this.client = mqtt.connect(this.config.mqttBroker, {
username: this.config.mqttUsername,
clientId: `hassio-plejd_${Math.random().toString(16).substr(2, 8)}`,
password: this.config.mqttPassword,
protocolVersion: 4, // v5 not supported by HassIO Mosquitto
queueQoSZero: true,
username: this.config.mqttUsername,
});
this.client.on('error', (err) => {
@ -97,13 +143,22 @@ class MqttClient extends EventEmitter {
this.client.on('connect', () => {
logger.info('Connected to MQTT.');
this.client.subscribe(startTopics, (err) => {
if (err) {
logger.error('Unable to subscribe to status topics', err);
}
this.client.subscribe(
startTopics,
// Add below when mqtt v5 is supported in Mosquitto 1.6 or 2.0 and forward
// {
// qos: 1,
// nl: true, // don't echo back messages sent
// rap: true, // retain as published - don't force retain = 0
// },
(err) => {
if (err) {
logger.error('Unable to subscribe to status topics', err);
}
this.emit(MqttClient.EVENTS.connected);
});
this.emit(MqttClient.EVENTS.connected);
},
);
this.client.subscribe(getSubscribePath(), (err) => {
if (err) {
@ -123,27 +178,24 @@ class MqttClient extends EventEmitter {
logger.info('Home Assistant has started. lets do discovery.');
this.emit(MqttClient.EVENTS.connected);
} else {
logger.verbose(`Received mqtt message on ${topic}`);
const decodedTopic = decodeTopic(topic);
if (decodedTopic) {
let device = this.deviceRegistry.getDevice(decodedTopic.id);
/** @type {import('types/DeviceRegistry').OutputDevice} */
let device;
if (decodedTopic.type === MQTT_TYPES.SCENE) {
logger.verbose(`Getting scene ${decodedTopic.id} from registry`);
device = this.deviceRegistry.getScene(decodedTopic.id);
} else {
logger.verbose(`Getting device ${decodedTopic.id} from registry`);
device = this.deviceRegistry.getOutputDevice(decodedTopic.id);
}
const messageString = message.toString();
const isJsonMessage = messageString.startsWith('{');
const command = isJsonMessage ? JSON.parse(messageString) : messageString;
if (
!isJsonMessage
&& messageString === 'ON'
&& this.deviceRegistry.getScene(decodedTopic.id)
) {
// Guess that id that got state command without dim value belongs to Scene, not Device
// This guess could very well be wrong depending on the installation...
logger.warn(
`Device id ${decodedTopic.id} belongs to both scene and device, guessing Scene is what should be set to ON. `
+ 'OFF commands still sent to device.',
);
device = this.deviceRegistry.getScene(decodedTopic.id);
}
const deviceName = device ? device.name : '';
switch (decodedTopic.command) {
@ -195,35 +247,121 @@ class MqttClient extends EventEmitter {
}
disconnect(callback) {
this.deviceRegistry.allDevices.forEach((device) => {
this.client.publish(getAvailabilityTopic(device), 'offline');
logger.info('Mqtt disconnect requested. Setting all devices as unavailable in HA...');
this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => {
const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
this.client.publish(
getTopicName(outputDevice.uniqueId, mqttType, 'availability'),
AVAILABLILITY.OFFLINE,
{
retain: true,
qos: 1,
},
);
});
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
allSceneDevices.forEach((sceneDevice) => {
this.client.publish(
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
AVAILABLILITY.OFFLINE,
{
retain: true,
qos: 1,
},
);
});
this.client.end(callback);
}
sendDiscoveryToHomeAssistant() {
logger.debug(`Sending discovery of ${this.deviceRegistry.allDevices.length} device(s).`);
const allOutputDevices = this.deviceRegistry.getAllOutputDevices();
logger.info(`Sending discovery for ${allOutputDevices.length} Plejd output devices`);
allOutputDevices.forEach((outputDevice) => {
logger.debug(`Sending discovery for ${outputDevice.name}`);
this.deviceRegistry.allDevices.forEach((device) => {
logger.debug(`Sending discovery for ${device.name}`);
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
const configPayload = getOutputDeviceDiscoveryPayload(outputDevice);
logger.info(
`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`,
`Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`,
);
this.client.publish(getConfigPath(device), JSON.stringify(payload));
const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
this.client.publish(
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.CONFIG),
JSON.stringify(configPayload),
{
retain: true,
qos: 1,
},
);
setTimeout(() => {
this.client.publish(getAvailabilityTopic(device), 'online');
this.client.publish(
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
AVAILABLILITY.ONLINE,
{
retain: true,
qos: 1,
},
);
}, 2000);
});
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`);
allSceneDevices.forEach((sceneDevice) => {
logger.debug(`Sending discovery for ${sceneDevice.name}`);
const sceneConfigPayload = getSceneDiscoveryPayload(sceneDevice);
logger.info(
`Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`,
);
this.client.publish(
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.CONFIG),
JSON.stringify(sceneConfigPayload),
{
retain: true,
qos: 1,
},
);
const sceneTriggerConfigPayload = getSceneDeviceTriggerhDiscoveryPayload(sceneDevice);
this.client.publish(
getTopicName(
getTriggerUniqueId(sceneDevice.uniqueId),
MQTT_TYPES.DEVICE_AUTOMATION,
TOPIC_TYPES.CONFIG,
),
JSON.stringify(sceneTriggerConfigPayload),
{
retain: true,
qos: 1,
},
);
setTimeout(() => {
this.client.publish(
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
AVAILABLILITY.ONLINE,
{
retain: true,
qos: 1,
},
);
}, 2000);
});
}
updateState(deviceId, data) {
const device = this.deviceRegistry.getDevice(deviceId);
/**
* @param {string} uniqueOutputId
* @param {{ state: boolean; brightness?: number; }} data
*/
updateOutputState(uniqueOutputId, data) {
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
if (!device) {
logger.warn(`Unknown device id ${deviceId} - not handled by us.`);
logger.warn(`Unknown output id ${uniqueOutputId} - not handled by us.`);
return;
}
@ -235,29 +373,40 @@ class MqttClient extends EventEmitter {
let payload = null;
if (device.type === 'switch') {
payload = data.state === 1 ? 'ON' : 'OFF';
payload = getMqttStateString(data.state);
} else {
if (device.dimmable) {
payload = {
state: data.state === 1 ? 'ON' : 'OFF',
state: getMqttStateString(data.state),
brightness: data.brightness,
};
} else {
payload = {
state: data.state === 1 ? 'ON' : 'OFF',
state: getMqttStateString(data.state),
};
}
payload = JSON.stringify(payload);
}
this.client.publish(getStateTopic(device), payload);
this.client.publish(getAvailabilityTopic(device), 'online');
const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
this.client.publish(getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.STATE), payload, {
retain: true,
qos: 1,
});
// this.client.publish(
// getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
// AVAILABLILITY.ONLINE,
// { retain: true, qos: 1 },
// );
}
/**
* @param {string} sceneId
*/
sceneTriggered(sceneId) {
logger.verbose(`Scene triggered: ${sceneId}`);
this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId }));
this.client.publish(getSceneEventTopic(sceneId), '', { qos: 1 });
}
}

View file

@ -65,57 +65,61 @@ class PlejdAddon extends EventEmitter {
});
// subscribe to changes from HA
this.mqttClient.on(MqttClient.EVENTS.stateChanged, (device, command) => {
try {
const deviceId = device.id;
this.mqttClient.on(
MqttClient.EVENTS.stateChanged,
/** @param device {import('./types/DeviceRegistry').OutputDevice} */
(device, command) => {
try {
const { uniqueId } = device;
if (device.typeName === 'Scene') {
// we're triggering a scene, lets do that and jump out.
// since scenes aren't "real" devices.
this.sceneManager.executeScene(device.id);
return;
if (device.typeName === 'Scene') {
// we're triggering a scene, lets do that and jump out.
// since scenes aren't "real" devices.
this.sceneManager.executeScene(uniqueId);
return;
}
let state = false;
let commandObj = {};
if (typeof command === 'string') {
// switch command
state = command === 'ON';
commandObj = {
state,
};
// since the switch doesn't get any updates on whether it's on or not,
// we fake this by directly send the updateState back to HA in order for
// it to change state.
this.mqttClient.updateOutputState(uniqueId, {
state,
});
} else {
// eslint-disable-next-line prefer-destructuring
state = command.state === 'ON';
commandObj = command;
}
if (state) {
this.plejdDeviceCommunication.turnOn(uniqueId, commandObj);
} else {
this.plejdDeviceCommunication.turnOff(uniqueId, commandObj);
}
} catch (err) {
logger.error('Error in MqttClient.stateChanged callback', err);
}
let state = 'OFF';
let commandObj = {};
if (typeof command === 'string') {
// switch command
state = command;
commandObj = {
state,
};
// since the switch doesn't get any updates on whether it's on or not,
// we fake this by directly send the updateState back to HA in order for
// it to change state.
this.mqttClient.updateState(deviceId, {
state: state === 'ON' ? 1 : 0,
});
} else {
// eslint-disable-next-line prefer-destructuring
state = command.state;
commandObj = command;
}
if (state === 'ON') {
this.plejdDeviceCommunication.turnOn(deviceId, commandObj);
} else {
this.plejdDeviceCommunication.turnOff(deviceId, commandObj);
}
} catch (err) {
logger.error('Error in MqttClient.stateChanged callback', err);
}
});
},
);
this.mqttClient.init();
// subscribe to changes from Plejd
this.plejdDeviceCommunication.on(
PlejdDeviceCommunication.EVENTS.stateChanged,
(deviceId, command) => {
(uniqueOutputId, command) => {
try {
this.mqttClient.updateState(deviceId, command);
this.mqttClient.updateOutputState(uniqueOutputId, command);
} catch (err) {
logger.error('Error in PlejdService.stateChanged callback', err);
}

View file

@ -10,15 +10,33 @@ const API_LOGIN_URL = 'login';
const API_SITE_LIST_URL = 'functions/getSiteList';
const API_SITE_DETAILS_URL = 'functions/getSiteById';
const TRAITS = {
NO_LOAD: 0,
NON_DIMMABLE: 9,
DIMMABLE: 11,
};
const logger = Logger.getLogger('plejd-api');
class PlejdApi {
/** @private @type {import('types/Configuration').Options} */
config;
/** @private @type {import('DeviceRegistry')} */
deviceRegistry;
/** @private @type {string} */
sessionToken;
/** @private @type {string} */
siteId;
/** @private @type {import('types/ApiSite').ApiSite} */
siteDetails;
/**
* @param {import("./DeviceRegistry")} deviceRegistry
*/
constructor(deviceRegistry) {
this.config = Configuration.getOptions();
this.deviceRegistry = deviceRegistry;
@ -58,19 +76,19 @@ class PlejdApi {
}
}
}
this.deviceRegistry.setApiSite(this.siteDetails);
this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey;
this.deviceRegistry.setApiSite(this.siteDetails);
this.getDevices();
}
/** @returns {Promise<import('types/ApiSite').CachedSite>} */
// eslint-disable-next-line class-methods-use-this
async getCachedCopy() {
logger.info('Getting cached api response from disk');
try {
const rawData = await fs.promises.readFile('/data/cachedApiResponse.json');
const cachedCopy = JSON.parse(rawData);
const cachedCopy = JSON.parse(rawData.toString());
return cachedCopy;
} catch (err) {
@ -82,12 +100,14 @@ class PlejdApi {
async saveCachedCopy() {
logger.info('Saving cached copy');
try {
const rawData = JSON.stringify({
/** @type {import('types/ApiSite').CachedSite} */
const cachedSite = {
siteId: this.siteId,
siteDetails: this.siteDetails,
sessionToken: this.sessionToken,
dtCache: new Date().toISOString(),
});
};
const rawData = JSON.stringify(cachedSite);
await fs.promises.writeFile('/data/cachedApiResponse.json', rawData);
} catch (err) {
logger.error('Failed to save cache of api response', err);
@ -194,6 +214,14 @@ class PlejdApi {
getDevices() {
logger.info('Getting devices from site details response...');
if (this.siteDetails.gateways && this.siteDetails.gateways.length) {
this.siteDetails.gateways.forEach((gwy) => {
logger.info(`Plejd gateway '${gwy.title}' found on site`);
});
} else {
logger.info('No Plejd gateway found on site');
}
this._getPlejdDevices();
this._getRoomDevices();
this._getSceneDevices();
@ -216,8 +244,11 @@ class PlejdApi {
}
// eslint-disable-next-line class-methods-use-this
_getDeviceType(hardwareId) {
switch (parseInt(hardwareId, 10)) {
_getDeviceType(plejdDevice) {
// Type name is also sometimes available in device.hardware.name
// (maybe only when GWY-01 is present?)
switch (parseInt(plejdDevice.hardwareId, 10)) {
case 1:
case 11:
return { name: 'DIM-01', type: 'light', dimmable: true };
@ -259,69 +290,115 @@ class PlejdApi {
case 20:
return { name: 'SPR-01', type: 'switch', dimmable: false };
default:
throw new Error(`Unknown device type with id ${hardwareId}`);
throw new Error(`Unknown device type with id ${plejdDevice.hardwareId}`);
}
}
/**
* Plejd API properties parsed
*
* * `devices` - physical Plejd devices, duplicated for devices with multiple outputs
* devices: [{deviceId, title, objectId, ...}, {...}]
* * `deviceAddress` - BLE address of each physical device
* deviceAddress: {[deviceId]: bleDeviceAddress}
* * `outputSettings` - lots of info about load settings, also links devices to output index
* outputSettings: [{deviceId, output, deviceParseId, ...}] //deviceParseId === objectId above
* * `outputAddress`: BLE address of [0] main output and [n] other output (loads)
* outputAddress: {[deviceId]: {[output]: bleDeviceAddress}}
* * `inputSettings` - detailed settings for inputs (buttons, RTR-01, ...), scenes triggered, ...
* inputSettings: [{deviceId, input, ...}] //deviceParseId === objectId above
* * `inputAddress` - Links inputs to what BLE device they control, or 255 for unassigned/scene
* inputAddress: {[deviceId]: {[input]: bleDeviceAddress}}
*/
_getPlejdDevices() {
this.deviceRegistry.clearPlejdDevices();
this.siteDetails.devices.forEach((device) => {
const { deviceId } = device;
this.deviceRegistry.addPhysicalDevice(device);
const settings = this.siteDetails.outputSettings.find(
const outputSettings = this.siteDetails.outputSettings.find(
(x) => x.deviceParseId === device.objectId,
);
let deviceNum = this.siteDetails.deviceAddress[deviceId];
if (!outputSettings) {
logger.verbose(
`No outputSettings found for ${device.title} (${device.deviceId}), assuming output 0`,
);
}
const deviceOutput = outputSettings ? outputSettings.output : 0;
const outputAddress = this.siteDetails.outputAddress[device.deviceId];
if (settings) {
const outputs = this.siteDetails.outputAddress[deviceId];
deviceNum = outputs[settings.output];
if (outputAddress) {
const bleOutputAddress = outputAddress[deviceOutput];
if (device.traits === TRAITS.NO_LOAD) {
logger.warn(
`Device ${device.title} (${device.deviceId}) has no load configured and will be excluded`,
);
} else {
const uniqueOutputId = this.deviceRegistry.getUniqueOutputId(
device.deviceId,
deviceOutput,
);
const plejdDevice = this.siteDetails.plejdDevices.find(
(x) => x.deviceId === device.deviceId,
);
const dimmable = device.traits === TRAITS.DIMMABLE;
// dimmable = settings.dimCurve !== 'NonDimmable';
const { name: typeName, type: deviceType } = this._getDeviceType(plejdDevice);
let loadType = deviceType;
if (device.outputType === 'RELAY') {
loadType = 'switch';
} else if (device.outputType === 'LIGHT') {
loadType = 'light';
}
/** @type {import('types/DeviceRegistry').OutputDevice} */
const outputDevice = {
bleOutputAddress,
deviceId: device.deviceId,
dimmable,
hiddenFromRoomList: device.hiddenFromRoomList,
hiddenFromIntegrations: device.hiddenFromIntegrations,
name: device.title,
output: deviceOutput,
roomId: device.roomId,
state: undefined,
type: loadType,
typeName,
version: plejdDevice.firmware.version,
uniqueId: uniqueOutputId,
};
this.deviceRegistry.addOutputDevice(outputDevice);
}
}
// check if device is dimmable
const plejdDevice = this.siteDetails.plejdDevices.find((x) => x.deviceId === deviceId);
const deviceType = this._getDeviceType(plejdDevice.hardwareId);
const { name, type } = deviceType;
let { dimmable } = deviceType;
// What should we do with inputs?!
// if (outputDevice.typeName === 'WPH-01') {
// // WPH-01 is special, it has two buttons which needs to be
// // registered separately.
// const inputs = this.siteDetails.inputAddress[deviceId];
// const first = inputs[0];
// const second = inputs[1];
if (settings) {
dimmable = settings.dimCurve !== 'NonDimmable';
}
// this.deviceRegistry.addPlejdDevice({
// ...outputDevice,
// id: first,
// name: `${device.title} left`,
// });
const newDevice = {
id: deviceNum,
name: device.title,
type,
typeName: name,
dimmable,
roomId: device.roomId,
version: plejdDevice.firmware.version,
serialNumber: plejdDevice.deviceId,
};
if (newDevice.typeName === 'WPH-01') {
// WPH-01 is special, it has two buttons which needs to be
// registered separately.
const inputs = this.siteDetails.inputAddress[deviceId];
const first = inputs[0];
const second = inputs[1];
this.deviceRegistry.addPlejdDevice({
...newDevice,
id: first,
name: `${device.title} left`,
});
this.deviceRegistry.addPlejdDevice({
...newDevice,
id: second,
name: `${device.title} right`,
});
} else {
this.deviceRegistry.addPlejdDevice(newDevice);
}
// this.deviceRegistry.addPlejdDevice({
// ...outputDevice,
// id: second,
// name: `${device.title} right`,
// });
// } else {
// this.deviceRegistry.addPlejdDevice(outputDevice);
// }
});
}
@ -332,39 +409,57 @@ class PlejdApi {
const { roomId } = room;
const roomAddress = this.siteDetails.roomAddress[roomId];
const deviceIdsByRoom = this.deviceRegistry.getDeviceIdsByRoom(roomId);
const deviceIdsByRoom = this.deviceRegistry.getOutputDeviceIdsByRoomId(roomId);
const dimmable = deviceIdsByRoom
&& deviceIdsByRoom.some((deviceId) => this.deviceRegistry.getDevice(deviceId).dimmable);
&& deviceIdsByRoom.some(
(deviceId) => this.deviceRegistry.getOutputDevice(deviceId).dimmable,
);
/** @type {import('types/DeviceRegistry').OutputDevice} */
const newDevice = {
id: roomAddress,
bleOutputAddress: roomAddress,
deviceId: null,
dimmable,
hiddenFromRoomList: false,
hiddenFromIntegrations: false,
name: room.title,
output: undefined,
roomId,
state: undefined,
type: 'light',
typeName: 'Room',
dimmable,
uniqueId: roomId,
version: undefined,
};
this.deviceRegistry.addRoomDevice(newDevice);
this.deviceRegistry.addOutputDevice(newDevice);
});
logger.debug('includeRoomsAsLights done.');
}
}
_getSceneDevices() {
this.deviceRegistry.clearSceneDevices();
// add scenes as switches
const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false);
scenes.forEach((scene) => {
const sceneNum = this.siteDetails.sceneIndex[scene.sceneId];
/** @type {import('types/DeviceRegistry').OutputDevice} */
const newScene = {
id: sceneNum,
name: scene.title,
type: 'switch',
typeName: 'Scene',
bleOutputAddress: sceneNum,
deviceId: undefined,
dimmable: false,
version: '1.0',
serialNumber: scene.objectId,
hiddenFromSceneList: scene.hiddenFromSceneList,
name: scene.title,
output: undefined,
roomId: undefined,
state: false,
type: 'scene',
typeName: 'Scene',
version: undefined,
uniqueId: scene.sceneId,
};
this.deviceRegistry.addScene(newScene);

View file

@ -1,7 +1,7 @@
const dbus = require('dbus-next');
const crypto = require('crypto');
const xor = require('buffer-xor');
const EventEmitter = require('events');
const { EventEmitter } = require('events');
const Configuration = require('./Configuration');
const constants = require('./constants');
@ -49,6 +49,8 @@ class PlejBLEHandler extends EventEmitter {
connectedDevice = null;
consecutiveWriteFails;
consecutiveReconnectAttempts = 0;
/** @type {import('./DeviceRegistry')} */
deviceRegistry;
discoveryTimeout = null;
plejdService = null;
pingRef = null;
@ -152,21 +154,26 @@ class PlejBLEHandler extends EventEmitter {
logger.info('BLE init done, waiting for devices.');
}
async sendCommand(command, deviceId, data) {
/**
* @param {string} command
* @param {number} bleOutputAddress
* @param {number} data
*/
async sendCommand(command, bleOutputAddress, data) {
let payload;
let brightnessVal;
switch (command) {
case COMMANDS.TURN_ON:
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '01');
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '01');
break;
case COMMANDS.TURN_OFF:
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '00');
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '00');
break;
case COMMANDS.DIM:
// eslint-disable-next-line no-bitwise
brightnessVal = (data << 8) | data;
payload = this._createHexPayload(
deviceId,
bleOutputAddress,
BLE_CMD_DIM2_CHANGE,
`01${brightnessVal.toString(16).padStart(4, '0')}`,
);
@ -194,9 +201,9 @@ class PlejBLEHandler extends EventEmitter {
plejd.instance = device;
const segments = plejd.path.split('/');
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
plejd.device = this.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath);
let plejdSerialNumber = segments[segments.length - 1].replace('dev_', '');
plejdSerialNumber = plejdSerialNumber.replace(/_/g, '');
plejd.device = this.deviceRegistry.getPhysicalDevice(plejdSerialNumber);
if (plejd.device) {
logger.debug(
@ -204,7 +211,7 @@ class PlejBLEHandler extends EventEmitter {
);
this.bleDevices.push(plejd);
} else {
logger.warn(`Device registry does not contain device with serial ${fixedPlejdPath}`);
logger.warn(`Device registry does not contain device with serial ${plejdSerialNumber}`);
}
} catch (err) {
logger.error(`Failed inspecting ${path}. `, err);
@ -796,7 +803,7 @@ class PlejBLEHandler extends EventEmitter {
return;
}
const deviceId = decoded.readUInt8(0);
const bleOutputAddress = decoded.readUInt8(0);
// Bytes 2-3 is Command/Request
const cmd = decoded.readUInt16BE(3);
@ -810,38 +817,52 @@ class PlejBLEHandler extends EventEmitter {
logger.silly(`Dim: ${dim.toString(16)}, full precision: ${dimFull.toString(16)}`);
}
const deviceName = this.deviceRegistry.getDeviceName(deviceId);
const device = this.deviceRegistry.getOutputDeviceByBleOutputAddress(bleOutputAddress);
const deviceName = device ? device.name : 'Unknown';
const outputUniqueId = device ? device.uniqueId : null;
if (Logger.shouldLog('verbose')) {
// decoded.toString() could potentially be expensive
logger.verbose(`Raw event received: ${decoded.toString('hex')}`);
logger.verbose(
`Decoded: Device ${deviceId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`,
`Decoded: Device ${outputUniqueId} (BLE address ${bleOutputAddress}), cmd ${cmd.toString(
16,
)}, state ${state}, dim ${dim}`,
);
}
let command;
let data = {};
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
logger.debug(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`);
logger.debug(
`${deviceName} (${outputUniqueId}) got state+dim update. S: ${state}, D: ${dim}`,
);
command = COMMANDS.DIM;
data = { state, dim };
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data);
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
} else if (cmd === BLE_CMD_STATE_CHANGE) {
logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`);
logger.debug(`${deviceName} (${outputUniqueId}) got state update. S: ${state}`);
command = state ? COMMANDS.TURN_ON : COMMANDS.TURN_OFF;
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data);
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
} else if (cmd === BLE_CMD_SCENE_TRIG) {
const sceneId = state;
const sceneName = this.deviceRegistry.getSceneName(sceneId);
const sceneBleAddress = state;
const scene = this.deviceRegistry.getSceneByBleAddress(sceneBleAddress);
if (!scene) {
logger.warn(
`Scene with BLE address ${sceneBleAddress} could not be found, can't process message`,
);
return;
}
logger.debug(
`${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`,
`${scene.name} (${sceneBleAddress}) scene triggered (device id ${outputUniqueId}).`,
);
command = COMMANDS.TRIGGER_SCENE;
data = { sceneId };
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data);
data = { sceneId: scene.uniqueId };
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
} else if (cmd === BLE_CMD_TIME_UPDATE) {
const now = new Date();
// Guess Plejd timezone based on HA time zone
@ -851,7 +872,7 @@ class PlejBLEHandler extends EventEmitter {
const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000;
const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000);
if (
deviceId !== BLE_BROADCAST_DEVICE_ID
bleOutputAddress !== BLE_BROADCAST_DEVICE_ID
|| Logger.shouldLog('verbose')
|| Math.abs(diffSeconds) > 60
) {
@ -863,7 +884,7 @@ class PlejBLEHandler extends EventEmitter {
logger.warn(
`Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`,
);
if (this.connectedDevice && deviceId === this.connectedDevice.id) {
if (this.connectedDevice && bleOutputAddress === this.connectedDevice.id) {
// Requested time sync by us
const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess;
logger.info(`Setting time to ${now.toString()}`);
@ -874,14 +895,14 @@ class PlejBLEHandler extends EventEmitter {
(pl) => pl.writeInt32LE(Math.trunc(newLocalTimestamp), 5),
);
try {
this.write(payload);
this._write(payload);
} catch (err) {
logger.error(
'Failed writing new time to Plejd. Will try again in one hour or at restart.',
);
}
}
} else if (deviceId !== BLE_BROADCAST_DEVICE_ID) {
} else if (bleOutputAddress !== BLE_BROADCAST_DEVICE_ID) {
logger.info('Got time response. Plejd clock time in sync with Home Assistant time');
}
}
@ -889,19 +910,19 @@ class PlejBLEHandler extends EventEmitter {
logger.verbose(
`Command ${cmd.toString(16)} unknown. ${decoded.toString(
'hex',
)}. Device ${deviceName} (${deviceId})`,
)}. Device ${deviceName} (${bleOutputAddress}: ${outputUniqueId})`,
);
}
}
_createHexPayload(
deviceId,
bleOutputAddress,
command,
hexDataString,
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
) {
return this._createPayload(
deviceId,
bleOutputAddress,
command,
5 + Math.ceil(hexDataString.length / 2),
(payload) => payload.write(hexDataString, 5, 'hex'),
@ -911,14 +932,14 @@ class PlejBLEHandler extends EventEmitter {
// eslint-disable-next-line class-methods-use-this
_createPayload(
deviceId,
bleOutputAddress,
command,
bufferLength,
payloadBufferAddDataFunc,
requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
) {
const payload = Buffer.alloc(bufferLength);
payload.writeUInt8(deviceId);
payload.writeUInt8(bleOutputAddress);
payload.writeUInt16BE(requestResponseCommand, 1);
payload.writeUInt16BE(command, 3);
payloadBufferAddDataFunc(payload);
@ -945,12 +966,12 @@ class PlejBLEHandler extends EventEmitter {
let ct = cipher.update(buf).toString('hex');
ct += cipher.final().toString('hex');
ct = Buffer.from(ct, 'hex');
const ctBuf = Buffer.from(ct, 'hex');
let output = '';
for (let i = 0, { length } = data; i < length; i++) {
// eslint-disable-next-line no-bitwise
output += String.fromCharCode(data[i] ^ ct[i % 16]);
output += String.fromCharCode(data[i] ^ ctBuf[i % 16]);
}
return Buffer.from(output, 'ascii');

View file

@ -1,4 +1,4 @@
const EventEmitter = require('events');
const { EventEmitter } = require('events');
const Configuration = require('./Configuration');
const constants = require('./constants');
const Logger = require('./Logger');
@ -12,10 +12,13 @@ const MAX_RETRY_COUNT = 10; // Could be made a setting
class PlejdDeviceCommunication extends EventEmitter {
bleConnected;
bleDeviceTransitionTimers = {};
bleOutputTransitionTimers = {};
plejdBleHandler;
config;
/** @type {import('./DeviceRegistry')} */
deviceRegistry;
// eslint-disable-next-line max-len
/** @type {{uniqueOutputId: string, command: string, data: any, shouldRetry: boolean, retryCount?: number}[]} */
writeQueue = [];
writeQueueRef = null;
@ -34,7 +37,7 @@ class PlejdDeviceCommunication extends EventEmitter {
}
cleanup() {
Object.values(this.bleDeviceTransitionTimers).forEach((t) => clearTimeout(t));
Object.values(this.bleOutputTransitionTimers).forEach((t) => clearTimeout(t));
this.plejdBleHandler.cleanup();
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived);
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected);
@ -46,7 +49,10 @@ class PlejdDeviceCommunication extends EventEmitter {
this.cleanup();
this.bleConnected = false;
// eslint-disable-next-line max-len
this.plejdBleHandler.on(PlejBLEHandler.EVENTS.commandReceived, (deviceId, command, data) => this._bleCommandReceived(deviceId, command, data));
this.plejdBleHandler.on(
PlejBLEHandler.EVENTS.commandReceived,
(uniqueOutputId, command, data) => this._bleCommandReceived(uniqueOutputId, command, data),
);
this.plejdBleHandler.on(PlejBLEHandler.EVENTS.connected, () => {
logger.info('Bluetooth connected. Plejd BLE up and running!');
@ -70,42 +76,42 @@ class PlejdDeviceCommunication extends EventEmitter {
}
}
turnOn(deviceId, command) {
const deviceName = this.deviceRegistry.getDeviceName(deviceId);
turnOn(uniqueOutputId, command) {
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
logger.info(
`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${
`Plejd got turn on command for ${deviceName} (${uniqueOutputId}), brightness ${
command.brightness
}${command.transition ? `, transition: ${command.transition}` : ''}`,
);
this._transitionTo(uniqueOutputId, command.brightness, command.transition, deviceName);
}
turnOff(uniqueOutputId, command) {
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
logger.info(
`Plejd got turn off command for ${deviceName} (${uniqueOutputId})${
command.transition ? `, transition: ${command.transition}` : ''
}`,
);
this._transitionTo(deviceId, command.brightness, command.transition, deviceName);
this._transitionTo(uniqueOutputId, 0, command.transition, deviceName);
}
turnOff(deviceId, command) {
const deviceName = this.deviceRegistry.getDeviceName(deviceId);
logger.info(
`Plejd got turn off command for ${deviceName} (${deviceId})${
command.transition ? `, transition: ${command.transition}` : ''
}`,
);
this._transitionTo(deviceId, 0, command.transition, deviceName);
}
_bleCommandReceived(deviceId, command, data) {
_bleCommandReceived(uniqueOutputId, command, data) {
try {
if (command === COMMANDS.DIM) {
this.deviceRegistry.setState(deviceId, data.state, data.dim);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
state: data.state,
this.deviceRegistry.setOutputState(uniqueOutputId, data.state, data.dim);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: !!data.state,
brightness: data.dim,
});
} else if (command === COMMANDS.TURN_ON) {
this.deviceRegistry.setState(deviceId, 1);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
this.deviceRegistry.setOutputState(uniqueOutputId, true);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: 1,
});
} else if (command === COMMANDS.TURN_OFF) {
this.deviceRegistry.setState(deviceId, 0);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, {
this.deviceRegistry.setOutputState(uniqueOutputId, false);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: 0,
});
} else if (command === COMMANDS.TRIGGER_SCENE) {
@ -118,18 +124,18 @@ class PlejdDeviceCommunication extends EventEmitter {
}
}
_clearDeviceTransitionTimer(deviceId) {
if (this.bleDeviceTransitionTimers[deviceId]) {
clearInterval(this.bleDeviceTransitionTimers[deviceId]);
_clearDeviceTransitionTimer(uniqueOutputId) {
if (this.bleOutputTransitionTimers[uniqueOutputId]) {
clearInterval(this.bleOutputTransitionTimers[uniqueOutputId]);
}
}
_transitionTo(deviceId, targetBrightness, transition, deviceName) {
const device = this.deviceRegistry.getDevice(deviceId);
_transitionTo(uniqueOutputId, targetBrightness, transition, deviceName) {
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
const initialBrightness = device ? device.state && device.dim : null;
this._clearDeviceTransitionTimer(deviceId);
this._clearDeviceTransitionTimer(uniqueOutputId);
const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable;
const isDimmable = this.deviceRegistry.getOutputDevice(uniqueOutputId).dimmable;
if (
transition > 1
@ -164,7 +170,7 @@ class PlejdDeviceCommunication extends EventEmitter {
let nSteps = 0;
this.bleDeviceTransitionTimers[deviceId] = setInterval(() => {
this.bleOutputTransitionTimers[uniqueOutputId] = setInterval(() => {
const tElapsedMs = new Date().getTime() - dtStart.getTime();
let tElapsed = tElapsedMs / 1000;
@ -178,20 +184,20 @@ class PlejdDeviceCommunication extends EventEmitter {
if (tElapsed === transition) {
nSteps++;
this._clearDeviceTransitionTimer(deviceId);
this._clearDeviceTransitionTimer(uniqueOutputId);
newBrightness = targetBrightness;
logger.debug(
`Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
`Queueing finalize ${deviceName} (${uniqueOutputId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
tElapsedMs / (nSteps || 1)
} ms.`,
);
this._setBrightness(deviceId, newBrightness, true, deviceName);
this._setBrightness(uniqueOutputId, newBrightness, true, deviceName);
} else {
nSteps++;
logger.verbose(
`Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
`Queueing dim transition for ${deviceName} (${uniqueOutputId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
);
this._setBrightness(deviceId, newBrightness, false, deviceName);
this._setBrightness(uniqueOutputId, newBrightness, false, deviceName);
}
}, transitionInterval);
} else {
@ -200,34 +206,34 @@ class PlejdDeviceCommunication extends EventEmitter {
`Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`,
);
}
this._setBrightness(deviceId, targetBrightness, true, deviceName);
this._setBrightness(uniqueOutputId, targetBrightness, true, deviceName);
}
}
_setBrightness(deviceId, brightness, shouldRetry, deviceName) {
_setBrightness(unqiueOutputId, brightness, shouldRetry, deviceName) {
if (!brightness && brightness !== 0) {
logger.debug(
`Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`,
`Queueing turn on ${deviceName} (${unqiueOutputId}). No brightness specified, setting DIM to previous.`,
);
this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_ON, null, shouldRetry);
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_ON, null, shouldRetry);
} else if (brightness <= 0) {
logger.debug(`Queueing turn off ${deviceId}`);
this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_OFF, null, shouldRetry);
logger.debug(`Queueing turn off ${unqiueOutputId}`);
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_OFF, null, shouldRetry);
} else {
if (brightness > 255) {
// eslint-disable-next-line no-param-reassign
brightness = 255;
}
logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`);
logger.debug(`Queueing ${unqiueOutputId} set brightness to ${brightness}`);
// eslint-disable-next-line no-bitwise
this._appendCommandToWriteQueue(deviceId, COMMANDS.DIM, brightness, shouldRetry);
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.DIM, brightness, shouldRetry);
}
}
_appendCommandToWriteQueue(deviceId, command, data, shouldRetry) {
_appendCommandToWriteQueue(uniqueOutputId, command, data, shouldRetry) {
this.writeQueue.unshift({
deviceId,
uniqueOutputId,
command,
data,
shouldRetry,
@ -249,28 +255,29 @@ class PlejdDeviceCommunication extends EventEmitter {
return;
}
const queueItem = this.writeQueue.pop();
const deviceName = this.deviceRegistry.getDeviceName(queueItem.deviceId);
const device = this.deviceRegistry.getOutputDevice(queueItem.uniqueOutputId);
logger.debug(
`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${
`Write queue: Processing ${device.name} (${queueItem.uniqueOutputId}). Command ${
queueItem.command
}${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${
this.writeQueue.length
}`,
);
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) {
if (this.writeQueue.some((item) => item.uniqueOutputId === queueItem.uniqueOutputId)) {
logger.verbose(
`Skipping ${deviceName} (${queueItem.deviceId}) `
`Skipping ${device.name} (${queueItem.uniqueOutputId}) `
+ `${queueItem.command} due to more recent command in queue.`,
);
// Skip commands if new ones exist for the same deviceId
// Skip commands if new ones exist for the same uniqueOutputId
// still process all messages in order
} else {
/* eslint-disable no-await-in-loop */
try {
await this.plejdBleHandler.sendCommand(
queueItem.command,
queueItem.deviceId,
device.bleOutputAddress,
queueItem.data,
);
} catch (err) {
@ -281,7 +288,7 @@ class PlejdDeviceCommunication extends EventEmitter {
this.writeQueue.push(queueItem); // Add back to top of queue to be processed next;
} else {
logger.error(
`Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.deviceId}). Command ${queueItem.command} failed.`,
`Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${device.name} (${queueItem.uniqueOutputId}). Command ${queueItem.command} failed.`,
);
break;
}

View file

@ -189,6 +189,11 @@ The code in this project follows the [Airbnb JavaScript guide](https://github.co
For a nice developer experience it is very convenient to have `eslint` and `prettier` installed in your favorite editor (such as VS Code) and use the "format on save" option (or invoke formatting by Alt+Shift+F in VS Code). Any code issues should appear in the problems window inside the editor, as well as when running the command above.
For partial type hinting you can run
- `npm install --global typings`
- `typings install`
When contributing, please do so by forking the repo and then using pull requests towards the dev branch.
### Logs

View file

@ -1,18 +1,20 @@
const SceneStep = require('./SceneStep');
class Scene {
constructor(idx, scene, steps) {
/**
* @param {import('./DeviceRegistry')} deviceRegistry
* @param {number} idx
* @param {import("./types/ApiSite").Scene} scene
*/
constructor(deviceRegistry, idx, scene) {
this.id = idx;
this.title = scene.title;
this.sceneId = scene.sceneId;
const sceneSteps = steps.filter((x) => x.sceneId === scene.sceneId);
this.steps = [];
// eslint-disable-next-line no-restricted-syntax
for (const step of sceneSteps) {
this.steps.push(new SceneStep(step));
}
this.steps = deviceRegistry
.getApiSite()
.sceneSteps.filter((step) => step.sceneId === scene.sceneId)
.map((step) => new SceneStep(step));
}
}

View file

@ -3,45 +3,52 @@ const Scene = require('./Scene');
const logger = Logger.getLogger('scene-manager');
class SceneManager {
/** @private @type {import('./DeviceRegistry')} */
deviceRegistry;
plejdBle;
/** @private @type {import('./PlejdDeviceCommunication')} */
plejdDeviceCommunication;
/** @private @type {Object.<string,Scene>} */
scenes;
constructor(deviceRegistry, plejdBle) {
constructor(deviceRegistry, plejdDeviceCommunication) {
this.deviceRegistry = deviceRegistry;
this.plejdBle = plejdBle;
this.plejdDeviceCommunication = plejdDeviceCommunication;
this.scenes = {};
}
init() {
const scenes = this.deviceRegistry.apiSite.scenes.filter(
(x) => x.hiddenFromSceneList === false,
);
const scenes = this.deviceRegistry
.getApiSite()
.scenes.filter((x) => x.hiddenFromSceneList === false);
this.scenes = {};
scenes.forEach((scene) => {
const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId];
this.scenes[idx] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps);
const sceneBleAddress = this.deviceRegistry.getApiSite().sceneIndex[scene.sceneId];
this.scenes[scene.sceneId] = new Scene(this.deviceRegistry, sceneBleAddress, scene);
});
}
executeScene(sceneId) {
const scene = this.scenes[sceneId];
/**
* @param {string} sceneUniqueId
*/
executeScene(sceneUniqueId) {
const scene = this.scenes[sceneUniqueId];
if (!scene) {
logger.info(`Scene with id ${sceneId} not found`);
logger.info(`Scene with id ${sceneUniqueId} not found`);
logger.verbose(`Scenes: ${JSON.stringify(this.scenes, null, 2)}`);
return;
}
scene.steps.forEach((step) => {
const device = this.deviceRegistry.getDeviceBySerialNumber(step.deviceId);
const uniqueId = this.deviceRegistry.getUniqueOutputId(step.deviceId, step.output);
const device = this.deviceRegistry.getOutputDevice(uniqueId);
if (device) {
if (device.dimmable && step.state) {
this.plejdBle.turnOn(device.id, { brightness: step.brightness });
this.plejdDeviceCommunication.turnOn(uniqueId, { brightness: step.brightness });
} else if (!device.dimmable && step.state) {
this.plejdBle.turnOn(device.id, {});
this.plejdDeviceCommunication.turnOn(uniqueId, {});
} else if (!step.state) {
this.plejdBle.turnOff(device.id, {});
this.plejdDeviceCommunication.turnOff(uniqueId, {});
}
}
});

View file

@ -1,7 +1,11 @@
class SceneStep {
/**
* @param {import("./types/ApiSite").SceneStep} step
*/
constructor(step) {
this.sceneId = step.sceneId;
this.deviceId = step.deviceId;
this.output = step.output;
this.state = step.state === 'On' ? 1 : 0;
this.brightness = step.value;
}

View file

@ -1,6 +1,6 @@
{
"name": "Plejd",
"version": "0.7.1",
"version": "0.8.0-dev",
"slug": "plejd",
"description": "Adds support for the Swedish home automation devices from Plejd.",
"url": "https://github.com/icanos/hassio-plejd/",

10
plejd/jsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"checkJs": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es6"
},
"exclude": ["node_modules", "rootfs"]
}

View file

@ -3,17 +3,17 @@
"@abandonware/bluetooth-hci-socket": "~0.5.3-7",
"axios": "~0.21.1",
"buffer-xor": "~2.0.2",
"dbus-next": "~0.9.1",
"dbus-next": "~0.9.2",
"fs": "0.0.1-security",
"jspack": "~0.0.4",
"mqtt": "~3.0.0",
"mqtt": "~4.2.6",
"winston": "~3.3.3"
},
"devDependencies": {
"babel-eslint": "~10.1.0",
"eslint": "~7.18.0",
"eslint": "~7.23.0",
"eslint-config-airbnb": "~18.2.1",
"eslint-config-prettier": "~7.2.0",
"eslint-config-prettier": "~8.1.0",
"eslint-plugin-import": "~2.22.1",
"eslint-plugin-prettier": "~3.3.1",
"prettier": "~2.2.1"

655
plejd/types/ApiSite.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
{
"globalDependencies": {
"node": "registry:dt/node#7.0.0+20170322231424"
}
}