Skip to content

Commit

Permalink
Merge pull request #13 from sajmonr/7-add-device-feature-detection
Browse files Browse the repository at this point in the history
  • Loading branch information
sajmonr authored Mar 5, 2023
2 parents 1b7942e + 95c517d commit de15e4e
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"comma-dangle": ["warn", "never"],
"dot-notation": "off",
"eqeqeq": "warn",
"curly": ["warn", "multi"],
"curly": ["warn", "multi-line"],
"brace-style": ["warn"],
"prefer-arrow-callback": ["warn"],
"max-len": ["warn", 140],
Expand Down
15 changes: 11 additions & 4 deletions src/accessories/fan-accessory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CharacteristicValue, PlatformAccessory } from 'homebridge';
import { DeviceAttribute } from '../models/device-attributes';
import { HubspacePlatform } from '../platform';
import { HubspaceAccessory } from './hubspace-accessory';
import { isNullOrUndefined } from '../utils';
import { DeviceFunction } from '../models/device-functions';

/**
* Fan accessory for Hubspace platform
Expand All @@ -17,9 +17,16 @@ export class FanAccessory extends HubspaceAccessory{
constructor(platform: HubspacePlatform, accessory: PlatformAccessory) {
super(platform, accessory, platform.Service.Fanv2);

this.configureActive();
this.configureRotationSpeed();
}

private configureActive(): void{
this.service.getCharacteristic(this.platform.Characteristic.Active)
.onGet(this.getActive.bind(this));
}

private configureRotationSpeed(): void{
this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed)
.onGet(this.getRotationSpeed.bind(this))
.onSet(this.setRotationSpeed.bind(this))
Expand All @@ -32,7 +39,7 @@ export class FanAccessory extends HubspaceAccessory{

private async getActive(): Promise<CharacteristicValue>{
// Try to get the value
const value = await this.deviceService.getValue(this.device.deviceId, DeviceAttribute.FanPower);
const value = await this.deviceService.getValue(this.device.deviceId, DeviceFunction.FanPower);

// If the value is not defined then show 'Not Responding'
if(isNullOrUndefined(value)){
Expand All @@ -45,7 +52,7 @@ export class FanAccessory extends HubspaceAccessory{

private async getRotationSpeed(): Promise<CharacteristicValue>{
// Try to get the value
const value = await this.deviceService.getValue(this.device.deviceId, DeviceAttribute.FanSpeed);
const value = await this.deviceService.getValue(this.device.deviceId, DeviceFunction.FanSpeed);

// If the value is not defined then show 'Not Responding'
if(isNullOrUndefined(value)){
Expand All @@ -57,7 +64,7 @@ export class FanAccessory extends HubspaceAccessory{
}

private async setRotationSpeed(value: CharacteristicValue): Promise<void>{
await this.deviceService.setValue(this.device.deviceId, DeviceAttribute.FanSpeed, value);
await this.deviceService.setValue(this.device.deviceId, DeviceFunction.FanSpeed, value);
}

}
13 changes: 12 additions & 1 deletion src/accessories/hubspace-accessory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Logger, PlatformAccessory, Service, WithUUID } from 'homebridge';
import { Device } from '../models/device';
import { DeviceFunction } from '../models/device-functions';
import { HubspacePlatform } from '../platform';
import { DeviceService } from '../services/device.service';

Expand Down Expand Up @@ -38,11 +39,21 @@ export abstract class HubspaceAccessory{
protected readonly platform: HubspacePlatform,
protected readonly accessory: PlatformAccessory,
service: WithUUID<typeof Service> | Service
) {
) {
this.service = accessory.getService(service as WithUUID<typeof Service>) || this.accessory.addService(service as Service);

this.log = platform.log;
this.deviceService = platform.deviceService;
this.device = accessory.context.device;
}

/**
* Checks whether function is supported by device
* @param deviceFunction Function to check
* @returns True if function is supported by the device otherwise false
*/
protected supportsFunction(deviceFunction: DeviceFunction): boolean{
return this.device.functions.some(fc => fc === deviceFunction);
}

}
20 changes: 14 additions & 6 deletions src/accessories/light-accessory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CharacteristicValue, PlatformAccessory } from 'homebridge';
import { DeviceAttribute } from '../models/device-attributes';
import { HubspacePlatform } from '../platform';
import { HubspaceAccessory } from './hubspace-accessory';
import { isNullOrUndefined } from '../utils';
import { DeviceFunction } from '../models/device-functions';

/**
* Light accessory for Hubspace platform
Expand All @@ -17,10 +17,18 @@ export class LightAccessory extends HubspaceAccessory{
constructor(platform: HubspacePlatform, accessory: PlatformAccessory) {
super(platform, accessory, platform.Service.Lightbulb);

// Configure power on/off handlers
this.configurePower();
this.configureBrightness();
}

private configurePower(): void{
this.service.getCharacteristic(this.platform.Characteristic.On)
.onGet(this.getOn.bind(this))
.onSet(this.setOn.bind(this));
}

private configureBrightness(): void{
if(!this.supportsFunction(DeviceFunction.Brightness)) return;

this.service.getCharacteristic(this.platform.Characteristic.Brightness)
.onGet(this.getBrightness.bind(this))
Expand All @@ -29,7 +37,7 @@ export class LightAccessory extends HubspaceAccessory{

private async getOn(): Promise<CharacteristicValue>{
// Try to get the value
const value = await this.deviceService.getValueAsBoolean(this.device.deviceId, DeviceAttribute.LightPower);
const value = await this.deviceService.getValueAsBoolean(this.device.deviceId, DeviceFunction.LightPower);

// If the value is not defined then show 'Not Responding'
if(isNullOrUndefined(value)){
Expand All @@ -41,12 +49,12 @@ export class LightAccessory extends HubspaceAccessory{
}

private async setOn(value: CharacteristicValue): Promise<void>{
await this.deviceService.setValue(this.device.deviceId, DeviceAttribute.LightPower, value);
await this.deviceService.setValue(this.device.deviceId, DeviceFunction.LightPower, value);
}

private async getBrightness(): Promise<CharacteristicValue>{
// Try to get the value
const value = await this.deviceService.getValueAsInteger(this.device.deviceId, DeviceAttribute.LightBrightness);
const value = await this.deviceService.getValueAsInteger(this.device.deviceId, DeviceFunction.Brightness);

// If the value is not defined then show 'Not Responding'
if(isNullOrUndefined(value) || value === -1){
Expand All @@ -58,7 +66,7 @@ export class LightAccessory extends HubspaceAccessory{
}

private async setBrightness(value: CharacteristicValue): Promise<void>{
this.deviceService.setValue(this.device.deviceId, DeviceAttribute.LightBrightness, value);
this.deviceService.setValue(this.device.deviceId, DeviceFunction.Brightness, value);
}

}
11 changes: 0 additions & 11 deletions src/models/device-attributes.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/models/device-function-def.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DeviceFunction } from './device-functions';

/**
* Device function definition
*/
export interface DeviceFunctionDef{
/** Type of device this applies to */
type: DeviceFunction;

/** API attribute ID */
attributeId: number;

/** API function instance name string */
functionInstanceName?: string;

/** Device function class */
functionClass: string;
}
59 changes: 59 additions & 0 deletions src/models/device-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { DeviceFunctionDef } from './device-function-def';

/**
* Device functions types
*/
export enum DeviceFunction{
LightPower,
Brightness,
FanPower,
FanSpeed
}

/**
* Supported/implemented device functions
* with identifiers for discovery and/or manipulation.
*/
export const DeviceFunctions: DeviceFunctionDef[] = [
{
type: DeviceFunction.LightPower,
attributeId: 2,
functionClass: 'power',
functionInstanceName: 'light-power'
},
{
type: DeviceFunction.Brightness,
attributeId: 4,
functionClass: 'brightness'
},
{
type: DeviceFunction.FanPower,
attributeId: 3,
functionClass: 'power',
functionInstanceName: 'fan-power'
},
{
type: DeviceFunction.FanSpeed,
attributeId: 6,
functionClass: 'fan-speed',
functionInstanceName: 'fan-speed'
}
];

/**
* Gets function definition for a type
* @param deviceFunction Function type
* @returns Function definition for type
* @throws {@link Error} when a type has no definition associated with it
*/
export function getDeviceFunctionDef(deviceFunction: DeviceFunction): DeviceFunctionDef{
const fc = DeviceFunctions.find(fc => fc.type === deviceFunction);

// Throw an error when not found - function definition must be set during development,
// otherwise the plugin will not work as expected.
if(!fc){
throw new Error(`Failed to get function definition for '${deviceFunction}'. Each function requires to set a definition.`);
}

return fc;
}
3 changes: 3 additions & 0 deletions src/models/device.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DeviceFunction } from './device-functions';
import { DeviceType } from './device-type';

/**
Expand All @@ -16,4 +17,6 @@ export interface Device{
manufacturer: string;
/** Device model */
model: string[];
/** Supported device functions */
functions: DeviceFunction[];
}
9 changes: 9 additions & 0 deletions src/responses/device-function-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Response for device function
*/
export interface DeviceFunctionResponse{
/** Class of the function */
functionClass: string;
/** Instance name of the function */
functionInstance: string;
}
3 changes: 3 additions & 0 deletions src/responses/devices-response.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DeviceFunctionResponse } from './device-function-response';

/**
* HTTP response for device discovery
*/
Expand All @@ -13,5 +15,6 @@ export interface DeviceResponse{
manufacturerName: string;
model: string;
};
functions: DeviceFunctionResponse[];
};
}
30 changes: 16 additions & 14 deletions src/services/device.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { HubspacePlatform } from '../platform';
import { Endpoints } from '../api/endpoints';
import { createHttpClientWithBearerInterceptor } from '../api/http-client-factory';
import { DeviceAttribute } from '../models/device-attributes';
import { AxiosError, AxiosResponse } from 'axios';
import { DeviceStatusResponse } from '../responses/device-status-response';
import { CharacteristicValue } from 'homebridge';
import { convertNumberToHex } from '../utils';
import { isAferoError } from '../responses/afero-error-response';
import { DeviceFunction, getDeviceFunctionDef } from '../models/device-functions';

/**
* Service for interacting with devices
Expand All @@ -23,16 +23,17 @@ export class DeviceService{
/**
* Sets an attribute value for a device
* @param deviceId ID of a device
* @param attribute Attribute to set
* @param deviceFunction Function to set value for
* @param value Value to set to attribute
*/
async setValue(deviceId: string, attribute: DeviceAttribute, value: CharacteristicValue): Promise<void>{
async setValue(deviceId: string, deviceFunction: DeviceFunction, value: CharacteristicValue): Promise<void>{
const functionDef = getDeviceFunctionDef(deviceFunction);
let response: AxiosResponse;

try{
response = await this._httpClient.post(`accounts/${this._platform.accountService.accountId}/devices/${deviceId}/actions`, {
type: 'attribute_write',
attrId: attribute,
attrId: functionDef.attributeId,
data: this.getDataValue(value)
});
}catch(ex){
Expand All @@ -49,10 +50,11 @@ export class DeviceService{
/**
* Gets a value for attribute
* @param deviceId ID of a device
* @param attribute Attribute to get value for
* @param deviceFunction Function to get value for
* @returns Data value
*/
async getValue(deviceId: string, attribute: DeviceAttribute): Promise<CharacteristicValue | undefined>{
async getValue(deviceId: string, deviceFunction: DeviceFunction): Promise<CharacteristicValue | undefined>{
const functionDef = getDeviceFunctionDef(deviceFunction);
let deviceStatus: DeviceStatusResponse;

try{
Expand All @@ -66,10 +68,10 @@ export class DeviceService{
return undefined;
}

const attributeResponse = deviceStatus.attributes.find(a => a.id === attribute);
const attributeResponse = deviceStatus.attributes.find(a => a.id === functionDef.attributeId);

if(!attributeResponse){
this._platform.log.error(`Failed to find value for ${attribute} for device (device ID: ${deviceId})`);
this._platform.log.error(`Failed to find value for ${functionDef.functionInstanceName} for device (device ID: ${deviceId})`);
return undefined;
}

Expand All @@ -79,11 +81,11 @@ export class DeviceService{
/**
* Gets a value for attribute as boolean
* @param deviceId ID of a device
* @param attribute Attribute to get value for
* @param deviceFunction Function to get value for
* @returns Boolean value
*/
async getValueAsBoolean(deviceId: string, attribute: DeviceAttribute): Promise<boolean | undefined>{
const value = await this.getValue(deviceId, attribute);
async getValueAsBoolean(deviceId: string, deviceFunction: DeviceFunction): Promise<boolean | undefined>{
const value = await this.getValue(deviceId, deviceFunction);

if(!value) return undefined;

Expand All @@ -93,11 +95,11 @@ export class DeviceService{
/**
* Gets a value for attribute as integer
* @param deviceId ID of a device
* @param attribute Attribute to get value for
* @param deviceFunction Function to get value for
* @returns Integer value
*/
async getValueAsInteger(deviceId: string, attribute: DeviceAttribute): Promise<number | undefined>{
const value = await this.getValue(deviceId, attribute);
async getValueAsInteger(deviceId: string, deviceFunction: DeviceFunction): Promise<number | undefined>{
const value = await this.getValue(deviceId, deviceFunction);

if(!value || typeof value !== 'string') return undefined;

Expand Down
Loading

0 comments on commit de15e4e

Please sign in to comment.