@dotwee/homebridge-z2m
Version:
Expose your Zigbee devices to HomeKit with ease, by integrating Zigbee2MQTT with Homebridge.
245 lines • 12.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CoverCreator = void 0;
const z2mModels_1 = require("../z2mModels");
const hap_1 = require("../hap");
const helpers_1 = require("../helpers");
const timer_1 = require("../timer");
const monitor_1 = require("./monitor");
class CoverCreator {
createServicesFromExposes(accessory, exposes) {
exposes
.filter((e) => e.type === z2mModels_1.ExposesKnownTypes.COVER &&
(0, z2mModels_1.exposesHasFeatures)(e) &&
!accessory.isServiceHandlerIdKnown(CoverHandler.generateIdentifier(e.endpoint)))
.forEach((e) => this.createService(e, accessory));
}
createService(expose, accessory) {
try {
const handler = new CoverHandler(expose, accessory);
accessory.registerServiceHandler(handler);
}
catch (error) {
accessory.log.warn(`Failed to setup cover for accessory ${accessory.displayName} from expose "${JSON.stringify(expose)}": ${error}`);
}
}
}
exports.CoverCreator = CoverCreator;
class CoverHandler {
constructor(expose, accessory) {
this.accessory = accessory;
this.monitors = [];
this.lastPositionSet = -1;
this.positionCurrent = -1;
const endpoint = expose.endpoint;
this.identifier = CoverHandler.generateIdentifier(endpoint);
let positionExpose = expose.features.find((e) => (0, z2mModels_1.exposesHasNumericRangeProperty)(e) && e.name === 'position' && (0, z2mModels_1.exposesCanBeSet)(e) && (0, z2mModels_1.exposesIsPublished)(e));
this.tiltExpose = expose.features.find((e) => (0, z2mModels_1.exposesHasNumericRangeProperty)(e) && e.name === 'tilt' && (0, z2mModels_1.exposesCanBeSet)(e) && (0, z2mModels_1.exposesIsPublished)(e));
this.stateExpose = expose.features.find((e) => e.type === z2mModels_1.ExposesKnownTypes.ENUM &&
e.name === 'state' &&
(0, z2mModels_1.exposesCanBeSet)(e) &&
(0, z2mModels_1.exposesHasEnumProperty)(e) &&
e.values.includes(CoverHandler.STATE_HOLD_POSITION));
if (positionExpose === undefined) {
if (this.tiltExpose !== undefined) {
// Tilt only device
positionExpose = this.tiltExpose;
this.tiltExpose = undefined;
}
else {
throw new Error('Required "position" property not found for WindowCovering and no "tilt" as backup.');
}
}
this.positionExpose = positionExpose;
const serviceName = accessory.getDefaultServiceDisplayName(endpoint);
accessory.log.debug(`Configuring WindowCovering for ${serviceName}`);
this.service = accessory.getOrAddService(new hap_1.hap.Service.WindowCovering(serviceName, endpoint));
const current = (0, helpers_1.getOrAddCharacteristic)(this.service, hap_1.hap.Characteristic.CurrentPosition);
if (current.props.minValue === undefined || current.props.maxValue === undefined) {
throw new Error('CurrentPosition for Cover does not hav a rang (minValue, maxValue) defined.');
}
this.current_min = current.props.minValue;
this.current_max = current.props.maxValue;
(0, helpers_1.getOrAddCharacteristic)(this.service, hap_1.hap.Characteristic.PositionState);
const target = (0, helpers_1.getOrAddCharacteristic)(this.service, hap_1.hap.Characteristic.TargetPosition);
if (target.props.minValue === undefined || target.props.maxValue === undefined) {
throw new Error('TargetPosition for Cover does not have a rang (minValue, maxValue) defined.');
}
this.target_min = target.props.minValue;
this.target_max = target.props.maxValue;
target.on('set', this.handleSetTargetPosition.bind(this));
if (this.target_min !== this.current_min || this.target_max !== this.current_max) {
this.accessory.log.error(accessory.displayName + ': cover: TargetPosition and CurrentPosition do not have the same range!');
}
// Tilt
if (this.tiltExpose !== undefined) {
(0, helpers_1.getOrAddCharacteristic)(this.service, hap_1.hap.Characteristic.CurrentHorizontalTiltAngle);
this.monitors.push(new monitor_1.NumericCharacteristicMonitor(this.tiltExpose.property, this.service, hap_1.hap.Characteristic.CurrentHorizontalTiltAngle, this.tiltExpose.value_min, this.tiltExpose.value_max));
const target_tilt = (0, helpers_1.getOrAddCharacteristic)(this.service, hap_1.hap.Characteristic.TargetHorizontalTiltAngle);
if (target_tilt.props.minValue === undefined || target_tilt.props.maxValue === undefined) {
throw new Error('TargetHorizontalTiltAngle for Cover does not have a rang (minValue, maxValue) defined.');
}
this.target_tilt_min = target_tilt.props.minValue;
this.target_tilt_max = target_tilt.props.maxValue;
target_tilt.on('set', this.handleSetTargetHorizontalTilt.bind(this));
}
else {
this.target_tilt_min = -90;
this.target_tilt_max = 90;
}
// Hold Position
if (this.stateExpose !== undefined) {
(0, helpers_1.getOrAddCharacteristic)(this.service, hap_1.hap.Characteristic.HoldPosition).on('set', this.handleSetHoldPosition.bind(this));
}
if ((0, z2mModels_1.exposesCanBeGet)(this.positionExpose)) {
this.updateTimer = new timer_1.ExtendedTimer(this.requestPositionUpdate.bind(this), 4000);
}
this.waitingForUpdate = false;
this.ignoreNextUpdateIfEqualToTarget = false;
}
get getableKeys() {
const keys = [];
if ((0, z2mModels_1.exposesCanBeGet)(this.positionExpose)) {
keys.push(this.positionExpose.property);
}
if (this.tiltExpose !== undefined && (0, z2mModels_1.exposesCanBeGet)(this.tiltExpose)) {
keys.push(this.tiltExpose.property);
}
return keys;
}
updateState(state) {
this.monitors.forEach((m) => m.callback(state));
if (this.positionExpose.property in state) {
const latestPosition = state[this.positionExpose.property];
// Ignore "first" update?
const doIgnoreIfEqual = this.ignoreNextUpdateIfEqualToTarget;
this.ignoreNextUpdateIfEqualToTarget = false;
if (latestPosition === this.lastPositionSet && doIgnoreIfEqual) {
this.accessory.log.debug(`${this.accessory.displayName}: cover: ignore position update (equal to last target)`);
return;
}
// Received an update: Reset flag
this.waitingForUpdate = false;
// If we cannot retrieve the position or we were not expecting an update,
// always assume the state is "stopped".
let didStop = true;
// As long as the update timer is running, we are expecting updates.
if (this.updateTimer !== undefined && this.updateTimer.isActive) {
if (latestPosition === this.positionCurrent) {
// Stop requesting frequent updates if no change is detected.
this.updateTimer.stop();
}
else {
// Assume cover is still moving as the position is still changing
didStop = false;
this.startOrRestartUpdateTimer();
}
}
// Update current position
this.positionCurrent = latestPosition;
this.scaleAndUpdateCurrentPosition(this.positionCurrent, didStop);
}
}
startOrRestartUpdateTimer() {
if (this.updateTimer === undefined) {
return;
}
this.waitingForUpdate = true;
if (this.updateTimer.isActive) {
this.updateTimer.restart();
}
else {
this.updateTimer.start();
}
}
requestPositionUpdate() {
if (!(0, z2mModels_1.exposesCanBeGet)(this.positionExpose)) {
return;
}
if (this.waitingForUpdate) {
// Manually polling for the state, as we have not yet received an update.
this.accessory.queueKeyForGetAction(this.positionExpose.property);
}
}
scaleNumber(value, input_min, input_max, output_min, output_max) {
if (value <= input_min) {
return output_min;
}
if (value >= input_max) {
return output_max;
}
const percentage = (value - input_min) / (input_max - input_min);
return output_min + percentage * (output_max - output_min);
}
scaleAndUpdateCurrentPosition(value, isStopped) {
const characteristicValue = this.scaleNumber(value, this.positionExpose.value_min, this.positionExpose.value_max, this.current_min, this.current_max);
this.service.updateCharacteristic(hap_1.hap.Characteristic.CurrentPosition, characteristicValue);
if (isStopped) {
// Update target position and position state
// This should improve the UX in the Home.app
this.accessory.log.debug(`${this.accessory.displayName}: cover: assume movement stopped`);
this.service.updateCharacteristic(hap_1.hap.Characteristic.PositionState, hap_1.hap.Characteristic.PositionState.STOPPED);
this.service.updateCharacteristic(hap_1.hap.Characteristic.TargetPosition, characteristicValue);
}
}
handleSetTargetPosition(value, callback) {
const target = this.scaleNumber(value, this.target_min, this.target_max, this.positionExpose.value_min, this.positionExpose.value_max);
const data = {};
data[this.positionExpose.property] = target;
this.accessory.queueDataForSetAction(data);
// Assume position state based on new target
if (target > this.positionCurrent) {
this.service.updateCharacteristic(hap_1.hap.Characteristic.PositionState, hap_1.hap.Characteristic.PositionState.INCREASING);
}
else if (target < this.positionCurrent) {
this.service.updateCharacteristic(hap_1.hap.Characteristic.PositionState, hap_1.hap.Characteristic.PositionState.DECREASING);
}
else {
this.service.updateCharacteristic(hap_1.hap.Characteristic.PositionState, hap_1.hap.Characteristic.PositionState.STOPPED);
}
// Store last sent position for future reference
this.lastPositionSet = target;
// Ignore next status update if it is equal to the target position set here
// and the position can be get.
// This was needed for my Swedish blinds when reporting was enabled.
// (First update would contain the target position that was sent, followed by the actual position.)
if ((0, z2mModels_1.exposesCanBeGet)(this.positionExpose)) {
this.ignoreNextUpdateIfEqualToTarget = true;
}
// Start requesting frequent updates (if we do not receive them automatically)
this.startOrRestartUpdateTimer();
callback(null);
}
handleSetTargetHorizontalTilt(value, callback) {
if (this.tiltExpose) {
// map value: angle back to target: percentage
// must be rounded for set action
const targetTilt = Math.round(this.scaleNumber(value, this.target_tilt_min, this.target_tilt_max, this.tiltExpose.value_min, this.tiltExpose.value_max));
const data = {};
data[this.tiltExpose.property] = targetTilt;
this.accessory.queueDataForSetAction(data);
callback(null);
}
else {
callback(new Error('tilt not supported'));
}
}
handleSetHoldPosition(value, callback) {
const doHold = value;
if (doHold && this.stateExpose) {
const data = {};
data[this.stateExpose.property] = CoverHandler.STATE_HOLD_POSITION;
this.accessory.queueDataForSetAction(data);
}
callback(null);
}
static generateIdentifier(endpoint) {
let identifier = hap_1.hap.Service.WindowCovering.UUID;
if (endpoint !== undefined) {
identifier += '_' + endpoint.trim();
}
return identifier;
}
}
CoverHandler.STATE_HOLD_POSITION = 'STOP';
//# sourceMappingURL=cover.js.map