diff --git a/.eslintrc.js b/.eslintrc.js index e54bca7..a76153b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,7 +43,7 @@ module.exports = { }, }, { - files: ['.eslintrc.js', 'jest.config.js', 'prettier.config.js'], + files: ['**/*.js'], env: { node: true, browser: false, diff --git a/.npmignore b/.npmignore index fc95519..cff722e 100644 --- a/.npmignore +++ b/.npmignore @@ -142,3 +142,4 @@ prettier.config.js .eslintrc.js jest.config.js nodemon.json +/code-generation/ diff --git a/code-generation/hap-gen.js b/code-generation/hap-gen.js new file mode 100755 index 0000000..ffaab84 --- /dev/null +++ b/code-generation/hap-gen.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +const hap = require('hap-nodejs') +const { format } = require('prettier') +const prettierConfig = require('../prettier.config') +const { writeFileSync } = require('fs') +const { join, basename } = require('path') + +const uuidToServiceMap = {} +const serviceToUuidMap = {} + +for (const [name, service] of Object.entries(hap.Service)) { + if (typeof service !== 'function' || typeof service.UUID !== 'string') { + console.log(`Skipping ${typeof service} ${name}`) + continue + } + + uuidToServiceMap[service.UUID] = name + serviceToUuidMap[name] = service.UUID +} + +const code = format( + ` +// Auto-generated by "${join(basename(__dirname), basename(__filename))}", don’t manually edit +export const Uuids: Record = ${JSON.stringify(uuidToServiceMap)} as const + +export const Services: Record = ${JSON.stringify(serviceToUuidMap)} as const +`, + { filepath: 'codegen.ts', ...prettierConfig }, +) + +writeFileSync(join(__dirname, '../src/generated/services.ts'), code) diff --git a/package-lock.json b/package-lock.json index 6d21460..d2a526e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", + "hap-nodejs": "^0.10.4", "homebridge": "^1.3.5", "homebridge-cmdswitch2": "^0.2.10", "jest": "^29.3.0", diff --git a/package.json b/package.json index b87ccf9..2a892ae 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,14 @@ }, "main": "dist/src/index.js", "scripts": { - "lint": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; `npm bin`/prettier --ignore-path=.gitignore `ifNotCi --write --check` '**/**.{ts,js,json}' && `npm bin`/eslint `ifNotCi --fix` --ignore-path=.gitignore '**/**.{ts,js,json}'", + "lint": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; `npm bin`/tsc --noEmit && `npm bin`/prettier --ignore-path=.gitignore `ifNotCi --write --check` '**/**.{ts,js,json}' && `npm bin`/eslint `ifNotCi --fix` --ignore-path=.gitignore '**/**.{ts,js,json}'", "start": "npm run build && npm run link && nodemon", - "test": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; `npm bin`/jest `ifNotCi --watchAll`", + "test": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; npm run code-generation && `npm bin`/jest `ifNotCi --watchAll`", "link": "npm install --no-save file:///$PWD/", - "build": "rimraf ./dist && tsc", - "prepublishOnly": "npm run lint && npm run build", - "release": "release() { test \"$1\" && test `git rev-parse --abbrev-ref HEAD` == \"develop\" && git pull origin develop --rebase && npm version $1 && npm publish && git push origin develop && git push origin --tags; }; release" + "build": "rimraf ./dist && npm run code-generation && tsc", + "code-generation": "./code-generation/hap-gen.js", + "prepublishOnly": "npm run code-generation && npm run lint && npm run build", + "release": "release() { test \"$1\" && test `git rev-parse --abbrev-ref HEAD` == \"develop\" && npm run code-generation && git pull origin develop --rebase && npm version $1 && npm publish && git push origin develop && git push origin --tags; }; release" }, "keywords": [ "homebridge-plugin", @@ -40,6 +41,7 @@ "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.2.1", + "hap-nodejs": "^0.10.4", "homebridge": "^1.3.5", "homebridge-cmdswitch2": "^0.2.10", "jest": "^29.3.0", diff --git a/src/generated/services.ts b/src/generated/services.ts new file mode 100644 index 0000000..52d46b0 --- /dev/null +++ b/src/generated/services.ts @@ -0,0 +1,160 @@ +// Auto-generated by "code-generation/hap-gen.js", don’t manually edit +export const Uuids: Record = { + '00000260-0000-1000-8000-0026BB765291': 'AccessCode', + '000000DA-0000-1000-8000-0026BB765291': 'AccessControl', + '0000003E-0000-1000-8000-0026BB765291': 'AccessoryInformation', + '00000270-0000-1000-8000-0026BB765291': 'AccessoryMetrics', + '00000239-0000-1000-8000-0026BB765291': 'AccessoryRuntimeInformation', + '000000BB-0000-1000-8000-0026BB765291': 'AirPurifier', + '0000008D-0000-1000-8000-0026BB765291': 'AirQualitySensor', + '00000267-0000-1000-8000-0026BB765291': 'AssetUpdate', + '0000026A-0000-1000-8000-0026BB765291': 'Assistant', + '00000127-0000-1000-8000-0026BB765291': 'AudioStreamManagement', + '00000096-0000-1000-8000-0026BB765291': 'Battery', + '000000A1-0000-1000-8000-0026BB765291': 'BridgeConfiguration', + '00000062-0000-1000-8000-0026BB765291': 'BridgingState', + '00000111-0000-1000-8000-0026BB765291': 'CameraControl', + '0000021A-0000-1000-8000-0026BB765291': 'CameraOperatingMode', + '00000204-0000-1000-8000-0026BB765291': 'CameraRecordingManagement', + '00000110-0000-1000-8000-0026BB765291': 'CameraRTPStreamManagement', + '00000097-0000-1000-8000-0026BB765291': 'CarbonDioxideSensor', + '0000007F-0000-1000-8000-0026BB765291': 'CarbonMonoxideSensor', + '0000005A-0000-1000-8000-0026BB765291': 'CloudRelay', + '00000080-0000-1000-8000-0026BB765291': 'ContactSensor', + '00000129-0000-1000-8000-0026BB765291': 'DataStreamTransportManagement', + '00000237-0000-1000-8000-0026BB765291': 'Diagnostics', + '00000081-0000-1000-8000-0026BB765291': 'Door', + '00000121-0000-1000-8000-0026BB765291': 'Doorbell', + '00000040-0000-1000-8000-0026BB765291': 'Fan', + '000000B7-0000-1000-8000-0026BB765291': 'Fanv2', + '000000D7-0000-1000-8000-0026BB765291': 'Faucet', + '000000BA-0000-1000-8000-0026BB765291': 'FilterMaintenance', + '00000041-0000-1000-8000-0026BB765291': 'GarageDoorOpener', + '000000BC-0000-1000-8000-0026BB765291': 'HeaterCooler', + '000000BD-0000-1000-8000-0026BB765291': 'HumidifierDehumidifier', + '00000082-0000-1000-8000-0026BB765291': 'HumiditySensor', + '000000D9-0000-1000-8000-0026BB765291': 'InputSource', + '000000CF-0000-1000-8000-0026BB765291': 'IrrigationSystem', + '00000083-0000-1000-8000-0026BB765291': 'LeakSensor', + '00000043-0000-1000-8000-0026BB765291': 'Lightbulb', + '00000084-0000-1000-8000-0026BB765291': 'LightSensor', + '00000044-0000-1000-8000-0026BB765291': 'LockManagement', + '00000045-0000-1000-8000-0026BB765291': 'LockMechanism', + '00000112-0000-1000-8000-0026BB765291': 'Microphone', + '00000085-0000-1000-8000-0026BB765291': 'MotionSensor', + '00000266-0000-1000-8000-0026BB765291': 'NFCAccess', + '00000086-0000-1000-8000-0026BB765291': 'OccupancySensor', + '00000047-0000-1000-8000-0026BB765291': 'Outlet', + '00000055-0000-1000-8000-0026BB765291': 'Pairing', + '00000221-0000-1000-8000-0026BB765291': 'PowerManagement', + '000000A2-0000-1000-8000-0026BB765291': 'ProtocolInformation', + '0000007E-0000-1000-8000-0026BB765291': 'SecuritySystem', + '000000CC-0000-1000-8000-0026BB765291': 'ServiceLabel', + '00000133-0000-1000-8000-0026BB765291': 'Siri', + '00000253-0000-1000-8000-0026BB765291': 'SiriEndpoint', + '000000B9-0000-1000-8000-0026BB765291': 'Slats', + '00000228-0000-1000-8000-0026BB765291': 'SmartSpeaker', + '00000087-0000-1000-8000-0026BB765291': 'SmokeSensor', + '00000113-0000-1000-8000-0026BB765291': 'TelevisionSpeaker', + '00000088-0000-1000-8000-0026BB765291': 'StatefulProgrammableSwitch', + '00000089-0000-1000-8000-0026BB765291': 'StatelessProgrammableSwitch', + '00000049-0000-1000-8000-0026BB765291': 'Switch', + '00000125-0000-1000-8000-0026BB765291': 'TargetControl', + '00000122-0000-1000-8000-0026BB765291': 'TargetControlManagement', + '000000D8-0000-1000-8000-0026BB765291': 'Television', + '0000008A-0000-1000-8000-0026BB765291': 'TemperatureSensor', + '0000004A-0000-1000-8000-0026BB765291': 'Thermostat', + '00000701-0000-1000-8000-0026BB765291': 'ThreadTransport', + '00000099-0000-1000-8000-0026BB765291': 'TimeInformation', + '00000203-0000-1000-8000-0026BB765291': 'TransferTransportManagement', + '00000056-0000-1000-8000-0026BB765291': 'Tunnel', + '000000D0-0000-1000-8000-0026BB765291': 'Valve', + '0000020A-0000-1000-8000-0026BB765291': 'WiFiRouter', + '0000020F-0000-1000-8000-0026BB765291': 'WiFiSatellite', + '0000022A-0000-1000-8000-0026BB765291': 'WiFiTransport', + '0000008B-0000-1000-8000-0026BB765291': 'Window', + '0000008C-0000-1000-8000-0026BB765291': 'WindowCovering', +} as const + +export const Services: Record = { + AccessCode: '00000260-0000-1000-8000-0026BB765291', + AccessControl: '000000DA-0000-1000-8000-0026BB765291', + AccessoryInformation: '0000003E-0000-1000-8000-0026BB765291', + AccessoryMetrics: '00000270-0000-1000-8000-0026BB765291', + AccessoryRuntimeInformation: '00000239-0000-1000-8000-0026BB765291', + AirPurifier: '000000BB-0000-1000-8000-0026BB765291', + AirQualitySensor: '0000008D-0000-1000-8000-0026BB765291', + AssetUpdate: '00000267-0000-1000-8000-0026BB765291', + Assistant: '0000026A-0000-1000-8000-0026BB765291', + AudioStreamManagement: '00000127-0000-1000-8000-0026BB765291', + BatteryService: '00000096-0000-1000-8000-0026BB765291', + Battery: '00000096-0000-1000-8000-0026BB765291', + BridgeConfiguration: '000000A1-0000-1000-8000-0026BB765291', + BridgingState: '00000062-0000-1000-8000-0026BB765291', + CameraControl: '00000111-0000-1000-8000-0026BB765291', + CameraOperatingMode: '0000021A-0000-1000-8000-0026BB765291', + CameraEventRecordingManagement: '00000204-0000-1000-8000-0026BB765291', + CameraRecordingManagement: '00000204-0000-1000-8000-0026BB765291', + CameraRTPStreamManagement: '00000110-0000-1000-8000-0026BB765291', + CarbonDioxideSensor: '00000097-0000-1000-8000-0026BB765291', + CarbonMonoxideSensor: '0000007F-0000-1000-8000-0026BB765291', + Relay: '0000005A-0000-1000-8000-0026BB765291', + CloudRelay: '0000005A-0000-1000-8000-0026BB765291', + ContactSensor: '00000080-0000-1000-8000-0026BB765291', + DataStreamTransportManagement: '00000129-0000-1000-8000-0026BB765291', + Diagnostics: '00000237-0000-1000-8000-0026BB765291', + Door: '00000081-0000-1000-8000-0026BB765291', + Doorbell: '00000121-0000-1000-8000-0026BB765291', + Fan: '00000040-0000-1000-8000-0026BB765291', + Fanv2: '000000B7-0000-1000-8000-0026BB765291', + Faucet: '000000D7-0000-1000-8000-0026BB765291', + FilterMaintenance: '000000BA-0000-1000-8000-0026BB765291', + GarageDoorOpener: '00000041-0000-1000-8000-0026BB765291', + HeaterCooler: '000000BC-0000-1000-8000-0026BB765291', + HumidifierDehumidifier: '000000BD-0000-1000-8000-0026BB765291', + HumiditySensor: '00000082-0000-1000-8000-0026BB765291', + InputSource: '000000D9-0000-1000-8000-0026BB765291', + IrrigationSystem: '000000CF-0000-1000-8000-0026BB765291', + LeakSensor: '00000083-0000-1000-8000-0026BB765291', + Lightbulb: '00000043-0000-1000-8000-0026BB765291', + LightSensor: '00000084-0000-1000-8000-0026BB765291', + LockManagement: '00000044-0000-1000-8000-0026BB765291', + LockMechanism: '00000045-0000-1000-8000-0026BB765291', + Microphone: '00000112-0000-1000-8000-0026BB765291', + MotionSensor: '00000085-0000-1000-8000-0026BB765291', + NFCAccess: '00000266-0000-1000-8000-0026BB765291', + OccupancySensor: '00000086-0000-1000-8000-0026BB765291', + Outlet: '00000047-0000-1000-8000-0026BB765291', + Pairing: '00000055-0000-1000-8000-0026BB765291', + PowerManagement: '00000221-0000-1000-8000-0026BB765291', + ProtocolInformation: '000000A2-0000-1000-8000-0026BB765291', + SecuritySystem: '0000007E-0000-1000-8000-0026BB765291', + ServiceLabel: '000000CC-0000-1000-8000-0026BB765291', + Siri: '00000133-0000-1000-8000-0026BB765291', + SiriEndpoint: '00000253-0000-1000-8000-0026BB765291', + Slat: '000000B9-0000-1000-8000-0026BB765291', + Slats: '000000B9-0000-1000-8000-0026BB765291', + SmartSpeaker: '00000228-0000-1000-8000-0026BB765291', + SmokeSensor: '00000087-0000-1000-8000-0026BB765291', + Speaker: '00000113-0000-1000-8000-0026BB765291', + StatefulProgrammableSwitch: '00000088-0000-1000-8000-0026BB765291', + StatelessProgrammableSwitch: '00000089-0000-1000-8000-0026BB765291', + Switch: '00000049-0000-1000-8000-0026BB765291', + TargetControl: '00000125-0000-1000-8000-0026BB765291', + TargetControlManagement: '00000122-0000-1000-8000-0026BB765291', + Television: '000000D8-0000-1000-8000-0026BB765291', + TelevisionSpeaker: '00000113-0000-1000-8000-0026BB765291', + TemperatureSensor: '0000008A-0000-1000-8000-0026BB765291', + Thermostat: '0000004A-0000-1000-8000-0026BB765291', + ThreadTransport: '00000701-0000-1000-8000-0026BB765291', + TimeInformation: '00000099-0000-1000-8000-0026BB765291', + TransferTransportManagement: '00000203-0000-1000-8000-0026BB765291', + TunneledBTLEAccessoryService: '00000056-0000-1000-8000-0026BB765291', + Tunnel: '00000056-0000-1000-8000-0026BB765291', + Valve: '000000D0-0000-1000-8000-0026BB765291', + WiFiRouter: '0000020A-0000-1000-8000-0026BB765291', + WiFiSatellite: '0000020F-0000-1000-8000-0026BB765291', + WiFiTransport: '0000022A-0000-1000-8000-0026BB765291', + Window: '0000008B-0000-1000-8000-0026BB765291', + WindowCovering: '0000008C-0000-1000-8000-0026BB765291', +} as const diff --git a/src/metrics.ts b/src/metrics.ts index e2c7736..b62f104 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,7 +1,6 @@ import type { Accessory, Device, Service } from './boundaries/hap' +import { Uuids } from './generated/services' import { assertTypeExhausted, isType } from './std' -// eslint-disable-next-line import/no-extraneous-dependencies -import { Service as HapService } from 'hap-nodejs' export class Metric { constructor( @@ -48,7 +47,7 @@ export function aggregate(devices: Device[], timestamp: Date): Metric[] { break } const name = formatName( - uuidToServerName(service.type), + Uuids[service.type] || 'custom', characteristic.description, characteristic.unit, ) @@ -127,15 +126,3 @@ function getServiceLabels(service: Service): Record { return labels } - -function uuidToServerName(uuid: string): string { - for (const name of Object.getOwnPropertyNames(HapService)) { - const maybeService = (HapService as unknown as Record)[name] - if (typeof maybeService === 'function' && 'UUID' in maybeService) { - if ((maybeService as Record)['UUID'] === uuid) { - return name - } - } - } - return 'custom' -} diff --git a/tests/aggregator.test.ts b/tests/aggregator.test.ts index 17168cc..e588bb9 100644 --- a/tests/aggregator.test.ts +++ b/tests/aggregator.test.ts @@ -149,10 +149,10 @@ describe('Metrics aggregator', () => { new Metric('input_source_current_visibility_state', 0, timestamp, expectedLabels3), new Metric('input_source_target_visibility_state', 0, timestamp, expectedLabels3), - new Metric('speaker_active', 1, timestamp, expectedLabels4), - new Metric('speaker_volume_control_type', 3, timestamp, expectedLabels4), - new Metric('speaker_mute', 0, timestamp, expectedLabels4), - new Metric('speaker_volume_percentage', 50, timestamp, expectedLabels4), + new Metric('television_speaker_active', 1, timestamp, expectedLabels4), + new Metric('television_speaker_volume_control_type', 3, timestamp, expectedLabels4), + new Metric('television_speaker_mute', 0, timestamp, expectedLabels4), + new Metric('television_speaker_volume_percentage', 50, timestamp, expectedLabels4), ]) })