mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2024-11-17 02:48:31 -07:00
229 lines
7.0 KiB
JavaScript
229 lines
7.0 KiB
JavaScript
const equals = require('fast-deep-equal/es6');
|
|
const humanizeDuration = require('humanize-duration');
|
|
const data = require('./data');
|
|
const vm = require('vm');
|
|
const fs = require('fs');
|
|
|
|
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
|
|
const xiaomiManufacturerID = [4151, 4447];
|
|
const ikeaTradfriManufacturerID = [4476];
|
|
|
|
// construct a local ISO8601 string (instead of UTC-based)
|
|
// Example:
|
|
// - ISO8601 (UTC) = 2019-03-01T15:32:45.941+0000
|
|
// - ISO8601 (local) = 2019-03-01T16:32:45.941+0100 (for timezone GMT+1)
|
|
function toLocalISOString(dDate) {
|
|
const tzOffset = -dDate.getTimezoneOffset();
|
|
const plusOrMinus = tzOffset >= 0 ? '+' : '-';
|
|
const pad = function(num) {
|
|
const norm = Math.floor(Math.abs(num));
|
|
return (norm < 10 ? '0' : '') + norm;
|
|
};
|
|
|
|
return dDate.getFullYear() +
|
|
'-' + pad(dDate.getMonth() + 1) +
|
|
'-' + pad(dDate.getDate()) +
|
|
'T' + pad(dDate.getHours()) +
|
|
':' + pad(dDate.getMinutes()) +
|
|
':' + pad(dDate.getSeconds()) +
|
|
plusOrMinus + pad(tzOffset / 60) +
|
|
':' + pad(tzOffset % 60);
|
|
}
|
|
|
|
const endpointNames = [
|
|
'left', 'right', 'center', 'bottom_left', 'bottom_right', 'default',
|
|
'top_left', 'top_right', 'white', 'rgb', 'cct', 'system', 'top', 'bottom', 'center_left', 'center_right',
|
|
'ep1', 'ep2', 'row_1', 'row_2', 'row_3', 'row_4', 'relay',
|
|
'l1', 'l2', 'l3', 'l4', 'l5', 'l6', 'l7', 'l8',
|
|
'button_1', 'button_2', 'button_3', 'button_4', 'button_5',
|
|
'button_6', 'button_7', 'button_8', 'button_9', 'button_10',
|
|
'button_11', 'button_12', 'button_13', 'button_14', 'button_15',
|
|
'button_16', 'button_17', 'button_18', 'button_19', 'button_20',
|
|
'button_light', 'button_fan_high', 'button_fan_med', 'button_fan_low',
|
|
'heat', 'cool', 'water',
|
|
];
|
|
|
|
function capitalize(s) {
|
|
return s[0].toUpperCase() + s.slice(1);
|
|
}
|
|
|
|
async function getZigbee2mqttVersion() {
|
|
return new Promise((resolve, reject) => {
|
|
const git = require('git-last-commit');
|
|
const packageJSON = require('../../package.json');
|
|
const version = packageJSON.version;
|
|
|
|
git.getLastCommit((err, commit) => {
|
|
let commitHash = null;
|
|
|
|
if (err) {
|
|
try {
|
|
commitHash = require('../../.hash.json').hash;
|
|
} catch (error) {
|
|
commitHash = 'unknown';
|
|
}
|
|
} else {
|
|
commitHash = commit.shortHash;
|
|
}
|
|
|
|
resolve({commitHash, version});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function getDependencyVersion(depend) {
|
|
return new Promise((resolve, reject) => {
|
|
const packageJSON = require('../../node_modules/'+depend+'/package.json');
|
|
const version = packageJSON.version;
|
|
resolve({version});
|
|
});
|
|
}
|
|
|
|
function formatDate(date, type) {
|
|
let result;
|
|
|
|
switch (type) {
|
|
case 'ISO_8601':
|
|
result = new Date(date).toISOString();
|
|
break;
|
|
case 'ISO_8601_local':
|
|
result = toLocalISOString(new Date(date));
|
|
break;
|
|
case 'epoch':
|
|
result = date;
|
|
break;
|
|
case 'relative':
|
|
// https://github.com/EvanHahn/HumanizeDuration.js#options
|
|
result = humanizeDuration(Date.now() - date, {language: 'en', largest: 2, round: true}) + ' ago';
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported type '${type}'`);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function objectHasProperties(object, properties) {
|
|
for (const property of properties) {
|
|
if (!object.hasOwnProperty(property)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function equalsPartial(object, expected) {
|
|
for (const [key, value] of Object.entries(expected)) {
|
|
if (!equals(object[key], value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getObjectProperty(object, key, defaultValue) {
|
|
return object.hasOwnProperty(key) ? object[key] : defaultValue;
|
|
}
|
|
|
|
function getResponse(request, data, error) {
|
|
const response = {data, status: error ? 'error' : 'ok'};
|
|
if (error) response.error = error;
|
|
if (typeof request === 'object' && request.hasOwnProperty('transaction')) {
|
|
response.transaction = request.transaction;
|
|
}
|
|
return response;
|
|
}
|
|
|
|
function parseJSON(value, failedReturnValue) {
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch (e) {
|
|
return failedReturnValue;
|
|
}
|
|
}
|
|
|
|
function* getExternalConvertersDefinitions(settings) {
|
|
const externalConverters = settings.get().external_converters;
|
|
|
|
for (const moduleName of externalConverters) {
|
|
let converter;
|
|
|
|
if (moduleName.endsWith('.js')) {
|
|
const sandbox = {
|
|
require,
|
|
module: {},
|
|
};
|
|
const converterCode = fs.readFileSync(data.joinPath(moduleName), {encoding: 'utf8'});
|
|
vm.runInNewContext(converterCode, sandbox);
|
|
converter = sandbox.module.exports;
|
|
} else {
|
|
converter = require(moduleName);
|
|
}
|
|
|
|
if (Array.isArray(converter)) {
|
|
for (const item of converter) {
|
|
yield item;
|
|
}
|
|
} else {
|
|
yield converter;
|
|
}
|
|
}
|
|
}
|
|
|
|
function toSnakeCase(value) {
|
|
if (typeof value === 'object') {
|
|
value = {...value};
|
|
for (const key of Object.keys(value)) {
|
|
const keySnakeCase = toSnakeCase(key);
|
|
if (key !== keySnakeCase) {
|
|
value[keySnakeCase] = value[key];
|
|
delete value[key];
|
|
}
|
|
}
|
|
return value;
|
|
} else {
|
|
return value.replace(/\.?([A-Z])/g, (x, y) => '_' + y.toLowerCase()).replace(/^_/, '').replace('_i_d', '_id');
|
|
}
|
|
}
|
|
|
|
function validateFriendlyName(name) {
|
|
const errors = [];
|
|
for (const endpointName of endpointNames) {
|
|
if (name.toLowerCase().endsWith('/' + endpointName)) {
|
|
errors.push(`friendly_name is not allowed to end with: '/${endpointName}'`);
|
|
}
|
|
}
|
|
|
|
if (endpointNames.includes(name)) errors.push(`Following friendly_name are not allowed: '${endpointNames}'`);
|
|
if (name.match(/.*\/\d*$/)) errors.push(`Friendly name cannot end with a "/DIGIT" ('${name}')`);
|
|
if (name.includes('#') || name.includes('+')) {
|
|
errors.push(`MQTT wildcard (+ and #) not allowed in friendly_name ('${name}')`);
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
module.exports = {
|
|
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
|
|
secondsToMilliseconds: (seconds) => seconds * 1000,
|
|
getZigbee2mqttVersion,
|
|
getDependencyVersion,
|
|
objectHasProperties,
|
|
toSnakeCase,
|
|
getObjectProperty,
|
|
getEndpointNames: () => endpointNames,
|
|
isXiaomiDevice: (device) => {
|
|
return device.modelID !== 'lumi.router' && xiaomiManufacturerID.includes(device.manufacturerID) &&
|
|
(!device.manufacturerName || !device.manufacturerName.startsWith('Trust'));
|
|
},
|
|
isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufacturerID),
|
|
formatDate: (date, type) => formatDate(date, type),
|
|
equalsPartial,
|
|
getResponse,
|
|
capitalize,
|
|
parseJSON,
|
|
getExternalConvertersDefinitions,
|
|
validateFriendlyName,
|
|
};
|