redmatic-homekit
Version:
HAP-Nodejs based Node-RED nodes to create HomeKit Accessories
385 lines (338 loc) • 17.7 kB
JavaScript
module.exports = function (RED) {
class RedMaticHomeKitHomematicGarage {
constructor(config) {
RED.nodes.createNode(this, config);
this.bridgeConfig = RED.nodes.getNode(config.bridgeConfig);
if (!this.bridgeConfig) {
return;
}
this.ccu = RED.nodes.getNode(config.ccuConfig);
if (!this.ccu) {
return;
}
this.iface = config.ifaceActuator;
this.ccu.register(this);
config.durationClose = config.durationClose || config.duration;
this.closed = false;
this.opened = false;
const move = (direction, revert) => {
let channel;
switch (config.channelActuatorType) {
case '1':
channel = config.channelActuator;
break;
case '2':
channel = direction === 0 ? config.channelActuatorOpen : config.channelActuatorClose;
revert = false;
break;
default:
}
channel = channel.split(' ')[0];
const dev = this.ccu.metadata.devices[config.ifaceActuator] && this.ccu.metadata.devices[config.ifaceActuator][channel];
const ps = this.ccu.getParamsetDescription(config.ifaceActuator, dev, 'VALUES');
return new Promise((resolve, reject) => {
if (ps && ps.ON_TIME) {
this.ccu.methodCall(config.ifaceActuator, 'putParamset', [channel, 'VALUES', {
STATE: true,
ON_TIME: this.ccu.paramCast(config.ifaceActuator, channel, 'VALUES', 'ON_TIME', config.onTime || 0.4)
}]).then(() => {
if (revert) {
this.valueCurrent = 4;
this.updateSensor();
setTimeout(() => {
move(direction).then(resolve).catch(reject);
}, (parseFloat(config.revertTime) || 0.5) * 1000);
} else {
resolve();
}
}).catch(reject);
} else {
this.ccu.setValue(config.ifaceActuator, channel, 'STATE', true)
.then(() => {
setTimeout(() => {
this.ccu.setValue(config.ifaceActuator, channel, 'STATE', false)
.then(() => {
if (revert) {
this.valueCurrent = 4;
this.updateSensor();
setTimeout(() => {
move(direction).then(resolve).catch(reject);
}, (parseFloat(config.revertTime) || 0.5) * 1000);
} else {
resolve();
}
}).catch(reject);
}, (parseFloat(config.onTime) || 0.4) * 1000);
})
.catch(reject);
}
});
};
const {hap, version} = this.bridgeConfig;
this.name = config.name || ('Garage ' + this.id);
const acc = this.bridgeConfig.accessory({id: this.id, name: this.name});
const subtype = '0';
let service;
if (acc.isConfigured) {
service = acc.getService(subtype);
} else {
acc.getService(hap.Service.AccessoryInformation)
.setCharacteristic(hap.Characteristic.Manufacturer, 'RedMatic')
.setCharacteristic(hap.Characteristic.Model, 'Homematic Garage')
.setCharacteristic(hap.Characteristic.SerialNumber, this.id)
.setCharacteristic(hap.Characteristic.FirmwareRevision, version);
service = acc.addService(hap.Service.GarageDoorOpener, this.name, subtype);
acc.isConfigured = true;
}
/*
Characteristic.CurrentDoorState.OPEN = 0;
Characteristic.CurrentDoorState.CLOSED = 1;
Characteristic.CurrentDoorState.OPENING = 2;
Characteristic.CurrentDoorState.CLOSING = 3;
Characteristic.CurrentDoorState.STOPPED = 4;
*/
this.updateSensor = (timeout, source) => {
let valueCurrent = 4;
let obstruction = false;
this.debug('input updateSensor timeout=' + timeout + ' moving=' + this.moving + ' current=' + this.valueCurrent + ' target=' + this.valueTarget);
switch (config.channelSensorType) {
case 'o': {
this.debug('o updateSensor opened=' + this.opened + ' lastMove=' + this.lastMove);
if (this.moving) {
if (this.opened && this.lastMove === 2) {
valueCurrent = 0;
clearTimeout(this.timer);
this.moving = false;
} else {
valueCurrent = this.moving;
}
} else if (timeout && this.lastMove === 2) {
if (this.opened) {
valueCurrent = 0;
} else {
obstruction = true;
}
} else if (timeout && this.lastMove === 3) {
if (this.opened) {
obstruction = true;
} else {
valueCurrent = 1;
}
} else {
valueCurrent = this.opened ? 0 : 1;
}
break;
}
case 'c': {
this.debug('c updateSensor closed=' + this.closed + ' lastMove=' + this.lastMove);
if (this.moving) {
if (this.closed && this.lastMove === 3) {
valueCurrent = 1;
clearTimeout(this.timer);
this.moving = false;
} else {
valueCurrent = this.moving;
}
} else if (timeout && this.lastMove === 3) {
if (this.closed) {
valueCurrent = 1;
} else {
obstruction = true;
}
} else if (timeout && this.lastMove === 2) {
if (this.closed) {
obstruction = true;
} else {
valueCurrent = 0;
}
} else {
valueCurrent = this.closed ? 1 : 0;
}
break;
}
default: {
this.debug('co updateSensor moving=' + this.moving + ' opened=' + this.opened + ' closed=' + this.closed + ' lastMove=' + this.lastMove + ' source=' + source);
if (this.opened && !this.closed) {
if (this.lastMove === 2 || !this.moving) {
valueCurrent = 0;
clearTimeout(this.timer);
this.moving = false;
} else if (this.moving) {
valueCurrent = this.moving;
}
} else if (this.closed && !this.opened) {
if (this.lastMove === 3 || !this.moving) {
valueCurrent = 1;
clearTimeout(this.timer);
this.moving = false;
} else if (this.moving) {
valueCurrent = this.moving;
}
} else if (this.moving) {
valueCurrent = this.moving;
} else if (timeout) {
obstruction = true;
} else if (source === 'o' && !this.opened) {
this.moving = 3;
this.lastMove = 3;
valueCurrent = 3;
this.valueTarget = 1;
this.timer = setTimeout(() => {
this.moving = false;
this.updateSensor(true);
}, config.durationClose * 1000);
} else if (source === 'c' && !this.closed) {
this.moving = 2;
this.lastMove = 2;
valueCurrent = 2;
this.valueTarget = 0;
this.timer = setTimeout(() => {
this.moving = false;
this.updateSensor(true);
}, config.duration * 1000);
}
}
}
if (!this.moving && !timeout && (valueCurrent < 2)) {
this.debug('valueTarget=valueCurrent=' + valueCurrent);
this.valueTarget = valueCurrent;
}
this.valueCurrent = valueCurrent;
if (typeof this.valueTarget === 'undefined' && this.valueCurrent < 2) {
this.valueTarget = valueCurrent;
}
this.debug('result updateSensor timeout=' + timeout + ' moving=' + this.moving + ' current=' + this.valueCurrent + ' target=' + this.valueTarget);
let text = obstruction ? 'obstruction' : 'stopped';
let fill = obstruction ? 'red' : 'yellow';
let shape = 'ring';
switch (this.valueCurrent) {
case 0:
text = 'open';
shape = 'dot';
fill = 'blue';
break;
case 1:
text = 'closed';
shape = 'dot';
fill = 'green';
break;
case 2:
text = 'opening';
fill = 'blue';
break;
case 3:
text = 'closing';
fill = 'green';
break;
default:
}
this.status({fill, shape, text});
this.send({topic: this.name, payload: this.valueCurrent === 1, text, CurrentDoorState: this.valueCurrent, ObstructionDetected: obstruction, TargetDoorState: this.valueTarget});
this.debug('update GarageDoorOpener 0 CurrentDoorState ' + this.valueCurrent);
service.updateCharacteristic(hap.Characteristic.CurrentDoorState, this.valueCurrent);
this.debug('update GarageDoorOpener 0 TargetDoorState ' + this.valueTarget);
service.updateCharacteristic(hap.Characteristic.TargetDoorState, this.valueTarget);
this.debug('update GarageDoorOpener 0 ObstructionDetected ' + obstruction);
service.updateCharacteristic(hap.Characteristic.ObstructionDetected, obstruction);
};
if (config.channelSensorType.includes('c')) {
this.debug('subscribe ' + config.channelSensorClosed);
this.idSubSensorClosed = this.ccu.subscribe({
cache: true,
change: true,
iface: config.ifaceSensor,
channel: config.channelSensorClosed.split(' ')[0],
datapoint: /STATE|MOTION|SENSOR/
}, msg => {
this.closed = config.directionClosed ? msg.value : !msg.value;
this.log(config.channelSensorClosed + ' ' + msg.value + ' closed=' + this.closed);
this.updateSensor(false, 'c');
});
}
if (config.channelSensorType.includes('o')) {
this.debug('subscribe ' + config.channelSensorOpened);
this.idSubSensorOpened = this.ccu.subscribe({
cache: true,
change: true,
iface: config.ifaceSensor,
channel: config.channelSensorOpened.split(' ')[0],
datapoint: /STATE|MOTION|SENSOR/
}, msg => {
this.opened = config.directionOpened ? msg.value : !msg.value;
this.log(config.channelSensorOpened + ' ' + msg.value + ' openend=' + this.opened);
this.updateSensor(false, 'o');
});
}
const getCurrentDoorStateListener = callback => {
this.debug('get GarageDoorOpener 0 CurrentDoorState ' + this.valueCurrent);
callback(null, this.valueCurrent);
};
const getTargetDoorStateListener = callback => {
this.debug('get GarageDoorOpener 0 TargetDoorState ' + this.valueTarget);
callback(null, this.valueTarget);
};
const setTargetDoorStateListener = (value, callback) => {
this.debug('set GarageDoorOpener 0 TargetDoorState ' + value);
clearTimeout(this.timer);
const revert = this.moving && ((this.moving - 2) !== value);
this.debug('revert=' + revert + ' moving=' + this.moving + ' lastMove=' + this.lastMove + ' currentState=' + this.currentState + ' opened=' + this.opened + ' closed=' + this.closed);
this.moving = value + 2;
this.lastMove = this.moving;
this.valueTarget = value;
this.updateSensor();
move(value, revert).then(() => {
this.timer = setTimeout(() => {
this.moving = false;
this.updateSensor(true);
}, (value ? config.duration : config.durationClose) * 1000);
callback(null);
}).catch(() => {
callback(new Error(hap.HAPServer.Status.SERVICE_COMMUNICATION_FAILURE));
});
};
this.on('input', msg => {
let value;
switch (msg.payload) {
case 'close':
value = 1;
break;
case 'open':
value = 0;
break;
default:
value = value ? 1 : 0;
}
if (!this.moving && this.valueCurrent === value) {
return;
}
const revert = this.moving && ((this.moving - 2) !== value);
this.moving = value + 2;
this.lastMove = this.moving;
this.valueTarget = value;
move(value, revert).then(() => {
this.timer = setTimeout(() => {
this.moving = false;
this.updateSensor(true);
}, (value ? config.duration : config.durationClose) * 1000);
}).catch(error => {
this.error(error);
});
});
service.getCharacteristic(hap.Characteristic.CurrentDoorState).on('get', getCurrentDoorStateListener);
service.getCharacteristic(hap.Characteristic.TargetDoorState).on('get', getTargetDoorStateListener);
service.getCharacteristic(hap.Characteristic.TargetDoorState).on('set', setTargetDoorStateListener);
this.on('close', () => {
service.getCharacteristic(hap.Characteristic.CurrentDoorState).removeListener('get', getCurrentDoorStateListener);
service.getCharacteristic(hap.Characteristic.TargetDoorState).removeListener('get', getTargetDoorStateListener);
service.getCharacteristic(hap.Characteristic.TargetDoorState).removeListener('set', setTargetDoorStateListener);
if (this.idSubSensorClosed) {
this.ccu.unsubscribe(this.idSubSensorClosed);
}
if (this.idSubSensorOpened) {
this.ccu.unsubscribe(this.idSubSensorOpened);
}
});
}
}
RED.nodes.registerType('redmatic-homekit-homematic-garage', RedMaticHomeKitHomematicGarage);
};