homebridge-connector-hub
Version:
A Homebridge plugin to expose Connector+ compatible blinds, curtains, awnings etc to Homekit
240 lines • 14.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectorAccessory = void 0;
const connector_hub_constants_1 = require("./connectorhub/connector-hub-constants");
const connector_hub_helpers_1 = require("./connectorhub/connector-hub-helpers");
const connectorDeviceHandler_1 = require("./connectorhub/connectorDeviceHandler");
const connectorHubClient_1 = require("./connectorhub/connectorHubClient");
const log_1 = require("./util/log");
/**
* An instance of this class is created for each accessory. Exposes both the
* WindowCovering and Battery services for the device.
*/
class ConnectorAccessory extends connectorDeviceHandler_1.ConnectorDeviceHandler {
constructor(platform, accessory) {
// Initialize the superclass constructor.
super(accessory.context.device, platform.config);
this.platform = platform;
this.accessory = accessory;
this.performActiveRead = true;
// Current target position for this device.
this.currentTargetPos = -1;
// Create a new client connection for this device.
this.client = new connectorHubClient_1.ConnectorHubClient(this.platform.config, this.deviceInfo, this.deviceInfo.hubIp, this.deviceInfo.hubToken);
// Get the WindowCovering service if it exists, otherwise create one.
this.wcService =
this.accessory.getService(this.platform.Service.WindowCovering) ||
this.accessory.addService(this.platform.Service.WindowCovering);
// Add a service to report the battery level.
this.batteryService =
this.accessory.getService(this.platform.Service.Battery) ||
this.accessory.addService(this.platform.Service.Battery);
// Initialize the device state and set up a periodic refresh.
this.updateDeviceStatus();
this.periodicRefreshTimer = setInterval(() => this.updateDeviceStatus(), connector_hub_constants_1.kNetworkSettings.refreshIntervalMs);
// Set up a timer to indicate when we should perform active reads.
this.activeReadTimer = setInterval(() => {
this.performActiveRead = true;
}, ConnectorAccessory.kActiveReadInterval);
// Register handlers for the CurrentPosition Characteristic.
this.wcService
.getCharacteristic(this.platform.Characteristic.CurrentPosition)
.onGet(this.getCurrentPosition.bind(this));
// Register handlers for the PositionState Characteristic.
this.wcService.getCharacteristic(this.platform.Characteristic.PositionState)
.onGet(this.getPositionState.bind(this));
// Register handlers for the TargetPosition Characteristic
this.wcService
.getCharacteristic(this.platform.Characteristic.TargetPosition)
.onGet(this.getTargetPosition.bind(this))
.onSet(this.setTargetPosition.bind(this));
}
// Update the device information displayed in Homekit. Only called once.
setAccessoryInformation(deviceState) {
const Characteristic = this.platform.Characteristic;
// Update the accessory display name, in case it wasn't set already.
this.accessory.displayName = (0, connector_hub_helpers_1.makeDeviceName)(this.deviceInfo);
this.platform.api.updatePlatformAccessories([this.accessory]);
// Set the service names. These are the default names displayed by Homekit.
this.wcService.setCharacteristic(Characteristic.Name, this.accessory.displayName);
this.batteryService.setCharacteristic(Characteristic.Name, `${this.accessory.displayName} Battery`);
// Update default accessory name and additional information in Homekit.
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(Characteristic.Name, this.accessory.displayName)
.setCharacteristic(Characteristic.Manufacturer, 'Dooya')
.setCharacteristic(Characteristic.SerialNumber, this.deviceInfo.mac)
.setCharacteristic(Characteristic.Model, (0, connector_hub_helpers_1.getDeviceModel)(deviceState.deviceType, deviceState.data.type));
}
/**
* This function is the main driver of the plugin. It periodically reads the
* current device state from the hub and, if relevant values have changed,
* pushes the new state to Homekit. This approach is taken because pulling the
* status from the hub whenever Homekit requests it is too slow. It also means
* that Homekit will stay in sync with any external changes to the device
* state, e.g. if the device is moved using a physical remote.
*
* Note that the hub does not report real-time values; it only updates the
* device state when a movement completes.
*/
async updateDeviceStatus() {
var _a, _b;
// Determine whether we should perform an active or passive read, obtain the
// latest status from the device, and reset the active read tracker.
let newState = (await this.client.getDeviceState(this.performActiveRead ? connector_hub_constants_1.ReadDeviceType.kActive :
connector_hub_constants_1.ReadDeviceType.kPassive));
this.performActiveRead = false;
// Check whether the response from the hub is valid.
if (newState && (0, connector_hub_helpers_1.isInvalidAck)(newState)) {
// Read reply with 'actionResult' implies the device has been removed.
if (newState.msgType === 'ReadDeviceAck' && newState.actionResult) {
log_1.Log.info('Stale device response received:', newState);
this.platform.unregisterDevice(this.accessory);
clearInterval(this.periodicRefreshTimer);
clearInterval(this.activeReadTimer);
return;
}
// Otherwise, we may have a write reply error due to invalid access token.
log_1.Log.error('Error received from hub. App key may be invalid:', newState);
return;
}
// Update the cached current and last-good copies of the device status.
this.lastState = (this.currentState || this.lastState);
this.currentState = newState;
// If we didn't hear back from the device, exit early.
if (!newState) {
log_1.Log.debug('Periodic refresh failed:', this.accessory.displayName);
return;
}
// Log a debug message showing the new device state received from the hub.
log_1.Log.debug(`Latest ${this.accessory.displayName} state:`, newState);
// Sanitize the device state for the specific device that we are handling.
this.currentState = newState = this.sanitizeDeviceState(newState);
// The first time we read the device, we update the accessory details.
if (!this.lastState) {
this.setAccessoryInformation(newState);
}
// We extract 'lastPos' as below because lastState will be undefined on the
// first iteration, so we wish to force an update.
const lastPos = ((_a = this.lastState) === null || _a === void 0 ? void 0 : _a.data.currentPosition);
if (newState.data.currentPosition !== lastPos) {
// Log a message for the user to signify that the position has changed.
const newPos = this.toHomekitPercent(newState.data.currentPosition);
log_1.Log.info('Updating position:', [this.accessory.displayName, newPos]);
// The hub updates only after completing each movement. Update the target
// position to match the new currentPosition. Usually this is a no-op, but
// it will keep Homekit in sync if the device is moved externally.
this.currentTargetPos = newState.data.currentPosition;
// Push the new state of the window covering properties to Homekit.
this.updateWindowCoveringService();
}
// Update the battery level if it has changed since the last refresh.
const lastBatteryPC = (0, connector_hub_helpers_1.getBatteryPercent)((_b = this.lastState) === null || _b === void 0 ? void 0 : _b.data.batteryLevel);
const batteryPC = (0, connector_hub_helpers_1.getBatteryPercent)(newState === null || newState === void 0 ? void 0 : newState.data.batteryLevel);
if (batteryPC !== lastBatteryPC) {
// Log a message for the user, then push the new battery state to Homekit.
log_1.Log.info('Updating battery:', [this.accessory.displayName, batteryPC]);
this.updateBatteryService();
}
}
// Push the current status of the window covering properties to Homekit.
updateWindowCoveringService() {
// We only update if we have an up-to-date device state. Note that the hub
// reports 0 as fully open and 100 as closed, but Homekit expects the
// opposite. Correct the values before reporting.
if (this.currentState) {
this.wcService.updateCharacteristic(this.platform.Characteristic.CurrentPosition, this.toHomekitPercent(this.currentState.data.currentPosition));
this.wcService.updateCharacteristic(this.platform.Characteristic.TargetPosition, this.toHomekitPercent(this.currentTargetPos));
this.wcService.updateCharacteristic(this.platform.Characteristic.PositionState, this.getDirection(this.currentState.data.currentPosition, this.currentTargetPos));
}
}
// Push the current values of the battery service properties to Homekit.
updateBatteryService() {
// We only update if we have an up-to-date device state.
if (this.currentState) {
this.batteryService.updateCharacteristic(this.platform.Characteristic.BatteryLevel, (0, connector_hub_helpers_1.getBatteryPercent)(this.currentState.data.batteryLevel));
this.batteryService.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, (0, connector_hub_helpers_1.isLowBattery)(this.currentState.data.batteryLevel));
this.batteryService.updateCharacteristic(this.platform.Characteristic.ChargingState, this.currentState.data.chargingState ||
this.platform.Characteristic.ChargingState.NOT_CHARGING);
}
}
/**
* Handle "set TargetPosition" requests from HomeKit. These are sent when the
* user changes the state of the device. Throws SERVICE_COMMUNICATION_FAILURE
* if the hub cannot be contacted.
*/
async setTargetPosition(targetVal) {
// Adjust the target value from Homekit to Hub values, and construct a
// target request appropriate to this device.
const [hubTarget, targetReq] = this.makeTargetRequest(targetVal);
// Send the targeting request in the appropriate format for this device.
const ack = await (() => {
return this.client.setDeviceState(targetReq);
})();
// If we didn't receive an ack, or if the ack reports an exception from the
// hub, or if the ack is invalid, throw a communications error to Homekit.
if (!ack || (0, connector_hub_helpers_1.isInvalidAck)(ack)) {
log_1.Log.error(`Failed to set ${this.accessory.displayName} to ${targetVal}:`, (ack || 'No response from hub'));
throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */);
}
// Record the current targeted position, and inform Homekit.
this.currentTargetPos = hubTarget;
this.updateWindowCoveringService();
// Log the result of the operation for the user.
log_1.Log.info('Targeted:', [this.accessory.displayName, targetVal]);
log_1.Log.debug('Target response:', (ack || 'None'));
}
async getTargetPosition() {
// If a target position hasn't been set yet, report a communication error.
if (this.currentTargetPos < 0) {
throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */);
}
// Target is cached in Connector hub format, convert to Homekit format.
const currentTarget = this.toHomekitPercent(this.currentTargetPos);
log_1.Log.debug('Returning target:', [this.accessory.displayName, currentTarget]);
return currentTarget;
}
/**
* Handle "get CurrentPosition" requests from HomeKit. Returns the most recent
* value cached by the periodic updater; throws SERVICE_COMMUNICATION_FAILURE
* if the most recent attempt to contact the hub failed.
*/
async getCurrentPosition() {
if (!this.currentState) {
log_1.Log.debug('Failed to get position:', this.accessory.displayName);
throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */);
}
// Position is cached in Connector hub format, convert to Homekit format.
const currentPos = this.toHomekitPercent(this.currentState.data.currentPosition);
log_1.Log.debug('Returning position:', [this.accessory.displayName, currentPos]);
return currentPos;
}
/**
* In theory, the value of 'currentState.data.operation' would provide us with
* the correct PositionState. However, real-time polling of the devices causes
* severe degradation of responsiveness over time; we therefore use passive
* read requests, which only update the state after each movement is complete.
* This means that only the position ever changes, while the PositionState is
* always in the STOPPED state.
*
* Conversely, for devices which only use binary open/close commands, the op
* state is *never* STOPPED; it is always opening/open or closing/closed. But
* it is not possible to tell whether the motion is still in progress, or
* whether the operation code represents the current resting state.
*
* For these reasons, we compute the PositionState manually using the current
* and target positions.
*/
async getPositionState() {
// If we don't know the current or target position, throw an exception.
if (this.currentTargetPos < 0 || !this.currentState) {
throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */);
}
const posState = this.getDirection(this.currentState.data.currentPosition, this.currentTargetPos);
log_1.Log.debug('Returning pos state:', [this.accessory.displayName, posState]);
return posState;
}
}
exports.ConnectorAccessory = ConnectorAccessory;
// Interval at which we actively rather than passively read the device state.
ConnectorAccessory.kActiveReadInterval = 60 * 60 * 1000;
//# sourceMappingURL=connectorAccessory.js.map