From b74721ee7673c084e2b54bffdd7d5370e62b81cd Mon Sep 17 00:00:00 2001 From: Lars Strojny Date: Sun, 13 Nov 2022 11:13:13 +0100 Subject: [PATCH] Handle null values in characteristics (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently `characteristic.value` can also be null. Let’s handle it properly. Fixes #19 --- src/boundaries/hap.ts | 14 ++++-- src/metrics.ts | 3 +- tests/aggregator.test.ts | 7 +++ .../fixtures/issues/gh-19-nullable-value.json | 48 +++++++++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/issues/gh-19-nullable-value.json diff --git a/src/boundaries/hap.ts b/src/boundaries/hap.ts index 0265def..75b5ad9 100644 --- a/src/boundaries/hap.ts +++ b/src/boundaries/hap.ts @@ -1,4 +1,4 @@ -import z from 'zod' +import z, { type ZodNull, type ZodOptional, type ZodType, type ZodUnion } from 'zod' const NumericTypesBoundary = z.union([ z.literal('bool'), @@ -11,17 +11,21 @@ const NumericTypesBoundary = z.union([ ]) export type NumericTypes = z.infer +function optionalNullable(type: T): ZodOptional> { + return z.optional(z.union([z.null(), type])) +} + export const CharacteristicBoundary = z.intersection( z.object({ type: z.string(), description: z.string() }), z.union([ z.object({ format: NumericTypesBoundary, - value: z.optional(z.number()), unit: z.optional(z.string()), + value: optionalNullable(z.number()), }), - z.object({ format: z.literal('string'), value: z.string() }), - z.object({ format: z.literal('data'), value: z.optional(z.string()) }), - z.object({ format: z.literal('tlv8'), value: z.string(z.string()) }), + z.object({ format: z.literal('string'), value: optionalNullable(z.string()) }), + z.object({ format: z.literal('data'), value: optionalNullable(z.string()) }), + z.object({ format: z.literal('tlv8'), value: optionalNullable(z.string()) }), ]), ) export type Characteristic = z.infer diff --git a/src/metrics.ts b/src/metrics.ts index b62f104..8c17ee7 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -42,7 +42,7 @@ export function aggregate(devices: Device[], timestamp: Date): Metric[] { case 'uint16': case 'uint32': case 'uint64': - if (typeof characteristic.value !== 'undefined') { + if (characteristic.value != null) { if (METRICS_FILTER.includes(characteristic.description)) { break } @@ -108,6 +108,7 @@ function getServiceLabels(service: Service): Record { for (const characteristic of service.characteristics) { if ( + characteristic.value != null && characteristic.format === 'string' && [ 'Name', diff --git a/tests/aggregator.test.ts b/tests/aggregator.test.ts index e588bb9..d1f2337 100644 --- a/tests/aggregator.test.ts +++ b/tests/aggregator.test.ts @@ -6,6 +6,7 @@ import emptyData from './fixtures/empty.json' import tpLinkData from './fixtures/tp-link.json' import harmonyData from './fixtures/harmony.json' import unknownUuidData from './fixtures/issues/gh-9-unknown-uuid.json' +import nullableValueData from './fixtures/issues/gh-19-nullable-value.json' describe('Metrics aggregator', () => { const timestamp = new Date('2000-01-01 00:00:00 UTC') @@ -171,4 +172,10 @@ describe('Metrics aggregator', () => { new Metric('custom_restart', 0, timestamp, expectedLabels), ]) }) + + test('Aggregates metrics with nullable values', () => { + const unknowmnValue = DeviceBoundary.parse(nullableValueData) + + expect(aggregate([unknowmnValue], timestamp)).toEqual([]) + }) }) diff --git a/tests/fixtures/issues/gh-19-nullable-value.json b/tests/fixtures/issues/gh-19-nullable-value.json new file mode 100644 index 0000000..42d71e6 --- /dev/null +++ b/tests/fixtures/issues/gh-19-nullable-value.json @@ -0,0 +1,48 @@ +{ + "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": "Test bridge" + }, + "accessories": { + "accessories": [ + { + "aid": 3, + "services": [ + { + "type": "00000012-0000-1000-8000-656261617577", + "iid": 8, + "characteristics": [ + { + "type": "00000073-0000-1000-8000-0026BB765291", + "iid": 11, + "value": null, + "perms": ["ev", "pr"], + "description": "Programmable Switch Event", + "format": "uint8", + "minValue": 0, + "maxValue": 2, + "minStep": 1, + "valid-values": [0] + } + ] + } + ] + } + ] + } +}