node-red-contrib-homekit-bridged
Version:
Node-RED nodes to simulate Apple HomeKit devices.
387 lines (386 loc) • 17.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const util = __importStar(require("node:util"));
const logger_1 = require("@nrchkb/logger");
const hap_nodejs_1 = require("hap-nodejs");
const NRCHKBError_1 = __importDefault(require("../NRCHKBError"));
module.exports = function (node) {
const log = (0, logger_1.logger)('NRCHKB', 'ServiceUtils', node.config.name, node);
const HapNodeJS = require('hap-nodejs');
const Service = HapNodeJS.Service;
const Characteristic = HapNodeJS.Characteristic;
const CameraSource = require('../cameraSource').Camera;
const NO_RESPONSE_MSG = 'NO_RESPONSE';
const prepareHapData = (context, connection) => {
const hap = {};
if (connection) {
hap.session = {
sessionID: connection.sessionID,
username: connection.username,
remoteAddress: connection.remoteAddress,
localAddress: connection.localAddress,
httpPort: connection.remotePort,
};
hap.context = {};
}
if (context) {
hap.context = context;
}
return hap;
};
const onCharacteristicGet = function (callback, context, connection) {
var _a, _b;
log.debug(`onCharacteristicGet with status: ${this.statusCode}, value: ${this.value}, reachability is ${((_a = node.parentNode) !== null && _a !== void 0 ? _a : node).reachable} with context ${util.inspect(context)} on connection ${connection === null || connection === void 0 ? void 0 : connection.sessionID}`);
if (callback) {
try {
callback(((_b = node.parentNode) !== null && _b !== void 0 ? _b : node).reachable
? null
: new hap_nodejs_1.HapStatusError(-70402), this.value);
}
catch (_) { }
}
};
const onValueChange = function (allCharacteristics, outputNumber, { oldValue, newValue, context }, connection) {
var _a, _b, _c, _d, _e;
const topic = node.config.topic ? node.config.topic : node.topic_in;
const msg = { payload: {}, hap: {}, name: node.name, topic: topic };
const key = this.constructor.name;
msg.payload[key] = newValue;
msg.hap = prepareHapData(context, connection);
msg.hap.allChars = allCharacteristics.reduce((allChars, singleChar) => {
const cKey = singleChar.constructor.name;
allChars[cKey] = singleChar.value;
return allChars;
}, {});
if (oldValue !== undefined) {
msg.hap.oldValue = oldValue;
}
msg.hap.reachable = (_a = node.reachable) !== null && _a !== void 0 ? _a : (_b = node.parentNode) === null || _b === void 0 ? void 0 : _b.reachable;
if (msg.hap.reachable === false) {
;
[node, ...((_c = node.childNodes) !== null && _c !== void 0 ? _c : [])].forEach((n) => n.nodeStatusUtils.setStatus({
fill: 'red',
shape: 'ring',
text: 'Not reachable',
type: 'NO_RESPONSE',
}));
}
else {
msg.hap.newValue = newValue;
node.nodeStatusUtils.setStatus({
fill: 'yellow',
shape: 'dot',
text: key + ': ' + newValue,
}, 3000);
(_d = node.childNodes) === null || _d === void 0 ? void 0 : _d.forEach((n) => n.nodeStatusUtils.clearStatusByType('NO_RESPONSE'));
(_e = node.parentNode) === null || _e === void 0 ? void 0 : _e.nodeStatusUtils.clearStatusByType('NO_RESPONSE');
}
log.debug(`${node.name} received ${key} : ${newValue}`);
if (connection ||
context ||
node.hostNode.config.allowMessagePassthrough) {
if (outputNumber === 0) {
node.send(msg);
}
else if (outputNumber === 1) {
node.send([null, msg]);
}
}
};
const onCharacteristicSet = (allCharacteristics) => function (newValue, callback, context, connection) {
var _a, _b;
log.debug(`onCharacteristicSet with status: ${this.statusCode}, value: ${this.value}, reachability is ${((_a = node.parentNode) !== null && _a !== void 0 ? _a : node).reachable}
with context ${util.inspect(context)} on connection ${connection === null || connection === void 0 ? void 0 : connection.sessionID}`);
try {
if (callback) {
callback(((_b = node.parentNode) !== null && _b !== void 0 ? _b : node).reachable
? null
: new hap_nodejs_1.HapStatusError(-70402));
}
}
catch (_) { }
onValueChange.call(this, allCharacteristics, 1, {
newValue,
context,
}, connection);
};
const onCharacteristicChange = (allCharacteristics) => function (change) {
var _a;
const { oldValue, newValue, context, originator, reason } = change;
log.debug(`onCharacteristicChange with reason: ${reason}, oldValue: ${oldValue}, newValue: ${newValue}, reachability is ${((_a = node.parentNode) !== null && _a !== void 0 ? _a : node).reachable}
with context ${util.inspect(context)} on connection ${originator === null || originator === void 0 ? void 0 : originator.sessionID}`);
if (oldValue != newValue) {
onValueChange.call(this, allCharacteristics, 0, {
oldValue,
newValue,
context,
}, originator);
}
};
const onInput = function (msg) {
var _a, _b;
if (msg.payload) {
const type = typeof msg.payload;
if (type !== 'object') {
log.error(`Invalid payload type: ${type}`);
return;
}
}
else {
log.error('Invalid message (payload missing)');
return;
}
const topic = (_a = node.config.topic) !== null && _a !== void 0 ? _a : node.name;
if (node.config.filter && msg.topic !== topic) {
log.debug("msg.topic doesn't match configured value and filter is enabled. Dropping message.");
return;
}
let context = null;
if (msg.payload.Context) {
context = msg.payload.Context;
delete msg.payload.Context;
}
node.topic_in = (_b = msg.topic) !== null && _b !== void 0 ? _b : '';
Object.keys(msg.payload).map((key) => {
var _a, _b, _c, _d;
if (node.supported.indexOf(key) < 0) {
if (key === 'AdaptiveLightingController' &&
node.adaptiveLightingController) {
const value = (_a = msg.payload) === null || _a === void 0 ? void 0 : _a[key];
const event = value === null || value === void 0 ? void 0 : value.event;
if (event === 'disable') {
(_b = node.adaptiveLightingController) === null || _b === void 0 ? void 0 : _b.disableAdaptiveLighting();
}
}
else {
log.error(`Instead of '${key}' try one of these characteristics: '${node.supported.join("', '")}'`);
}
}
else {
const value = (_c = msg.payload) === null || _c === void 0 ? void 0 : _c[key];
const parentNode = (_d = node.parentNode) !== null && _d !== void 0 ? _d : node;
parentNode.reachable = value !== NO_RESPONSE_MSG;
const characteristic = node.service.getCharacteristic(Characteristic[key]);
if (context !== null) {
characteristic.setValue(value, undefined, context);
}
else {
characteristic.setValue(value);
}
}
});
};
const onClose = function (removed, done) {
const characteristics = node.service.characteristics.concat(node.service.optionalCharacteristics);
characteristics.forEach(function (characteristic) {
characteristic.removeListener('get', node.onCharacteristicGet);
characteristic.removeListener('set', node.onCharacteristicSet);
characteristic.removeListener('change', node.onCharacteristicChange);
});
if (node.config.isParent) {
node.accessory.removeListener('identify', node.onIdentify);
}
if (removed) {
if (node.config.isParent) {
node.hostNode.host.removeBridgedAccessories([node.accessory]);
node.accessory.destroy();
}
else {
node.accessory.removeService(node.service);
node.parentService.removeLinkedService(node.service);
}
}
done();
};
const getOrCreate = function (accessory, serviceInformation, parentService) {
const newService = new Service[serviceInformation.serviceName](serviceInformation.name, serviceInformation.UUID);
log.debug(`Looking for service with UUID ${serviceInformation.UUID} ...`);
let service = accessory.services.find((service) => {
return newService.subtype === service.subtype;
});
if (service && newService.UUID !== service.UUID) {
log.debug('... service type changed! Removing the old service.');
accessory.removeService(service);
service = undefined;
}
if (!service) {
log.debug(`... didn't find it. Adding new ${serviceInformation.serviceName} service.`);
if (serviceInformation.serviceName === 'CameraControl') {
configureCameraSource(accessory, newService, serviceInformation.config);
service = newService;
}
else {
service = accessory.addService(newService);
}
}
else {
log.debug('... found it! Updating it.');
service
.getCharacteristic(Characteristic.Name)
.setValue(serviceInformation.name);
}
if (parentService) {
if (serviceInformation.serviceName === 'CameraControl') {
log.debug('... and adding service to accessory.');
}
else if (service) {
log.debug('... and linking service to parent.');
parentService.addLinkedService(service);
}
}
return service;
};
const configureCameraSource = function (accessory, service, config) {
if (config.cameraConfigSource) {
log.debug('Configuring Camera Source');
if (!config.cameraConfigVideoProcessor) {
log.error('Missing configuration for CameraControl: videoProcessor cannot be empty!');
}
else {
accessory.configureCameraSource(new CameraSource(service, config, node));
}
}
else {
log.error('Missing configuration for CameraControl.');
}
};
const waitForParent = () => {
log.debug('Waiting for Parent Service');
return new Promise((resolve) => {
node.nodeStatusUtils.setStatus({
fill: 'blue',
shape: 'dot',
text: 'Waiting for Parent Service',
});
const checkAndWait = () => {
const parentNode = node.RED.nodes.getNode(node.config.parentService);
if (parentNode && parentNode.configured) {
resolve(parentNode);
}
else {
setTimeout(checkAndWait, 1000);
}
};
checkAndWait();
}).catch((error) => {
log.error(`Waiting for Parent Service failed due to: ${error}`);
throw new NRCHKBError_1.default(error);
});
};
const handleWaitForSetup = (config, msg, resolve) => {
if (node.setupDone) {
return;
}
if (msg.hasOwnProperty('payload') &&
msg.payload.hasOwnProperty('nrchkb') &&
msg.payload.nrchkb.hasOwnProperty('setup')) {
node.setupDone = true;
const newConfig = Object.assign(Object.assign({}, config), msg.payload.nrchkb.setup);
node.removeListener('input', node.handleWaitForSetup);
resolve(newConfig);
}
else {
log.error('Invalid message (required {"payload":{"nrchkb":{"setup":{}}}})');
}
};
const configureAdaptiveLightning = () => {
if (node.service.UUID === Service.Lightbulb.UUID &&
node.config.adaptiveLightingOptionsEnable) {
try {
node.service.getCharacteristic(Characteristic.Brightness);
node.service.getCharacteristic(Characteristic.ColorTemperature);
const options = {
controllerMode: node.config.adaptiveLightingOptionsMode
? +node.config.adaptiveLightingOptionsMode
: 1,
customTemperatureAdjustment: node.config
.adaptiveLightingOptionsCustomTemperatureAdjustment
? +node.config
.adaptiveLightingOptionsCustomTemperatureAdjustment
: undefined,
};
log.trace(`Configuring Adaptive Lighting with options: ${options}`);
const adaptiveLightingController = new hap_nodejs_1.AdaptiveLightingController(node.service, options);
adaptiveLightingController.on('update', () => {
const activeAdaptiveLightingTransition = {
transitionStartMillis: adaptiveLightingController.getAdaptiveLightingStartTimeOfTransition(),
timeMillisOffset: adaptiveLightingController.getAdaptiveLightingTimeOffset(),
transitionCurve: adaptiveLightingController.getAdaptiveLightingTransitionCurve(),
brightnessAdjustmentRange: adaptiveLightingController.getAdaptiveLightingBrightnessMultiplierRange(),
updateInterval: adaptiveLightingController.getAdaptiveLightingUpdateInterval(),
notifyIntervalThreshold: adaptiveLightingController.getAdaptiveLightingNotifyIntervalThreshold(),
};
node.send({
payload: {
AdaptiveLightingController: {
event: 'update',
data: activeAdaptiveLightingTransition,
},
},
});
});
adaptiveLightingController.on('disable', () => {
node.send({
payload: {
AdaptiveLightingController: {
event: 'disable',
},
},
});
});
node.accessory.configureController(adaptiveLightingController);
node.adaptiveLightingController = adaptiveLightingController;
}
catch (error) {
log.error(`Failed to configure Adaptive Lightning due to ${error}`);
}
}
};
return {
getOrCreate,
onCharacteristicGet,
onCharacteristicSet,
onCharacteristicChange,
onInput,
onClose,
waitForParent,
handleWaitForSetup,
configureAdaptiveLightning,
};
};