homebridge-prometheus-exporter/src/metrics.ts

140 lines
4.6 KiB
TypeScript
Raw Normal View History

import type { Accessory, Device, Service } from './boundaries/hap'
2022-11-08 01:19:15 +01:00
import { assertTypeExhausted, isType } from './std'
2022-11-08 23:58:46 +01:00
// eslint-disable-next-line import/no-extraneous-dependencies
2022-11-07 23:43:44 +01:00
import { Service as HapService } from 'hap-nodejs'
2022-11-06 13:50:39 +01:00
export class Metric {
constructor(
public readonly name: string,
public readonly value: number,
2022-11-07 20:49:11 +01:00
public readonly timestamp: Date | null = null,
public readonly labels: Record<string, string> = {},
2022-11-06 13:50:39 +01:00
) {}
}
/**
* Characteristics that would be nonsensical to report as metrics
*/
const METRICS_FILTER = ['Identifier']
2022-11-07 20:49:11 +01:00
export function aggregate(devices: Device[], timestamp: Date): Metric[] {
2022-11-06 13:50:39 +01:00
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) {
2022-11-08 01:19:15 +01:00
const format = characteristic.format
switch (format) {
case 'string':
break
case 'bool':
case 'float':
case 'int':
case 'uint8':
case 'uint16':
case 'uint32':
case 'uint64':
if (typeof characteristic.value !== 'undefined') {
if (METRICS_FILTER.includes(characteristic.description)) {
break
}
const name = formatName(
uuidToServerName(service.type),
characteristic.description,
characteristic.unit,
)
2022-11-07 20:49:11 +01:00
metrics.push(new Metric(name, characteristic.value, timestamp, labels))
2022-11-06 13:50:39 +01:00
}
2022-11-08 01:19:15 +01:00
break
default:
assertTypeExhausted(format)
2022-11-06 13:50:39 +01:00
}
}
}
}
}
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,
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
}
2022-11-07 23:43:44 +01:00
function uuidToServerName(uuid: string): string {
for (const name of Object.getOwnPropertyNames(HapService)) {
const maybeService = (HapService as unknown as Record<string, unknown>)[name]
if (typeof maybeService === 'function' && 'UUID' in maybeService) {
2022-11-08 01:19:15 +01:00
if ((maybeService as Record<string, string>)['UUID'] === uuid) {
2022-11-07 23:43:44 +01:00
return name
}
}
}
throw new Error(`Could not resolve UUID ${uuid} to service`)
2022-11-08 01:19:15 +01:00
}