homebridge-broadlink-rm
Version:
Broadlink RM plugin (including the mini and pro) for homebridge: https://github.com/nfarina/homebridge
238 lines (173 loc) • 8.15 kB
JavaScript
const { assert } = require('chai');
const delayForDuration = require('../helpers/delayForDuration');
const catchDelayCancelError = require('../helpers/catchDelayCancelError');
const BroadlinkRMAccessory = require('./accessory');
class WindowCoveringAccessory extends BroadlinkRMAccessory {
serviceType () { return Service.WindowCovering }
setDefaults () {
const { config, state } = this;
const { currentPosition, positionState } = state;
const { initialDelay, totalDurationOpen, totalDurationClose } = config;
// Check required propertoes
assert.isNumber(totalDurationOpen, '`totalDurationOpen` is required and should be numeric.')
assert.isNumber(totalDurationClose, '`totalDurationClose` is required and should be numeric.')
// Set config default values
if (!initialDelay) config.initialDelay = 0.1;
// Set state default values
if (currentPosition === undefined) this.state.currentPosition = 0;
if (positionState === undefined) this.state.positionState = Characteristic.PositionState.STOPPED;
}
async reset () {
super.reset();
// Clear existing timeouts
if (this.initialDelayPromise) {
this.initialDelayPromise.cancel();
this.initialDelayPromise = null;
}
if (this.updateCurrentPositionPromise) {
this.updateCurrentPositionPromise.cancel();
this.updateCurrentPositionPromise = null;
}
if (this.autoStopPromise) {
this.autoStopPromise.cancel();
this.autoStopPromise = null;
}
}
// User requested a specific position or asked the window-covering to be open or closed
async setTargetPosition (hexData, previousValue) {
await catchDelayCancelError(async () => {
const { config, host, debug, data, log, name, state, serviceManager } = this;
const { initialDelay } = config;
const { open, close, stop } = data;
this.reset();
// Ignore if no change to the targetPosition
if (state.targetPosition === previousValue) return;
// `initialDelay` allows multiple `window-covering` accessories to be updated at the same time
// without RF interference by adding an offset to each `window-covering` accessory
this.initialDelayPromise = delayForDuration(initialDelay);
await this.initialDelayPromise;
const closeCompletely = await this.checkOpenOrCloseCompletely();
if (closeCompletely) return;
log(`${name} setTargetPosition: (currentPosition: ${state.currentPosition})`);
// Determine if we're opening or closing
let difference = state.targetPosition - state.currentPosition;
state.opening = (difference > 0);
if (!state.opening) difference = -1 * difference;
hexData = state.opening ? open : close
// Perform the actual open/close asynchronously i.e. without await so that HomeKit status can be updated
this.openOrClose({ hexData, previousValue });
});
}
async openOrClose ({ hexData, previousValue }) {
await catchDelayCancelError(async () => {
let { config, data, host, name, log, state, debug, serviceManager } = this;
let { totalDurationOpen, totalDurationClose } = config;
const { stop } = data;
const newPositionState = state.opening ? Characteristic.PositionState.INCREASING : Characteristic.PositionState.DECREASING;
serviceManager.setCharacteristic(Characteristic.PositionState, newPositionState);
log(`${name} setTargetPosition: currently ${state.currentPosition}%, moving to ${state.targetPosition}%`);
await this.performSend(hexData);
let difference = state.targetPosition - state.currentPosition
if (!state.opening) difference = -1 * difference;
const fullOpenCloseTime = state.opening ? totalDurationOpen : totalDurationClose;
const durationPerPercentage = fullOpenCloseTime / 100;
const totalTime = durationPerPercentage * difference;
log(`${name} setTargetPosition: ${totalTime}s (${fullOpenCloseTime} / 100 * ${difference}) until auto-stop`);
this.startUpdatingCurrentPositionAtIntervals();
this.autoStopPromise = delayForDuration(totalTime);
await this.autoStopPromise;
await this.stopWindowCovering();
serviceManager.setCharacteristic(Characteristic.CurrentPosition, state.targetPosition);
});
}
async stopWindowCovering () {
const { config, data, host, log, name, state, debug, serviceManager } = this;
const { sendStopAt0, sendStopAt100 } = config;
const { stop } = data;
log(`${name} setTargetPosition: (stop window covering)`);
// Reset the state and timers
this.reset();
if (state.targetPosition === 100 && sendStopAt100) await this.performSend(stop);
if (state.targetPosition === 0 && sendStopAt0) await this.performSend(stop);
if (state.targetPosition !== 0 && state.targetPosition != 100) await this.performSend(stop);
serviceManager.setCharacteristic(Characteristic.PositionState, Characteristic.PositionState.STOPPED);
}
async checkOpenOrCloseCompletely () {
const { data, debug, host, log, name, serviceManager, state } = this;
const { openCompletely, closeCompletely } = data;
// Completely Close
if (state.targetPosition === 0 && closeCompletely) {
serviceManager.setCharacteristic(Characteristic.CurrentPosition, state.targetPosition);
await this.performSend(closeCompletely);
this.stopWindowCovering();
return true;
}
// Completely Open
if (state.targetPosition === 100 && openCompletely) {
serviceManager.setCharacteristic(Characteristic.CurrentPosition, state.targetPosition);
await this.performSend(openCompletely);
this.stopWindowCovering();
return true;
}
return false;
}
// Determine how long it should take to increase/decrease a single %
determineOpenCloseDurationPerPercent ({ opening, totalDurationOpen, totalDurationClose }) {
assert.isBoolean(opening);
assert.isNumber(totalDurationOpen);
assert.isNumber(totalDurationClose);
assert.isAbove(totalDurationOpen, 0);
assert.isAbove(totalDurationClose, 0);
const fullOpenCloseTime = opening ? totalDurationOpen : totalDurationClose;
const durationPerPercentage = fullOpenCloseTime / 100;
return durationPerPercentage;
}
async startUpdatingCurrentPositionAtIntervals () {
catchDelayCancelError(async () => {
const { config, serviceManager, state } = this;
const { totalDurationOpen, totalDurationClose } = config;
const durationPerPercentage = this.determineOpenCloseDurationPerPercent({ opening: state.opening, totalDurationOpen, totalDurationClose })
// Wait for a single % to increase/decrease
this.updateCurrentPositionPromise = delayForDuration(durationPerPercentage)
await this.updateCurrentPositionPromise
// Set the new currentPosition
let currentValue = state.currentPosition || 0;
if (state.opening) currentValue++;
if (!state.opening) currentValue--;
serviceManager.setCharacteristic(Characteristic.CurrentPosition, currentValue);
// Let's go again
this.startUpdatingCurrentPositionAtIntervals();
});
}
configureServiceManager (serviceManager) {
serviceManager.addToggleCharacteristic({
name: 'currentPosition',
type: Characteristic.CurrentPosition,
bind: this,
getMethod: this.getCharacteristicValue,
setMethod: this.setCharacteristicValue,
props: {
}
});
serviceManager.addToggleCharacteristic({
name: 'positionState',
type: Characteristic.PositionState,
bind: this,
getMethod: this.getCharacteristicValue,
setMethod: this.setCharacteristicValue,
props: {
}
});
serviceManager.addToggleCharacteristic({
name: 'targetPosition',
type: Characteristic.TargetPosition,
bind: this,
getMethod: this.getCharacteristicValue,
setMethod: this.setCharacteristicValue,
props: {
setValuePromise: this.setTargetPosition.bind(this)
}
});
}
}
module.exports = WindowCoveringAccessory;