homebridge-virtual-accessories
Version:
Virtual HomeKit accessories for Homebridge.
165 lines • 8.75 kB
JavaScript
import { Accessory } from './accessory.js';
import { Timer } from '../utils/timer.js';
/**
* OpeningAccessory - Abstract accessory
*/
export class OpeningAccessory extends Accessory {
static CLOSED = 0; // 0%
static OPEN = 100; // 100%
static DECREASING = 0; // Characteristic.PositionState.DECREASING -> CLOSING
static INCREASING = 1; // Characteristic.PositionState.INCREASING -> OPENING
static STOPPED = 2; // Characteristic.PositionState.STOPPED -> OPEN or CLOSED
static MIN_TIMEOUT_SECS = 1;
static DEFAULT_TIMEOUT_SECS = 3;
stateStorageKey = 'Position';
transitionTimer;
transitionSteps = 0;
openingAccessoryConfiguration;
states = {
CurrentPosition: OpeningAccessory.CLOSED, // %
TargetPosition: OpeningAccessory.CLOSED, // %
PositionState: OpeningAccessory.STOPPED,
};
constructor(platform, accessory, accessoryConfiguration) {
super(platform, accessory, accessoryConfiguration);
// First configure the device based on the accessory details
this.openingAccessoryConfiguration = this.getOpeningAccessoryConfiguration();
this.defaultState = this.openingAccessoryConfiguration.defaultState === 'open' ? OpeningAccessory.OPEN : OpeningAccessory.CLOSED;
this.states.CurrentPosition = this.defaultState;
// If the accessory is stateful retrieve stored state
if (this.accessoryConfiguration.accessoryIsStateful) {
const accessoryState = this.loadAccessoryState(this.storagePath);
const cachedState = accessoryState[this.stateStorageKey];
if (cachedState !== undefined) {
this.states.CurrentPosition = cachedState;
}
}
this.states.TargetPosition = this.states.CurrentPosition;
const timerIsResettable = true;
this.transitionTimer = new Timer(this.accessoryConfiguration.accessoryName, this.log, timerIsResettable);
// set accessory information
const service = this.getOpeningAccessoryService();
this.service = this.accessory.getService(service) || this.accessory.addService(service);
this.service.setCharacteristic(this.platform.Characteristic.Name, this.accessoryConfiguration.accessoryName);
// Update the initial state of the accessory
// eslint-disable-next-line max-len
this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Setting Window Covering Current Position: ${OpeningAccessory.getStateName(this.states.CurrentPosition)}`);
this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, (this.states.CurrentPosition));
this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, (this.states.TargetPosition));
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, (this.states.PositionState));
// register handlers
this.service.getCharacteristic(this.platform.Characteristic.CurrentPosition)
.onGet(this.getCurrentPosition.bind(this));
this.service.getCharacteristic(this.platform.Characteristic.TargetPosition)
.onSet(this.setTargetPosition.bind(this))
.onGet(this.getTargetPosition.bind(this));
this.service.getCharacteristic(this.platform.Characteristic.PositionState)
.onGet(this.getPositionState.bind(this));
}
// Handlers
async getCurrentPosition() {
// If timer is running, then blinds are moving, so calculate the interim position
if (this.transitionTimer.isTimerRunning()) {
const runtimeMillis = this.transitionTimer.getRuntime() * 1000;
const remainingSteps = Math.ceil(this.transitionTimer.getRemainingDurationMillis() / runtimeMillis * this.transitionSteps);
this.states.CurrentPosition = this.states.TargetPosition - remainingSteps;
}
const currentPosition = this.states.CurrentPosition;
this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Current Position: ${OpeningAccessory.getStateName(currentPosition)}`);
return currentPosition;
}
async setTargetPosition(value) {
this.states.TargetPosition = value;
this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Target Position: ${OpeningAccessory.getStateName(this.states.TargetPosition)}`);
this.states.PositionState = (this.states.TargetPosition > this.states.CurrentPosition) ? OpeningAccessory.INCREASING : OpeningAccessory.DECREASING;
this.service.setCharacteristic(this.platform.Characteristic.PositionState, (this.states.PositionState));
this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Position State: ${OpeningAccessory.getPositionName(this.states.PositionState)}`);
const transitionDuration = this.openingAccessoryConfiguration.transitionDuration;
const transitionDelay = (transitionDuration ? transitionDuration : OpeningAccessory.DEFAULT_TIMEOUT_SECS);
this.transitionSteps = this.states.TargetPosition - this.states.CurrentPosition;
this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Transition Steps: ${this.transitionSteps}`);
const proportionalTransitionDelay = Math.max(
// Round up to the nearest second
Math.ceil(transitionDelay / 100 * Math.abs(this.transitionSteps)), OpeningAccessory.MIN_TIMEOUT_SECS);
this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Proportional Delay: ${proportionalTransitionDelay}/(${transitionDelay})`);
const updateIntervalMillis = 100;
// Stop transition timer, if running
this.transitionTimer.stop();
this.transitionTimer.start(() => {
this.states.PositionState = OpeningAccessory.STOPPED;
this.service.setCharacteristic(this.platform.Characteristic.PositionState, (this.states.PositionState));
this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Position State: ${OpeningAccessory.getPositionName(this.states.PositionState)}`);
this.states.CurrentPosition = this.states.TargetPosition;
this.service.setCharacteristic(this.platform.Characteristic.CurrentPosition, (this.states.CurrentPosition));
this.transitionSteps = 0;
this.storeState();
this.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Current Position: ${OpeningAccessory.getStateName(this.states.CurrentPosition)}`);
}, proportionalTransitionDelay, updateIntervalMillis);
}
async getTargetPosition() {
const targetPosition = this.states.TargetPosition;
this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Target Position: ${OpeningAccessory.getStateName(targetPosition)}`);
return targetPosition;
}
async getPositionState() {
const positionState = this.states.PositionState;
this.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Position State: ${OpeningAccessory.getPositionName(positionState)}`);
return positionState;
}
getJsonState() {
const json = JSON.stringify({
[this.stateStorageKey]: this.states.CurrentPosition,
});
return json;
}
static getStateName(position) {
let positionName;
switch (position) {
case undefined: {
positionName = 'undefined';
break;
}
case OpeningAccessory.CLOSED: {
positionName = 'CLOSED';
break;
}
case OpeningAccessory.OPEN: {
positionName = 'OPEN';
break;
}
default: {
positionName = `POSITION: ${position.toString()}%`;
}
}
if (position > OpeningAccessory.OPEN) {
positionName = `INVALID ${positionName}%`;
}
return positionName;
}
static getPositionName(state) {
let stateName;
switch (state) {
case undefined: {
stateName = 'undefined';
break;
}
case OpeningAccessory.DECREASING: {
stateName = 'DECREASING';
break;
}
case OpeningAccessory.INCREASING: {
stateName = 'INCREASING';
break;
}
case OpeningAccessory.STOPPED: {
stateName = 'STOPPED';
break;
}
default: {
stateName = state.toString();
}
}
return stateName;
}
}
//# sourceMappingURL=openingAccessory.js.map