Initial version

This commit is contained in:
Lars Strojny 2022-11-06 13:50:39 +01:00
parent dd4a135022
commit 00dd20559a
35 changed files with 16250 additions and 6737 deletions

View file

@ -1,38 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended" // uses the recommended rules from the @typescript-eslint/eslint-plugin
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"ignorePatterns": [
"dist"
],
"rules": {
"quotes": ["warn", "single"],
"indent": ["warn", 2, { "SwitchCase": 1 }],
"semi": ["off"],
"comma-dangle": ["warn", "always-multiline"],
"dot-notation": "off",
"eqeqeq": "warn",
"curly": ["warn", "all"],
"brace-style": ["warn"],
"prefer-arrow-callback": ["warn"],
"max-len": ["warn", 140],
"no-console": ["warn"], // use the provided Homebridge log method instead
"no-non-null-assertion": ["off"],
"comma-spacing": ["error"],
"no-multi-spaces": ["warn", { "ignoreEOLComments": true }],
"no-trailing-spaces": ["warn"],
"lines-between-class-members": ["warn", "always", {"exceptAfterSingleLine": true}],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/semi": ["warn"],
"@typescript-eslint/member-delimiter-style": ["warn"]
}
}

23
.eslintrc.js Normal file
View file

@ -0,0 +1,23 @@
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'prettier'],
root: true,
rules: {
'prettier/prettier': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'prefer-arrow-callback': 'error',
},
overrides: [
{
files: ['.eslintrc.js', 'jest.config.js', 'prettier.config.js'],
env: {
node: true,
browser: false,
},
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
},
],
}

9
.gitignore vendored
View file

@ -111,10 +111,17 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
/.vscode/
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
.pnp.*
# IntelliJ
/.idea/
/dot-homebridge/accessories/
/dot-homebridge/persist/

View file

@ -1,5 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

View file

@ -1,8 +0,0 @@
{
"files.eol": "\n",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.rulers": [ 140 ],
"eslint.enable": true
}

2
TODOs Normal file
View file

@ -0,0 +1,2 @@
- Extract PrometheusFormatter
- Extract HttpServer

View file

@ -1,16 +1,50 @@
{
"pluginAlias": "ExampleHomebridgePlugin",
"pluginType": "platform",
"singular": true,
"schema": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"required": true,
"default": "Example Dynamic Platform"
}
"pluginAlias": "PrometheusExporter",
"pluginType": "platform",
"singular": true,
"schema": {
"type": "object",
"properties": {
"pin": {
"title": "Pin",
"description": "Homebridge PIN for service authentication",
"type": "string",
"required": true
},
"debug": {
"title": "Debug",
"type": "boolean",
"required": false,
"default": false
},
"probe_port": {
"title": "Probe server port",
"description": "TCP port for the prometheus probe server to listen to",
"type": "integer",
"required": false,
"default": 36123
},
"refresh_interval": {
"title": "Service refresh interval",
"description": "Discover new services every <interval> seconds",
"type": "integer",
"required": false,
"default": 60
},
"request_timeout": {
"title": "Request timeout",
"description": "Request timeout when interacting with homebridge instances",
"type": "integer",
"required": false,
"default": 10
},
"discovery_timeout": {
"title": "Service discovery timeout",
"description": "Discovery timeout after which the current discovery is considered failed",
"type": "integer",
"required": false,
"default": 20
}
}
}
}
}
}

View file

@ -0,0 +1,25 @@
{
"accessories": [],
"bridge": {
"name": "Prometheus Exporter Test Homebridge",
"pin": "121-11-121"
},
"platforms": [
{
"pin": "808-00-808",
"platform": "PrometheusExporter",
"debug": true
},
{
"platform": "cmdSwitch2",
"switches": [
{
"name": "Test",
"on_cmd": "echo on",
"off_cmd": "echo off",
"state_cmd": "test $(echo \"`date +\"%s\"` % 4\" | bc) -eq 0"
}
]
}
]
}

43
flake.lock Normal file
View file

@ -0,0 +1,43 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1667629849,
"narHash": "sha256-P+v+nDOFWicM4wziFK9S/ajF2lc0N2Rg9p6Y35uMoZI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3bacde6273b09a21a8ccfba15586fb165078fb62",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

21
flake.nix Normal file
View file

@ -0,0 +1,21 @@
{
description = "Ansible environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
config = { allowUnfree = true; };
system = system;
};
in with pkgs; rec {
devShell = pkgs.mkShell rec { buildInputs = with pkgs; [ nodejs ]; };
});
}

6
jest.config.js Normal file
View file

@ -0,0 +1,6 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
verbose: true,
}

View file

@ -1,12 +1,10 @@
{
"watch": [
"src"
],
"ext": "ts",
"ignore": [],
"exec": "tsc && homebridge -I -D",
"signal": "SIGTERM",
"env": {
"NODE_OPTIONS": "--trace-warnings"
}
}
"watch": ["src"],
"ext": "ts",
"ignore": [],
"exec": "tsc && homebridge -D -U dot-homebridge",
"signal": "SIGTERM",
"env": {
"NODE_OPTIONS": "--trace-warnings"
}
}

19729
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,41 +1,52 @@
{
"private": true,
"displayName": "Plugin Name",
"name": "homebridge-plugin-name",
"version": "1.0.0",
"description": "A short description about what your plugin does.",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/USERNAME/GITHUB_PROJECT_NAME.git"
},
"bugs": {
"url": "https://github.com/USERNAME/GITHUB_PROJECT_NAME/issues"
},
"engines": {
"node": ">=14.18.1",
"homebridge": ">=1.3.5"
},
"main": "dist/index.js",
"scripts": {
"lint": "eslint src/**.ts --max-warnings=0",
"watch": "npm run build && npm link && nodemon",
"build": "rimraf ./dist && tsc",
"prepublishOnly": "npm run lint && npm run build"
},
"keywords": [
"homebridge-plugin"
],
"dependencies": {},
"devDependencies": {
"@types/node": "^16.10.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"homebridge": "^1.3.5",
"nodemon": "^2.0.13",
"rimraf": "^3.0.2",
"ts-node": "^10.3.0",
"typescript": "^4.4.4"
}
"private": true,
"name": "homebridge-prometheus-exporter",
"version": "1.0.0",
"description": "Prometheus exporter for homebridge accessories.",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/lstrojny/homebridge-prometheus-exporter.git"
},
"bugs": {
"url": "https://github.com/lstrojny/homebridge-prometheus-exporter/issues"
},
"engines": {
"node": ">=14.18.1",
"homebridge": ">=1.3.5"
},
"main": "dist/index.js",
"scripts": {
"lint": "npm run format && eslint --ignore-path=.gitignore '**/**.{ts,js,json}'",
"watch": "npm run build && npm run link && nodemon",
"watch-tests": "jest --watch",
"tests": "jest",
"link": "npm install --no-save file:///$PWD/",
"build": "rimraf ./dist && tsc",
"format": "prettier --ignore-path=.gitignore --write '**/**.{ts,js,json}'",
"prepublishOnly": "npm run lint && npm run build"
},
"keywords": [
"homebridge-plugin"
],
"devDependencies": {
"@types/node": "^16.10.9",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"eslint": "^8.0.1",
"eslint-plugin-prettier": "^4.2.1",
"homebridge": "^1.3.5",
"homebridge-cmdswitch2": "^0.2.10",
"nodemon": "^2.0.13",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"ts-jest": "^29.0.3",
"ts-node": "^10.3.0",
"typescript": "^4.4.4"
},
"dependencies": {
"fastify": "^4.9.2",
"hap-node-client": "git+https://github.com/NorthernMan54/Hap-Node-Client.git#fe200ba",
"runtypes": "^6.6.0"
}
}

9
prettier.config.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = {
tabWidth: 4,
semi: false,
singleQuote: true,
printWidth: 120,
trailingComma: 'all',
quoteProps: 'as-needed',
proseWrap: 'always',
}

1
src/ambient.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module 'hap-node-client'

46
src/boundaries.ts Normal file
View file

@ -0,0 +1,46 @@
import { Number, String, Literal, Array, Record, Union, Static, Optional, Intersect } from 'runtypes'
export const NUMBER_TYPES = ['float', 'int', 'uint8', 'uint16', 'uint32', 'uint64'] as const
const NumberTypesLiterals = NUMBER_TYPES.map(Literal)
const NumberTypesBoundary = Union(NumberTypesLiterals[0], ...NumberTypesLiterals)
export type NumberTypes = Static<typeof NumberTypesBoundary>
export const CharacteristicBoundary = Intersect(
Record({ type: String, description: String }),
Union(
Record({
format: NumberTypesBoundary,
value: Optional(Number),
unit: Optional(String),
}),
Record({ format: Literal('string'), value: String }),
Record({ format: Literal('bool') }),
),
)
export type Characteristic = Static<typeof CharacteristicBoundary>
export const ServiceBoundary = Record({
type: String,
characteristics: Array(CharacteristicBoundary),
})
export type Service = Static<typeof ServiceBoundary>
export const AccessoryBoundary = Record({
services: Array(ServiceBoundary),
})
export type Accessory = Static<typeof AccessoryBoundary>
export const InstanceBoundary = Record({
deviceID: String,
name: String,
url: String,
})
export type Instance = Static<typeof InstanceBoundary>
export const DeviceBoundary = Record({
instance: InstanceBoundary,
accessories: Record({
accessories: Array(AccessoryBoundary),
}),
})
export type Device = Static<typeof DeviceBoundary>

14
src/discovery/api.ts Normal file
View file

@ -0,0 +1,14 @@
import type { Device } from '../boundaries'
import { Logger } from 'homebridge'
type Pin = string
export type HapConfig = {
pin: Pin
refreshInterval: number
discoveryTimeout: number
requestTimeout: number
logger: Logger
debug: boolean
}
export type HapDiscover = (config: HapConfig) => Promise<Device[]>

View file

@ -0,0 +1,45 @@
import type { HapDiscover } from './api'
import { HAPNodeJSClient } from 'hap-node-client'
import { Device, DeviceBoundary } from '../boundaries'
import { Array, Unknown } from 'runtypes'
const MaybeDevices = Array(Unknown)
export const discover: HapDiscover = ({ pin, refreshInterval, discoveryTimeout, requestTimeout, logger, debug }) => {
return new Promise((resolve, reject) => {
try {
const client = new HAPNodeJSClient({
debug: debug,
refresh: refreshInterval,
timeout: discoveryTimeout,
reqTimeout: requestTimeout,
pin,
})
client.on('Ready', (deviceData: unknown) => {
try {
const devices: Device[] = []
for (const device of MaybeDevices.check(deviceData)) {
try {
devices.push(DeviceBoundary.check(device))
} catch (e) {
logger.error(
'Boundary check for device data failed %o %s',
e,
JSON.stringify(device, null, 4),
)
}
}
resolve(devices)
} catch (e) {
reject(e)
}
})
} catch (e) {
reject(e)
}
})
}

700
src/hap.ts Normal file
View file

@ -0,0 +1,700 @@
/** Copied from https://github.com/oznu/hap-client/blob/master/src/hap-types.ts */
export const Services = {
'00000260-0000-1000-8000-0026BB765291': 'AccessCode',
AccessCode: '00000260-0000-1000-8000-0026BB765291',
'000000DA-0000-1000-8000-0026BB765291': 'AccessControl',
AccessControl: '000000DA-0000-1000-8000-0026BB765291',
'0000003E-0000-1000-8000-0026BB765291': 'AccessoryInformation',
AccessoryInformation: '0000003E-0000-1000-8000-0026BB765291',
'00000270-0000-1000-8000-0026BB765291': 'AccessoryMetrics',
AccessoryMetrics: '00000270-0000-1000-8000-0026BB765291',
'00000239-0000-1000-8000-0026BB765291': 'AccessoryRuntimeInformation',
AccessoryRuntimeInformation: '00000239-0000-1000-8000-0026BB765291',
'000000BB-0000-1000-8000-0026BB765291': 'AirPurifier',
AirPurifier: '000000BB-0000-1000-8000-0026BB765291',
'0000008D-0000-1000-8000-0026BB765291': 'AirQualitySensor',
AirQualitySensor: '0000008D-0000-1000-8000-0026BB765291',
'00000267-0000-1000-8000-0026BB765291': 'AssetUpdate',
AssetUpdate: '00000267-0000-1000-8000-0026BB765291',
'0000026A-0000-1000-8000-0026BB765291': 'Assistant',
Assistant: '0000026A-0000-1000-8000-0026BB765291',
'00000127-0000-1000-8000-0026BB765291': 'AudioStreamManagement',
AudioStreamManagement: '00000127-0000-1000-8000-0026BB765291',
'00000096-0000-1000-8000-0026BB765291': 'BatteryService',
BatteryService: '00000096-0000-1000-8000-0026BB765291',
Battery: '00000096-0000-1000-8000-0026BB765291',
'000000A1-0000-1000-8000-0026BB765291': 'BridgeConfiguration',
BridgeConfiguration: '000000A1-0000-1000-8000-0026BB765291',
'00000062-0000-1000-8000-0026BB765291': 'BridgingState',
BridgingState: '00000062-0000-1000-8000-0026BB765291',
'00000111-0000-1000-8000-0026BB765291': 'CameraControl',
CameraControl: '00000111-0000-1000-8000-0026BB765291',
'0000021A-0000-1000-8000-0026BB765291': 'CameraOperatingMode',
CameraOperatingMode: '0000021A-0000-1000-8000-0026BB765291',
'00000204-0000-1000-8000-0026BB765291': 'CameraEventRecordingManagement',
CameraEventRecordingManagement: '00000204-0000-1000-8000-0026BB765291',
CameraRecordingManagement: '00000204-0000-1000-8000-0026BB765291',
'00000110-0000-1000-8000-0026BB765291': 'CameraRTPStreamManagement',
CameraRTPStreamManagement: '00000110-0000-1000-8000-0026BB765291',
'00000097-0000-1000-8000-0026BB765291': 'CarbonDioxideSensor',
CarbonDioxideSensor: '00000097-0000-1000-8000-0026BB765291',
'0000007F-0000-1000-8000-0026BB765291': 'CarbonMonoxideSensor',
CarbonMonoxideSensor: '0000007F-0000-1000-8000-0026BB765291',
'0000005A-0000-1000-8000-0026BB765291': 'Relay',
Relay: '0000005A-0000-1000-8000-0026BB765291',
CloudRelay: '0000005A-0000-1000-8000-0026BB765291',
'00000080-0000-1000-8000-0026BB765291': 'ContactSensor',
ContactSensor: '00000080-0000-1000-8000-0026BB765291',
'00000129-0000-1000-8000-0026BB765291': 'DataStreamTransportManagement',
DataStreamTransportManagement: '00000129-0000-1000-8000-0026BB765291',
'00000237-0000-1000-8000-0026BB765291': 'Diagnostics',
Diagnostics: '00000237-0000-1000-8000-0026BB765291',
'00000081-0000-1000-8000-0026BB765291': 'Door',
Door: '00000081-0000-1000-8000-0026BB765291',
'00000121-0000-1000-8000-0026BB765291': 'Doorbell',
Doorbell: '00000121-0000-1000-8000-0026BB765291',
'00000040-0000-1000-8000-0026BB765291': 'Fan',
Fan: '00000040-0000-1000-8000-0026BB765291',
'000000B7-0000-1000-8000-0026BB765291': 'Fanv2',
Fanv2: '000000B7-0000-1000-8000-0026BB765291',
'000000D7-0000-1000-8000-0026BB765291': 'Faucet',
Faucet: '000000D7-0000-1000-8000-0026BB765291',
'000000BA-0000-1000-8000-0026BB765291': 'FilterMaintenance',
FilterMaintenance: '000000BA-0000-1000-8000-0026BB765291',
'00000041-0000-1000-8000-0026BB765291': 'GarageDoorOpener',
GarageDoorOpener: '00000041-0000-1000-8000-0026BB765291',
'000000BC-0000-1000-8000-0026BB765291': 'HeaterCooler',
HeaterCooler: '000000BC-0000-1000-8000-0026BB765291',
'000000BD-0000-1000-8000-0026BB765291': 'HumidifierDehumidifier',
HumidifierDehumidifier: '000000BD-0000-1000-8000-0026BB765291',
'00000082-0000-1000-8000-0026BB765291': 'HumiditySensor',
HumiditySensor: '00000082-0000-1000-8000-0026BB765291',
'000000D9-0000-1000-8000-0026BB765291': 'InputSource',
InputSource: '000000D9-0000-1000-8000-0026BB765291',
'000000CF-0000-1000-8000-0026BB765291': 'IrrigationSystem',
IrrigationSystem: '000000CF-0000-1000-8000-0026BB765291',
'00000083-0000-1000-8000-0026BB765291': 'LeakSensor',
LeakSensor: '00000083-0000-1000-8000-0026BB765291',
'00000043-0000-1000-8000-0026BB765291': 'Lightbulb',
Lightbulb: '00000043-0000-1000-8000-0026BB765291',
'00000084-0000-1000-8000-0026BB765291': 'LightSensor',
LightSensor: '00000084-0000-1000-8000-0026BB765291',
'00000044-0000-1000-8000-0026BB765291': 'LockManagement',
LockManagement: '00000044-0000-1000-8000-0026BB765291',
'00000045-0000-1000-8000-0026BB765291': 'LockMechanism',
LockMechanism: '00000045-0000-1000-8000-0026BB765291',
'00000112-0000-1000-8000-0026BB765291': 'Microphone',
Microphone: '00000112-0000-1000-8000-0026BB765291',
'00000085-0000-1000-8000-0026BB765291': 'MotionSensor',
MotionSensor: '00000085-0000-1000-8000-0026BB765291',
'00000266-0000-1000-8000-0026BB765291': 'NFCAccess',
NFCAccess: '00000266-0000-1000-8000-0026BB765291',
'00000086-0000-1000-8000-0026BB765291': 'OccupancySensor',
OccupancySensor: '00000086-0000-1000-8000-0026BB765291',
'00000047-0000-1000-8000-0026BB765291': 'Outlet',
Outlet: '00000047-0000-1000-8000-0026BB765291',
'00000055-0000-1000-8000-0026BB765291': 'Pairing',
Pairing: '00000055-0000-1000-8000-0026BB765291',
'00000221-0000-1000-8000-0026BB765291': 'PowerManagement',
PowerManagement: '00000221-0000-1000-8000-0026BB765291',
'000000A2-0000-1000-8000-0026BB765291': 'ProtocolInformation',
ProtocolInformation: '000000A2-0000-1000-8000-0026BB765291',
'0000007E-0000-1000-8000-0026BB765291': 'SecuritySystem',
SecuritySystem: '0000007E-0000-1000-8000-0026BB765291',
'000000CC-0000-1000-8000-0026BB765291': 'ServiceLabel',
ServiceLabel: '000000CC-0000-1000-8000-0026BB765291',
'00000133-0000-1000-8000-0026BB765291': 'Siri',
Siri: '00000133-0000-1000-8000-0026BB765291',
'00000253-0000-1000-8000-0026BB765291': 'SiriEndpoint',
SiriEndpoint: '00000253-0000-1000-8000-0026BB765291',
'000000B9-0000-1000-8000-0026BB765291': 'Slat',
Slat: '000000B9-0000-1000-8000-0026BB765291',
Slats: '000000B9-0000-1000-8000-0026BB765291',
'00000228-0000-1000-8000-0026BB765291': 'SmartSpeaker',
SmartSpeaker: '00000228-0000-1000-8000-0026BB765291',
'00000087-0000-1000-8000-0026BB765291': 'SmokeSensor',
SmokeSensor: '00000087-0000-1000-8000-0026BB765291',
'00000113-0000-1000-8000-0026BB765291': 'Speaker',
Speaker: '00000113-0000-1000-8000-0026BB765291',
'00000088-0000-1000-8000-0026BB765291': 'StatefulProgrammableSwitch',
StatefulProgrammableSwitch: '00000088-0000-1000-8000-0026BB765291',
'00000089-0000-1000-8000-0026BB765291': 'StatelessProgrammableSwitch',
StatelessProgrammableSwitch: '00000089-0000-1000-8000-0026BB765291',
'00000049-0000-1000-8000-0026BB765291': 'Switch',
Switch: '00000049-0000-1000-8000-0026BB765291',
'00000125-0000-1000-8000-0026BB765291': 'TargetControl',
TargetControl: '00000125-0000-1000-8000-0026BB765291',
'00000122-0000-1000-8000-0026BB765291': 'TargetControlManagement',
TargetControlManagement: '00000122-0000-1000-8000-0026BB765291',
'000000D8-0000-1000-8000-0026BB765291': 'Television',
Television: '000000D8-0000-1000-8000-0026BB765291',
TelevisionSpeaker: '00000113-0000-1000-8000-0026BB765291',
'0000008A-0000-1000-8000-0026BB765291': 'TemperatureSensor',
TemperatureSensor: '0000008A-0000-1000-8000-0026BB765291',
'0000004A-0000-1000-8000-0026BB765291': 'Thermostat',
Thermostat: '0000004A-0000-1000-8000-0026BB765291',
'00000701-0000-1000-8000-0026BB765291': 'ThreadTransport',
ThreadTransport: '00000701-0000-1000-8000-0026BB765291',
'00000099-0000-1000-8000-0026BB765291': 'TimeInformation',
TimeInformation: '00000099-0000-1000-8000-0026BB765291',
'00000203-0000-1000-8000-0026BB765291': 'TransferTransportManagement',
TransferTransportManagement: '00000203-0000-1000-8000-0026BB765291',
'00000056-0000-1000-8000-0026BB765291': 'TunneledBTLEAccessoryService',
TunneledBTLEAccessoryService: '00000056-0000-1000-8000-0026BB765291',
Tunnel: '00000056-0000-1000-8000-0026BB765291',
'000000D0-0000-1000-8000-0026BB765291': 'Valve',
Valve: '000000D0-0000-1000-8000-0026BB765291',
'0000020A-0000-1000-8000-0026BB765291': 'WiFiRouter',
WiFiRouter: '0000020A-0000-1000-8000-0026BB765291',
'0000020F-0000-1000-8000-0026BB765291': 'WiFiSatellite',
WiFiSatellite: '0000020F-0000-1000-8000-0026BB765291',
'0000022A-0000-1000-8000-0026BB765291': 'WiFiTransport',
WiFiTransport: '0000022A-0000-1000-8000-0026BB765291',
'0000008B-0000-1000-8000-0026BB765291': 'Window',
Window: '0000008B-0000-1000-8000-0026BB765291',
'0000008C-0000-1000-8000-0026BB765291': 'WindowCovering',
WindowCovering: '0000008C-0000-1000-8000-0026BB765291',
} as const
export const Characteristics = {
'00000262-0000-1000-8000-0026BB765291': 'AccessCodeControlPoint',
AccessCodeControlPoint: '00000262-0000-1000-8000-0026BB765291',
'00000261-0000-1000-8000-0026BB765291': 'AccessCodeSupportedConfiguration',
AccessCodeSupportedConfiguration: '00000261-0000-1000-8000-0026BB765291',
'000000E5-0000-1000-8000-0026BB765291': 'AccessControlLevel',
AccessControlLevel: '000000E5-0000-1000-8000-0026BB765291',
'000000A6-0000-1000-8000-0026BB765291': 'AccessoryFlags',
AccessoryFlags: '000000A6-0000-1000-8000-0026BB765291',
'00000057-0000-1000-8000-0026BB765291': 'AccessoryIdentifier',
AccessoryIdentifier: '00000057-0000-1000-8000-0026BB765291',
'000000B0-0000-1000-8000-0026BB765291': 'Active',
Active: '000000B0-0000-1000-8000-0026BB765291',
'000000E7-0000-1000-8000-0026BB765291': 'ActiveIdentifier',
ActiveIdentifier: '000000E7-0000-1000-8000-0026BB765291',
'0000023B-0000-1000-8000-0026BB765291': 'ActivityInterval',
ActivityInterval: '0000023B-0000-1000-8000-0026BB765291',
'00000001-0000-1000-8000-0026BB765291': 'AdministratorOnlyAccess',
AdministratorOnlyAccess: '00000001-0000-1000-8000-0026BB765291',
'00000064-0000-1000-8000-0026BB765291': 'AirParticulateDensity',
AirParticulateDensity: '00000064-0000-1000-8000-0026BB765291',
'00000065-0000-1000-8000-0026BB765291': 'AirParticulateSize',
AirParticulateSize: '00000065-0000-1000-8000-0026BB765291',
'0000025B-0000-1000-8000-0026BB765291': 'AirPlayEnable',
AirPlayEnable: '0000025B-0000-1000-8000-0026BB765291',
'00000095-0000-1000-8000-0026BB765291': 'AirQuality',
AirQuality: '00000095-0000-1000-8000-0026BB765291',
'000000A4-0000-1000-8000-0026BB765291': 'AppMatchingIdentifier',
AppMatchingIdentifier: '000000A4-0000-1000-8000-0026BB765291',
'00000269-0000-1000-8000-0026BB765291': 'AssetUpdateReadiness',
AssetUpdateReadiness: '00000269-0000-1000-8000-0026BB765291',
'00000005-0000-1000-8000-0026BB765291': 'AudioFeedback',
AudioFeedback: '00000005-0000-1000-8000-0026BB765291',
'00000068-0000-1000-8000-0026BB765291': 'BatteryLevel',
BatteryLevel: '00000068-0000-1000-8000-0026BB765291',
'00000008-0000-1000-8000-0026BB765291': 'Brightness',
Brightness: '00000008-0000-1000-8000-0026BB765291',
'00000126-0000-1000-8000-0026BB765291': 'ButtonEvent',
ButtonEvent: '00000126-0000-1000-8000-0026BB765291',
'0000021D-0000-1000-8000-0026BB765291': 'CameraOperatingModeIndicator',
CameraOperatingModeIndicator: '0000021D-0000-1000-8000-0026BB765291',
'00000092-0000-1000-8000-0026BB765291': 'CarbonDioxideDetected',
CarbonDioxideDetected: '00000092-0000-1000-8000-0026BB765291',
'00000093-0000-1000-8000-0026BB765291': 'CarbonDioxideLevel',
CarbonDioxideLevel: '00000093-0000-1000-8000-0026BB765291',
'00000094-0000-1000-8000-0026BB765291': 'CarbonDioxidePeakLevel',
CarbonDioxidePeakLevel: '00000094-0000-1000-8000-0026BB765291',
'00000069-0000-1000-8000-0026BB765291': 'CarbonMonoxideDetected',
CarbonMonoxideDetected: '00000069-0000-1000-8000-0026BB765291',
'00000090-0000-1000-8000-0026BB765291': 'CarbonMonoxideLevel',
CarbonMonoxideLevel: '00000090-0000-1000-8000-0026BB765291',
'00000091-0000-1000-8000-0026BB765291': 'CarbonMonoxidePeakLevel',
CarbonMonoxidePeakLevel: '00000091-0000-1000-8000-0026BB765291',
'000000A3-0000-1000-8000-0026BB765291': 'Category',
Category: '000000A3-0000-1000-8000-0026BB765291',
'00000246-0000-1000-8000-0026BB765291': 'CCAEnergyDetectThreshold',
CCAEnergyDetectThreshold: '00000246-0000-1000-8000-0026BB765291',
'00000245-0000-1000-8000-0026BB765291': 'CCASignalDetectThreshold',
CCASignalDetectThreshold: '00000245-0000-1000-8000-0026BB765291',
'0000024B-0000-1000-8000-0026BB765291': 'CharacteristicValueActiveTransitionCount',
CharacteristicValueActiveTransitionCount: '0000024B-0000-1000-8000-0026BB765291',
'00000143-0000-1000-8000-0026BB765291': 'CharacteristicValueTransitionControl',
CharacteristicValueTransitionControl: '00000143-0000-1000-8000-0026BB765291',
'0000008F-0000-1000-8000-0026BB765291': 'ChargingState',
ChargingState: '0000008F-0000-1000-8000-0026BB765291',
'000000DD-0000-1000-8000-0026BB765291': 'ClosedCaptions',
ClosedCaptions: '000000DD-0000-1000-8000-0026BB765291',
'000000CE-0000-1000-8000-0026BB765291': 'ColorTemperature',
ColorTemperature: '000000CE-0000-1000-8000-0026BB765291',
'00000263-0000-1000-8000-0026BB765291': 'ConfigurationState',
ConfigurationState: '00000263-0000-1000-8000-0026BB765291',
'000000A0-0000-1000-8000-0026BB765291': 'ConfigureBridgedAccessory',
ConfigureBridgedAccessory: '000000A0-0000-1000-8000-0026BB765291',
'0000009D-0000-1000-8000-0026BB765291': 'ConfigureBridgedAccessoryStatus',
ConfigureBridgedAccessoryStatus: '0000009D-0000-1000-8000-0026BB765291',
'000000E3-0000-1000-8000-0026BB765291': 'ConfiguredName',
ConfiguredName: '000000E3-0000-1000-8000-0026BB765291',
'0000006A-0000-1000-8000-0026BB765291': 'ContactSensorState',
ContactSensorState: '0000006A-0000-1000-8000-0026BB765291',
'0000000D-0000-1000-8000-0026BB765291': 'CoolingThresholdTemperature',
CoolingThresholdTemperature: '0000000D-0000-1000-8000-0026BB765291',
'000000A9-0000-1000-8000-0026BB765291': 'CurrentAirPurifierState',
CurrentAirPurifierState: '000000A9-0000-1000-8000-0026BB765291',
'0000006B-0000-1000-8000-0026BB765291': 'CurrentAmbientLightLevel',
CurrentAmbientLightLevel: '0000006B-0000-1000-8000-0026BB765291',
'0000000E-0000-1000-8000-0026BB765291': 'CurrentDoorState',
CurrentDoorState: '0000000E-0000-1000-8000-0026BB765291',
'000000AF-0000-1000-8000-0026BB765291': 'CurrentFanState',
CurrentFanState: '000000AF-0000-1000-8000-0026BB765291',
'000000B1-0000-1000-8000-0026BB765291': 'CurrentHeaterCoolerState',
CurrentHeaterCoolerState: '000000B1-0000-1000-8000-0026BB765291',
'0000000F-0000-1000-8000-0026BB765291': 'CurrentHeatingCoolingState',
CurrentHeatingCoolingState: '0000000F-0000-1000-8000-0026BB765291',
'0000006C-0000-1000-8000-0026BB765291': 'CurrentHorizontalTiltAngle',
CurrentHorizontalTiltAngle: '0000006C-0000-1000-8000-0026BB765291',
'000000B3-0000-1000-8000-0026BB765291': 'CurrentHumidifierDehumidifierState',
CurrentHumidifierDehumidifierState: '000000B3-0000-1000-8000-0026BB765291',
'000000E0-0000-1000-8000-0026BB765291': 'CurrentMediaState',
CurrentMediaState: '000000E0-0000-1000-8000-0026BB765291',
'0000006D-0000-1000-8000-0026BB765291': 'CurrentPosition',
CurrentPosition: '0000006D-0000-1000-8000-0026BB765291',
'00000010-0000-1000-8000-0026BB765291': 'CurrentRelativeHumidity',
CurrentRelativeHumidity: '00000010-0000-1000-8000-0026BB765291',
'000000AA-0000-1000-8000-0026BB765291': 'CurrentSlatState',
CurrentSlatState: '000000AA-0000-1000-8000-0026BB765291',
'00000011-0000-1000-8000-0026BB765291': 'CurrentTemperature',
CurrentTemperature: '00000011-0000-1000-8000-0026BB765291',
'000000C1-0000-1000-8000-0026BB765291': 'CurrentTiltAngle',
CurrentTiltAngle: '000000C1-0000-1000-8000-0026BB765291',
'0000009B-0000-1000-8000-0026BB765291': 'CurrentTime',
CurrentTime: '0000009B-0000-1000-8000-0026BB765291',
'0000022B-0000-1000-8000-0026BB765291': 'CurrentTransport',
CurrentTransport: '0000022B-0000-1000-8000-0026BB765291',
'0000006E-0000-1000-8000-0026BB765291': 'CurrentVerticalTiltAngle',
CurrentVerticalTiltAngle: '0000006E-0000-1000-8000-0026BB765291',
'00000135-0000-1000-8000-0026BB765291': 'CurrentVisibilityState',
CurrentVisibilityState: '00000135-0000-1000-8000-0026BB765291',
'00000138-0000-1000-8000-0026BB765291': 'DataStreamHAPTransport',
DataStreamHAPTransport: '00000138-0000-1000-8000-0026BB765291',
'00000139-0000-1000-8000-0026BB765291': 'DataStreamHAPTransportInterrupt',
DataStreamHAPTransportInterrupt: '00000139-0000-1000-8000-0026BB765291',
'00000098-0000-1000-8000-0026BB765291': 'DayoftheWeek',
DayoftheWeek: '00000098-0000-1000-8000-0026BB765291',
'00000224-0000-1000-8000-0026BB765291': 'DiagonalFieldOfView',
DiagonalFieldOfView: '00000224-0000-1000-8000-0026BB765291',
'0000011D-0000-1000-8000-0026BB765291': 'DigitalZoom',
DigitalZoom: '0000011D-0000-1000-8000-0026BB765291',
'0000009E-0000-1000-8000-0026BB765291': 'DiscoverBridgedAccessories',
DiscoverBridgedAccessories: '0000009E-0000-1000-8000-0026BB765291',
'0000009F-0000-1000-8000-0026BB765291': 'DiscoveredBridgedAccessories',
DiscoveredBridgedAccessories: '0000009F-0000-1000-8000-0026BB765291',
'00000136-0000-1000-8000-0026BB765291': 'DisplayOrder',
DisplayOrder: '00000136-0000-1000-8000-0026BB765291',
'0000023D-0000-1000-8000-0026BB765291': 'EventRetransmissionMaximum',
EventRetransmissionMaximum: '0000023D-0000-1000-8000-0026BB765291',
'00000223-0000-1000-8000-0026BB765291': 'EventSnapshotsActive',
EventSnapshotsActive: '00000223-0000-1000-8000-0026BB765291',
'0000023E-0000-1000-8000-0026BB765291': 'EventTransmissionCounters',
EventTransmissionCounters: '0000023E-0000-1000-8000-0026BB765291',
'000000AC-0000-1000-8000-0026BB765291': 'FilterChangeIndication',
FilterChangeIndication: '000000AC-0000-1000-8000-0026BB765291',
'000000AB-0000-1000-8000-0026BB765291': 'FilterLifeLevel',
FilterLifeLevel: '000000AB-0000-1000-8000-0026BB765291',
'00000052-0000-1000-8000-0026BB765291': 'FirmwareRevision',
FirmwareRevision: '00000052-0000-1000-8000-0026BB765291',
'00000234-0000-1000-8000-0026BB765291': 'FirmwareUpdateReadiness',
FirmwareUpdateReadiness: '00000234-0000-1000-8000-0026BB765291',
'00000235-0000-1000-8000-0026BB765291': 'FirmwareUpdateStatus',
FirmwareUpdateStatus: '00000235-0000-1000-8000-0026BB765291',
'0000026C-0000-1000-8000-0026BB765291': 'HardwareFinish',
HardwareFinish: '0000026C-0000-1000-8000-0026BB765291',
'00000053-0000-1000-8000-0026BB765291': 'HardwareRevision',
HardwareRevision: '00000053-0000-1000-8000-0026BB765291',
'0000024A-0000-1000-8000-0026BB765291': 'HeartBeat',
HeartBeat: '0000024A-0000-1000-8000-0026BB765291',
'00000012-0000-1000-8000-0026BB765291': 'HeatingThresholdTemperature',
HeatingThresholdTemperature: '00000012-0000-1000-8000-0026BB765291',
'0000006F-0000-1000-8000-0026BB765291': 'HoldPosition',
HoldPosition: '0000006F-0000-1000-8000-0026BB765291',
'0000021B-0000-1000-8000-0026BB765291': 'HomeKitCameraActive',
HomeKitCameraActive: '0000021B-0000-1000-8000-0026BB765291',
'00000013-0000-1000-8000-0026BB765291': 'Hue',
Hue: '00000013-0000-1000-8000-0026BB765291',
'000000E6-0000-1000-8000-0026BB765291': 'Identifier',
Identifier: '000000E6-0000-1000-8000-0026BB765291',
'00000014-0000-1000-8000-0026BB765291': 'Identify',
Identify: '00000014-0000-1000-8000-0026BB765291',
'0000011F-0000-1000-8000-0026BB765291': 'ImageMirroring',
ImageMirroring: '0000011F-0000-1000-8000-0026BB765291',
'0000011E-0000-1000-8000-0026BB765291': 'ImageRotation',
ImageRotation: '0000011E-0000-1000-8000-0026BB765291',
'000000DC-0000-1000-8000-0026BB765291': 'InputDeviceType',
InputDeviceType: '000000DC-0000-1000-8000-0026BB765291',
'000000DB-0000-1000-8000-0026BB765291': 'InputSourceType',
InputSourceType: '000000DB-0000-1000-8000-0026BB765291',
'000000D2-0000-1000-8000-0026BB765291': 'InUse',
InUse: '000000D2-0000-1000-8000-0026BB765291',
'000000D6-0000-1000-8000-0026BB765291': 'IsConfigured',
IsConfigured: '000000D6-0000-1000-8000-0026BB765291',
'00000070-0000-1000-8000-0026BB765291': 'LeakDetected',
LeakDetected: '00000070-0000-1000-8000-0026BB765291',
'0000009C-0000-1000-8000-0026BB765291': 'LinkQuality',
LinkQuality: '0000009C-0000-1000-8000-0026BB765291',
'00000050-0000-1000-8000-0026BB765291': 'ListPairings',
ListPairings: '00000050-0000-1000-8000-0026BB765291',
'00000019-0000-1000-8000-0026BB765291': 'LockControlPoint',
LockControlPoint: '00000019-0000-1000-8000-0026BB765291',
'0000001D-0000-1000-8000-0026BB765291': 'LockCurrentState',
LockCurrentState: '0000001D-0000-1000-8000-0026BB765291',
'0000001C-0000-1000-8000-0026BB765291': 'LockLastKnownAction',
LockLastKnownAction: '0000001C-0000-1000-8000-0026BB765291',
'0000001A-0000-1000-8000-0026BB765291': 'LockManagementAutoSecurityTimeout',
LockManagementAutoSecurityTimeout: '0000001A-0000-1000-8000-0026BB765291',
'000000A7-0000-1000-8000-0026BB765291': 'LockPhysicalControls',
LockPhysicalControls: '000000A7-0000-1000-8000-0026BB765291',
'0000001E-0000-1000-8000-0026BB765291': 'LockTargetState',
LockTargetState: '0000001E-0000-1000-8000-0026BB765291',
'0000001F-0000-1000-8000-0026BB765291': 'Logs',
Logs: '0000001F-0000-1000-8000-0026BB765291',
'00000247-0000-1000-8000-0026BB765291': 'MACRetransmissionMaximum',
MACRetransmissionMaximum: '00000247-0000-1000-8000-0026BB765291',
'00000248-0000-1000-8000-0026BB765291': 'MACTransmissionCounters',
MACTransmissionCounters: '00000248-0000-1000-8000-0026BB765291',
'00000215-0000-1000-8000-0026BB765291': 'ManagedNetworkEnable',
ManagedNetworkEnable: '00000215-0000-1000-8000-0026BB765291',
'00000227-0000-1000-8000-0026BB765291': 'ManuallyDisabled',
ManuallyDisabled: '00000227-0000-1000-8000-0026BB765291',
'00000020-0000-1000-8000-0026BB765291': 'Manufacturer',
Manufacturer: '00000020-0000-1000-8000-0026BB765291',
'00000243-0000-1000-8000-0026BB765291': 'MaximumTransmitPower',
MaximumTransmitPower: '00000243-0000-1000-8000-0026BB765291',
'00000021-0000-1000-8000-0026BB765291': 'Model',
Model: '00000021-0000-1000-8000-0026BB765291',
'00000022-0000-1000-8000-0026BB765291': 'MotionDetected',
MotionDetected: '00000022-0000-1000-8000-0026BB765291',
'0000026B-0000-1000-8000-0026BB765291': 'MultifunctionButton',
MultifunctionButton: '0000026B-0000-1000-8000-0026BB765291',
'0000011A-0000-1000-8000-0026BB765291': 'Mute',
Mute: '0000011A-0000-1000-8000-0026BB765291',
'00000023-0000-1000-8000-0026BB765291': 'Name',
Name: '00000023-0000-1000-8000-0026BB765291',
'0000021F-0000-1000-8000-0026BB765291': 'NetworkAccessViolationControl',
NetworkAccessViolationControl: '0000021F-0000-1000-8000-0026BB765291',
'0000020C-0000-1000-8000-0026BB765291': 'NetworkClientProfileControl',
NetworkClientProfileControl: '0000020C-0000-1000-8000-0026BB765291',
'0000020D-0000-1000-8000-0026BB765291': 'NetworkClientStatusControl',
NetworkClientStatusControl: '0000020D-0000-1000-8000-0026BB765291',
'00000264-0000-1000-8000-0026BB765291': 'NFCAccessControlPoint',
NFCAccessControlPoint: '00000264-0000-1000-8000-0026BB765291',
'00000265-0000-1000-8000-0026BB765291': 'NFCAccessSupportedConfiguration',
NFCAccessSupportedConfiguration: '00000265-0000-1000-8000-0026BB765291',
'0000011B-0000-1000-8000-0026BB765291': 'NightVision',
NightVision: '0000011B-0000-1000-8000-0026BB765291',
'000000C4-0000-1000-8000-0026BB765291': 'NitrogenDioxideDensity',
NitrogenDioxideDensity: '000000C4-0000-1000-8000-0026BB765291',
'00000024-0000-1000-8000-0026BB765291': 'ObstructionDetected',
ObstructionDetected: '00000024-0000-1000-8000-0026BB765291',
'00000071-0000-1000-8000-0026BB765291': 'OccupancyDetected',
OccupancyDetected: '00000071-0000-1000-8000-0026BB765291',
'00000025-0000-1000-8000-0026BB765291': 'On',
On: '00000025-0000-1000-8000-0026BB765291',
'00000232-0000-1000-8000-0026BB765291': 'OperatingStateResponse',
OperatingStateResponse: '00000232-0000-1000-8000-0026BB765291',
'0000011C-0000-1000-8000-0026BB765291': 'OpticalZoom',
OpticalZoom: '0000011C-0000-1000-8000-0026BB765291',
'00000026-0000-1000-8000-0026BB765291': 'OutletInUse',
OutletInUse: '00000026-0000-1000-8000-0026BB765291',
'000000C3-0000-1000-8000-0026BB765291': 'OzoneDensity',
OzoneDensity: '000000C3-0000-1000-8000-0026BB765291',
'0000004F-0000-1000-8000-0026BB765291': 'PairingFeatures',
PairingFeatures: '0000004F-0000-1000-8000-0026BB765291',
'0000004C-0000-1000-8000-0026BB765291': 'PairSetup',
PairSetup: '0000004C-0000-1000-8000-0026BB765291',
'0000004E-0000-1000-8000-0026BB765291': 'PairVerify',
PairVerify: '0000004E-0000-1000-8000-0026BB765291',
'000000E4-0000-1000-8000-0026BB765291': 'PasswordSetting',
PasswordSetting: '000000E4-0000-1000-8000-0026BB765291',
'00000225-0000-1000-8000-0026BB765291': 'PeriodicSnapshotsActive',
PeriodicSnapshotsActive: '00000225-0000-1000-8000-0026BB765291',
'000000E2-0000-1000-8000-0026BB765291': 'PictureMode',
PictureMode: '000000E2-0000-1000-8000-0026BB765291',
'0000023C-0000-1000-8000-0026BB765291': 'Ping',
Ping: '0000023C-0000-1000-8000-0026BB765291',
'000000C7-0000-1000-8000-0026BB765291': 'PM10Density',
PM10Density: '000000C7-0000-1000-8000-0026BB765291',
'000000C6-0000-1000-8000-0026BB765291': 'PM2_5Density',
PM2_5Density: '000000C6-0000-1000-8000-0026BB765291',
'00000072-0000-1000-8000-0026BB765291': 'PositionState',
PositionState: '00000072-0000-1000-8000-0026BB765291',
'000000DF-0000-1000-8000-0026BB765291': 'PowerModeSelection',
PowerModeSelection: '000000DF-0000-1000-8000-0026BB765291',
'00000220-0000-1000-8000-0026BB765291': 'ProductData',
ProductData: '00000220-0000-1000-8000-0026BB765291',
'00000073-0000-1000-8000-0026BB765291': 'ProgrammableSwitchEvent',
ProgrammableSwitchEvent: '00000073-0000-1000-8000-0026BB765291',
'00000074-0000-1000-8000-0026BB765291': 'ProgrammableSwitchOutputState',
ProgrammableSwitchOutputState: '00000074-0000-1000-8000-0026BB765291',
'000000D1-0000-1000-8000-0026BB765291': 'ProgramMode',
ProgramMode: '000000D1-0000-1000-8000-0026BB765291',
'00000063-0000-1000-8000-0026BB765291': 'Reachable',
Reachable: '00000063-0000-1000-8000-0026BB765291',
'0000023F-0000-1000-8000-0026BB765291': 'ReceivedSignalStrengthIndication',
ReceivedSignalStrengthIndication: '0000023F-0000-1000-8000-0026BB765291',
'00000244-0000-1000-8000-0026BB765291': 'ReceiverSensitivity',
ReceiverSensitivity: '00000244-0000-1000-8000-0026BB765291',
'00000226-0000-1000-8000-0026BB765291': 'RecordingAudioActive',
RecordingAudioActive: '00000226-0000-1000-8000-0026BB765291',
'000000C9-0000-1000-8000-0026BB765291': 'RelativeHumidityDehumidifierThreshold',
RelativeHumidityDehumidifierThreshold: '000000C9-0000-1000-8000-0026BB765291',
'000000CA-0000-1000-8000-0026BB765291': 'RelativeHumidityHumidifierThreshold',
RelativeHumidityHumidifierThreshold: '000000CA-0000-1000-8000-0026BB765291',
'0000005E-0000-1000-8000-0026BB765291': 'RelayControlPoint',
RelayControlPoint: '0000005E-0000-1000-8000-0026BB765291',
'0000005B-0000-1000-8000-0026BB765291': 'RelayEnabled',
RelayEnabled: '0000005B-0000-1000-8000-0026BB765291',
'0000005C-0000-1000-8000-0026BB765291': 'RelayState',
RelayState: '0000005C-0000-1000-8000-0026BB765291',
'000000D4-0000-1000-8000-0026BB765291': 'RemainingDuration',
RemainingDuration: '000000D4-0000-1000-8000-0026BB765291',
'000000E1-0000-1000-8000-0026BB765291': 'RemoteKey',
RemoteKey: '000000E1-0000-1000-8000-0026BB765291',
'000000AD-0000-1000-8000-0026BB765291': 'ResetFilterIndication',
ResetFilterIndication: '000000AD-0000-1000-8000-0026BB765291',
'00000028-0000-1000-8000-0026BB765291': 'RotationDirection',
RotationDirection: '00000028-0000-1000-8000-0026BB765291',
'00000029-0000-1000-8000-0026BB765291': 'RotationSpeed',
RotationSpeed: '00000029-0000-1000-8000-0026BB765291',
'0000020E-0000-1000-8000-0026BB765291': 'RouterStatus',
RouterStatus: '0000020E-0000-1000-8000-0026BB765291',
'0000002F-0000-1000-8000-0026BB765291': 'Saturation',
Saturation: '0000002F-0000-1000-8000-0026BB765291',
'0000008E-0000-1000-8000-0026BB765291': 'SecuritySystemAlarmType',
SecuritySystemAlarmType: '0000008E-0000-1000-8000-0026BB765291',
'00000066-0000-1000-8000-0026BB765291': 'SecuritySystemCurrentState',
SecuritySystemCurrentState: '00000066-0000-1000-8000-0026BB765291',
'00000067-0000-1000-8000-0026BB765291': 'SecuritySystemTargetState',
SecuritySystemTargetState: '00000067-0000-1000-8000-0026BB765291',
'00000128-0000-1000-8000-0026BB765291': 'SelectedAudioStreamConfiguration',
SelectedAudioStreamConfiguration: '00000128-0000-1000-8000-0026BB765291',
'00000209-0000-1000-8000-0026BB765291': 'SelectedCameraRecordingConfiguration',
SelectedCameraRecordingConfiguration: '00000209-0000-1000-8000-0026BB765291',
'0000024D-0000-1000-8000-0026BB765291': 'SelectedDiagnosticsModes',
SelectedDiagnosticsModes: '0000024D-0000-1000-8000-0026BB765291',
'00000117-0000-1000-8000-0026BB765291': 'SelectedRTPStreamConfiguration',
SelectedRTPStreamConfiguration: '00000117-0000-1000-8000-0026BB765291',
'00000030-0000-1000-8000-0026BB765291': 'SerialNumber',
SerialNumber: '00000030-0000-1000-8000-0026BB765291',
'000000CB-0000-1000-8000-0026BB765291': 'ServiceLabelIndex',
ServiceLabelIndex: '000000CB-0000-1000-8000-0026BB765291',
'000000CD-0000-1000-8000-0026BB765291': 'ServiceLabelNamespace',
ServiceLabelNamespace: '000000CD-0000-1000-8000-0026BB765291',
'000000D3-0000-1000-8000-0026BB765291': 'SetDuration',
SetDuration: '000000D3-0000-1000-8000-0026BB765291',
'00000131-0000-1000-8000-0026BB765291': 'SetupDataStreamTransport',
SetupDataStreamTransport: '00000131-0000-1000-8000-0026BB765291',
'00000118-0000-1000-8000-0026BB765291': 'SetupEndpoints',
SetupEndpoints: '00000118-0000-1000-8000-0026BB765291',
'00000201-0000-1000-8000-0026BB765291': 'SetupTransferTransport',
SetupTransferTransport: '00000201-0000-1000-8000-0026BB765291',
'00000241-0000-1000-8000-0026BB765291': 'SignalToNoiseRatio',
SignalToNoiseRatio: '00000241-0000-1000-8000-0026BB765291',
'00000255-0000-1000-8000-0026BB765291': 'SiriEnable',
SiriEnable: '00000255-0000-1000-8000-0026BB765291',
'00000254-0000-1000-8000-0026BB765291': 'SiriEndpointSessionStatus',
SiriEndpointSessionStatus: '00000254-0000-1000-8000-0026BB765291',
'0000025A-0000-1000-8000-0026BB765291': 'SiriEngineVersion',
SiriEngineVersion: '0000025A-0000-1000-8000-0026BB765291',
'00000132-0000-1000-8000-0026BB765291': 'SiriInputType',
SiriInputType: '00000132-0000-1000-8000-0026BB765291',
'00000258-0000-1000-8000-0026BB765291': 'SiriLightOnUse',
SiriLightOnUse: '00000258-0000-1000-8000-0026BB765291',
'00000256-0000-1000-8000-0026BB765291': 'SiriListening',
SiriListening: '00000256-0000-1000-8000-0026BB765291',
'00000257-0000-1000-8000-0026BB765291': 'SiriTouchToUse',
SiriTouchToUse: '00000257-0000-1000-8000-0026BB765291',
'000000C0-0000-1000-8000-0026BB765291': 'SlatType',
SlatType: '000000C0-0000-1000-8000-0026BB765291',
'000000E8-0000-1000-8000-0026BB765291': 'SleepDiscoveryMode',
SleepDiscoveryMode: '000000E8-0000-1000-8000-0026BB765291',
'0000023A-0000-1000-8000-0026BB765291': 'SleepInterval',
SleepInterval: '0000023A-0000-1000-8000-0026BB765291',
'00000076-0000-1000-8000-0026BB765291': 'SmokeDetected',
SmokeDetected: '00000076-0000-1000-8000-0026BB765291',
'00000054-0000-1000-8000-0026BB765291': 'SoftwareRevision',
SoftwareRevision: '00000054-0000-1000-8000-0026BB765291',
'00000249-0000-1000-8000-0026BB765291': 'StagedFirmwareVersion',
StagedFirmwareVersion: '00000249-0000-1000-8000-0026BB765291',
'00000075-0000-1000-8000-0026BB765291': 'StatusActive',
StatusActive: '00000075-0000-1000-8000-0026BB765291',
'00000077-0000-1000-8000-0026BB765291': 'StatusFault',
StatusFault: '00000077-0000-1000-8000-0026BB765291',
'00000078-0000-1000-8000-0026BB765291': 'StatusJammed',
StatusJammed: '00000078-0000-1000-8000-0026BB765291',
'00000079-0000-1000-8000-0026BB765291': 'StatusLowBattery',
StatusLowBattery: '00000079-0000-1000-8000-0026BB765291',
'0000007A-0000-1000-8000-0026BB765291': 'StatusTampered',
StatusTampered: '0000007A-0000-1000-8000-0026BB765291',
'00000120-0000-1000-8000-0026BB765291': 'StreamingStatus',
StreamingStatus: '00000120-0000-1000-8000-0026BB765291',
'000000C5-0000-1000-8000-0026BB765291': 'SulphurDioxideDensity',
SulphurDioxideDensity: '000000C5-0000-1000-8000-0026BB765291',
'00000268-0000-1000-8000-0026BB765291': 'SupportedAssetTypes',
SupportedAssetTypes: '00000268-0000-1000-8000-0026BB765291',
'00000207-0000-1000-8000-0026BB765291': 'SupportedAudioRecordingConfiguration',
SupportedAudioRecordingConfiguration: '00000207-0000-1000-8000-0026BB765291',
'00000115-0000-1000-8000-0026BB765291': 'SupportedAudioStreamConfiguration',
SupportedAudioStreamConfiguration: '00000115-0000-1000-8000-0026BB765291',
'00000205-0000-1000-8000-0026BB765291': 'SupportedCameraRecordingConfiguration',
SupportedCameraRecordingConfiguration: '00000205-0000-1000-8000-0026BB765291',
'00000144-0000-1000-8000-0026BB765291': 'SupportedCharacteristicValueTransitionConfiguration',
SupportedCharacteristicValueTransitionConfiguration: '00000144-0000-1000-8000-0026BB765291',
'00000130-0000-1000-8000-0026BB765291': 'SupportedDataStreamTransportConfiguration',
SupportedDataStreamTransportConfiguration: '00000130-0000-1000-8000-0026BB765291',
'0000024C-0000-1000-8000-0026BB765291': 'SupportedDiagnosticsModes',
SupportedDiagnosticsModes: '0000024C-0000-1000-8000-0026BB765291',
'00000238-0000-1000-8000-0026BB765291': 'SupportedDiagnosticsSnapshot',
SupportedDiagnosticsSnapshot: '00000238-0000-1000-8000-0026BB765291',
'00000233-0000-1000-8000-0026BB765291': 'SupportedFirmwareUpdateConfiguration',
SupportedFirmwareUpdateConfiguration: '00000233-0000-1000-8000-0026BB765291',
'00000210-0000-1000-8000-0026BB765291': 'SupportedRouterConfiguration',
SupportedRouterConfiguration: '00000210-0000-1000-8000-0026BB765291',
'00000116-0000-1000-8000-0026BB765291': 'SupportedRTPConfiguration',
SupportedRTPConfiguration: '00000116-0000-1000-8000-0026BB765291',
'00000202-0000-1000-8000-0026BB765291': 'SupportedTransferTransportConfiguration',
SupportedTransferTransportConfiguration: '00000202-0000-1000-8000-0026BB765291',
'00000206-0000-1000-8000-0026BB765291': 'SupportedVideoRecordingConfiguration',
SupportedVideoRecordingConfiguration: '00000206-0000-1000-8000-0026BB765291',
'00000114-0000-1000-8000-0026BB765291': 'SupportedVideoStreamConfiguration',
SupportedVideoStreamConfiguration: '00000114-0000-1000-8000-0026BB765291',
'000000B6-0000-1000-8000-0026BB765291': 'SwingMode',
SwingMode: '000000B6-0000-1000-8000-0026BB765291',
'000000A8-0000-1000-8000-0026BB765291': 'TargetAirPurifierState',
TargetAirPurifierState: '000000A8-0000-1000-8000-0026BB765291',
'000000AE-0000-1000-8000-0026BB765291': 'TargetAirQuality',
TargetAirQuality: '000000AE-0000-1000-8000-0026BB765291',
'00000124-0000-1000-8000-0026BB765291': 'TargetControlList',
TargetControlList: '00000124-0000-1000-8000-0026BB765291',
'00000123-0000-1000-8000-0026BB765291': 'TargetControlSupportedConfiguration',
TargetControlSupportedConfiguration: '00000123-0000-1000-8000-0026BB765291',
'00000032-0000-1000-8000-0026BB765291': 'TargetDoorState',
TargetDoorState: '00000032-0000-1000-8000-0026BB765291',
'000000BF-0000-1000-8000-0026BB765291': 'TargetFanState',
TargetFanState: '000000BF-0000-1000-8000-0026BB765291',
'000000B2-0000-1000-8000-0026BB765291': 'TargetHeaterCoolerState',
TargetHeaterCoolerState: '000000B2-0000-1000-8000-0026BB765291',
'00000033-0000-1000-8000-0026BB765291': 'TargetHeatingCoolingState',
TargetHeatingCoolingState: '00000033-0000-1000-8000-0026BB765291',
'0000007B-0000-1000-8000-0026BB765291': 'TargetHorizontalTiltAngle',
TargetHorizontalTiltAngle: '0000007B-0000-1000-8000-0026BB765291',
'000000B4-0000-1000-8000-0026BB765291': 'TargetHumidifierDehumidifierState',
TargetHumidifierDehumidifierState: '000000B4-0000-1000-8000-0026BB765291',
'00000137-0000-1000-8000-0026BB765291': 'TargetMediaState',
TargetMediaState: '00000137-0000-1000-8000-0026BB765291',
'0000007C-0000-1000-8000-0026BB765291': 'TargetPosition',
TargetPosition: '0000007C-0000-1000-8000-0026BB765291',
'00000034-0000-1000-8000-0026BB765291': 'TargetRelativeHumidity',
TargetRelativeHumidity: '00000034-0000-1000-8000-0026BB765291',
'000000BE-0000-1000-8000-0026BB765291': 'TargetSlatState',
TargetSlatState: '000000BE-0000-1000-8000-0026BB765291',
'00000035-0000-1000-8000-0026BB765291': 'TargetTemperature',
TargetTemperature: '00000035-0000-1000-8000-0026BB765291',
'000000C2-0000-1000-8000-0026BB765291': 'TargetTiltAngle',
TargetTiltAngle: '000000C2-0000-1000-8000-0026BB765291',
'0000007D-0000-1000-8000-0026BB765291': 'TargetVerticalTiltAngle',
TargetVerticalTiltAngle: '0000007D-0000-1000-8000-0026BB765291',
'00000134-0000-1000-8000-0026BB765291': 'TargetVisibilityState',
TargetVisibilityState: '00000134-0000-1000-8000-0026BB765291',
'00000036-0000-1000-8000-0026BB765291': 'TemperatureDisplayUnits',
TemperatureDisplayUnits: '00000036-0000-1000-8000-0026BB765291',
'0000021C-0000-1000-8000-0026BB765291': 'ThirdPartyCameraActive',
ThirdPartyCameraActive: '0000021C-0000-1000-8000-0026BB765291',
'00000704-0000-1000-8000-0026BB765291': 'ThreadControlPoint',
ThreadControlPoint: '00000704-0000-1000-8000-0026BB765291',
'00000702-0000-1000-8000-0026BB765291': 'ThreadNodeCapabilities',
ThreadNodeCapabilities: '00000702-0000-1000-8000-0026BB765291',
'00000706-0000-1000-8000-0026BB765291': 'ThreadOpenThreadVersion',
ThreadOpenThreadVersion: '00000706-0000-1000-8000-0026BB765291',
'00000703-0000-1000-8000-0026BB765291': 'ThreadStatus',
ThreadStatus: '00000703-0000-1000-8000-0026BB765291',
'0000009A-0000-1000-8000-0026BB765291': 'TimeUpdate',
TimeUpdate: '0000009A-0000-1000-8000-0026BB765291',
'00000242-0000-1000-8000-0026BB765291': 'TransmitPower',
TransmitPower: '00000242-0000-1000-8000-0026BB765291',
'00000061-0000-1000-8000-0026BB765291': 'TunnelConnectionTimeout',
TunnelConnectionTimeout: '00000061-0000-1000-8000-0026BB765291',
'00000060-0000-1000-8000-0026BB765291': 'TunneledAccessoryAdvertising',
TunneledAccessoryAdvertising: '00000060-0000-1000-8000-0026BB765291',
'00000059-0000-1000-8000-0026BB765291': 'TunneledAccessoryConnected',
TunneledAccessoryConnected: '00000059-0000-1000-8000-0026BB765291',
'00000058-0000-1000-8000-0026BB765291': 'TunneledAccessoryStateNumber',
TunneledAccessoryStateNumber: '00000058-0000-1000-8000-0026BB765291',
'000000D5-0000-1000-8000-0026BB765291': 'ValveType',
ValveType: '000000D5-0000-1000-8000-0026BB765291',
'00000037-0000-1000-8000-0026BB765291': 'Version',
Version: '00000037-0000-1000-8000-0026BB765291',
'00000229-0000-1000-8000-0026BB765291': 'VideoAnalysisActive',
VideoAnalysisActive: '00000229-0000-1000-8000-0026BB765291',
'000000C8-0000-1000-8000-0026BB765291': 'VOCDensity',
VOCDensity: '000000C8-0000-1000-8000-0026BB765291',
'00000119-0000-1000-8000-0026BB765291': 'Volume',
Volume: '00000119-0000-1000-8000-0026BB765291',
'000000E9-0000-1000-8000-0026BB765291': 'VolumeControlType',
VolumeControlType: '000000E9-0000-1000-8000-0026BB765291',
'000000EA-0000-1000-8000-0026BB765291': 'VolumeSelector',
VolumeSelector: '000000EA-0000-1000-8000-0026BB765291',
'00000222-0000-1000-8000-0026BB765291': 'WakeConfiguration',
WakeConfiguration: '00000222-0000-1000-8000-0026BB765291',
'00000211-0000-1000-8000-0026BB765291': 'WANConfigurationList',
WANConfigurationList: '00000211-0000-1000-8000-0026BB765291',
'00000212-0000-1000-8000-0026BB765291': 'WANStatusList',
WANStatusList: '00000212-0000-1000-8000-0026BB765291',
'000000B5-0000-1000-8000-0026BB765291': 'WaterLevel',
WaterLevel: '000000B5-0000-1000-8000-0026BB765291',
'0000022C-0000-1000-8000-0026BB765291': 'WiFiCapabilities',
WiFiCapabilities: '0000022C-0000-1000-8000-0026BB765291',
'0000022D-0000-1000-8000-0026BB765291': 'WiFiConfigurationControl',
WiFiConfigurationControl: '0000022D-0000-1000-8000-0026BB765291',
'0000021E-0000-1000-8000-0026BB765291': 'WiFiSatelliteStatus',
WiFiSatelliteStatus: '0000021E-0000-1000-8000-0026BB765291',
} as const
export const Categories = {
OTHER: 1,
BRIDGE: 2,
FAN: 3,
GARAGE_DOOR_OPENER: 4,
LIGHTBULB: 5,
DOOR_LOCK: 6,
OUTLET: 7,
SWITCH: 8,
THERMOSTAT: 9,
SENSOR: 10,
ALARM_SYSTEM: 11,
SECURITY_SYSTEM: 11,
DOOR: 12,
WINDOW: 13,
WINDOW_COVERING: 14,
PROGRAMMABLE_SWITCH: 15,
RANGE_EXTENDER: 16,
CAMERA: 17,
IP_CAMERA: 17,
VIDEO_DOORBELL: 18,
AIR_PURIFIER: 19,
AIR_HEATER: 20,
AIR_CONDITIONER: 21,
AIR_HUMIDIFIER: 22,
AIR_DEHUMIDIFIER: 23,
APPLE_TV: 24,
HOMEPOD: 25,
SPEAKER: 26,
AIRPORT: 27,
SPRINKLER: 28,
FAUCET: 29,
SHOWER_HEAD: 30,
TELEVISION: 31,
TARGET_CONTROLLER: 32,
ROUTER: 33,
AUDIO_RECEIVER: 34,
TV_SET_TOP_BOX: 35,
TV_STREAMING_STICK: 36,
} as const

22
src/http/api.ts Normal file
View file

@ -0,0 +1,22 @@
import { Logger } from 'homebridge'
export type HttpResponse = {
statusCode: number
body: string
headers: Record<string, string>
}
export type HttpServerConfig = {
port: number
requestInterceptor: () => HttpResponse | undefined
probeController: () => HttpResponse
notFoundController: () => HttpResponse
errorController: () => HttpResponse
logger: Logger
}
export type HttpServerController = {
shutdown(): void
}
export type HttpServer = (config: HttpServerConfig) => Promise<HttpServerController>

82
src/http/fastify.ts Normal file
View file

@ -0,0 +1,82 @@
import Fastify, { FastifyBaseLogger, FastifyReply, FastifyRequest, HookHandlerDoneFunction } from 'fastify'
import { HttpResponse, HttpServer } from './api'
import { Logger } from 'homebridge'
import { Bindings, ChildLoggerOptions } from 'fastify/types/logger'
import { LogFn } from 'pino'
/*class PinoLoggerBridge implements FastifyBaseLogger {
constructor(private readonly logger: Logger) {
}
child(bindings: Bindings, options: ChildLoggerOptions | undefined): FastifyBaseLogger {
return this
}
debug(m: string|unknown, ...args: unknown[]) {
this.logger.debug(typeof m === 'string' ? m : JSON.stringify(m), ...args)
}
error: LogFn;
fatal: pino.LogFn;
info: pino.LogFn;
level: pino.LevelWithSilent | string;
silent: pino.LogFn;
trace: pino.LogFn;
warn: pino.LogFn;
}*/
function adaptResponseToReply(response: HttpResponse, reply: FastifyReply): void {
if (response.statusCode) {
reply.code(response.statusCode)
}
if (response.body) {
reply.send(response.body)
}
if (response.headers) {
reply.headers(response.headers)
}
}
export const serve: HttpServer = async ({
port,
requestInterceptor,
probeController,
notFoundController,
errorController,
}) => {
const fastify = Fastify({
// logger
})
fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply, next: HookHandlerDoneFunction) => {
const response = requestInterceptor()
if (response) {
adaptResponseToReply(response, reply)
}
next()
})
fastify.setErrorHandler(async (error, request: FastifyRequest, reply: FastifyReply) => {
adaptResponseToReply(errorController(), reply)
})
fastify.setNotFoundHandler(async (request: FastifyRequest, reply: FastifyReply) => {
adaptResponseToReply(notFoundController(), reply)
})
fastify.get('/probe', async (request: FastifyRequest, reply: FastifyReply) => {
adaptResponseToReply(probeController(), reply)
})
await fastify.listen({ port })
return {
shutdown() {
fastify.close()
},
}
}

View file

@ -1,11 +1,8 @@
import { API } from 'homebridge';
import { API } from 'homebridge'
import { PLATFORM_NAME } from './settings';
import { ExampleHomebridgePlatform } from './platform';
import { PLATFORM_NAME } from './settings'
import { PrometheusExporterPlatform } from './platform'
/**
* This method registers the platform with Homebridge
*/
export = (api: API) => {
api.registerPlatform(PLATFORM_NAME, ExampleHomebridgePlatform);
};
export = (api: API): void => {
api.registerPlatform(PLATFORM_NAME, PrometheusExporterPlatform)
}

138
src/metrics.ts Normal file
View file

@ -0,0 +1,138 @@
import type { Device, Accessory, NumberTypes, Service } from './boundaries'
import { isType } from './std'
import { NUMBER_TYPES } from './boundaries'
import { Services } from './hap'
export class Metric {
constructor(
public readonly name: string,
public readonly value: number,
public readonly type: NumberTypes,
public readonly labels: Record<string, string>,
) {}
}
/**
* Characteristics that would be nonsensical to report as metrics
*/
const METRICS_FILTER = ['Identifier']
function debug(devices: Device[]): void {
const debugInfo = []
for (const device of devices) {
for (const accessory of device.accessories.accessories) {
for (const service of accessory.services) {
const info = []
for (const characteristic of service.characteristics) {
info.push(
[characteristic.description, 'value' in characteristic ? characteristic.value : '<none>'].join(
': ',
),
)
}
debugInfo.push(['Service ' + Services[service.type as keyof typeof Services], info])
}
}
}
console.log(JSON.stringify(debugInfo, null, 4))
}
export function aggregate(devices: Device[]): Metric[] {
const metrics: Metric[] = []
for (const device of devices) {
for (const accessory of device.accessories.accessories) {
for (const service of accessory.services) {
const labels = {
...getDeviceLabels(device),
...getAccessoryLabels(accessory),
...getServiceLabels(service),
}
for (const characteristic of service.characteristics) {
for (const numberType of NUMBER_TYPES) {
if (characteristic.format === numberType && typeof characteristic.value !== 'undefined') {
if (METRICS_FILTER.includes(characteristic.description)) {
continue
}
const name = formatName(
Services[service.type as keyof typeof Services],
characteristic.description,
characteristic.unit,
)
if (!METRICS_FILTER.includes(name)) {
metrics.push(new Metric(name, characteristic.value, characteristic.format, labels))
}
}
}
}
}
}
}
return metrics
}
export function formatName(serviceName: string, description: string, unit: string | undefined = undefined): string {
return (
[serviceName, description, typeof unit === 'string' ? unit.toLowerCase() : undefined]
.filter(isType('string'))
.map((v) => camelCaseToSnakeCase(v))
// Remove duplicate prefix
.reduce((carry, value) => (value.startsWith(carry) ? value : carry + '_' + value))
)
}
function camelCaseToSnakeCase(str: string): string {
return str
.replace(/\B([A-Z][a-z])/g, ' $1')
.toLowerCase()
.trim()
.replace(/\s+/g, '_')
}
function getDeviceLabels(device: Device): Record<string, string> {
return {
bridge: device.instance.name,
instance: device.instance.url,
device_id: device.instance.deviceID,
}
}
function getAccessoryLabels(accessory: Accessory): Record<string, string> {
const labels: Record<string, string> = {}
for (const service of accessory.services) {
if (service.type === '0000003E-0000-1000-8000-0026BB765291') {
return getServiceLabels(service)
}
}
return labels
}
function getServiceLabels(service: Service): Record<string, string> {
const labels: Record<string, string> = {}
for (const characteristic of service.characteristics) {
if (
characteristic.format === 'string' &&
[
'Name',
'Configured Name',
'Model',
'Manufacturer',
'Serial Number',
'Version',
'Firmware Revision',
'Hardware Revision',
].includes(characteristic.description)
) {
labels[camelCaseToSnakeCase(characteristic.description)] = characteristic.value
}
}
return labels
}

View file

@ -1,116 +1,120 @@
import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge';
import { API, Logger, PlatformConfig, IndependentPlatformPlugin } from 'homebridge'
import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
import { ExamplePlatformAccessory } from './platformAccessory';
import { Metric, aggregate } from './metrics'
import { discover } from './discovery/hap_node_js_client'
import { serve } from './http/fastify'
import { HttpServerController } from './http/api'
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class ExampleHomebridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
private metrics: Metric[] = []
private metricsDiscovered = false
private http: HttpServerController | undefined = undefined
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) {
this.log.debug('Initializing platform %s', this.config.platform)
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.log.debug('Finished initializing platform:', this.config.name);
this.configure()
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
this.discoverDevices();
});
}
this.api.on('shutdown', () => {
this.log.debug('Shutting down %s', this.config.platform)
if (this.http) {
this.http.shutdown()
}
})
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
this.startHttpServer()
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
// EXAMPLE ONLY
// A real plugin you would discover accessories from the local network, cloud services
// or a user-defined array in the platform config.
const exampleDevices = [
{
exampleUniqueId: 'ABCD',
exampleDisplayName: 'Bedroom',
},
{
exampleUniqueId: 'EFGH',
exampleDisplayName: 'Kitchen',
},
];
// loop over the discovered devices and register each one if it has not already been registered
for (const device of exampleDevices) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.exampleUniqueId);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new ExamplePlatformAccessory(this, existingAccessory);
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.exampleDisplayName);
// create a new accessory
const accessory = new this.api.platformAccessory(device.exampleDisplayName, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new ExamplePlatformAccessory(this, accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
this.api.on('didFinishLaunching', () => {
this.log.debug('Finished launching %s', this.config.platform)
this.startHapDiscovery()
})
}
private configure(): void {
if (this.config.pin !== 'string' || !this.config.pin.match(/^\d{3}-\d{2}-\d{3}$/)) {
this.log.error('"pin" must be defined in config and match format 000-00-000')
}
this.config.debug = this.config.debug ?? false
this.config.probe_port = this.config.probe_port ?? 36123
this.config.refresh_interval = this.config.refresh_interval || 60
this.config.request_timeout = this.config.request_timeout || 10
this.config.discovery_timeout = this.config.discovery_timeout || 20
this.log.debug('Configuration materialized: %o', this.config)
}
private startHttpServer(): void {
this.log.debug('Starting probe HTTP server on port %d', this.config.probe_port)
const contentTypeHeader = { 'Content-Type': 'text/plain; charset=UTF-8' }
serve({
port: this.config.probe_port,
logger: this.log,
requestInterceptor: () => {
if (!this.metricsDiscovered) {
return {
statusCode: 503,
headers: { ...contentTypeHeader, 'Retry-After': '10' },
body: 'Discovery pending',
}
}
},
probeController: () => {
const prefix = 'homebridge_'
const metrics = this.metrics
.map((metric) => [
`# TYPE ${prefix}${metric.name} gauge`,
`${prefix}${metric.name}{${Object.entries(metric.labels)
.map(([key, value]) => `${key}="${value}"`)
.join(',')}} ${metric.value}`,
])
.flat()
.join('\n')
return {
statusCode: 200,
headers: contentTypeHeader,
body: metrics,
}
},
notFoundController: () => ({
statusCode: 404,
headers: contentTypeHeader,
body: 'Not found. Try /probe',
}),
errorController: () => ({
statusCode: 500,
headers: contentTypeHeader,
body: 'Server error',
}),
})
.then((http) => {
this.log.debug('HTTP server started on port %d', this.config.probe_port)
this.http = http
})
.catch((e) => {
this.log.error('Failed to start probe HTTP server on port %d: %o', this.config.probe_port, e)
})
}
private startHapDiscovery(): void {
this.log.debug('Starting HAP discovery')
discover({
logger: this.log,
refreshInterval: this.config.refresh_interval,
discoveryTimeout: this.config.discovery_timeout,
requestTimeout: this.config.request_timeout,
pin: this.config.pin,
debug: this.config.debug,
})
.then((devices) => {
this.metrics = aggregate(devices)
this.metricsDiscovered = true
this.log.debug('HAP discovery completed, %d metrics discovered', this.metrics.length)
})
.catch((e) => {
this.log.error('HAP discovery error', e)
})
}
}
}

View file

@ -1,141 +0,0 @@
import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge';
import { ExampleHomebridgePlatform } from './platform';
/**
* Platform Accessory
* An instance of this class is created for each accessory your platform registers
* Each accessory may expose multiple services of different service types.
*/
export class ExamplePlatformAccessory {
private service: Service;
/**
* These are just used to create a working example
* You should implement your own code to track the state of your accessory
*/
private exampleStates = {
On: false,
Brightness: 100,
};
constructor(
private readonly platform: ExampleHomebridgePlatform,
private readonly accessory: PlatformAccessory,
) {
// set accessory information
this.accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Default-Manufacturer')
.setCharacteristic(this.platform.Characteristic.Model, 'Default-Model')
.setCharacteristic(this.platform.Characteristic.SerialNumber, 'Default-Serial');
// get the LightBulb service if it exists, otherwise create a new LightBulb service
// you can create multiple services for each accessory
this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb);
// set the service name, this is what is displayed as the default name on the Home app
// in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method.
this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.exampleDisplayName);
// each service must implement at-minimum the "required characteristics" for the given service type
// see https://developers.homebridge.io/#/service/Lightbulb
// register handlers for the On/Off Characteristic
this.service.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setOn.bind(this)) // SET - bind to the `setOn` method below
.onGet(this.getOn.bind(this)); // GET - bind to the `getOn` method below
// register handlers for the Brightness Characteristic
this.service.getCharacteristic(this.platform.Characteristic.Brightness)
.onSet(this.setBrightness.bind(this)); // SET - bind to the 'setBrightness` method below
/**
* Creating multiple services of the same type.
*
* To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error,
* when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id:
* this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID');
*
* The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory
* can use the same sub type id.)
*/
// Example: add two "motion sensor" services to the accessory
const motionSensorOneService = this.accessory.getService('Motion Sensor One Name') ||
this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor One Name', 'YourUniqueIdentifier-1');
const motionSensorTwoService = this.accessory.getService('Motion Sensor Two Name') ||
this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor Two Name', 'YourUniqueIdentifier-2');
/**
* Updating characteristics values asynchronously.
*
* Example showing how to update the state of a Characteristic asynchronously instead
* of using the `on('get')` handlers.
* Here we change update the motion sensor trigger states on and off every 10 seconds
* the `updateCharacteristic` method.
*
*/
let motionDetected = false;
setInterval(() => {
// EXAMPLE - inverse the trigger
motionDetected = !motionDetected;
// push the new value to HomeKit
motionSensorOneService.updateCharacteristic(this.platform.Characteristic.MotionDetected, motionDetected);
motionSensorTwoService.updateCharacteristic(this.platform.Characteristic.MotionDetected, !motionDetected);
this.platform.log.debug('Triggering motionSensorOneService:', motionDetected);
this.platform.log.debug('Triggering motionSensorTwoService:', !motionDetected);
}, 10000);
}
/**
* Handle "SET" requests from HomeKit
* These are sent when the user changes the state of an accessory, for example, turning on a Light bulb.
*/
async setOn(value: CharacteristicValue) {
// implement your own code to turn your device on/off
this.exampleStates.On = value as boolean;
this.platform.log.debug('Set Characteristic On ->', value);
}
/**
* Handle the "GET" requests from HomeKit
* These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on.
*
* GET requests should return as fast as possbile. A long delay here will result in
* HomeKit being unresponsive and a bad user experience in general.
*
* If your device takes time to respond you should update the status of your device
* asynchronously instead using the `updateCharacteristic` method instead.
* @example
* this.service.updateCharacteristic(this.platform.Characteristic.On, true)
*/
async getOn(): Promise<CharacteristicValue> {
// implement your own code to check if the device is on
const isOn = this.exampleStates.On;
this.platform.log.debug('Get Characteristic On ->', isOn);
// if you need to return an error to show the device as "Not Responding" in the Home app:
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
return isOn;
}
/**
* Handle "SET" requests from HomeKit
* These are sent when the user changes the state of an accessory, for example, changing the Brightness
*/
async setBrightness(value: CharacteristicValue) {
// implement your own code to set the brightness
this.exampleStates.Brightness = value as number;
this.platform.log.debug('Set Characteristic Brightness -> ', value);
}
}

View file

@ -1,9 +1,2 @@
/**
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
*/
export const PLATFORM_NAME = 'ExampleHomebridgePlugin';
/**
* This must match the name of your plugin as defined the package.json
*/
export const PLUGIN_NAME = 'homebridge-plugin-name';
export const PLATFORM_NAME = 'PrometheusExporter'
export const PLUGIN_NAME = 'homebridge-prometheus-exporter'

11
src/std.ts Normal file
View file

@ -0,0 +1,11 @@
type Types = 'string' | 'number' | 'boolean' | 'object'
type TypeMap = {
string: string
number: number
boolean: boolean
object: object
}
export function isType<T extends Types>(type: T): (v: unknown) => v is TypeMap[T] {
return (v: unknown): v is TypeMap[T] => typeof v === type
}

156
tests/aggregator.test.ts Normal file
View file

@ -0,0 +1,156 @@
import { describe, expect, test } from '@jest/globals'
import { DeviceBoundary } from '../src/boundaries'
import { Metric, aggregate } from '../src/metrics'
import dysonData from './fixtures/dyson.json'
import emptyData from './fixtures/empty.json'
import tpLinkData from './fixtures/tp-link.json'
import harmonyData from './fixtures/harmony.json'
describe('Metrics aggregator', () => {
test('Aggregates homebridge-dyson fan metrics', () => {
const dyson = DeviceBoundary.check(dysonData)
const expectedLabels = {
bridge: 'Dyson bridge',
device_id: 'AA:AA:AA:AA:AA:AA',
instance: 'http://192.168.0.1:38333',
firmware_revision: '21.04.03',
hardware_revision: 'TP02',
manufacturer: 'Dyson',
model: 'Dyson Pure Cool Link Tower',
name: 'Office',
serial_number: 'NN2-EU-KKA0717A',
}
expect(aggregate([dyson])).toEqual([
new Metric('air_purifier_active', 0, 'uint8', expectedLabels),
new Metric('air_purifier_current_air_purifier_state', 0, 'uint8', expectedLabels),
new Metric('air_purifier_target_air_purifier_state', 0, 'uint8', expectedLabels),
new Metric('air_purifier_filter_life_level_percentage', 85, 'float', expectedLabels),
new Metric('air_purifier_rotation_speed_percentage', 100, 'float', expectedLabels),
new Metric('air_purifier_current_temperature_celsius', 22.8, 'float', expectedLabels),
new Metric('air_purifier_swing_mode', 0, 'uint8', expectedLabels),
new Metric('air_purifier_filter_change_indication', 0, 'uint8', expectedLabels),
new Metric('air_purifier_current_relative_humidity_percentage', 52, 'float', expectedLabels),
new Metric('air_purifier_air_quality', 2, 'uint8', expectedLabels),
])
})
test('Aggregates empty accessory metrics to empty metrics', () => {
const empty = DeviceBoundary.check(emptyData)
expect(aggregate([empty])).toEqual([])
})
test('Aggregates TP-Link plugs metrics', () => {
const tpLink = DeviceBoundary.check(tpLinkData)
const expectedLabelsAccessory1 = {
bridge: 'TP-Link bridge',
device_id: 'AA:AA:AA:AA:AA:AA',
instance: 'http://192.168.0.1:38333',
firmware_revision: '1.5.6 Build 191125 Rel.083657',
hardware_revision: '2.0',
manufacturer: 'TP-Link',
model: 'HS110(EU)',
name: 'Drucker',
serial_number: 'AA:AA:AA:AA:AA:AA 8006106A8B451F91FA0498A6615963E019CCD80A',
}
const expectedLabelsAccessory2 = {
bridge: 'TP-Link bridge',
device_id: 'AA:AA:AA:AA:AA:AA',
instance: 'http://192.168.0.1:38333',
firmware_revision: '1.5.6 Build 191125 Rel.083657',
hardware_revision: '2.0',
manufacturer: 'TP-Link',
model: 'HS110(EU)',
name: 'Espressomaschine',
serial_number: 'AA:AA:AA:AA:AA:AA 800614AA26608873C919E4592765404019F64D07',
}
expect(aggregate([tpLink])).toEqual([
new Metric('outlet_amperes_a', 0.03, 'float', expectedLabelsAccessory1),
new Metric('outlet_total_consumption_kwh', 0.051, 'float', expectedLabelsAccessory1),
new Metric('outlet_apparent_power_va', 53248.8, 'float', expectedLabelsAccessory1),
new Metric('outlet_volts_v', 230.8, 'float', expectedLabelsAccessory1),
new Metric('outlet_consumption_w', 0, 'float', expectedLabelsAccessory1),
new Metric('outlet_amperes_a', 0.03, 'float', expectedLabelsAccessory2),
new Metric('outlet_total_consumption_kwh', 13.025, 'float', expectedLabelsAccessory2),
new Metric('outlet_apparent_power_va', 53365.6, 'float', expectedLabelsAccessory2),
new Metric('outlet_volts_v', 231, 'float', expectedLabelsAccessory2),
new Metric('outlet_consumption_w', 0, 'float', expectedLabelsAccessory2),
])
})
test('Aggregates homebridge-harmony remote metrics', () => {
const harmony = DeviceBoundary.check(harmonyData)
const expectedLabels1 = {
bridge: 'Harmony bridge',
device_id: 'AA:AA:AA:AA:AA:AA',
instance: 'http://192.168.0.1:38333',
configured_name: 'Fernbedienung',
firmware_revision: '1.6.2',
manufacturer: 'Logitech',
model: 'Fernbedienung Wohnzimmer',
name: 'Fernbedienung Wohnzimmer',
serial_number: '0e88f449-2720-4000-8c4b-06775986e8ac',
}
const expectedLabels2 = {
bridge: 'Harmony bridge',
device_id: 'AA:AA:AA:AA:AA:AA',
instance: 'http://192.168.0.1:38333',
configured_name: 'CD',
firmware_revision: '1.6.2',
manufacturer: 'Logitech',
model: 'Fernbedienung Wohnzimmer',
name: 'CD',
serial_number: '0e88f449-2720-4000-8c4b-06775986e8ac',
}
const expectedLabels3 = {
bridge: 'Harmony bridge',
device_id: 'AA:AA:AA:AA:AA:AA',
instance: 'http://192.168.0.1:38333',
configured_name: 'AirPlay',
firmware_revision: '1.6.2',
manufacturer: 'Logitech',
model: 'Fernbedienung Wohnzimmer',
name: 'AirPlay',
serial_number: '0e88f449-2720-4000-8c4b-06775986e8ac',
}
const expectedLabels4 = {
bridge: 'Harmony bridge',
device_id: 'AA:AA:AA:AA:AA:AA',
instance: 'http://192.168.0.1:38333',
firmware_revision: '1.6.2',
manufacturer: 'Logitech',
model: 'Fernbedienung Wohnzimmer',
name: 'Fernbedienung Wohnzimmer',
serial_number: '0e88f449-2720-4000-8c4b-06775986e8ac',
}
expect(aggregate([harmony])).toEqual([
new Metric('television_sleep_discovery_mode', 1, 'uint8', expectedLabels1),
new Metric('television_active', 0, 'uint8', expectedLabels1),
new Metric('television_active_identifier', 0, 'uint32', expectedLabels1),
new Metric('input_source_type', 10, 'uint8', expectedLabels2),
new Metric('input_source_is_configured', 1, 'uint8', expectedLabels2),
new Metric('input_source_current_visibility_state', 0, 'uint8', expectedLabels2),
new Metric('input_source_target_visibility_state', 0, 'uint8', expectedLabels2),
new Metric('input_source_type', 10, 'uint8', expectedLabels3),
new Metric('input_source_is_configured', 1, 'uint8', expectedLabels3),
new Metric('input_source_current_visibility_state', 0, 'uint8', expectedLabels3),
new Metric('input_source_target_visibility_state', 0, 'uint8', expectedLabels3),
new Metric('speaker_active', 1, 'uint8', expectedLabels4),
new Metric('speaker_volume_control_type', 3, 'uint8', expectedLabels4),
new Metric('speaker_volume_percentage', 50, 'uint8', expectedLabels4),
])
})
})

300
tests/fixtures/dyson.json vendored Normal file
View file

@ -0,0 +1,300 @@
{
"ipAddress": "192.168.0.1",
"instance": {
"host": "192.168.0.1",
"port": 38333,
"url": "http://192.168.0.1:38333",
"deviceID": "AA:AA:AA:AA:AA:AA",
"txt": {
"c#": "5",
"ff": "0",
"id": "AA:AA:AA:AA:AA:AA",
"md": "homebridge",
"pv": "1.1",
"s#": "1",
"sf": "0",
"ci": "2",
"sh": "VOLG3g=="
},
"name": "Dyson bridge"
},
"accessories": {
"accessories": [
{
"aid": 1,
"services": [
{
"type": "0000003E-0000-1000-8000-0026BB765291",
"iid": 1,
"characteristics": [
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pw"],
"description": "Identify",
"format": "bool"
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"value": "homebridge.io",
"perms": ["pr"],
"description": "Manufacturer",
"format": "string",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"value": "homebridge",
"perms": ["pr"],
"description": "Model",
"format": "string",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 5,
"value": "Dyson Ventilator Bridge 2437",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"value": "AA:AA:AA:AA:AA:AA",
"perms": ["pr"],
"description": "Serial Number",
"format": "string",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"value": "1.5.1",
"perms": ["pr"],
"description": "Firmware Revision",
"format": "string"
}
]
},
{
"type": "000000A2-0000-1000-8000-0026BB765291",
"iid": 2000000008,
"characteristics": [
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 9,
"value": "1.1.0",
"perms": ["pr"],
"description": "Version",
"format": "string",
"maxLen": 64
}
]
}
]
},
{
"aid": 2,
"services": [
{
"type": "0000003E-0000-1000-8000-0026BB765291",
"iid": 1,
"characteristics": [
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pw"],
"description": "Identify",
"format": "bool"
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"value": "Dyson",
"perms": ["pr"],
"description": "Manufacturer",
"format": "string",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"value": "Dyson Pure Cool Link Tower",
"perms": ["pr"],
"description": "Model",
"format": "string",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 5,
"value": "Office",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"value": "NN2-EU-KKA0717A",
"perms": ["pr"],
"description": "Serial Number",
"format": "string",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"value": "21.04.03",
"perms": ["pr"],
"description": "Firmware Revision",
"format": "string"
},
{
"type": "00000053-0000-1000-8000-0026BB765291",
"iid": 8,
"value": "TP02",
"perms": ["pr"],
"description": "Hardware Revision",
"format": "string"
}
]
},
{
"type": "000000BB-0000-1000-8000-0026BB765291",
"iid": 9,
"characteristics": [
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 10,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Active",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000A9-0000-1000-8000-0026BB765291",
"iid": 11,
"value": 0,
"perms": ["ev", "pr"],
"description": "Current Air Purifier State",
"format": "uint8",
"minValue": 0,
"maxValue": 2,
"minStep": 1,
"valid-values": [0, 1, 2]
},
{
"type": "000000A8-0000-1000-8000-0026BB765291",
"iid": 12,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Target Air Purifier State",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000AB-0000-1000-8000-0026BB765291",
"iid": 13,
"value": 85,
"perms": ["ev", "pr"],
"description": "Filter Life Level",
"format": "float",
"unit": "percentage",
"minValue": 0,
"maxValue": 100,
"minStep": 1
},
{
"type": "00000029-0000-1000-8000-0026BB765291",
"iid": 14,
"value": 100,
"perms": ["ev", "pr", "pw"],
"description": "Rotation Speed",
"format": "float",
"unit": "percentage",
"minValue": 0,
"maxValue": 100,
"minStep": 10
},
{
"type": "00000011-0000-1000-8000-0026BB765291",
"iid": 15,
"value": 22.8,
"perms": ["ev", "pr"],
"description": "Current Temperature",
"format": "float",
"unit": "celsius",
"minValue": -50,
"maxValue": 100,
"minStep": 0.1
},
{
"type": "000000B6-0000-1000-8000-0026BB765291",
"iid": 16,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Swing Mode",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000AC-0000-1000-8000-0026BB765291",
"iid": 17,
"value": 0,
"perms": ["ev", "pr"],
"description": "Filter Change Indication",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "00000010-0000-1000-8000-0026BB765291",
"iid": 18,
"value": 52,
"perms": ["ev", "pr"],
"description": "Current Relative Humidity",
"format": "float",
"unit": "percentage",
"minValue": 0,
"maxValue": 100,
"minStep": 1
},
{
"type": "00000095-0000-1000-8000-0026BB765291",
"iid": 19,
"value": 2,
"perms": ["ev", "pr"],
"description": "Air Quality",
"format": "uint8",
"minValue": 0,
"maxValue": 5,
"minStep": 1,
"valid-values": [0, 1, 2, 3, 4, 5]
}
]
}
]
}
]
},
"deviceID": "AA:AA:AA:AA:AA:AA",
"name": "homebridge"
}

104
tests/fixtures/empty.json vendored Normal file
View file

@ -0,0 +1,104 @@
{
"ipAddress": "192.168.0.1",
"instance": {
"host": "192.168.0.1",
"port": 51826,
"url": "http://192.168.0.1:51826",
"deviceID": "AA:AA:AA:AA:AA:AA",
"txt": {
"c#": "5",
"ff": "0",
"id": "AA:AA:AA:AA:AA:AA",
"md": "homebridge",
"pv": "1.1",
"s#": "1",
"sf": "0",
"ci": "2",
"sh": "Hv0v9A=="
},
"name": "Empty bridge"
},
"accessories": {
"accessories": [
{
"aid": 1,
"services": [
{
"type": "0000003E-0000-1000-8000-0026BB765291",
"iid": 1,
"characteristics": [
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pw"],
"description": "Identify",
"format": "bool"
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"value": "homebridge.io",
"perms": ["pr"],
"description": "Manufacturer",
"format": "string",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"value": "homebridge",
"perms": ["pr"],
"description": "Model",
"format": "string",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 5,
"value": "Homebridge D2FB",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"value": "AA:AA:AA:AA:AA:AA",
"perms": ["pr"],
"description": "Serial Number",
"format": "string",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"value": "1.5.1",
"perms": ["pr"],
"description": "Firmware Revision",
"format": "string"
}
]
},
{
"type": "000000A2-0000-1000-8000-0026BB765291",
"iid": 2000000008,
"characteristics": [
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 9,
"value": "1.1.0",
"perms": ["pr"],
"description": "Version",
"format": "string",
"maxLen": 64
}
]
}
]
}
]
},
"deviceID": "AA:AA:AA:AA:AA:AA",
"name": "homebridge"
}

410
tests/fixtures/harmony.json vendored Normal file
View file

@ -0,0 +1,410 @@
{
"ipAddress": "192.168.0.1",
"instance": {
"host": "192.168.0.1",
"port": 38333,
"url": "http://192.168.0.1:38333",
"deviceID": "AA:AA:AA:AA:AA:AA",
"txt": {
"c#": "2",
"ff": "0",
"id": "AA:AA:AA:AA:AA:AA",
"md": "Fernbedienung Wohnzimmer",
"pv": "1.1",
"s#": "1",
"sf": "0",
"ci": "31",
"sh": "Aa6YjQ=="
},
"name": "Harmony bridge"
},
"accessories": {
"accessories": [
{
"aid": 1,
"services": [
{
"type": "0000003E-0000-1000-8000-0026BB765291",
"iid": 1,
"characteristics": [
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pw"],
"description": "Identify",
"format": "bool"
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"value": "Logitech",
"perms": ["pr"],
"description": "Manufacturer",
"format": "string",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"value": "Fernbedienung Wohnzimmer",
"perms": ["pr"],
"description": "Model",
"format": "string",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 5,
"value": "Fernbedienung Wohnzimmer-TV 970C",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"value": "0e88f449-2720-4000-8c4b-06775986e8ac",
"perms": ["pr"],
"description": "Serial Number",
"format": "string",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"value": "1.6.2",
"perms": ["pr"],
"description": "Firmware Revision",
"format": "string"
}
]
},
{
"type": "000000D8-0000-1000-8000-0026BB765291",
"iid": 8,
"characteristics": [
{
"type": "000000E1-0000-1000-8000-0026BB765291",
"iid": 13,
"perms": ["pw"],
"description": "Remote Key",
"format": "uint8",
"minValue": 0,
"maxValue": 16,
"minStep": 1,
"valid-values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
},
{
"type": "000000DF-0000-1000-8000-0026BB765291",
"iid": 15,
"perms": ["pw"],
"description": "Power Mode Selection",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 9,
"value": "Fernbedienung Wohnzimmer",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "000000E3-0000-1000-8000-0026BB765291",
"iid": 12,
"value": "Fernbedienung",
"perms": ["ev", "pr", "pw"],
"description": "Configured Name",
"format": "string"
},
{
"type": "000000E8-0000-1000-8000-0026BB765291",
"iid": 14,
"value": 1,
"perms": ["ev", "pr"],
"description": "Sleep Discovery Mode",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 10,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Active",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000E7-0000-1000-8000-0026BB765291",
"iid": 11,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Active Identifier",
"format": "uint32"
}
],
"linked": [16, 24, 32]
},
{
"type": "000000D9-0000-1000-8000-0026BB765291",
"iid": 16,
"characteristics": [
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 17,
"value": "CD",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "000000E3-0000-1000-8000-0026BB765291",
"iid": 18,
"value": "CD",
"perms": ["ev", "pr", "pw"],
"description": "Configured Name",
"format": "string"
},
{
"type": "000000DB-0000-1000-8000-0026BB765291",
"iid": 19,
"value": 10,
"perms": ["ev", "pr"],
"description": "Input Source Type",
"format": "uint8",
"minValue": 0,
"maxValue": 10,
"minStep": 1,
"valid-values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
{
"type": "000000D6-0000-1000-8000-0026BB765291",
"iid": 20,
"value": 1,
"perms": ["ev", "pr", "pw"],
"description": "Is Configured",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000E6-0000-1000-8000-0026BB765291",
"iid": 22,
"value": 1,
"perms": ["pr"],
"description": "Identifier",
"format": "uint32"
},
{
"type": "00000135-0000-1000-8000-0026BB765291",
"iid": 21,
"value": 0,
"perms": ["ev", "pr"],
"description": "Current Visibility State",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "00000134-0000-1000-8000-0026BB765291",
"iid": 23,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Target Visibility State",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
}
]
},
{
"type": "000000D9-0000-1000-8000-0026BB765291",
"iid": 24,
"characteristics": [
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 25,
"value": "AirPlay",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "000000E3-0000-1000-8000-0026BB765291",
"iid": 26,
"value": "AirPlay",
"perms": ["ev", "pr", "pw"],
"description": "Configured Name",
"format": "string"
},
{
"type": "000000DB-0000-1000-8000-0026BB765291",
"iid": 27,
"value": 10,
"perms": ["ev", "pr"],
"description": "Input Source Type",
"format": "uint8",
"minValue": 0,
"maxValue": 10,
"minStep": 1,
"valid-values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
{
"type": "000000D6-0000-1000-8000-0026BB765291",
"iid": 28,
"value": 1,
"perms": ["ev", "pr", "pw"],
"description": "Is Configured",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000E6-0000-1000-8000-0026BB765291",
"iid": 30,
"value": 2,
"perms": ["pr"],
"description": "Identifier",
"format": "uint32"
},
{
"type": "00000135-0000-1000-8000-0026BB765291",
"iid": 29,
"value": 0,
"perms": ["ev", "pr"],
"description": "Current Visibility State",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "00000134-0000-1000-8000-0026BB765291",
"iid": 31,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Target Visibility State",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
}
]
},
{
"type": "00000113-0000-1000-8000-0026BB765291",
"iid": 32,
"characteristics": [
{
"type": "000000EA-0000-1000-8000-0026BB765291",
"iid": 37,
"perms": ["pw"],
"description": "Volume Selector",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 33,
"value": "Fernbedienung Wohnzimmer",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 35,
"value": 1,
"perms": ["ev", "pr", "pw"],
"description": "Active",
"format": "uint8",
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [0, 1]
},
{
"type": "000000E9-0000-1000-8000-0026BB765291",
"iid": 36,
"value": 3,
"perms": ["ev", "pr"],
"description": "Volume Control Type",
"format": "uint8",
"minValue": 0,
"maxValue": 3,
"minStep": 1,
"valid-values": [0, 1, 2, 3]
},
{
"type": "0000011A-0000-1000-8000-0026BB765291",
"iid": 34,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "Mute",
"format": "bool"
},
{
"type": "00000119-0000-1000-8000-0026BB765291",
"iid": 38,
"value": 50,
"perms": ["ev", "pr", "pw"],
"description": "Volume",
"format": "uint8",
"unit": "percentage",
"minValue": 0,
"maxValue": 100,
"minStep": 1
}
]
},
{
"type": "000000A2-0000-1000-8000-0026BB765291",
"iid": 39,
"characteristics": [
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 40,
"value": "1.1.0",
"perms": ["pr"],
"description": "Version",
"format": "string",
"maxLen": 64
}
]
}
]
}
]
},
"deviceID": "AA:AA:AA:AA:AA:AA",
"name": "Fernbedienung Wohnzimmer"
}

426
tests/fixtures/tp-link.json vendored Normal file
View file

@ -0,0 +1,426 @@
{
"ipAddress": "192.168.0.1",
"instance": {
"host": "192.168.0.1",
"port": 38333,
"url": "http://192.168.0.1:38333",
"deviceID": "AA:AA:AA:AA:AA:AA",
"txt": {
"c#": "3",
"ff": "0",
"id": "AA:AA:AA:AA:AA:AA",
"md": "homebridge",
"pv": "1.1",
"s#": "1",
"sf": "0",
"ci": "2",
"sh": "RovnEA=="
},
"name": "TP-Link bridge"
},
"accessories": {
"accessories": [
{
"aid": 1,
"services": [
{
"type": "0000003E-0000-1000-8000-0026BB765291",
"iid": 1,
"characteristics": [
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pw"],
"description": "Identify",
"format": "bool"
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"value": "homebridge.io",
"perms": ["pr"],
"description": "Manufacturer",
"format": "string",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"value": "homebridge",
"perms": ["pr"],
"description": "Model",
"format": "string",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 5,
"value": "TP-Link Steckdosen Bridge 8EB6",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"value": "AA:AA:AA:AA:AA:AA",
"perms": ["pr"],
"description": "Serial Number",
"format": "string",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"value": "1.5.1",
"perms": ["pr"],
"description": "Firmware Revision",
"format": "string"
}
]
},
{
"type": "000000A2-0000-1000-8000-0026BB765291",
"iid": 2000000008,
"characteristics": [
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 9,
"value": "1.1.0",
"perms": ["pr"],
"description": "Version",
"format": "string",
"maxLen": 64
}
]
}
]
},
{
"aid": 2,
"services": [
{
"type": "0000003E-0000-1000-8000-0026BB765291",
"iid": 1,
"characteristics": [
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pw"],
"description": "Identify",
"format": "bool"
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"value": "TP-Link",
"perms": ["pr"],
"description": "Manufacturer",
"format": "string",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"value": "HS110(EU)",
"perms": ["pr"],
"description": "Model",
"format": "string",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 5,
"value": "Drucker",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"value": "AA:AA:AA:AA:AA:AA 8006106A8B451F91FA0498A6615963E019CCD80A",
"perms": ["pr"],
"description": "Serial Number",
"format": "string",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"value": "1.5.6 Build 191125 Rel.083657",
"perms": ["pr"],
"description": "Firmware Revision",
"format": "string"
},
{
"type": "00000053-0000-1000-8000-0026BB765291",
"iid": 8,
"value": "2.0",
"perms": ["pr"],
"description": "Hardware Revision",
"format": "string"
}
]
},
{
"type": "00000047-0000-1000-8000-0026BB765291",
"iid": 9,
"characteristics": [
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 10,
"value": "Drucker",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000025-0000-1000-8000-0026BB765291",
"iid": 11,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "On",
"format": "bool"
},
{
"type": "00000026-0000-1000-8000-0026BB765291",
"iid": 12,
"value": 0,
"perms": ["ev", "pr"],
"description": "Outlet In Use",
"format": "bool"
},
{
"type": "E863F126-079E-48FF-8F27-9C2605A29F52",
"iid": 13,
"value": 0.03,
"perms": ["pr", "ev"],
"description": "Amperes",
"format": "float",
"unit": "A",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.01
},
{
"type": "E863F10C-079E-48FF-8F27-9C2605A29F52",
"iid": 14,
"value": 0.051,
"perms": ["pr", "ev"],
"description": "Total Consumption",
"format": "float",
"unit": "kWh",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.001
},
{
"type": "E863F110-079E-48FF-8F27-9C2605A29F52",
"iid": 15,
"value": 53248.8,
"perms": ["pr", "ev"],
"description": "Apparent Power",
"format": "float",
"unit": "VA",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.1
},
{
"type": "E863F10A-079E-48FF-8F27-9C2605A29F52",
"iid": 16,
"value": 230.8,
"perms": ["pr", "ev"],
"description": "Volts",
"format": "float",
"unit": "V",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.1
},
{
"type": "E863F10D-079E-48FF-8F27-9C2605A29F52",
"iid": 17,
"value": 0,
"perms": ["pr", "ev"],
"description": "Consumption",
"format": "float",
"unit": "W",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.1
}
]
}
]
},
{
"aid": 3,
"services": [
{
"type": "0000003E-0000-1000-8000-0026BB765291",
"iid": 1,
"characteristics": [
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pw"],
"description": "Identify",
"format": "bool"
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"value": "TP-Link",
"perms": ["pr"],
"description": "Manufacturer",
"format": "string",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"value": "HS110(EU)",
"perms": ["pr"],
"description": "Model",
"format": "string",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 5,
"value": "Espressomaschine",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"value": "AA:AA:AA:AA:AA:AA 800614AA26608873C919E4592765404019F64D07",
"perms": ["pr"],
"description": "Serial Number",
"format": "string",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"value": "1.5.6 Build 191125 Rel.083657",
"perms": ["pr"],
"description": "Firmware Revision",
"format": "string"
},
{
"type": "00000053-0000-1000-8000-0026BB765291",
"iid": 8,
"value": "2.0",
"perms": ["pr"],
"description": "Hardware Revision",
"format": "string"
}
]
},
{
"type": "00000047-0000-1000-8000-0026BB765291",
"iid": 9,
"characteristics": [
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 10,
"value": "Espressomaschine",
"perms": ["pr"],
"description": "Name",
"format": "string",
"maxLen": 64
},
{
"type": "00000025-0000-1000-8000-0026BB765291",
"iid": 11,
"value": 0,
"perms": ["ev", "pr", "pw"],
"description": "On",
"format": "bool"
},
{
"type": "00000026-0000-1000-8000-0026BB765291",
"iid": 12,
"value": 0,
"perms": ["ev", "pr"],
"description": "Outlet In Use",
"format": "bool"
},
{
"type": "E863F126-079E-48FF-8F27-9C2605A29F52",
"iid": 13,
"value": 0.03,
"perms": ["pr", "ev"],
"description": "Amperes",
"format": "float",
"unit": "A",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.01
},
{
"type": "E863F10C-079E-48FF-8F27-9C2605A29F52",
"iid": 14,
"value": 13.025,
"perms": ["pr", "ev"],
"description": "Total Consumption",
"format": "float",
"unit": "kWh",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.001
},
{
"type": "E863F110-079E-48FF-8F27-9C2605A29F52",
"iid": 15,
"value": 53365.6,
"perms": ["pr", "ev"],
"description": "Apparent Power",
"format": "float",
"unit": "VA",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.1
},
{
"type": "E863F10A-079E-48FF-8F27-9C2605A29F52",
"iid": 16,
"value": 231,
"perms": ["pr", "ev"],
"description": "Volts",
"format": "float",
"unit": "V",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.1
},
{
"type": "E863F10D-079E-48FF-8F27-9C2605A29F52",
"iid": 17,
"value": 0,
"perms": ["pr", "ev"],
"description": "Consumption",
"format": "float",
"unit": "W",
"minValue": 0,
"maxValue": 65535,
"minStep": 0.1
}
]
}
]
}
]
},
"deviceID": "AA:AA:AA:AA:AA:AA",
"name": "homebridge"
}

18
tests/formatter.test.ts Normal file
View file

@ -0,0 +1,18 @@
import { describe, expect, test } from '@jest/globals'
import { formatName } from '../src/metrics'
describe('Metrics formatter', () => {
test('Converts to snake case', () => {
expect(formatName('Service', 'Metric')).toEqual('service_metric')
})
test('Handles camel case', () => {
expect(formatName('YetAnotherService', 'Some Metric')).toEqual('yet_another_service_some_metric')
})
test('Strips duplicate prefixes', () => {
expect(formatName('Some Service', 'Some Service Metric')).toEqual('some_service_metric')
expect(formatName('Some Service', 'SomeServiceMetric')).toEqual('some_service_metric')
expect(formatName('SomeService', 'SomeServiceMetric')).toEqual('some_service_metric')
expect(formatName('SomeService', 'Some Service Metric')).toEqual('some_service_metric')
})
})

View file

@ -1,26 +1,18 @@
{
"compilerOptions": {
"target": "ES2018", // ~node10
"module": "commonjs",
"lib": [
"es2015",
"es2016",
"es2017",
"es2018"
],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": false
},
"include": [
"src/"
],
"exclude": [
"**/*.spec.ts"
]
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"lib": ["es2015", "es2016", "es2017", "es2018"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"resolveJsonModule": true
},
"include": ["src/"],
"exclude": ["**/*.test.ts", "node_modules/hap-nodejs/**/**"]
}