homebridge-blaq
Version:
Control and view your garage door(s) remotely with real-time updates using Konnected's BlaQ hardware
354 lines • 18.4 kB
JavaScript
import { BaseBlaQAccessory } from './base.js';
const BINARY_SENSOR_PREFIX = 'binary_sensor-';
const COVER_PREFIX = 'cover-';
const LOCK_PREFIX = 'lock-';
/**
* Platform Accessory
* An instance of this class is created for each accessory your platform registers
* Each accessory may expose multiple services of different service types.
*/
export class BlaQGarageDoorAccessory extends BaseBlaQAccessory {
garageDoorService;
state;
position; // percentage open(100)/closed(0); might not always be accurate
targetPosition; // percentage open(100)/closed(0); might not always be accurate
currentOperation;
obstructed;
lockState = 'UNKNOWN';
lockType = 'lock';
coverType = 'garage_door';
preClosing;
constructor(args) {
super(args);
this.garageDoorService = this.getOrAddService(args.type === 'garage' ? this.platform.service.GarageDoorOpener : this.platform.service.WindowCovering);
this.removeService(args.type !== 'garage' ? this.platform.service.GarageDoorOpener : this.platform.service.WindowCovering);
// Set the service name. This is what is displayed as the name on the Home
// app. We use what we stored in `accessory.context` in `discoverDevices`.
this.garageDoorService.setCharacteristic(this.platform.characteristic.Name, this.accessory.context.device.displayName);
this.garageDoorService.getCharacteristic(this.platform.characteristic.CurrentDoorState)
.onGet(this.getCurrentDoorState.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.PositionState)
.onGet(this.getCurrentPositionState.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.HoldPosition)
.onGet(this.getHoldPositionState.bind(this))
.onSet(this.setHoldPositionState.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.CurrentPosition)
.onGet(this.getCurrentPosition.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.TargetDoorState)
.onGet(this.getTargetDoorState.bind(this))
.onSet(this.setTargetDoorState.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.TargetPosition)
.onGet(this.getTargetDoorPosition.bind(this))
.onSet(this.updateTargetDoorPosition.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.ObstructionDetected)
.onGet(this.getObstructed.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.LockCurrentState)
.onGet(this.getLockState.bind(this));
this.garageDoorService.getCharacteristic(this.platform.characteristic.LockTargetState)
.onSet(this.updateLockState.bind(this));
this.logger.debug(`Initialized ${this.getSelfClassName()}!`);
}
async updateLockState(lockState) {
const lockDesired = lockState === this.platform.characteristic.LockTargetState.SECURED;
const apiTarget = lockDesired ? 'lock' : 'unlock';
const currentlyLocked = this.getLockState() === this.platform.characteristic.LockCurrentState.SECURED;
if (lockDesired !== currentlyLocked) {
await this.authFetch(`${this.apiBaseURL}/lock/${this.lockType}/${apiTarget}`, { method: 'POST' });
}
}
getLockState() {
const lockStateMap = {
'UNSECURED': this.platform.characteristic.LockCurrentState.UNSECURED,
'SECURED': this.platform.characteristic.LockCurrentState.SECURED,
'JAMMED': this.platform.characteristic.LockCurrentState.JAMMED,
'UNKNOWN': this.platform.characteristic.LockCurrentState.UNKNOWN,
};
return lockStateMap[this.lockState];
}
setLockState(lockState) {
this.lockState = lockState;
const currentlyLocked = this.getLockState() === this.platform.characteristic.LockCurrentState.SECURED;
this.garageDoorService.setCharacteristic(this.platform.characteristic.LockTargetState, currentlyLocked ?
this.platform.characteristic.LockTargetState.SECURED :
this.platform.characteristic.LockTargetState.UNSECURED);
this.garageDoorService.setCharacteristic(this.platform.characteristic.LockCurrentState, this.getLockState());
}
getCurrentDoorState() {
if (this.preClosing || this.currentOperation === 'CLOSING' || (this.position !== undefined && this.targetPosition !== undefined && this.targetPosition < this.position)) {
// let closingReason = 'current operation is CLOSING';
// if(this.preClosing){
// closingReason = 'the pre-close warning is active';
// } else if(this.position !== undefined && this.targetPosition !== undefined && this.targetPosition < this.position){
// closingReason = `the target position (${this.targetPosition}) is less than the current position (${this.position})`;
// }
// this.logger.debug(`Returning current state CLOSING because: ${closingReason}`);
return this.platform.characteristic.CurrentDoorState.CLOSING;
}
else if (this.currentOperation === 'OPENING' || (this.position !== undefined && this.targetPosition !== undefined && this.targetPosition > this.position)) {
// const openingReason =
// this.currentOperation === 'OPENING' ?
// 'current operation is OPENING' :
// `the target position (${this.targetPosition}) is greater than the current position (${this.position})`;
// this.logger.debug(`Returning current state OPENING because: ${openingReason}`);
return this.platform.characteristic.CurrentDoorState.OPENING;
}
else if (this.state === 'OPEN' || (this.position !== undefined && this.position > 0)) {
// const openReason = this.state === 'OPEN' ? 'the current state is OPEN' : 'the current position is greater than zero';
// this.logger.debug(`Returning current state OPEN because: ${openReason}`);
return this.platform.characteristic.CurrentDoorState.OPEN;
}
else if (this.state === 'CLOSED' || (this.position !== undefined && this.position <= 0)) {
// const closedReason = this.state === 'CLOSED' ? 'the current state is CLOSED' : 'the current position is less than or equal to zero';
// this.logger.debug(`Returning current state CLOSED because: ${closedReason}`);
return this.platform.characteristic.CurrentDoorState.CLOSED;
}
throw new Error(`Invalid door state: ${this.state}`);
}
setCurrentDoorState(state) {
this.state = state;
this.updateCurrentDoorState();
}
getHoldPositionState() {
return [
this.platform.characteristic.CurrentDoorState.STOPPED,
this.platform.characteristic.CurrentDoorState.CLOSED,
this.platform.characteristic.CurrentDoorState.OPEN,
].includes(this.getCurrentDoorState());
}
getCurrentPositionState() {
return {
[this.platform.characteristic.CurrentDoorState.STOPPED]: this.platform.characteristic.PositionState.STOPPED,
[this.platform.characteristic.CurrentDoorState.CLOSED]: this.platform.characteristic.PositionState.STOPPED,
[this.platform.characteristic.CurrentDoorState.OPEN]: this.platform.characteristic.PositionState.STOPPED,
[this.platform.characteristic.CurrentDoorState.CLOSING]: this.platform.characteristic.PositionState.DECREASING,
[this.platform.characteristic.CurrentDoorState.OPENING]: this.platform.characteristic.PositionState.INCREASING,
}[this.getCurrentDoorState()];
}
updateCurrentDoorState() {
this.garageDoorService.setCharacteristic(this.platform.characteristic.CurrentDoorState, this.getCurrentDoorState());
this.garageDoorService.updateCharacteristic(this.platform.characteristic.PositionState, this.getCurrentPositionState());
this.garageDoorService.updateCharacteristic(this.platform.characteristic.HoldPosition, this.getHoldPositionState());
this.garageDoorService.updateCharacteristic(this.platform.characteristic.TargetDoorState, this.getTargetDoorState());
this.garageDoorService.setCharacteristic(this.platform.characteristic.CurrentPosition, this.getCurrentPosition());
this.garageDoorService.updateCharacteristic(this.platform.characteristic.TargetPosition, this.getTargetDoorPosition());
}
setPreClosing(preClosing) {
this.preClosing = preClosing;
this.updateCurrentDoorState();
}
setCurrentOperation(operation) {
this.currentOperation = operation;
this.updateCurrentDoorState();
}
getCurrentPosition() {
if (this.position) {
return this.position;
}
else if (this.state === 'OPEN') {
return 100;
}
else if (this.state === 'CLOSED') {
return 0;
}
else {
return 50; // unknown state
}
}
setCurrentPosition(position) {
this.position = position;
this.updateCurrentDoorState();
}
getTargetDoorState() {
if (this.currentOperation === 'CLOSING' || this.preClosing || (this.targetPosition !== undefined && this.targetPosition <= 0)) {
// let closedReason = 'current operation is CLOSING';
// if(this.preClosing){
// closedReason = 'the pre-close warning is active';
// }else if(this.targetPosition !== undefined && this.targetPosition <= 0){
// closedReason = 'target position is less than or equal to zero';
// }
// this.logger.debug(`Returning target state CLOSED because: ${closedReason}`);
return this.platform.characteristic.TargetDoorState.CLOSED;
}
else if (this.currentOperation === 'OPENING' || (this.targetPosition !== undefined && this.targetPosition > 0)) {
// const openReason = this.currentOperation === 'OPENING' ? 'current operation is OPENING' : 'target position is greater than zero';
// this.logger.debug(`Returning target state OPEN because: ${openReason}`);
return this.platform.characteristic.TargetDoorState.OPEN;
}
else if (this.currentOperation === 'IDLE') {
if (this.state === 'CLOSED') {
// this.logger.debug('Returning target state CLOSED because: current operation is IDLE and current state is CLOSED');
return this.platform.characteristic.TargetDoorState.CLOSED;
}
else if (this.state === 'OPEN') {
// this.logger.debug('Returning target state OPEN because: current operation is IDLE and current state is OPEN');
return this.platform.characteristic.TargetDoorState.OPEN;
}
}
// this.logger.debug('Returning target state CLOSED because: no other conditions matched');
return this.platform.characteristic.TargetDoorState.CLOSED;
// throw new Error(`Invalid target door state: ${this.currentOperation}`);
}
async setHoldPositionState(target) {
const shouldHold = target;
if (shouldHold) {
await this.authFetch(`${this.apiBaseURL}/cover/${this.coverType}/stop`, { method: 'POST' });
}
}
async setTargetDoorState(target) {
let apiTarget;
if (target === this.platform.characteristic.TargetDoorState.CLOSED) {
apiTarget = 'close';
this.targetPosition = 0;
}
else if (target === this.platform.characteristic.TargetDoorState.OPEN) {
this.targetPosition = 100;
apiTarget = 'open';
}
else {
throw new Error(`Invalid target door state: ${target}`);
}
this.updateCurrentDoorState();
await this.authFetch(`${this.apiBaseURL}/cover/${this.coverType}/${apiTarget}`, { method: 'POST' });
}
getTargetDoorPosition() {
if (this.targetPosition) {
return this.targetPosition;
}
else if (this.currentOperation === 'OPENING') {
return 100;
}
else if (this.currentOperation === 'CLOSING' || this.preClosing) {
return 0;
}
else if (this.currentOperation === 'IDLE') {
if (this.state === 'CLOSED') {
return 0;
}
else if (this.state === 'OPEN') {
return 100;
}
}
return 0;
// throw new Error(`Invalid target door position: ${this.targetPosition}`);
}
async updateTargetDoorPosition(target) {
if (isNaN(+target)) {
throw new Error(`Invalid target door position: ${target}`);
}
const roundedTarget = Math.round(+target);
this.targetPosition = roundedTarget;
if (this.position) {
this.setCurrentOperation(roundedTarget < this.position ? 'CLOSING' : 'OPENING');
}
this.updateCurrentDoorState();
if (this.position !== roundedTarget) {
await this.authFetch(`${this.apiBaseURL}/cover/${this.coverType}/set?position=${roundedTarget / 100}`, { method: 'POST' });
}
}
async setTargetDoorPosition(target) {
if (isNaN(+target)) {
throw new Error(`Invalid target door position: ${target}`);
}
const roundedTarget = Math.round(+target);
this.targetPosition = roundedTarget;
}
getObstructed() {
if (this.obstructed === undefined) {
this.logger.warn('No obstruction status has been received yet!');
}
return this.obstructed || false;
}
setObstructed(obstructed) {
this.obstructed = obstructed;
this.garageDoorService.setCharacteristic(this.platform.characteristic.ObstructionDetected, this.getObstructed());
}
handleStateEvent(stateEvent) {
super.handleStateEvent(stateEvent);
if (!this.synced) {
return;
}
try {
const stateInfo = JSON.parse(stateEvent.data);
if (['cover-garage_door', 'cover-door'].includes(stateInfo.id)) {
const doorEvent = stateInfo;
this.coverType = stateInfo.id.split(COVER_PREFIX).pop();
this.setCurrentDoorState(doorEvent.state);
this.setCurrentOperation(doorEvent.current_operation);
const curPos = Math.round(doorEvent.position * 100);
if (doorEvent.current_operation === 'IDLE') {
this.setTargetDoorPosition(curPos);
}
this.setCurrentPosition(curPos);
}
else if (stateInfo.id.startsWith(BINARY_SENSOR_PREFIX)) {
const binarySensorEvent = stateInfo;
const short_id = binarySensorEvent.id.split(BINARY_SENSOR_PREFIX).pop();
if (short_id === 'obstruction') {
this.setObstructed(binarySensorEvent.value);
}
}
else if (['lock-lock', 'lock-lock_remotes'].includes(stateInfo.id)) {
this.lockType = stateInfo.id.split(LOCK_PREFIX).pop();
const b = stateInfo;
const lockStateMap = {
'LOCKED': 'SECURED',
'SECURED': 'SECURED',
'UNLOCKED': 'UNSECURED',
'UNSECURED': 'UNSECURED',
};
const selectedLockState = lockStateMap[b.state] || 'UNKNOWN';
this.setLockState(selectedLockState);
}
}
catch (e) {
this.logger.error('Cannot deserialize message:', stateEvent);
this.logger.error('Deserialization yielded:', e);
}
}
handleLogEvent(logEvent) {
super.handleLogEvent(logEvent);
if (!this.synced) {
return;
}
try {
const logStr = logEvent.data;
const lowercaseLogStr = logStr.toLowerCase();
if (lowercaseLogStr.includes('door') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('opening')) {
this.setCurrentOperation('OPENING');
}
else if (lowercaseLogStr.includes('door') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('closing')) {
this.setCurrentOperation('CLOSING');
}
else if (lowercaseLogStr.includes('door') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('open')) {
this.setCurrentDoorState('OPEN');
}
else if (lowercaseLogStr.includes('door') && lowercaseLogStr.includes('state') && lowercaseLogStr.includes('closed')) {
this.setCurrentDoorState('CLOSED');
}
const positionMatch = lowercaseLogStr.match(/position[:=] ?([\d.]+%?)/g)?.shift();
if (positionMatch) {
const positionNum = +positionMatch.replaceAll(/[^\d.]+/g, '');
const multiplier = positionMatch.includes('%') || positionNum > 1 ? 1 : 100;
this.setCurrentPosition(positionNum * multiplier);
}
if (lowercaseLogStr.includes('moving') && lowercaseLogStr.includes('to position')) {
const movePos = lowercaseLogStr.split('to position').pop()?.trim().split(/[^0-9.]/g).shift();
if (movePos) {
const multiplier = lowercaseLogStr.includes('%') || +movePos > 1 ? 1 : 100;
this.setTargetDoorPosition(+movePos * multiplier);
}
}
if (lowercaseLogStr.includes('warning for ')) {
const warningDuration = parseInt(lowercaseLogStr.split('warning for ')?.pop()?.split('ms')?.shift()?.trim() || '5000');
this.setPreClosing(true);
setTimeout(() => {
this.setPreClosing(false);
}, warningDuration + 100); // add 100ms for closing state to happen right after
}
}
catch (e) {
this.logger.error('Log parsing error:', e);
}
}
}
//# sourceMappingURL=garage-door.js.map