2022-11-08 19:13:34 +01:00
|
|
|
import type { Accessory, Device, Service } from './boundaries/hap'
|
2022-11-10 11:07:31 +01:00
|
|
|
import { Uuids } from './generated/services'
|
2022-11-08 01:19:15 +01:00
|
|
|
import { assertTypeExhausted, isType } from './std'
|
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':
|
2022-11-09 14:10:50 +01:00
|
|
|
case 'tlv8':
|
|
|
|
case 'data':
|
2022-11-08 01:19:15 +01:00
|
|
|
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(
|
2022-11-10 11:07:31 +01:00
|
|
|
Uuids[service.type] || 'custom',
|
2022-11-08 01:19:15 +01:00
|
|
|
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
|
|
|
|
}
|