Skip to content

Commit

Permalink
Update plugin to account for binary open / close devices. Fixes #2.
Browse files Browse the repository at this point in the history
  • Loading branch information
gormanb committed Dec 14, 2022
1 parent c2a8952 commit ed5c86c
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 17 deletions.
38 changes: 28 additions & 10 deletions src/blindAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export class BlindAccessory {
private currentState: ReadDeviceResponse;
private lastState: ReadDeviceResponse;

// Does the device only support binary open / close?
private usesBinaryState = false;

constructor(
private readonly platform: ConnectorHubPlatform,
private readonly accessory: PlatformAccessory,
Expand Down Expand Up @@ -103,6 +106,11 @@ export class BlindAccessory {
return;
}

// Determine whether the device only reports binary open / closed state,
// then sanitize the status object to conform to the expected format.
this.usesBinaryState = (newState.data.currentPosition === undefined);
this.currentState = helpers.sanitizeDeviceState(newState);

// If this is the first time we've read the device, update the model type.
if (!this.lastState) {
this.setAccessoryInformation(newState.data.type);
Expand All @@ -116,9 +124,7 @@ export class BlindAccessory {
Log.debug(`Updated ${this.accessory.displayName} state:`, newState);
// Note that the hub reports 0 as fully open and 100 as closed, but
// Homekit expects the opposite. Correct the value before reporting.
const newPos = newState.data.currentPosition !== undefined ?
(100 - newState.data.currentPosition) :
(100 * newState.data.operation);
const newPos = (100 - newState.data.currentPosition);
Log.info('Updating position ', [this.accessory.displayName, newPos]);
// Update the TargetPosition, since we've just reached it, and the actual
// CurrentPosition. Syncs Homekit if blinds are moved by another app.
Expand Down Expand Up @@ -169,21 +175,35 @@ export class BlindAccessory {
* user changes the state of the blind. Throws SERVICE_COMMUNICATION_FAILURE
* if the hub cannot be contacted.
*/
async setTargetPosition(targetValue: CharacteristicValue) {
async setTargetPosition(targetVal: CharacteristicValue) {
// Homekit positions are the inverse of what the hub expects.
const adjustedTarget = (100 - <number>targetValue);
let adjustedTarget = (100 - <number>targetVal);

// Make sure the target value is supported for this device.
if (this.usesBinaryState) {
adjustedTarget = helpers.binarizeTargetPosition(
adjustedTarget, <ReadDeviceAck>(this.currentState || this.lastState));
}

// Send the request to the hub and wait for a response.
const ack = <WriteDeviceResponse>(
await this.client.setTargetPosition(adjustedTarget));

// Log the response from the hub if we are in debug mode.
Log.debug('Target response:', ack ? ack : 'None');

if (!ack || ack.actionResult) {
// Check whether the ack we received is valid for the request we sent.
const invalidAck = ack &&
(!this.usesBinaryState && ack.data.currentPosition === undefined);

// 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 || ack.actionResult || invalidAck) {
Log.error('Failed to target', this.accessory.displayName);
throw new this.platform.api.hap.HapStatusError(
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
Log.info('Targeted: ', [this.accessory.displayName, targetValue]);
Log.info('Targeted: ', [this.accessory.displayName, targetVal]);
}

/**
Expand All @@ -201,9 +221,7 @@ export class BlindAccessory {
Log.debug(`${this.accessory.displayName} state:`, this.currentState);
// Note that the hub reports 0 as fully open and 100 as closed, but
// Homekit expects the opposite. Correct the value before reporting.
const currentPos = this.currentState.data.currentPosition !== undefined ?
(100 - this.currentState.data.currentPosition) :
(100 * this.currentState.data.operation);
const currentPos = (100 - this.currentState.data.currentPosition);
Log.info('Returning position: ', [this.accessory.displayName, currentPos]);
return currentPos;
}
Expand Down
3 changes: 3 additions & 0 deletions src/connectorhub/connector-hub-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export enum BlindPositionState {
export const opCodes =
['close', 'open', 'stop', undefined, undefined, 'status'];

// Maps opCodes to corresponding percentage position.
export const opCodePositions = [100, 0];

// States that the Connector hub can be in.
export const hubStats = [undefined, 'Working', 'Pairing', 'Updating'];

Expand Down
44 changes: 44 additions & 0 deletions src/connectorhub/connector-hub-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,54 @@ export function makeWriteDeviceRequest(
};
}

// Convert a percentage position into a binary open / closed state. Note that
// the input is a Connector hub position, not an inverted Homekit position.
export function positionToOpCode(position: number): hubapi.DeviceOpCode {
return position >= 50 ? hubapi.DeviceOpCode.kClose :
hubapi.DeviceOpCode.kOpen;
}

// Given a kOpen or kClose opcode, return the equivalent Connector hub position.
export function opCodeToPosition(opCode: hubapi.DeviceOpCode): number {
return consts.opCodePositions[opCode];
}

//
// Helpers which assist in interpreting the responses from the hub.
//

// Helper function which ensures that the device state received from the hub is
// in the format expected by the plugin. Mutates and returns the input object.
export function sanitizeDeviceState(deviceState: hubapi.ReadDeviceAck) {
// Depending on the device type, the hub may return an explicit position or a
// simple open / closed state. In the former case, we don't change anything.
if (deviceState.data.currentPosition !== undefined) {
return deviceState;
}
// Otherwise, convert the open / closed state into a currentPosition.
if (deviceState.data.operation <= hubapi.DeviceOpCode.kOpen) {
// kClose = 0 and kOpen = 1, but currentPosition is 0 for open.
deviceState.data.currentPosition =
(deviceState.data.operation === hubapi.DeviceOpCode.kOpen ? 0 : 100);
return deviceState;
}
// If we reach here, then neither state nor position are available.
Log.warn('Failed to sanitize device state:', deviceState);
deviceState.data.currentPosition = 100;
return deviceState;
}

// Homekit may set a percentage position for a device that only supports binary
// open and close. This function is used to handle this scenario. Note that the
// input targetPos is a Connector hub position, not a Homekit position.
export function binarizeTargetPosition(
targetPos: number, deviceState: hubapi.ReadDeviceAck): number {
// If the target is the same as the current position, do nothing. If not,
// return the inverse of the current state as the new target position.
const currentPos = opCodeToPosition(deviceState.data.operation);
return targetPos !== currentPos ? (100 - currentPos) : targetPos;
}

// Input is the "data.type" field from the ReadDeviceAck response.
export function getDeviceModel(type: number): string {
return consts.deviceModels[type] || 'Generic Blind';
Expand Down
15 changes: 8 additions & 7 deletions src/connectorhub/connectorHubClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import {DgramAsPromised} from 'dgram-as-promised';
import {PlatformConfig} from 'homebridge';

import {Log} from '../util/log';

import * as hubapi from './connector-hub-api';
import * as consts from './connector-hub-constants';
import * as helpers from './connector-hub-helpers';
Expand Down Expand Up @@ -75,13 +73,16 @@ export class ConnectorHubClient {
return sendCommand(command, this.sendIp);
}

private setOpenCloseState(op: hubapi.DeviceOpCode): Promise<DeviceResponse> {
return this.setDeviceState({operation: op});
}

public setTargetPosition(position: number): Promise<DeviceResponse> {
if (position === 100 || position === 0) {
const opCode = position === 0 ? hubapi.DeviceOpCode.kOpen :
hubapi.DeviceOpCode.kClose;
Log.debug('Simple target command:', {operation: opCode});
return this.setDeviceState({operation: opCode});
// Where feasible, use binary state commands for greater compatibility.
if (position === 0 || position === 100) {
return this.setOpenCloseState(helpers.positionToOpCode(position));
}
// Otherwise, target the specified percentage position explicitly.
return this.setDeviceState({targetPosition: position});
}

Expand Down

0 comments on commit ed5c86c

Please sign in to comment.