diff --git a/.eslintrc b/.eslintrc index fdb129d..d34637b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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], diff --git a/src/accessories/fan-accessory.ts b/src/accessories/fan-accessory.ts index c081341..2cc5173 100644 --- a/src/accessories/fan-accessory.ts +++ b/src/accessories/fan-accessory.ts @@ -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 @@ -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)) @@ -32,7 +39,7 @@ export class FanAccessory extends HubspaceAccessory{ private async getActive(): Promise{ // 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)){ @@ -45,7 +52,7 @@ export class FanAccessory extends HubspaceAccessory{ private async getRotationSpeed(): Promise{ // 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)){ @@ -57,7 +64,7 @@ export class FanAccessory extends HubspaceAccessory{ } private async setRotationSpeed(value: CharacteristicValue): Promise{ - await this.deviceService.setValue(this.device.deviceId, DeviceAttribute.FanSpeed, value); + await this.deviceService.setValue(this.device.deviceId, DeviceFunction.FanSpeed, value); } } \ No newline at end of file diff --git a/src/accessories/hubspace-accessory.ts b/src/accessories/hubspace-accessory.ts index e70bbba..9aa3175 100644 --- a/src/accessories/hubspace-accessory.ts +++ b/src/accessories/hubspace-accessory.ts @@ -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'; @@ -38,11 +39,21 @@ export abstract class HubspaceAccessory{ protected readonly platform: HubspacePlatform, protected readonly accessory: PlatformAccessory, service: WithUUID | Service - ) { + ) { this.service = accessory.getService(service as WithUUID) || 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); + } + } \ No newline at end of file diff --git a/src/accessories/light-accessory.ts b/src/accessories/light-accessory.ts index 3d7050d..e813ea4 100644 --- a/src/accessories/light-accessory.ts +++ b/src/accessories/light-accessory.ts @@ -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 @@ -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)) @@ -29,7 +37,7 @@ export class LightAccessory extends HubspaceAccessory{ private async getOn(): Promise{ // 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)){ @@ -41,12 +49,12 @@ export class LightAccessory extends HubspaceAccessory{ } private async setOn(value: CharacteristicValue): Promise{ - await this.deviceService.setValue(this.device.deviceId, DeviceAttribute.LightPower, value); + await this.deviceService.setValue(this.device.deviceId, DeviceFunction.LightPower, value); } private async getBrightness(): Promise{ // 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){ @@ -58,7 +66,7 @@ export class LightAccessory extends HubspaceAccessory{ } private async setBrightness(value: CharacteristicValue): Promise{ - this.deviceService.setValue(this.device.deviceId, DeviceAttribute.LightBrightness, value); + this.deviceService.setValue(this.device.deviceId, DeviceFunction.Brightness, value); } } \ No newline at end of file diff --git a/src/models/device-attributes.ts b/src/models/device-attributes.ts deleted file mode 100644 index b7c0e9f..0000000 --- a/src/models/device-attributes.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Attributes (functions) for each device type - */ -export enum DeviceAttribute{ - // Light attributes - LightPower = 2, - LightBrightness = 4, - // Fan attributes - FanPower = 3, - FanSpeed = 6 -} \ No newline at end of file diff --git a/src/models/device-function-def.ts b/src/models/device-function-def.ts new file mode 100644 index 0000000..4bdc40b --- /dev/null +++ b/src/models/device-function-def.ts @@ -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; +} \ No newline at end of file diff --git a/src/models/device-functions.ts b/src/models/device-functions.ts new file mode 100644 index 0000000..4f6366f --- /dev/null +++ b/src/models/device-functions.ts @@ -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; +} \ No newline at end of file diff --git a/src/models/device.ts b/src/models/device.ts index d6bd7bd..2df24e7 100644 --- a/src/models/device.ts +++ b/src/models/device.ts @@ -1,3 +1,4 @@ +import { DeviceFunction } from './device-functions'; import { DeviceType } from './device-type'; /** @@ -16,4 +17,6 @@ export interface Device{ manufacturer: string; /** Device model */ model: string[]; + /** Supported device functions */ + functions: DeviceFunction[]; } \ No newline at end of file diff --git a/src/responses/device-function-response.ts b/src/responses/device-function-response.ts new file mode 100644 index 0000000..cec7b2d --- /dev/null +++ b/src/responses/device-function-response.ts @@ -0,0 +1,9 @@ +/** + * Response for device function + */ +export interface DeviceFunctionResponse{ + /** Class of the function */ + functionClass: string; + /** Instance name of the function */ + functionInstance: string; +} diff --git a/src/responses/devices-response.ts b/src/responses/devices-response.ts index a940f10..ab82027 100644 --- a/src/responses/devices-response.ts +++ b/src/responses/devices-response.ts @@ -1,3 +1,5 @@ +import { DeviceFunctionResponse } from './device-function-response'; + /** * HTTP response for device discovery */ @@ -13,5 +15,6 @@ export interface DeviceResponse{ manufacturerName: string; model: string; }; + functions: DeviceFunctionResponse[]; }; } \ No newline at end of file diff --git a/src/services/device.service.ts b/src/services/device.service.ts index c4bdc49..e4540e3 100644 --- a/src/services/device.service.ts +++ b/src/services/device.service.ts @@ -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 @@ -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{ + async setValue(deviceId: string, deviceFunction: DeviceFunction, value: CharacteristicValue): Promise{ + 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){ @@ -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{ + async getValue(deviceId: string, deviceFunction: DeviceFunction): Promise{ + const functionDef = getDeviceFunctionDef(deviceFunction); let deviceStatus: DeviceStatusResponse; try{ @@ -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; } @@ -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{ - const value = await this.getValue(deviceId, attribute); + async getValueAsBoolean(deviceId: string, deviceFunction: DeviceFunction): Promise{ + const value = await this.getValue(deviceId, deviceFunction); if(!value) return undefined; @@ -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{ - const value = await this.getValue(deviceId, attribute); + async getValueAsInteger(deviceId: string, deviceFunction: DeviceFunction): Promise{ + const value = await this.getValue(deviceId, deviceFunction); if(!value || typeof value !== 'string') return undefined; diff --git a/src/services/discovery.service.ts b/src/services/discovery.service.ts index 6c2c0fe..9250568 100644 --- a/src/services/discovery.service.ts +++ b/src/services/discovery.service.ts @@ -1,13 +1,15 @@ import { PlatformAccessory } from 'homebridge'; import { HubspacePlatform } from '../platform'; import { DeviceResponse } from '../responses/devices-response'; -import { PACKAGE_VERSION, PLATFORM_NAME, PLUGIN_NAME } from '../settings'; +import { PLATFORM_NAME, PLUGIN_NAME } from '../settings'; import { Endpoints } from '../api/endpoints'; import { createHttpClientWithBearerInterceptor } from '../api/http-client-factory'; import { getDeviceTypeForKey } from '../models/device-type'; import { Device } from '../models/device'; import { createAccessoryForDevice } from '../accessories/device-accessory-factory'; import { AxiosError } from 'axios'; +import { DeviceFunction, DeviceFunctions } from '../models/device-functions'; +import { DeviceFunctionResponse } from '../responses/device-function-response'; /** * Service for discovering and managing devices @@ -74,16 +76,10 @@ export class DiscoveryService{ } private registerCachedAccessory(accessory: PlatformAccessory, device: Device): void{ - createAccessoryForDevice(device, this._platform, accessory); - - // If the accessory has been discovered previously and package number has changed - // then update the metadata as things might have changed. - if(!accessory.context.discoveredIn || accessory.context.discoveredIn !== PACKAGE_VERSION){ - accessory.context.discoveredIn = PACKAGE_VERSION; - accessory.context.device = device; + accessory.context.device = device; + this._platform.api.updatePlatformAccessories([ accessory ]); - this._platform.api.updatePlatformAccessories([ accessory ]); - } + createAccessoryForDevice(device, this._platform, accessory); } private registerNewAccessory(device: Device): void{ @@ -122,8 +118,26 @@ export class DiscoveryService{ name: response.friendlyName, type: type, manufacturer: response.description.device.manufacturerName, - model: response.description.device.model.split(',').map(m => m.trim()) + model: response.description.device.model.split(',').map(m => m.trim()), + functions: this.getFunctionsFromResponse(response.description.functions) }; } + private getFunctionsFromResponse(supportedFunctions: DeviceFunctionResponse[]): DeviceFunction[]{ + const output: DeviceFunction[] = []; + + for(const fc of supportedFunctions){ + // Get the type for the function + const type = DeviceFunctions + .find(df => df.functionInstanceName === fc.functionInstance && df.functionClass === fc.functionClass) + ?.type; + + if(type === undefined || output.indexOf(type) >= 0) continue; + + output.push(type); + } + + return output; + } + } \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts index 4d8a1be..0071b16 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -6,10 +6,4 @@ export const PLATFORM_NAME = 'Hubspace'; /** * This must match the name of your plugin as defined the package.json */ -export const PLUGIN_NAME = 'homebridge-hubspace'; - -/** - * Current NPM package version - */ -// eslint-disable-next-line @typescript-eslint/no-var-requires -export const PACKAGE_VERSION: string = require('../package.json').version; \ No newline at end of file +export const PLUGIN_NAME = 'homebridge-hubspace'; \ No newline at end of file