UNPKG

redmatic-homekit

Version:

HAP-Nodejs based Node-RED nodes to create HomeKit Accessories

301 lines (257 loc) 11.5 kB
class Service { constructor(acc, subtype) { this.acc = acc; this.subtype = subtype; return this; } get(characteristic, datapointNameOrCallback, transform) { if (typeof datapointNameOrCallback === 'function') { this.acc.addListener('get', this.subtype, characteristic, datapointNameOrCallback); } else { this.acc.datapointGet(this.subtype, characteristic, datapointNameOrCallback, transform); } return this; } set(characteristic, datapointNameOrCallback, transform) { if (typeof datapointNameOrCallback === 'function') { this.acc.addListener('set', this.subtype, characteristic, datapointNameOrCallback); } else { this.acc.datapointSet(this.subtype, characteristic, datapointNameOrCallback, transform); } return this; } update(characteristic, value) { this.acc.updateCharacteristic(this.subtype, characteristic, value); } setProps(characteristic, props) { this.acc.setProps(this.subtype, characteristic, props); return this; } fault(datapointNameArr, transformArr) { this.acc.datapointsFault(this.subtype, datapointNameArr, transformArr); return this; } } module.exports = class Accessory { constructor(config, node) { const {bridgeConfig, ccu} = node; const {hap} = bridgeConfig; this.ccu = ccu; this.hap = hap; this.node = node; this.config = config; node.debug('create accessory ' + config.description.ADDRESS + ' ' + config.name); this.acc = bridgeConfig.accessory({id: config.description.ADDRESS, name: config.name}); if (!this.acc) { return; } this.acc.getService(hap.Service.AccessoryInformation) .setCharacteristic(hap.Characteristic.Manufacturer, 'eQ-3') .setCharacteristic(hap.Characteristic.Model, config.description.TYPE) .setCharacteristic(hap.Characteristic.SerialNumber, config.description.ADDRESS) .setCharacteristic(hap.Characteristic.FirmwareRevision, config.description.FIRMWARE); this.acc.on('identify', (paired, callback) => { this.identify(paired, callback); }); this.listeners = []; this.subscriptions = []; this.subtypeCounter = 0; node.on('close', () => { node.debug('removing listeners ' + config.description.TYPE + ' ' + config.name); this.acc.removeListener('identify', () => this.identify()); this.removeListeners(); this.removeSubscriptions(); }); if (typeof this.init === 'function') { node.debug('init accessory ' + config.description.ADDRESS + ' ' + config.description.TYPE + ' ' + config.name); setImmediate(() => { this.init(config, node); }); } } ccuSetValue(address, value, callback) { const force = this.ccu.values[address] && this.ccu.values[address].stable === false; const [iface, channel, dp] = address.split('.'); this.ccu.setValueQueued(iface, channel, dp, value, false, force) .then(() => { if (typeof callback === 'function') { callback(); } }) .catch(() => { if (typeof callback === 'function') { callback(new Error(this.hap.HAPServer.Status.SERVICE_COMMUNICATION_FAILURE)); } }); } addService(type, name, subtypeIdentifier = '') { const subtype = subtypeIdentifier + String(this.subtypeCounter++); if (this.acc.getService(subtype)) { this.node.debug('service (' + subtype + ') already existing ' + this.config.description.TYPE + ' ' + this.config.name); } else { this.node.debug('add service ' + type + ' (' + subtype + ') to ' + this.config.description.TYPE + ' ' + this.config.name); this.acc.addService(this.hap.Service[type], name, subtype); } this.datapointUnreach(this.config.deviceAddress + ':0.UNREACH'); return new Service(this, subtype); } addListener(event, subtype, characteristic, callback) { if (this.acc.getService(subtype)) { this.acc.getService(subtype).getCharacteristic(this.hap.Characteristic[characteristic]).on(event, callback); this.node.debug('add ' + event + ' listener ' + characteristic + ' (' + subtype + ') to ' + this.config.description.TYPE + ' ' + this.config.name); this.listeners.push({event, subtype, characteristic, callback}); } else { this.node.error('service (' + subtype + ') does not exist on ' + this.config.description.TYPE + ' ' + this.config.name); } } removeListeners() { if (this.listeners.length > 0) { const {event, subtype, characteristic, callback} = this.listeners.shift(); this.node.debug('remove ' + event + ' listener ' + characteristic + ' (' + subtype + ') from ' + this.config.description.TYPE + ' ' + this.config.name); this.acc.getService(subtype).getCharacteristic(this.hap.Characteristic[characteristic]).removeListener(event, callback); this.removeListeners(); } } removeSubscriptions() { if (this.subscriptions.length > 0) { this.ccu.unsubscribe(this.subscriptions.shift()); this.removeSubscriptions(); } } getError() { return (this.unreach && !['HM-CC-VG-1', 'HmIP-HEATING'].includes(this.config.description.TYPE)) ? new Error(this.hap.HAPServer.Status.SERVICE_COMMUNICATION_FAILURE) : null; } subscribe(datapointName, callback) { this.subscriptions.push(this.ccu.subscribe({ cache: true, change: true, stable: true, datapointName }, msg => { callback(msg.value); })); } datapointUnreach(datapointName) { this.subscriptions.push(this.ccu.subscribe({ cache: true, change: true, datapointName }, msg => { this.unreach = msg.value; })); } datapointsFault(subtype, datapointNameArr, transformArr) { if (!transformArr) { transformArr = []; } const values = {}; datapointNameArr.forEach((dp, i) => { this.subscriptions.push(this.ccu.subscribe({ cache: true, change: true, datapointName: dp }, msg => { values[msg.datapointName] = msg.value; let value = this.hap.Characteristic.StatusFault.NO_FAULT; if (typeof transformArr[i] === 'function') { value = transformArr[i](value); } Object.keys(values).forEach(key => { if (values[key]) { value = this.hap.Characteristic.StatusFault.GENERAL_FAULT; } }); this.node.debug('update ' + this.config.name + ' (' + subtype + ') StatusFault ' + value); this.acc.getService(subtype).updateCharacteristic(this.hap.Characteristic.StatusFault, value); })); }); } datapointGet(subtype, characteristic, datapointName, transform) { this.addListener('get', subtype, characteristic, callback => { const valueOrig = this.ccu.values && this.ccu.values[datapointName] && this.ccu.values[datapointName].value; let value = valueOrig; if (typeof transform === 'function') { value = transform(value, this.hap.Characteristic[characteristic]); } this.node.debug('get ' + this.config.name + ' (' + subtype + ') ' + characteristic + ' ' + valueOrig + ' -> ' + this.getError() + ' ' + value); callback(this.getError(), value); }); this.node.debug('subscribe ' + datapointName); this.subscriptions.push(this.ccu.subscribe({ cache: true, change: true, stable: !datapointName.endsWith('.DIRECTION') && !datapointName.endsWith('.ACTIVITY_STATE'), datapointName }, msg => { const valueOrig = msg.value; let value = valueOrig; if (typeof transform === 'function') { value = transform(value, this.hap.Characteristic[characteristic]); } this.node.debug('update ' + this.config.name + ' (' + subtype + ') ' + characteristic + ' ' + valueOrig + ' -> ' + this.getError() + ' ' + value); this.acc.getService(subtype).updateCharacteristic(this.hap.Characteristic[characteristic], value); })); } datapointSet(subtype, characteristic, datapointName, transform) { this.addListener('set', subtype, characteristic, (value, callback) => { const valueOrig = value; if (typeof transform === 'function') { value = transform(value, this.hap.Characteristic[characteristic]); } const force = this.ccu.values[datapointName] && this.ccu.values[datapointName].stable === false; const [iface, channel, dp] = datapointName.split('.'); this.node.debug('set ' + this.config.name + ' (' + subtype + ') ' + characteristic + ' ' + valueOrig + ' -> ' + datapointName + ' ' + value); this.ccu.setValueQueued(iface, channel, dp, value, false, force) .then(() => { callback(); }) .catch(() => { callback(new Error(this.hap.HAPServer.Status.SERVICE_COMMUNICATION_FAILURE)); }); }); } updateCharacteristic(subtype, characteristic, value) { this.node.debug('update ' + this.config.name + ' (' + subtype + ') ' + characteristic + ' ' + value); this.acc.getService(subtype) .updateCharacteristic(this.hap.Characteristic[characteristic], value); } setProps(subtype, characteristic, props) { this.acc.getService(subtype) .getCharacteristic(this.hap.Characteristic[characteristic]) .setProps(props); } identify(paired, callback) { this.node.log('identify ' + (paired ? '(paired)' : '(unpaired)') + ' ' + this.config.name + ' ' + this.config.description.TYPE + ' ' + this.config.description.ADDRESS); try { callback(); } catch (error) { this.node.error(error); } } option(id, option) { let addr = this.config.description.ADDRESS; if (!addr.includes(':')) { addr = addr + ':' + id; } let res; if (option) { res = this.config.options[addr] && this.config.options[addr][option]; } else { res = !(this.config.options[addr] && this.config.options[addr].disabled); } this.node.debug('option ' + addr + ' ' + id + ' ' + option + ' ' + res); return res; } percent(value, _, lower = 2, upper = 3) { let p = Math.round((value - lower) * (100 / (upper - lower))); if (!p || p < 0) { p = 0; } else if (p > 100) { p = 100; } return p; } lux(value) { return Math.round(10 ** (value / 50)) || 1; } };