UNPKG

@dotwee/homebridge-z2m

Version:

Expose your Zigbee devices to HomeKit with ease, by integrating Zigbee2MQTT with Homebridge.

245 lines 12.5 kB
"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