Adjust code to airbnb style guide, including eslint rules and prettier configuration for code base
This commit is contained in:
parent
1b55cabf63
commit
281acd6ad8
23 changed files with 919 additions and 2225 deletions
1
plejd/.eslintignore
Normal file
1
plejd/.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
39
plejd/.eslintrc.js
Normal file
39
plejd/.eslintrc.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// const path = require('path');
|
||||
|
||||
// {
|
||||
// "extends": ["airbnb-base", "plugin:prettier/recommended"],
|
||||
// "plugins": ["prettier"],
|
||||
// "rules": {
|
||||
// "prettier/prettier": "error"
|
||||
// }
|
||||
// }
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
// 'prettier',
|
||||
// 'plugin:prettier/recommended'
|
||||
],
|
||||
parser: 'babel-eslint',
|
||||
// plugins: ['prettier'],
|
||||
rules: getRules(),
|
||||
};
|
||||
|
||||
function getRules() {
|
||||
return {
|
||||
// Allows modification of properties passed to functions.
|
||||
// Notably used in array.forEach(e => {e.prop = val;})
|
||||
'no-param-reassign': ['error', { props: false }],
|
||||
// ++ operator widely used
|
||||
'no-plusplus': ['off'],
|
||||
// Hassio-Plejd team feals _ prefix is great for "private" variables.
|
||||
// They will still be available for use from the outside
|
||||
'no-underscore-dangle': ['off'],
|
||||
// Allow function hoisting to improve code readability
|
||||
'no-use-before-define': ['error', { functions: false, classes: true, variables: true }],
|
||||
// Allow direct indexing of arrays only (array[0])
|
||||
'prefer-destructuring': ['error', { array: false, object: true }],
|
||||
};
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
const fs = require('fs');
|
||||
|
||||
class Configuration {
|
||||
static _config = null;
|
||||
|
||||
static getConfiguration() {
|
||||
if (!Configuration._config) {
|
||||
const rawData = fs.readFileSync('/data/options.json');
|
||||
Configuration._config = JSON.parse(rawData);
|
||||
}
|
||||
return Configuration._config;
|
||||
static _config = null;
|
||||
|
||||
static getConfiguration() {
|
||||
if (!Configuration._config) {
|
||||
const rawData = fs.readFileSync('/data/options.json');
|
||||
Configuration._config = JSON.parse(rawData);
|
||||
}
|
||||
return Configuration._config;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Configuration;
|
||||
|
|
|
|||
203
plejd/Logger.js
203
plejd/Logger.js
|
|
@ -1,115 +1,116 @@
|
|||
const winston = require("winston");
|
||||
const { colorize, combine, label, printf, timestamp } = winston.format;
|
||||
const winston = require('winston');
|
||||
|
||||
const Configuration = require("./Configuration");
|
||||
const {
|
||||
colorize, combine, label, printf, timestamp,
|
||||
} = winston.format;
|
||||
|
||||
const LEVELS = ["error", "warn", "info", "debug", "verbose", "silly"];
|
||||
const Configuration = require('./Configuration');
|
||||
|
||||
const logFormat = printf(info => {
|
||||
if(info.stack) {
|
||||
return `${info.timestamp} ${info.level} [${info.label}] ${info.message}\n${info.stack}`;
|
||||
}
|
||||
return `${info.timestamp} ${info.level} [${info.label}] ${info.message}`;
|
||||
const LEVELS = ['error', 'warn', 'info', 'debug', 'verbose', 'silly'];
|
||||
|
||||
const logFormat = printf((info) => {
|
||||
if (info.stack) {
|
||||
return `${info.timestamp} ${info.level} [${info.label}] ${info.message}\n${info.stack}`;
|
||||
}
|
||||
return `${info.timestamp} ${info.level} [${info.label}] ${info.message}`;
|
||||
});
|
||||
|
||||
/** Winston-based logger */
|
||||
class Logger {
|
||||
constructor () {
|
||||
throw new Error("Please call createLogger instead");
|
||||
}
|
||||
constructor() {
|
||||
throw new Error('Please call createLogger instead');
|
||||
}
|
||||
|
||||
/** Created logger will follow Winston createLogger, but
|
||||
* - add module name to logger
|
||||
* - swap debug/verbose levels and omit http to mimic HA standard
|
||||
* Levels (in order): error, warn, info, debug, verbose, silly
|
||||
* */
|
||||
static getLogger(moduleName) {
|
||||
const config = Configuration.getConfiguration();
|
||||
const level = (config.logLevel && LEVELS.find(l => l.startsWith(config.logLevel[0].toLowerCase()))) || "info";
|
||||
/** Created logger will follow Winston createLogger, but
|
||||
* - add module name to logger
|
||||
* - swap debug/verbose levels and omit http to mimic HA standard
|
||||
* Levels (in order): error, warn, info, debug, verbose, silly
|
||||
* */
|
||||
static getLogger(moduleName) {
|
||||
const config = Configuration.getConfiguration();
|
||||
// eslint-disable-next-line max-len
|
||||
const level = (config.logLevel && LEVELS.find((l) => l.startsWith(config.logLevel[0].toLowerCase())))
|
||||
|| 'info';
|
||||
|
||||
const logger = winston.createLogger({
|
||||
format: combine(
|
||||
winston.format(info => {
|
||||
switch (info.level) {
|
||||
case "warning":
|
||||
info.level = "WRN";
|
||||
break;
|
||||
case "verbose":
|
||||
info.level = "VRB";
|
||||
break;
|
||||
case "debug":
|
||||
info.level = "DBG";
|
||||
break;
|
||||
default:
|
||||
info.level = info.level.substring(0,3).toUpperCase()
|
||||
}
|
||||
|
||||
return info;
|
||||
})(),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
label({ label: moduleName}),
|
||||
colorize(),
|
||||
logFormat,
|
||||
),
|
||||
level: level,
|
||||
levels: Logger.logLevels().levels,
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
]
|
||||
});
|
||||
winston.addColors(Logger.logLevels().colors);
|
||||
|
||||
|
||||
if (moduleName === "plejd-main") {
|
||||
logger.log(level, `Log level set to ${level}`);
|
||||
}
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
|
||||
static logLevels() {
|
||||
// Default (npm) levels
|
||||
// levels = {
|
||||
// error: 0,
|
||||
// warn: 1,
|
||||
// info: 2,
|
||||
// http: 3,
|
||||
// verbose: 4,
|
||||
// debug: 5,
|
||||
// silly: 6
|
||||
// }
|
||||
// colors = {
|
||||
// error: 'red',
|
||||
// warn: 'yellow',
|
||||
// info: 'green',
|
||||
// http: 'green',
|
||||
// verbose: 'cyan',
|
||||
// debug: 'blue',
|
||||
// silly: 'magenta'
|
||||
// };
|
||||
|
||||
// Mimic HA standard below
|
||||
// Debug/verbose swapped compared to npm levels, http omitted
|
||||
return {
|
||||
levels: {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
debug: 3,
|
||||
verbose: 4,
|
||||
silly: 6
|
||||
},
|
||||
colors: {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
debug: 'cyan',
|
||||
verbose: 'blue',
|
||||
silly: 'magenta'
|
||||
const logger = winston.createLogger({
|
||||
format: combine(
|
||||
winston.format((info) => {
|
||||
switch (info.level) {
|
||||
case 'warning':
|
||||
info.level = 'WRN';
|
||||
break;
|
||||
case 'verbose':
|
||||
info.level = 'VRB';
|
||||
break;
|
||||
case 'debug':
|
||||
info.level = 'DBG';
|
||||
break;
|
||||
default:
|
||||
info.level = info.level.substring(0, 3).toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
return info;
|
||||
})(),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
label({ label: moduleName }),
|
||||
colorize(),
|
||||
logFormat,
|
||||
),
|
||||
level,
|
||||
levels: Logger.logLevels().levels,
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
winston.addColors(Logger.logLevels().colors);
|
||||
|
||||
if (moduleName === 'plejd-main') {
|
||||
logger.log(level, `Log level set to ${level}`);
|
||||
}
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
static logLevels() {
|
||||
// Default (npm) levels
|
||||
// levels = {
|
||||
// error: 0,
|
||||
// warn: 1,
|
||||
// info: 2,
|
||||
// http: 3,
|
||||
// verbose: 4,
|
||||
// debug: 5,
|
||||
// silly: 6
|
||||
// }
|
||||
// colors = {
|
||||
// error: 'red',
|
||||
// warn: 'yellow',
|
||||
// info: 'green',
|
||||
// http: 'green',
|
||||
// verbose: 'cyan',
|
||||
// debug: 'blue',
|
||||
// silly: 'magenta'
|
||||
// };
|
||||
|
||||
// Mimic HA standard below
|
||||
// Debug/verbose swapped compared to npm levels, http omitted
|
||||
return {
|
||||
levels: {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
debug: 3,
|
||||
verbose: 4,
|
||||
silly: 6,
|
||||
},
|
||||
colors: {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
debug: 'cyan',
|
||||
verbose: 'blue',
|
||||
silly: 'magenta',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const Logger = require('./Logger');
|
|||
|
||||
const startTopic = 'hass/status';
|
||||
|
||||
const logger = Logger.getLogger("plejd-mqtt");
|
||||
const logger = Logger.getLogger('plejd-mqtt');
|
||||
|
||||
// #region discovery
|
||||
|
||||
|
|
@ -12,14 +12,13 @@ const discoveryPrefix = 'homeassistant';
|
|||
const nodeId = 'plejd';
|
||||
|
||||
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 getCommandTopic = plug => `${getPath(plug)}/set`;
|
||||
const getSceneEventTopic = () => `plejd/event/scene`;
|
||||
const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`;
|
||||
const getConfigPath = (plug) => `${getPath(plug)}/config`;
|
||||
const getStateTopic = (plug) => `${getPath(plug)}/state`;
|
||||
const getCommandTopic = (plug) => `${getPath(plug)}/set`;
|
||||
const getSceneEventTopic = () => 'plejd/event/scene';
|
||||
|
||||
const getDiscoveryPayload = device => ({
|
||||
const getDiscoveryPayload = (device) => ({
|
||||
schema: 'json',
|
||||
name: device.name,
|
||||
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
|
||||
|
|
@ -28,26 +27,26 @@ const getDiscoveryPayload = device => ({
|
|||
optimistic: false,
|
||||
brightness: `${device.dimmable}`,
|
||||
device: {
|
||||
identifiers: device.serialNumber + '_' + device.id,
|
||||
identifiers: `${device.serialNumber}_${device.id}`,
|
||||
manufacturer: 'Plejd',
|
||||
model: device.typeName,
|
||||
name: device.name,
|
||||
sw_version: device.version
|
||||
}
|
||||
sw_version: device.version,
|
||||
},
|
||||
});
|
||||
|
||||
const getSwitchPayload = device => ({
|
||||
const getSwitchPayload = (device) => ({
|
||||
name: device.name,
|
||||
state_topic: getStateTopic(device),
|
||||
command_topic: getCommandTopic(device),
|
||||
optimistic: false,
|
||||
device: {
|
||||
identifiers: device.serialNumber + '_' + device.id,
|
||||
identifiers: `${device.serialNumber}_${device.id}`,
|
||||
manufacturer: 'Plejd',
|
||||
model: device.typeName,
|
||||
name: device.name,
|
||||
sw_version: device.version
|
||||
}
|
||||
sw_version: device.version,
|
||||
},
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
|
@ -64,12 +63,12 @@ class MqttClient extends EventEmitter {
|
|||
}
|
||||
|
||||
init() {
|
||||
logger.info("Initializing MQTT connection for Plejd addon");
|
||||
logger.info('Initializing MQTT connection for Plejd addon');
|
||||
const self = this;
|
||||
|
||||
this.client = mqtt.connect(this.mqttBroker, {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
this.client.on('connect', () => {
|
||||
|
|
@ -88,7 +87,6 @@ class MqttClient extends EventEmitter {
|
|||
logger.error('Unable to subscribe to control topics');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
this.client.on('close', () => {
|
||||
|
|
@ -97,21 +95,19 @@ class MqttClient extends EventEmitter {
|
|||
});
|
||||
|
||||
this.client.on('message', (topic, message) => {
|
||||
//const command = message.toString();
|
||||
const command = message.toString().substring(0, 1) === '{'
|
||||
// const command = message.toString();
|
||||
const command = message.toString().substring(0, 1) === '{'
|
||||
? JSON.parse(message.toString())
|
||||
: message.toString();
|
||||
|
||||
if (topic === startTopic) {
|
||||
logger.info('Home Assistant has started. lets do discovery.');
|
||||
self.emit('connected');
|
||||
}
|
||||
else if (topic.includes('set')) {
|
||||
} else if (topic.includes('set')) {
|
||||
logger.verbose(`Got mqtt command on ${topic} - ${message}`);
|
||||
const device = self.devices.find(x => getCommandTopic(x) === topic);
|
||||
const device = self.devices.find((x) => getCommandTopic(x) === topic);
|
||||
self.emit('stateChanged', device, command);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
logger.verbose(`Warning: Got unrecognized mqtt command on ${topic} - ${message}`);
|
||||
}
|
||||
});
|
||||
|
|
@ -130,20 +126,19 @@ class MqttClient extends EventEmitter {
|
|||
devices.forEach((device) => {
|
||||
logger.debug(`Sending discovery for ${device.name}`);
|
||||
|
||||
let payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
||||
logger.info(`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`);
|
||||
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
||||
logger.info(
|
||||
`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`,
|
||||
);
|
||||
|
||||
self.deviceMap[device.id] = payload.unique_id;
|
||||
|
||||
self.client.publish(
|
||||
getConfigPath(device),
|
||||
JSON.stringify(payload)
|
||||
);
|
||||
self.client.publish(getConfigPath(device), JSON.stringify(payload));
|
||||
});
|
||||
}
|
||||
|
||||
updateState(deviceId, data) {
|
||||
const device = this.devices.find(x => x.id === deviceId);
|
||||
const device = this.devices.find((x) => x.id === deviceId);
|
||||
|
||||
if (!device) {
|
||||
logger.warn(`Unknown device id ${deviceId} - not handled by us.`);
|
||||
|
|
@ -155,36 +150,28 @@ class MqttClient extends EventEmitter {
|
|||
|
||||
if (device.type === 'switch') {
|
||||
payload = data.state === 1 ? 'ON' : 'OFF';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (device.dimmable) {
|
||||
payload = {
|
||||
state: data.state === 1 ? 'ON' : 'OFF',
|
||||
brightness: data.brightness
|
||||
}
|
||||
}
|
||||
else {
|
||||
brightness: data.brightness,
|
||||
};
|
||||
} else {
|
||||
payload = {
|
||||
state: data.state === 1 ? 'ON' : 'OFF'
|
||||
}
|
||||
state: data.state === 1 ? 'ON' : 'OFF',
|
||||
};
|
||||
}
|
||||
|
||||
payload = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
this.client.publish(
|
||||
getStateTopic(device),
|
||||
payload
|
||||
);
|
||||
this.client.publish(getStateTopic(device), payload);
|
||||
}
|
||||
|
||||
sceneTriggered(scene) {
|
||||
logger.verbose(`Scene triggered: ${scene}`);
|
||||
this.client.publish(
|
||||
getSceneEventTopic(),
|
||||
JSON.stringify({ scene: scene })
|
||||
);
|
||||
this.client.publish(getSceneEventTopic(), JSON.stringify({ scene }));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MqttClient };
|
||||
module.exports = MqttClient;
|
||||
|
|
@ -2,13 +2,13 @@ const axios = require('axios');
|
|||
const EventEmitter = require('events');
|
||||
const Logger = require('./Logger');
|
||||
|
||||
API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak';
|
||||
API_BASE_URL = 'https://cloud.plejd.com/parse/';
|
||||
API_LOGIN_URL = 'login';
|
||||
API_SITE_LIST_URL = 'functions/getSiteList';
|
||||
API_SITE_DETAILS_URL = 'functions/getSiteById';
|
||||
const API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak';
|
||||
const API_BASE_URL = 'https://cloud.plejd.com/parse/';
|
||||
const API_LOGIN_URL = 'login';
|
||||
const API_SITE_LIST_URL = 'functions/getSiteList';
|
||||
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
||||
|
||||
const logger = Logger.getLogger("plejd-api");
|
||||
const logger = Logger.getLogger('plejd-api');
|
||||
|
||||
class PlejdApi extends EventEmitter {
|
||||
constructor(siteName, username, password, includeRoomsAsLights) {
|
||||
|
|
@ -25,25 +25,24 @@ class PlejdApi extends EventEmitter {
|
|||
|
||||
login() {
|
||||
logger.info('login()');
|
||||
logger.info('logging into ' + this.siteName);
|
||||
logger.info(`logging into ${this.siteName}`);
|
||||
const self = this;
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': API_APP_ID,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.debug('sending POST to ' + API_BASE_URL + API_LOGIN_URL);
|
||||
logger.debug(`sending POST to ${API_BASE_URL}${API_LOGIN_URL}`);
|
||||
|
||||
instance.post(
|
||||
API_LOGIN_URL,
|
||||
{
|
||||
'username': this.username,
|
||||
'password': this.password
|
||||
instance
|
||||
.post(API_LOGIN_URL, {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
})
|
||||
.then((response) => {
|
||||
logger.info('got session token response');
|
||||
|
|
@ -51,20 +50,21 @@ class PlejdApi extends EventEmitter {
|
|||
|
||||
if (!self.sessionToken) {
|
||||
logger.error('No session token received');
|
||||
reject('no session token received.');
|
||||
reject(new Error('no session token received.'));
|
||||
}
|
||||
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status === 400) {
|
||||
logger.error('Server returned status 400. probably invalid credentials, please verify.');
|
||||
}
|
||||
else {
|
||||
logger.error(
|
||||
'Server returned status 400. probably invalid credentials, please verify.',
|
||||
);
|
||||
} else {
|
||||
logger.error('Unable to retrieve session token response: ', error);
|
||||
}
|
||||
|
||||
reject('unable to retrieve session token response: ' + error);
|
||||
reject(new Error(`unable to retrieve session token response: ${error}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -78,21 +78,22 @@ class PlejdApi extends EventEmitter {
|
|||
headers: {
|
||||
'X-Parse-Application-Id': API_APP_ID,
|
||||
'X-Parse-Session-Token': this.sessionToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.debug('sending POST to ' + API_BASE_URL + API_SITE_LIST_URL);
|
||||
logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_LIST_URL}`);
|
||||
|
||||
instance.post(API_SITE_LIST_URL)
|
||||
instance
|
||||
.post(API_SITE_LIST_URL)
|
||||
.then((response) => {
|
||||
logger.info('got site list response');
|
||||
const site = response.data.result.find(x => x.site.title == self.siteName);
|
||||
const site = response.data.result.find((x) => x.site.title === self.siteName);
|
||||
|
||||
if (!site) {
|
||||
logger.error('error: failed to find a site named ' + self.siteName);
|
||||
reject('failed to find a site named ' + self.siteName);
|
||||
logger.error(`error: failed to find a site named ${self.siteName}`);
|
||||
reject(new Error(`failed to find a site named ${self.siteName}`));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -100,13 +101,13 @@ class PlejdApi extends EventEmitter {
|
|||
})
|
||||
.catch((error) => {
|
||||
logger.error('error: unable to retrieve list of sites. error: ', error);
|
||||
return reject('plejd-api: unable to retrieve list of sites. error: ' + error);
|
||||
return reject(new Error(`plejd-api: unable to retrieve list of sites. error: ${error}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSite(siteId) {
|
||||
logger.info(`Get site details...`);
|
||||
logger.info('Get site details...');
|
||||
const self = this;
|
||||
|
||||
const instance = axios.create({
|
||||
|
|
@ -114,19 +115,20 @@ class PlejdApi extends EventEmitter {
|
|||
headers: {
|
||||
'X-Parse-Application-Id': API_APP_ID,
|
||||
'X-Parse-Session-Token': this.sessionToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.debug('sending POST to ' + API_BASE_URL + API_SITE_DETAILS_URL);
|
||||
logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_DETAILS_URL}`);
|
||||
|
||||
instance.post(API_SITE_DETAILS_URL, { siteId: siteId })
|
||||
instance
|
||||
.post(API_SITE_DETAILS_URL, { siteId })
|
||||
.then((response) => {
|
||||
logger.info('got site details response');
|
||||
if (response.data.result.length === 0) {
|
||||
const msg = 'no site with ID ' + siteId + ' was found.';
|
||||
logger.error('error: ' + msg);
|
||||
const msg = `no site with ID ${siteId} was found.`;
|
||||
logger.error(`error: ${msg}`);
|
||||
reject(msg);
|
||||
return;
|
||||
}
|
||||
|
|
@ -138,13 +140,13 @@ class PlejdApi extends EventEmitter {
|
|||
})
|
||||
.catch((error) => {
|
||||
logger.error('error: unable to retrieve the crypto key. error: ', error);
|
||||
return reject('plejd-api: unable to retrieve the crypto key. error: ' + error);
|
||||
return reject(new Error(`plejd-api: unable to retrieve the crypto key. error: ${error}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getDevices() {
|
||||
let devices = [];
|
||||
const devices = [];
|
||||
|
||||
logger.verbose(JSON.stringify(this.site));
|
||||
|
||||
|
|
@ -152,9 +154,9 @@ class PlejdApi extends EventEmitter {
|
|||
|
||||
for (let i = 0; i < this.site.devices.length; i++) {
|
||||
const device = this.site.devices[i];
|
||||
const deviceId = device.deviceId;
|
||||
const { deviceId } = device;
|
||||
|
||||
const settings = this.site.outputSettings.find(x => x.deviceParseId == device.objectId);
|
||||
const settings = this.site.outputSettings.find((x) => x.deviceParseId === device.objectId);
|
||||
let deviceNum = this.site.deviceAddress[deviceId];
|
||||
|
||||
if (settings) {
|
||||
|
|
@ -162,22 +164,24 @@ class PlejdApi extends EventEmitter {
|
|||
deviceNum = outputs[settings.output];
|
||||
}
|
||||
|
||||
// check if device is dimmable
|
||||
const plejdDevice = this.site.plejdDevices.find(x => x.deviceId == deviceId);
|
||||
let { name, type, dimmable } = this._getDeviceType(plejdDevice.hardwareId);
|
||||
// check if device is dimmable
|
||||
const plejdDevice = this.site.plejdDevices.find((x) => x.deviceId === deviceId);
|
||||
const deviceType = this._getDeviceType(plejdDevice.hardwareId);
|
||||
const { name, type } = deviceType;
|
||||
let { dimmable } = deviceType;
|
||||
|
||||
if (settings) {
|
||||
dimmable = settings.dimCurve != 'NonDimmable';
|
||||
dimmable = settings.dimCurve !== 'NonDimmable';
|
||||
}
|
||||
|
||||
const newDevice = {
|
||||
id: deviceNum,
|
||||
name: device.title,
|
||||
type: type,
|
||||
type,
|
||||
typeName: name,
|
||||
dimmable: dimmable,
|
||||
dimmable,
|
||||
version: plejdDevice.firmware.version,
|
||||
serialNumber: plejdDevice.deviceId
|
||||
serialNumber: plejdDevice.deviceId,
|
||||
};
|
||||
|
||||
if (newDevice.typeName === 'WPH-01') {
|
||||
|
|
@ -189,45 +193,41 @@ class PlejdApi extends EventEmitter {
|
|||
|
||||
let switchDevice = {
|
||||
id: first,
|
||||
name: device.title + ' knapp vä',
|
||||
type: type,
|
||||
name: `${device.title} knapp vä`,
|
||||
type,
|
||||
typeName: name,
|
||||
dimmable: dimmable,
|
||||
dimmable,
|
||||
version: plejdDevice.firmware.version,
|
||||
serialNumber: plejdDevice.deviceId
|
||||
serialNumber: plejdDevice.deviceId,
|
||||
};
|
||||
|
||||
if (roomDevices[device.roomId]) {
|
||||
roomDevices[device.roomId].push(switchDevice);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
roomDevices[device.roomId] = [switchDevice];
|
||||
}
|
||||
devices.push(switchDevice);
|
||||
|
||||
switchDevice = {
|
||||
id: second,
|
||||
name: device.title + ' knapp hö',
|
||||
type: type,
|
||||
name: `${device.title} knapp hö`,
|
||||
type,
|
||||
typeName: name,
|
||||
dimmable: dimmable,
|
||||
dimmable,
|
||||
version: plejdDevice.firmware.version,
|
||||
serialNumber: plejdDevice.deviceId
|
||||
serialNumber: plejdDevice.deviceId,
|
||||
};
|
||||
|
||||
if (roomDevices[device.roomId]) {
|
||||
roomDevices[device.roomId].push(switchDevice);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
roomDevices[device.roomId] = [switchDevice];
|
||||
}
|
||||
devices.push(switchDevice);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (roomDevices[device.roomId]) {
|
||||
roomDevices[device.roomId].push(newDevice);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
roomDevices[device.roomId] = [newDevice];
|
||||
}
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ class PlejdApi extends EventEmitter {
|
|||
logger.debug('includeRoomsAsLights is set to true, adding rooms too.');
|
||||
for (let i = 0; i < this.site.rooms.length; i++) {
|
||||
const room = this.site.rooms[i];
|
||||
const roomId = room.roomId;
|
||||
const { roomId } = room;
|
||||
const roomAddress = this.site.roomAddress[roomId];
|
||||
|
||||
const newDevice = {
|
||||
|
|
@ -247,7 +247,7 @@ class PlejdApi extends EventEmitter {
|
|||
name: room.title,
|
||||
type: 'light',
|
||||
typeName: 'Room',
|
||||
dimmable: roomDevices[roomId].filter(x => x.dimmable).length > 0
|
||||
dimmable: roomDevices[roomId].filter((x) => x.dimmable).length > 0,
|
||||
};
|
||||
|
||||
devices.push(newDevice);
|
||||
|
|
@ -256,8 +256,9 @@ class PlejdApi extends EventEmitter {
|
|||
}
|
||||
|
||||
// add scenes as switches
|
||||
const scenes = this.site.scenes.filter(x => x.hiddenFromSceneList == false);
|
||||
const scenes = this.site.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const scene of scenes) {
|
||||
const sceneNum = this.site.sceneIndex[scene.sceneId];
|
||||
const newScene = {
|
||||
|
|
@ -267,7 +268,7 @@ class PlejdApi extends EventEmitter {
|
|||
typeName: 'Scene',
|
||||
dimmable: false,
|
||||
version: '1.0',
|
||||
serialNumber: scene.objectId
|
||||
serialNumber: scene.objectId,
|
||||
};
|
||||
|
||||
devices.push(newScene);
|
||||
|
|
@ -276,50 +277,53 @@ class PlejdApi extends EventEmitter {
|
|||
return devices;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_getDeviceType(hardwareId) {
|
||||
switch (parseInt(hardwareId)) {
|
||||
switch (parseInt(hardwareId, 10)) {
|
||||
case 1:
|
||||
case 11:
|
||||
return { name: "DIM-01", type: 'light', dimmable: true };
|
||||
return { name: 'DIM-01', type: 'light', dimmable: true };
|
||||
case 2:
|
||||
return { name: "DIM-02", type: 'light', dimmable: true };
|
||||
return { name: 'DIM-02', type: 'light', dimmable: true };
|
||||
case 3:
|
||||
return { name: "CTR-01", type: 'light', dimmable: false };
|
||||
return { name: 'CTR-01', type: 'light', dimmable: false };
|
||||
case 4:
|
||||
return { name: "GWY-01", type: 'sensor', dimmable: false };
|
||||
return { name: 'GWY-01', type: 'sensor', dimmable: false };
|
||||
case 5:
|
||||
return { name: "LED-10", type: 'light', dimmable: true };
|
||||
return { name: 'LED-10', type: 'light', dimmable: true };
|
||||
case 6:
|
||||
return { name: "WPH-01", type: 'switch', dimmable: false };
|
||||
return { name: 'WPH-01', type: 'switch', dimmable: false };
|
||||
case 7:
|
||||
return { name: "REL-01", type: 'switch', dimmable: false };
|
||||
return { name: 'REL-01', type: 'switch', dimmable: false };
|
||||
case 8:
|
||||
case 9:
|
||||
// Unknown
|
||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
||||
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||
case 10:
|
||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
||||
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||
case 12:
|
||||
// Unknown
|
||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
||||
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||
case 13:
|
||||
return { name: "Generic", type: 'light', dimmable: false };
|
||||
return { name: 'Generic', type: 'light', dimmable: false };
|
||||
case 14:
|
||||
case 15:
|
||||
case 16:
|
||||
// Unknown
|
||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
||||
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||
case 17:
|
||||
return { name: "REL-01", type: 'switch', dimmable: false };
|
||||
return { name: 'REL-01', type: 'switch', dimmable: false };
|
||||
case 18:
|
||||
return { name: "REL-02", type: 'switch', dimmable: false };
|
||||
return { name: 'REL-02', type: 'switch', dimmable: false };
|
||||
case 19:
|
||||
// Unknown
|
||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
||||
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||
case 20:
|
||||
return { name: "SPR-01", type: 'switch', dimmable: false };
|
||||
return { name: 'SPR-01', type: 'switch', dimmable: false };
|
||||
default:
|
||||
throw new Error(`Unknown device type with id ${hardwareId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PlejdApi };
|
||||
module.exports = PlejdApi;
|
||||
|
|
@ -4,8 +4,7 @@ const xor = require('buffer-xor');
|
|||
const EventEmitter = require('events');
|
||||
const Logger = require('./Logger');
|
||||
|
||||
const logger = Logger.getLogger("plejd-ble");
|
||||
|
||||
const logger = Logger.getLogger('plejd-ble');
|
||||
|
||||
// UUIDs
|
||||
const PLEJD_SERVICE = '31ba0001-6085-4726-be45-040c957391b5';
|
||||
|
|
@ -32,7 +31,7 @@ const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting
|
|||
const MAX_RETRY_COUNT = 5; // Could be made a setting
|
||||
|
||||
class PlejdService extends EventEmitter {
|
||||
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime, keepAlive = false) {
|
||||
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime) {
|
||||
super();
|
||||
|
||||
logger.info('Starting Plejd BLE, resetting all device states.');
|
||||
|
|
@ -59,7 +58,7 @@ class PlejdService extends EventEmitter {
|
|||
lastData: null,
|
||||
lastDataProperties: null,
|
||||
auth: null,
|
||||
ping: null
|
||||
ping: null,
|
||||
};
|
||||
|
||||
this.bus = dbus.systemBus();
|
||||
|
|
@ -82,7 +81,7 @@ class PlejdService extends EventEmitter {
|
|||
lastData: null,
|
||||
lastDataProperties: null,
|
||||
auth: null,
|
||||
ping: null
|
||||
ping: null,
|
||||
};
|
||||
|
||||
clearInterval(this.pingRef);
|
||||
|
|
@ -94,18 +93,20 @@ class PlejdService extends EventEmitter {
|
|||
|
||||
// We need to find the ble interface which implements the Adapter1 interface
|
||||
const managedObjects = await this.objectManager.GetManagedObjects();
|
||||
let result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID);
|
||||
const result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID);
|
||||
|
||||
if (result) {
|
||||
this.adapter = result[1];
|
||||
}
|
||||
|
||||
if (!this.adapter) {
|
||||
logger.error('unable to find a bluetooth adapter that is compatible.');
|
||||
return;
|
||||
logger.error('Unable to find a bluetooth adapter that is compatible.');
|
||||
return Promise.reject(new Error('Unable to find a bluetooth adapter that is compatible.'));
|
||||
}
|
||||
|
||||
for (let path of Object.keys(managedObjects)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const path of Object.keys(managedObjects)) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
const interfaces = Object.keys(managedObjects[path]);
|
||||
|
||||
if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
||||
|
|
@ -115,70 +116,81 @@ class PlejdService extends EventEmitter {
|
|||
const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value;
|
||||
|
||||
if (connected) {
|
||||
logger.info('disconnecting ' + path);
|
||||
logger.info(`disconnecting ${path}`);
|
||||
await device.Disconnect();
|
||||
}
|
||||
|
||||
await this.adapter.RemoveDevice(path);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
}
|
||||
|
||||
this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this));
|
||||
|
||||
this.adapter.SetDiscoveryFilter({
|
||||
'UUIDs': new dbus.Variant('as', [PLEJD_SERVICE]),
|
||||
'Transport': new dbus.Variant('s', 'le')
|
||||
UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]),
|
||||
Transport: new dbus.Variant('s', 'le'),
|
||||
});
|
||||
|
||||
try {
|
||||
await this.adapter.StartDiscovery();
|
||||
} catch (err) {
|
||||
logger.error('failed to start discovery. Make sure no other add-on is currently scanning.');
|
||||
return;
|
||||
logger.error('Failed to start discovery. Make sure no other add-on is currently scanning.');
|
||||
return Promise.reject(
|
||||
new Error('Failed to start discovery. Make sure no other add-on is currently scanning.'),
|
||||
);
|
||||
}
|
||||
return new Promise(resolve =>
|
||||
setTimeout(() => resolve(
|
||||
this._internalInit().catch((err) => { logger.error('InternalInit exception! Will rethrow.', err); throw err; })
|
||||
), this.connectionTimeout * 1000
|
||||
)
|
||||
);
|
||||
return new Promise((resolve) => setTimeout(
|
||||
() => resolve(
|
||||
this._internalInit().catch((err) => {
|
||||
logger.error('InternalInit exception! Will rethrow.', err);
|
||||
throw err;
|
||||
}),
|
||||
),
|
||||
this.connectionTimeout * 1000,
|
||||
));
|
||||
}
|
||||
|
||||
async _internalInit() {
|
||||
logger.debug(`Got ${this.bleDevices.length} device(s).`);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const plejd of this.bleDevices) {
|
||||
logger.debug(`Inspecting ${plejd['path']}`);
|
||||
/* eslint-disable no-await-in-loop */
|
||||
logger.debug(`Inspecting ${plejd.path}`);
|
||||
|
||||
try {
|
||||
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd['path']);
|
||||
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd.path);
|
||||
const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID);
|
||||
const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE);
|
||||
|
||||
plejd['rssi'] = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value;
|
||||
plejd['instance'] = device;
|
||||
plejd.rssi = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value;
|
||||
plejd.instance = device;
|
||||
|
||||
const segments = plejd['path'].split('/');
|
||||
const segments = plejd.path.split('/');
|
||||
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
|
||||
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
|
||||
plejd['device'] = this.devices.find(x => x.serialNumber === fixedPlejdPath);
|
||||
plejd.device = this.devices.find((x) => x.serialNumber === fixedPlejdPath);
|
||||
|
||||
logger.debug(`Discovered ${plejd['path']} with rssi ${plejd['rssi']}`);
|
||||
logger.debug(`Discovered ${plejd.path} with rssi ${plejd.rssi}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed inspecting ${plejd['path']}. `, err);
|
||||
logger.error(`Failed inspecting ${plejd.path}. `, err);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
}
|
||||
|
||||
const sortedDevices = this.bleDevices.sort((a, b) => b['rssi'] - a['rssi']);
|
||||
const sortedDevices = this.bleDevices.sort((a, b) => b.rssi - a.rssi);
|
||||
let connectedDevice = null;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const plejd of sortedDevices) {
|
||||
try {
|
||||
if (plejd['instance']) {
|
||||
logger.info(`Connecting to ${plejd['path']}`);
|
||||
await plejd['instance'].Connect();
|
||||
if (plejd.instance) {
|
||||
logger.info(`Connecting to ${plejd.path}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await plejd.instance.Connect();
|
||||
connectedDevice = plejd;
|
||||
break
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Warning: unable to connect, will retry. ', err);
|
||||
|
|
@ -194,11 +206,13 @@ class PlejdService extends EventEmitter {
|
|||
async _getInterface(managedObjects, iface) {
|
||||
const managedPaths = Object.keys(managedObjects);
|
||||
|
||||
for (let path of managedPaths) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const path of managedPaths) {
|
||||
const pathInterfaces = Object.keys(managedObjects[path]);
|
||||
if (pathInterfaces.indexOf(iface) > -1) {
|
||||
logger.debug(`Found BLE interface '${iface}' at ${path}`);
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
||||
return [path, adapterObject.getInterface(iface), adapterObject];
|
||||
} catch (err) {
|
||||
|
|
@ -215,10 +229,10 @@ class PlejdService extends EventEmitter {
|
|||
const interfaceKeys = Object.keys(interfaces);
|
||||
|
||||
if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
||||
if (interfaces[BLUEZ_DEVICE_ID]['UUIDs'].value.indexOf(PLEJD_SERVICE) > -1) {
|
||||
if (interfaces[BLUEZ_DEVICE_ID].UUIDs.value.indexOf(PLEJD_SERVICE) > -1) {
|
||||
logger.debug(`Found Plejd service on ${path}`);
|
||||
this.bleDevices.push({
|
||||
'path': path
|
||||
path,
|
||||
});
|
||||
} else {
|
||||
logger.error('Uh oh, no Plejd device!');
|
||||
|
|
@ -228,17 +242,24 @@ class PlejdService extends EventEmitter {
|
|||
|
||||
turnOn(deviceId, command) {
|
||||
const deviceName = this._getDeviceName(deviceId);
|
||||
logger.info(`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${command.transition ? `, transition: ${command.transition}` : ''}`);
|
||||
logger.info(
|
||||
`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${
|
||||
command.transition ? `, transition: ${command.transition}` : ''
|
||||
}`,
|
||||
);
|
||||
this._transitionTo(deviceId, command.brightness, command.transition, deviceName);
|
||||
}
|
||||
|
||||
turnOff(deviceId, command) {
|
||||
const deviceName = this._getDeviceName(deviceId);
|
||||
logger.info(`Plejd got turn off command for ${deviceName} (${deviceId}), brightness ${command.brightness}${command.transition ? `, transition: ${command.transition}` : ''}`);
|
||||
logger.info(
|
||||
`Plejd got turn off command for ${deviceName} (${deviceId}), brightness ${
|
||||
command.brightness
|
||||
}${command.transition ? `, transition: ${command.transition}` : ''}`,
|
||||
);
|
||||
this._transitionTo(deviceId, 0, command.transition, deviceName);
|
||||
}
|
||||
|
||||
|
||||
_clearDeviceTransitionTimer(deviceId) {
|
||||
if (this.bleDeviceTransitionTimers[deviceId]) {
|
||||
clearInterval(this.bleDeviceTransitionTimers[deviceId]);
|
||||
|
|
@ -246,55 +267,81 @@ class PlejdService extends EventEmitter {
|
|||
}
|
||||
|
||||
_transitionTo(deviceId, targetBrightness, transition, deviceName) {
|
||||
const initialBrightness = this.plejdDevices[deviceId] ? this.plejdDevices[deviceId].state && this.plejdDevices[deviceId].dim : null;
|
||||
const initialBrightness = this.plejdDevices[deviceId]
|
||||
? this.plejdDevices[deviceId].state && this.plejdDevices[deviceId].dim
|
||||
: null;
|
||||
this._clearDeviceTransitionTimer(deviceId);
|
||||
|
||||
const isDimmable = this.devices.find(d => d.id === deviceId).dimmable;
|
||||
const isDimmable = this.devices.find((d) => d.id === deviceId).dimmable;
|
||||
|
||||
if (transition > 1 && isDimmable && (initialBrightness || initialBrightness === 0) && (targetBrightness || targetBrightness === 0) && targetBrightness !== initialBrightness) {
|
||||
if (
|
||||
transition > 1
|
||||
&& isDimmable
|
||||
&& (initialBrightness || initialBrightness === 0)
|
||||
&& (targetBrightness || targetBrightness === 0)
|
||||
&& targetBrightness !== initialBrightness
|
||||
) {
|
||||
// Transition time set, known initial and target brightness
|
||||
// Calculate transition interval time based on delta brightness and max steps per second
|
||||
// During transition, measure actual transition interval time and adjust stepping continously
|
||||
// If transition <= 1 second, Plejd will do a better job than we can in transitioning so transitioning will be skipped
|
||||
// If transition <= 1 second, Plejd will do a better job
|
||||
// than we can in transitioning so transitioning will be skipped
|
||||
|
||||
const deltaBrightness = targetBrightness - initialBrightness;
|
||||
const transitionSteps = Math.min(Math.abs(deltaBrightness), MAX_TRANSITION_STEPS_PER_SECOND * transition);
|
||||
const transitionInterval = transition * 1000 / transitionSteps;
|
||||
const transitionSteps = Math.min(
|
||||
Math.abs(deltaBrightness),
|
||||
MAX_TRANSITION_STEPS_PER_SECOND * transition,
|
||||
);
|
||||
const transitionInterval = (transition * 1000) / transitionSteps;
|
||||
|
||||
logger.debug(`transitioning from ${initialBrightness} to ${targetBrightness} ${transition ? 'in ' + transition + ' seconds' : ''}.`);
|
||||
logger.verbose(`delta brightness ${deltaBrightness}, steps ${transitionSteps}, interval ${transitionInterval} ms`);
|
||||
logger.debug(
|
||||
`transitioning from ${initialBrightness} to ${targetBrightness} ${
|
||||
transition ? `in ${transition} seconds` : ''
|
||||
}.`,
|
||||
);
|
||||
logger.verbose(
|
||||
`delta brightness ${deltaBrightness}, steps ${transitionSteps}, interval ${transitionInterval} ms`,
|
||||
);
|
||||
|
||||
const dtStart = new Date();
|
||||
|
||||
let nSteps = 0;
|
||||
|
||||
this.bleDeviceTransitionTimers[deviceId] = setInterval(() => {
|
||||
let tElapsedMs = new Date().getTime() - dtStart.getTime();
|
||||
const tElapsedMs = new Date().getTime() - dtStart.getTime();
|
||||
let tElapsed = tElapsedMs / 1000;
|
||||
|
||||
if (tElapsed > transition || tElapsed < 0) {
|
||||
tElapsed = transition;
|
||||
}
|
||||
|
||||
let newBrightness = parseInt(initialBrightness + deltaBrightness * tElapsed / transition);
|
||||
let newBrightness = Math.round(
|
||||
initialBrightness + (deltaBrightness * tElapsed) / transition,
|
||||
);
|
||||
|
||||
if (tElapsed === transition) {
|
||||
nSteps++;
|
||||
this._clearDeviceTransitionTimer(deviceId);
|
||||
newBrightness = targetBrightness;
|
||||
logger.debug(`Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${tElapsedMs / (nSteps || 1)} ms.`);
|
||||
logger.debug(
|
||||
`Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
|
||||
tElapsedMs / (nSteps || 1)
|
||||
} ms.`,
|
||||
);
|
||||
this._setBrightness(deviceId, newBrightness, true, deviceName);
|
||||
} else {
|
||||
nSteps++;
|
||||
logger.verbose(`Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`);
|
||||
logger.verbose(
|
||||
`Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
|
||||
);
|
||||
this._setBrightness(deviceId, newBrightness, false, deviceName);
|
||||
}
|
||||
|
||||
}, transitionInterval);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (transition && isDimmable) {
|
||||
logger.debug(`Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`)
|
||||
logger.debug(
|
||||
`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);
|
||||
}
|
||||
|
|
@ -305,33 +352,45 @@ class PlejdService extends EventEmitter {
|
|||
let log = '';
|
||||
|
||||
if (!brightness && brightness !== 0) {
|
||||
logger.debug(`Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`);
|
||||
payload = Buffer.from((deviceId).toString(16).padStart(2, '0') + '0110009701', 'hex');
|
||||
logger.debug(
|
||||
`Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`,
|
||||
);
|
||||
payload = Buffer.from(`${deviceId.toString(16).padStart(2, '0')}0110009701`, 'hex');
|
||||
log = 'ON';
|
||||
}
|
||||
else {
|
||||
if (brightness <= 0) {
|
||||
logger.debug(`Queueing turn off ${deviceId}`);
|
||||
payload = Buffer.from((deviceId).toString(16).padStart(2, '0') + '0110009700', 'hex');
|
||||
log = 'OFF';
|
||||
}
|
||||
else {
|
||||
if (brightness > 255) {
|
||||
brightness = 255;
|
||||
}
|
||||
|
||||
logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`);
|
||||
const brightnessVal = (brightness << 8) | brightness;
|
||||
payload = Buffer.from((deviceId).toString(16).padStart(2, '0') + '0110009801' + (brightnessVal).toString(16).padStart(4, '0'), 'hex');
|
||||
log = `DIM ${brightness}`;
|
||||
} else if (brightness <= 0) {
|
||||
logger.debug(`Queueing turn off ${deviceId}`);
|
||||
payload = Buffer.from(`${deviceId.toString(16).padStart(2, '0')}0110009700`, 'hex');
|
||||
log = 'OFF';
|
||||
} else {
|
||||
if (brightness > 255) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
brightness = 255;
|
||||
}
|
||||
|
||||
logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const brightnessVal = (brightness << 8) | brightness;
|
||||
payload = Buffer.from(
|
||||
`${deviceId.toString(16).padStart(2, '0')}0110009801${brightnessVal
|
||||
.toString(16)
|
||||
.padStart(4, '0')}`,
|
||||
'hex',
|
||||
);
|
||||
log = `DIM ${brightness}`;
|
||||
}
|
||||
this.writeQueue.unshift({deviceId, log, shouldRetry, payload});
|
||||
this.writeQueue.unshift({
|
||||
deviceId,
|
||||
log,
|
||||
shouldRetry,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
triggerScene(sceneIndex) {
|
||||
const sceneName = this._getDeviceName(sceneIndex);
|
||||
logger.info(`Triggering scene ${sceneName} (${sceneIndex}). Scene name might be misleading if there is a device with the same numeric id.`);
|
||||
logger.info(
|
||||
`Triggering scene ${sceneName} (${sceneIndex}). Scene name might be misleading if there is a device with the same numeric id.`,
|
||||
);
|
||||
this.sceneManager.executeScene(sceneIndex, this);
|
||||
}
|
||||
|
||||
|
|
@ -356,27 +415,35 @@ class PlejdService extends EventEmitter {
|
|||
|
||||
// After we've authenticated, we need to hook up the event listener
|
||||
// for changes to lastData.
|
||||
this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this));
|
||||
this.characteristics.lastDataProperties.on(
|
||||
'PropertiesChanged',
|
||||
this.onLastDataUpdated.bind(this),
|
||||
);
|
||||
this.characteristics.lastData.StartNotify();
|
||||
}
|
||||
|
||||
async throttledInit(delay) {
|
||||
if(this.initInProgress){
|
||||
logger.debug('ThrottledInit already in progress. Skipping this call and returning existing promise.')
|
||||
if (this.initInProgress) {
|
||||
logger.debug(
|
||||
'ThrottledInit already in progress. Skipping this call and returning existing promise.',
|
||||
);
|
||||
return this.initInProgress;
|
||||
}
|
||||
this.initInProgress = new Promise((resolve) => setTimeout(async () => {
|
||||
const result = await this.init().catch((err) => { logger.error('TrottledInit exception calling init(). Will re-throw.', err); throw err; });
|
||||
this.initInProgress = null;
|
||||
resolve(result)
|
||||
}, delay))
|
||||
return this.initInProgress;
|
||||
this.initInProgress = new Promise((resolve) => setTimeout(async () => {
|
||||
const result = await this.init().catch((err) => {
|
||||
logger.error('TrottledInit exception calling init(). Will re-throw.', err);
|
||||
throw err;
|
||||
});
|
||||
this.initInProgress = null;
|
||||
resolve(result);
|
||||
}, delay));
|
||||
return this.initInProgress;
|
||||
}
|
||||
|
||||
async write(data) {
|
||||
if (!data || !this.plejdService || !this.characteristics.data) {
|
||||
logger.debug('data, plejdService or characteristics not available. Cannot write()');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -386,7 +453,7 @@ class PlejdService extends EventEmitter {
|
|||
return true;
|
||||
} catch (err) {
|
||||
if (err.message === 'In Progress') {
|
||||
logger.debug('Write failed due to \'In progress\' ', err);
|
||||
logger.debug("Write failed due to 'In progress' ", err);
|
||||
} else {
|
||||
logger.debug('Write failed ', err);
|
||||
}
|
||||
|
|
@ -405,22 +472,25 @@ class PlejdService extends EventEmitter {
|
|||
}, 3000);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onPingSuccess(nr) {
|
||||
logger.silly('pong: ' + nr);
|
||||
logger.silly(`pong: ${nr}`);
|
||||
}
|
||||
|
||||
async onPingFailed(error) {
|
||||
logger.debug('onPingFailed(' + error + ')');
|
||||
logger.debug(`onPingFailed(${error})`);
|
||||
logger.info('ping failed, reconnecting.');
|
||||
|
||||
clearInterval(this.pingRef);
|
||||
await this.init();
|
||||
return this.init().catch((err) => {
|
||||
logger.error('onPingFailed exception calling init(). Will swallow error.', err);
|
||||
});
|
||||
}
|
||||
|
||||
async ping() {
|
||||
logger.silly('ping()');
|
||||
|
||||
var ping = crypto.randomBytes(1);
|
||||
const ping = crypto.randomBytes(1);
|
||||
let pong = null;
|
||||
|
||||
try {
|
||||
|
|
@ -432,9 +502,10 @@ class PlejdService extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (((ping[0] + 1) & 0xff) !== pong[0]) {
|
||||
logger.error('Plejd ping failed');
|
||||
this.emit('pingFailed', 'plejd ping failed ' + ping[0] + ' - ' + pong[0]);
|
||||
this.emit('pingFailed', `plejd ping failed ${ping[0]} - ${pong[0]}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -453,26 +524,34 @@ class PlejdService extends EventEmitter {
|
|||
while (this.writeQueue.length > 0) {
|
||||
const queueItem = this.writeQueue.pop();
|
||||
const deviceName = this._getDeviceName(queueItem.deviceId);
|
||||
logger.debug(`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log}. Total queue length: ${this.writeQueue.length}`);
|
||||
logger.debug(
|
||||
`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log}. Total queue length: ${this.writeQueue.length}`,
|
||||
);
|
||||
|
||||
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) {
|
||||
logger.verbose(`Skipping ${deviceName} (${queueItem.deviceId}) ${queueItem.log} due to more recent command in queue.`);
|
||||
continue; // Skip commands if new ones exist for the same deviceId, but still process all messages in order
|
||||
}
|
||||
|
||||
const success = await this.write(queueItem.payload);
|
||||
if (!success && queueItem.shouldRetry) {
|
||||
queueItem.retryCount = (queueItem.retryCount || 0) + 1;
|
||||
logger.debug(`Will retry command, count failed so far ${queueItem.retryCount}`);
|
||||
if (queueItem.retryCount <= MAX_RETRY_COUNT) {
|
||||
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.log} failed.`);
|
||||
break;
|
||||
}
|
||||
if (queueItem.retryCount > 1) {
|
||||
break; // First retry directly, consecutive after writeQueueWaitTime ms
|
||||
logger.verbose(
|
||||
`Skipping ${deviceName} (${queueItem.deviceId}) `
|
||||
+ `${queueItem.log} due to more recent command in queue.`,
|
||||
);
|
||||
// Skip commands if new ones exist for the same deviceId
|
||||
// still process all messages in order
|
||||
} else {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const success = await this.write(queueItem.payload);
|
||||
if (!success && queueItem.shouldRetry) {
|
||||
queueItem.retryCount = (queueItem.retryCount || 0) + 1;
|
||||
logger.debug(`Will retry command, count failed so far ${queueItem.retryCount}`);
|
||||
if (queueItem.retryCount <= MAX_RETRY_COUNT) {
|
||||
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.log} failed.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (queueItem.retryCount > 1) {
|
||||
break; // First retry directly, consecutive after writeQueueWaitTime ms
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -485,7 +564,6 @@ class PlejdService extends EventEmitter {
|
|||
|
||||
async _processPlejdService(path, characteristics) {
|
||||
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
||||
const service = await proxyObject.getInterface(GATT_SERVICE_ID);
|
||||
const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE);
|
||||
|
||||
const uuid = (await properties.Get(GATT_SERVICE_ID, 'UUID')).value;
|
||||
|
|
@ -498,15 +576,12 @@ class PlejdService extends EventEmitter {
|
|||
const regex = /dev_([0-9A-F_]+)$/;
|
||||
const dirtyAddr = regex.exec(dev);
|
||||
const addr = this._reverseBuffer(
|
||||
Buffer.from(
|
||||
String(dirtyAddr[1])
|
||||
.replace(/\-/g, '')
|
||||
.replace(/\_/g, '')
|
||||
.replace(/\:/g, ''), 'hex'
|
||||
)
|
||||
Buffer.from(String(dirtyAddr[1]).replace(/-/g, '').replace(/_/g, '').replace(/:/g, ''), 'hex'),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const chPath of characteristics) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
const chProxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, chPath);
|
||||
const ch = await chProxyObject.getInterface(GATT_CHRC_ID);
|
||||
const prop = await chProxyObject.getInterface(DBUS_PROP_INTERFACE);
|
||||
|
|
@ -527,10 +602,11 @@ class PlejdService extends EventEmitter {
|
|||
logger.debug('found PING characteristic.');
|
||||
this.characteristics.ping = ch;
|
||||
}
|
||||
/* eslint-eslint no-await-in-loop */
|
||||
}
|
||||
|
||||
return {
|
||||
addr: addr
|
||||
addr,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -543,8 +619,9 @@ class PlejdService extends EventEmitter {
|
|||
|
||||
const objects = await this.objectManager.GetManagedObjects();
|
||||
const paths = Object.keys(objects);
|
||||
let characteristics = [];
|
||||
const characteristics = [];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const path of paths) {
|
||||
const interfaces = Object.keys(objects[path]);
|
||||
if (interfaces.indexOf(GATT_CHRC_ID) > -1) {
|
||||
|
|
@ -552,17 +629,19 @@ class PlejdService extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const path of paths) {
|
||||
const interfaces = Object.keys(objects[path]);
|
||||
if (interfaces.indexOf(GATT_SERVICE_ID) > -1) {
|
||||
let chPaths = [];
|
||||
const chPaths = [];
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const c of characteristics) {
|
||||
if (c.startsWith(path + '/')) {
|
||||
if (c.startsWith(`${path}/`)) {
|
||||
chPaths.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('trying ' + chPaths.length + ' characteristics');
|
||||
logger.info(`trying ${chPaths.length} characteristics`);
|
||||
|
||||
this.plejdService = await this._processPlejdService(path, chPaths);
|
||||
if (this.plejdService) {
|
||||
|
|
@ -572,7 +651,7 @@ class PlejdService extends EventEmitter {
|
|||
}
|
||||
|
||||
if (!this.plejdService) {
|
||||
logger.info('warning: wasn\'t able to connect to Plejd, will retry.');
|
||||
logger.info("warning: wasn't able to connect to Plejd, will retry.");
|
||||
this.emit('connectFailed');
|
||||
return;
|
||||
}
|
||||
|
|
@ -583,10 +662,11 @@ class PlejdService extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
this.connectedDevice = device['device'];
|
||||
this.connectedDevice = device.device;
|
||||
await this.authenticate();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async onLastDataUpdated(iface, properties, invalidated) {
|
||||
if (iface !== GATT_CHRC_ID) {
|
||||
return;
|
||||
|
|
@ -597,7 +677,7 @@ class PlejdService extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const value = await properties['Value'];
|
||||
const value = await properties.Value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -609,6 +689,7 @@ class PlejdService extends EventEmitter {
|
|||
// What is bytes 2-3?
|
||||
const cmd = decoded.toString('hex', 3, 5);
|
||||
const state = parseInt(decoded.toString('hex', 5, 6), 10); // Overflows for command 0x001b, scene command
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const data2 = parseInt(decoded.toString('hex', 6, 8), 16) >> 8;
|
||||
|
||||
if (decoded.length < 5) {
|
||||
|
|
@ -619,7 +700,9 @@ class PlejdService extends EventEmitter {
|
|||
|
||||
const deviceName = this._getDeviceName(deviceId);
|
||||
logger.verbose(`Raw event received: ${decoded.toString('hex')}`);
|
||||
logger.verbose(`Device ${deviceId}, cmd ${cmd.toString('hex')}, state ${state}, dim/data2 ${data2}`);
|
||||
logger.verbose(
|
||||
`Device ${deviceId}, cmd ${cmd.toString('hex')}, state ${state}, dim/data2 ${data2}`,
|
||||
);
|
||||
|
||||
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
|
||||
const dim = data2;
|
||||
|
|
@ -627,37 +710,37 @@ class PlejdService extends EventEmitter {
|
|||
logger.debug(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`);
|
||||
|
||||
this.emit('stateChanged', deviceId, {
|
||||
state: state,
|
||||
brightness: dim
|
||||
state,
|
||||
brightness: dim,
|
||||
});
|
||||
|
||||
this.plejdDevices[deviceId] = {
|
||||
state: state,
|
||||
dim: dim
|
||||
state,
|
||||
dim,
|
||||
};
|
||||
logger.verbose(`All states: ${JSON.stringify(this.plejdDevices)}`);
|
||||
} else if (cmd === BLE_CMD_STATE_CHANGE) {
|
||||
logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`);
|
||||
this.emit('stateChanged', deviceId, {
|
||||
state: state
|
||||
state,
|
||||
});
|
||||
this.plejdDevices[deviceId] = {
|
||||
state: state,
|
||||
dim: 0
|
||||
state,
|
||||
dim: 0,
|
||||
};
|
||||
logger.verbose(`All states: ${this.plejdDevices}`);
|
||||
} else if (cmd === BLE_CMD_SCENE_TRIG) {
|
||||
const sceneId = parseInt(decoded.toString('hex', 5, 6), 16);
|
||||
const sceneName = this._getDeviceName(sceneId);
|
||||
|
||||
logger.debug(`${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`);
|
||||
logger.debug(
|
||||
`${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`,
|
||||
);
|
||||
|
||||
this.emit('sceneTriggered', deviceId, sceneId);
|
||||
}
|
||||
else if (cmd === '001b') {
|
||||
} else if (cmd === '001b') {
|
||||
logger.silly('Command 001b seems to be some kind of often repeating ping/mesh data');
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
logger.verbose(`Command ${cmd.toString('hex')} unknown. Device ${deviceName} (${deviceId})`);
|
||||
}
|
||||
}
|
||||
|
|
@ -670,6 +753,7 @@ class PlejdService extends EventEmitter {
|
|||
this.on('pingSuccess', this.onPingSuccess.bind(self));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_createChallengeResponse(key, challenge) {
|
||||
const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest();
|
||||
const part1 = intermediate.subarray(0, 16);
|
||||
|
|
@ -680,18 +764,20 @@ class PlejdService extends EventEmitter {
|
|||
return resp;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_encryptDecrypt(key, addr, data) {
|
||||
var buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]);
|
||||
const buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]);
|
||||
|
||||
var cipher = crypto.createCipheriv('aes-128-ecb', key, '');
|
||||
const cipher = crypto.createCipheriv('aes-128-ecb', key, '');
|
||||
cipher.setAutoPadding(false);
|
||||
|
||||
var ct = cipher.update(buf).toString('hex');
|
||||
let ct = cipher.update(buf).toString('hex');
|
||||
ct += cipher.final().toString('hex');
|
||||
ct = Buffer.from(ct, 'hex');
|
||||
|
||||
var output = '';
|
||||
for (var i = 0, length = data.length; i < length; i++) {
|
||||
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]);
|
||||
}
|
||||
|
||||
|
|
@ -699,18 +785,19 @@ class PlejdService extends EventEmitter {
|
|||
}
|
||||
|
||||
_getDeviceName(deviceId) {
|
||||
return (this.devices.find(d => d.id === deviceId) || {}).name;
|
||||
return (this.devices.find((d) => d.id === deviceId) || {}).name;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_reverseBuffer(src) {
|
||||
var buffer = Buffer.allocUnsafe(src.length)
|
||||
const buffer = Buffer.allocUnsafe(src.length);
|
||||
|
||||
for (var i = 0, j = src.length - 1; i <= j; ++i, --j) {
|
||||
buffer[i] = src[j]
|
||||
buffer[j] = src[i]
|
||||
for (let i = 0, j = src.length - 1; i <= j; ++i, --j) {
|
||||
buffer[i] = src[j];
|
||||
buffer[j] = src[i];
|
||||
}
|
||||
|
||||
return buffer
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
171
plejd/README.md
171
plejd/README.md
|
|
@ -1,4 +1,5 @@
|
|||
# Hass.io Plejd add-on
|
||||
|
||||
Hass.io add-on for Plejd home automation devices. Gives you the ability to control the Plejd home automation devices through Home Assistant.
|
||||
It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range.
|
||||
|
||||
|
|
@ -15,54 +16,68 @@ I am in no way affiliated with Plejd and am solely doing this as a hobby project
|
|||
[](https://gitter.im/hassio-plejd/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started, make sure that the following requirements are met:
|
||||
|
||||
### Requirements
|
||||
* A Bluetooth device (BLE), for eg. the built-in device in Raspberry Pi 4.
|
||||
* An MQTT broker (the Mosquitto Hass.io add-on works perfectly well).
|
||||
|
||||
- A Bluetooth device (BLE), for eg. the built-in device in Raspberry Pi 4.
|
||||
- An MQTT broker (the Mosquitto Hass.io add-on works perfectly well).
|
||||
|
||||
### Tested on
|
||||
|
||||
The add-on has been tested on the following platforms:
|
||||
* Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
||||
* Raspberry Pi 4 with Hass.io
|
||||
|
||||
- Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
||||
- Raspberry Pi 4 with Hass.io
|
||||
|
||||
#### Tested Plejd devices
|
||||
* DIM-01
|
||||
* DIM-02
|
||||
* LED-10
|
||||
* CTR-01
|
||||
* REL-01
|
||||
* REL-02
|
||||
|
||||
- DIM-01
|
||||
- DIM-02
|
||||
- LED-10
|
||||
- CTR-01
|
||||
- REL-01
|
||||
- REL-02
|
||||
|
||||
### Easy Installation
|
||||
|
||||
Browse to your Home Assistant installation in a web browser and click on `Hass.io` in the navigation bar to the left.
|
||||
* Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||
* Click on `Add-on Store` in the top navigation bar of that page.
|
||||
* Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`.
|
||||
* Scroll down and you should find a Plejd add-on that can be installed. Open that and install.
|
||||
* Enjoy!
|
||||
|
||||
- Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||
- Click on `Add-on Store` in the top navigation bar of that page.
|
||||
- Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`.
|
||||
- Scroll down and you should find a Plejd add-on that can be installed. Open that and install.
|
||||
- Enjoy!
|
||||
|
||||
### Manual Installation
|
||||
|
||||
Browse your Hass.io installation using a tool that allows you to manage files, for eg. SMB or an SFTP client etc.
|
||||
* Open the `/addon` directory
|
||||
* Create a new folder named `hassio-plejd`
|
||||
* Copy all files from this repository into that newly created one.
|
||||
* Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||
* Click on `Add-on Store` in the top navigation bar of that page.
|
||||
* Click on the refresh button in the upper right corner.
|
||||
* A new Local Add-on should appear named Plejd. Open that and install.
|
||||
* Enjoy!
|
||||
|
||||
- Open the `/addon` directory
|
||||
- Create a new folder named `hassio-plejd`
|
||||
- Copy all files from this repository into that newly created one.
|
||||
- Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||
- Click on `Add-on Store` in the top navigation bar of that page.
|
||||
- Click on the refresh button in the upper right corner.
|
||||
- A new Local Add-on should appear named Plejd. Open that and install.
|
||||
- Enjoy!
|
||||
|
||||
### NOTE
|
||||
|
||||
When starting the add-on, the log displays this message:
|
||||
|
||||
```
|
||||
parse error: Expected string key before ':' at line 1, column 4
|
||||
[08:56:24] ERROR: Unknown HTTP error occured
|
||||
```
|
||||
|
||||
However, the add-on still works as expected and this is something I'm looking into, but not with that much effort yet though.
|
||||
|
||||
### Configuration
|
||||
|
||||
You need to add the following to your `configuration.yaml` file:
|
||||
|
||||
```
|
||||
mqtt:
|
||||
broker: [point to your broker IP eg. 'mqtt://localhost']
|
||||
|
|
@ -70,84 +85,102 @@ mqtt:
|
|||
password: !secret mqtt_password
|
||||
discovery: true
|
||||
discovery_prefix: homeassistant
|
||||
birth_message:
|
||||
birth_message:
|
||||
topic: 'hass/status'
|
||||
payload: 'online'
|
||||
will_message:
|
||||
will_message:
|
||||
topic: 'hass/status'
|
||||
payload: 'offline'
|
||||
```
|
||||
|
||||
The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing all devices).
|
||||
|
||||
The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it.
|
||||
|
||||
Parameter | Value
|
||||
--- | ---
|
||||
site | Name of your Plejd site, the name is displayed in the Plejd app (top bar).
|
||||
username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API.
|
||||
password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API.
|
||||
mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost
|
||||
mqttUsername | Username of the MQTT broker
|
||||
mqttPassword | Password of the MQTT broker
|
||||
includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. *Added in v. 5*.
|
||||
connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds.
|
||||
| Parameter | Value |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). |
|
||||
| username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. |
|
||||
| password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. |
|
||||
| mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost |
|
||||
| mqttUsername | Username of the MQTT broker |
|
||||
| mqttPassword | Password of the MQTT broker |
|
||||
| includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. _Added in v. 5_. |
|
||||
| connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds. |
|
||||
|
||||
## I want voice control!
|
||||
|
||||
With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information:
|
||||
https://www.home-assistant.io/integrations/google_assistant/
|
||||
|
||||
### I don't want voice, I want HomeKit!
|
||||
|
||||
Check this out for more information on how you can get your Plejd lights controlled using HomeKit:
|
||||
https://www.home-assistant.io/integrations/homekit/
|
||||
|
||||
## Changelog
|
||||
*v 0.3.4*:
|
||||
* NEW: `connectionTimeout` configuration parameter to enable tweaking of wait time on connection, usable for RPi 3B+.
|
||||
* FIX: Reworked some logging to get better understanding of what happens.
|
||||
|
||||
*v 0.3.0*:
|
||||
* NEW: New BLE manager, DBus instead of noble
|
||||
* FIX: Adding entities as devices now as well
|
||||
* FIX: Bug fixes
|
||||
_v 0.3.4_:
|
||||
|
||||
*v 0.2.8*:
|
||||
* FIX: Reset characteristic state on disconnect
|
||||
- NEW: `connectionTimeout` configuration parameter to enable tweaking of wait time on connection, usable for RPi 3B+.
|
||||
- FIX: Reworked some logging to get better understanding of what happens.
|
||||
|
||||
*v 0.2.7*:
|
||||
* FIX: Added exception handling to unsubscribing lastData characteristic if already disconnected
|
||||
_v 0.3.0_:
|
||||
|
||||
*v 0.2.6*:
|
||||
* FIX: Added null check to remove listeners for characteristics
|
||||
- NEW: New BLE manager, DBus instead of noble
|
||||
- FIX: Adding entities as devices now as well
|
||||
- FIX: Bug fixes
|
||||
|
||||
*v 0.2.5*:
|
||||
* FIX: Invalid scene id in events/scene message
|
||||
_v 0.2.8_:
|
||||
|
||||
*v 0.2.4*:
|
||||
* Stability improvements
|
||||
- FIX: Reset characteristic state on disconnect
|
||||
|
||||
*v 0.2.3*:
|
||||
* FIX: Container build error fix
|
||||
_v 0.2.7_:
|
||||
|
||||
*v 0.2.2*:
|
||||
* Stability improvements
|
||||
- FIX: Added exception handling to unsubscribing lastData characteristic if already disconnected
|
||||
|
||||
*v 0.2.1*:
|
||||
* Stability improvements
|
||||
_v 0.2.6_:
|
||||
|
||||
*v 0.2.0*:
|
||||
* Stability improvements
|
||||
* Bugfixes
|
||||
- FIX: Added null check to remove listeners for characteristics
|
||||
|
||||
*v 0.1.1*:
|
||||
* FIX: Fixed missing reference on startup, preventing add-on from starting
|
||||
_v 0.2.5_:
|
||||
|
||||
*v 0.1.0*:
|
||||
* NEW: Rewrote the BLE integration for more stability
|
||||
* FIX: discovery wasn't always sent
|
||||
- FIX: Invalid scene id in events/scene message
|
||||
|
||||
*previous*:
|
||||
* FIX: bug preventing add-on from building
|
||||
* NEW: Added support for Plejd devices with multiple outputs (such as DIM-02)
|
||||
_v 0.2.4_:
|
||||
|
||||
- Stability improvements
|
||||
|
||||
_v 0.2.3_:
|
||||
|
||||
- FIX: Container build error fix
|
||||
|
||||
_v 0.2.2_:
|
||||
|
||||
- Stability improvements
|
||||
|
||||
_v 0.2.1_:
|
||||
|
||||
- Stability improvements
|
||||
|
||||
_v 0.2.0_:
|
||||
|
||||
- Stability improvements
|
||||
- Bugfixes
|
||||
|
||||
_v 0.1.1_:
|
||||
|
||||
- FIX: Fixed missing reference on startup, preventing add-on from starting
|
||||
|
||||
_v 0.1.0_:
|
||||
|
||||
- NEW: Rewrote the BLE integration for more stability
|
||||
- FIX: discovery wasn't always sent
|
||||
|
||||
_previous_:
|
||||
|
||||
- FIX: bug preventing add-on from building
|
||||
- NEW: Added support for Plejd devices with multiple outputs (such as DIM-02)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
19
plejd/Scene.js
Normal file
19
plejd/Scene.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
const SceneStep = require('./SceneStep');
|
||||
|
||||
class Scene {
|
||||
constructor(idx, scene, steps) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scene;
|
||||
48
plejd/SceneManager.js
Normal file
48
plejd/SceneManager.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
const EventEmitter = require('events');
|
||||
const Scene = require('./Scene');
|
||||
|
||||
class SceneManager extends EventEmitter {
|
||||
constructor(site, devices) {
|
||||
super();
|
||||
|
||||
this.site = site;
|
||||
this.scenes = [];
|
||||
this.devices = devices;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const scenes = this.site.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const scene of scenes) {
|
||||
const idx = this.site.sceneIndex[scene.sceneId];
|
||||
this.scenes.push(new Scene(idx, scene, this.site.sceneSteps));
|
||||
}
|
||||
}
|
||||
|
||||
executeScene(sceneIndex, ble) {
|
||||
const scene = this.scenes.find((x) => x.id === sceneIndex);
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const step of scene.steps) {
|
||||
const device = this.devices.find((x) => x.serialNumber === step.deviceId);
|
||||
if (device) {
|
||||
if (device.dimmable && step.state) {
|
||||
ble.turnOn(device.id, { brightness: step.brightness });
|
||||
} else if (!device.dimmable && step.state) {
|
||||
ble.turnOn(device.id, {});
|
||||
} else if (!step.state) {
|
||||
ble.turnOff(device.id, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SceneManager;
|
||||
/* eslint-disable */
|
||||
10
plejd/SceneStep.js
Normal file
10
plejd/SceneStep.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
class SceneStep {
|
||||
constructor(step) {
|
||||
this.sceneId = step.sceneId;
|
||||
this.deviceId = step.deviceId;
|
||||
this.state = step.state === 'On' ? 1 : 0;
|
||||
this.brightness = step.value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SceneStep;
|
||||
|
|
@ -4,13 +4,7 @@
|
|||
"slug": "plejd",
|
||||
"description": "Adds support for the Swedish home automation devices from Plejd.",
|
||||
"url": "https://github.com/icanos/hassio-plejd/",
|
||||
"arch": [
|
||||
"armhf",
|
||||
"armv7",
|
||||
"aarch64",
|
||||
"amd64",
|
||||
"i386"
|
||||
],
|
||||
"arch": ["armhf", "armv7", "aarch64", "amd64", "i386"],
|
||||
"startup": "application",
|
||||
"boot": "auto",
|
||||
"host_network": true,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
const api = require('./api');
|
||||
const mqtt = require('./mqtt');
|
||||
const PlejdApi = require('./PlejdApi');
|
||||
const MqttClient = require('./MqttClient');
|
||||
|
||||
const Logger = require('./Logger');
|
||||
const PlejdService = require('./ble.bluez');
|
||||
const SceneManager = require('./scene.manager');
|
||||
const Configuration = require("./Configuration");
|
||||
const PlejdService = require('./PlejdService');
|
||||
const SceneManager = require('./SceneManager');
|
||||
const Configuration = require('./Configuration');
|
||||
|
||||
const logger = Logger.getLogger("plejd-main");
|
||||
const logger = Logger.getLogger('plejd-main');
|
||||
|
||||
|
||||
const version = "0.4.8";
|
||||
const version = '0.4.8';
|
||||
|
||||
async function main() {
|
||||
logger.info(`Starting Plejd add-on v. ${version}`);
|
||||
|
|
@ -20,8 +19,13 @@ async function main() {
|
|||
config.connectionTimeout = 2;
|
||||
}
|
||||
|
||||
const plejdApi = new api.PlejdApi(config.site, config.username, config.password, config.includeRoomsAsLights);
|
||||
const client = new mqtt.MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword);
|
||||
const plejdApi = new PlejdApi(
|
||||
config.site,
|
||||
config.username,
|
||||
config.password,
|
||||
config.includeRoomsAsLights,
|
||||
);
|
||||
const client = new MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword);
|
||||
|
||||
plejdApi.login().then(() => {
|
||||
// load all sites and find the one that we want (from config)
|
||||
|
|
@ -40,7 +44,13 @@ async function main() {
|
|||
|
||||
// init the BLE interface
|
||||
const sceneManager = new SceneManager(plejdApi.site, devices);
|
||||
const plejd = new PlejdService(cryptoKey, devices, sceneManager, config.connectionTimeout, config.writeQueueWaitTime, true);
|
||||
const plejd = new PlejdService(
|
||||
cryptoKey,
|
||||
devices,
|
||||
sceneManager,
|
||||
config.connectionTimeout,
|
||||
config.writeQueueWaitTime,
|
||||
);
|
||||
plejd.on('connectFailed', () => {
|
||||
logger.verbose('Were unable to connect, will retry connection in 10 seconds.');
|
||||
setTimeout(() => {
|
||||
|
|
@ -81,16 +91,17 @@ async function main() {
|
|||
// switch command
|
||||
state = command;
|
||||
commandObj = {
|
||||
state: state
|
||||
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.
|
||||
client.updateState(deviceId, {
|
||||
state: state === 'ON' ? 1 : 0
|
||||
state: state === 'ON' ? 1 : 0,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
state = command.state;
|
||||
commandObj = command;
|
||||
}
|
||||
|
|
|
|||
1545
plejd/package-lock.json
generated
1545
plejd/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,5 +9,23 @@
|
|||
"mqtt": "~3.0.0",
|
||||
"sleep": "~6.1.0",
|
||||
"winston": "~3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "~10.1.0",
|
||||
"eslint": "~7.18.0",
|
||||
"eslint-config-airbnb": "~18.2.1",
|
||||
"eslint-config-prettier": "~7.2.0",
|
||||
"eslint-plugin-import": "~2.22.1",
|
||||
"eslint-plugin-prettier": "~3.3.1",
|
||||
"prettier": "~2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "prettier \"../*.{js*,md}\" --check & eslint **/*.js",
|
||||
"lint:fix": "prettier .. --check --write & eslint **/*.js --fix",
|
||||
"lint:prettier:fix": "npm run lint:prettier --write",
|
||||
"lint:errors": "npm run lint:prettier & npm run lint:styles --quiet & npm run lint:types & npm run lint:scripts --quiet",
|
||||
"lint:errors:fix": "npm run lint:prettier --write & npm run lint:scripts --quiet --fix",
|
||||
"lint:prettier": "prettier --check \"**/*.js\"",
|
||||
"lint:scripts": "eslint --config ./.eslintrc.js \"**/*.js\""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
const EventEmitter = require('events');
|
||||
|
||||
class SceneManager extends EventEmitter {
|
||||
constructor(site, devices) {
|
||||
super();
|
||||
|
||||
this.site = site;
|
||||
this.scenes = [];
|
||||
this.devices = devices;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const scenes = this.site.scenes.filter(x => x.hiddenFromSceneList == false);
|
||||
for (const scene of scenes) {
|
||||
const idx = this.site.sceneIndex[scene.sceneId];
|
||||
this.scenes.push(new Scene(idx, scene, this.site.sceneSteps));
|
||||
}
|
||||
}
|
||||
|
||||
executeScene(sceneIndex, ble) {
|
||||
const scene = this.scenes.find(x => x.id === sceneIndex);
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const step of scene.steps) {
|
||||
const device = this.devices.find(x => x.serialNumber === step.deviceId);
|
||||
if (!device) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (device.dimmable && step.state) {
|
||||
ble.turnOn(device.id, { brightness: step.brightness });
|
||||
}
|
||||
else if (!device.dimmable && step.state) {
|
||||
ble.turnOn(device.id, {});
|
||||
}
|
||||
else if (!step.state) {
|
||||
ble.turnOff(device.id, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Scene {
|
||||
constructor(idx, scene, steps) {
|
||||
this.id = idx;
|
||||
this.title = scene.title;
|
||||
this.sceneId = scene.sceneId;
|
||||
|
||||
const sceneSteps = steps.filter(x => x.sceneId === scene.sceneId);
|
||||
this.steps = [];
|
||||
|
||||
for (const step of sceneSteps) {
|
||||
this.steps.push(new SceneStep(step));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SceneStep {
|
||||
constructor(step) {
|
||||
this.sceneId = step.sceneId;
|
||||
this.deviceId = step.deviceId;
|
||||
this.state = step.state === 'On' ? 1 : 0;
|
||||
this.brightness = step.value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SceneManager;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const PlejdService = require('../ble.bluez');
|
||||
const PlejdService = require('../PlejdService');
|
||||
|
||||
const cryptoKey = '';
|
||||
|
||||
const plejd = new PlejdService(cryptoKey, true);
|
||||
plejd.init();
|
||||
plejd.init();
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
const PlejdService = require('../ble');
|
||||
|
||||
const plejd = new PlejdService('todo-insert-crypto-key', true);
|
||||
plejd.on('authenticated', () => {
|
||||
plejd.disconnect();
|
||||
console.log('ok, done! disconnected.');
|
||||
});
|
||||
plejd.scan();
|
||||
Loading…
Add table
Add a link
Reference in a new issue