Skip to content

Commit a40a72b

Browse files
authored
Generate HAP type mappings from homebridge-lib custom Services/Characteristics (#55)
* Initial plan * Add homebridge-lib custom HAP type generation * Bump package to 5.0.0 and update changelog --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 26f58fa commit a40a72b

6 files changed

Lines changed: 536 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to `@homebridge/hap-client` will be documented in this file. This project tries to adhere to [Semantic Versioning](http://semver.org/).
44

5+
## v5.0.0 (2026-05-27)
6+
7+
### Changed
8+
9+
- feat: include `homebridge-lib` custom Services and Characteristics in generated HAP type mappings
10+
11+
### Homebridge Dependencies
12+
13+
- `@homebridge/hap-nodejs` @ `v2.1.7`
14+
515
## v4.0.6 (2026-05-26)
616

717
### Changed

package-lock.json

Lines changed: 65 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@homebridge/hap-client",
33
"type": "module",
4-
"version": "4.0.6",
4+
"version": "5.0.0",
55
"description": "A client for HAP-NodeJS.",
66
"author": {
77
"name": "Homebridge"
@@ -61,6 +61,7 @@
6161
"@types/node": "^25.9.1",
6262
"@vitest/coverage-v8": "^4.1.7",
6363
"eslint-plugin-format": "^2.0.1",
64+
"homebridge-lib": "^8.1.0",
6465
"rimraf": "^6.1.3",
6566
"ts-node": "^10.9.2",
6667
"typescript": "^6.0.3",

scripts/gen-hap-types.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,55 @@ import { writeFileSync } from 'node:fs'
22
import { dirname, resolve } from 'node:path'
33
import { fileURLToPath } from 'node:url'
44

5-
import { Categories, Characteristic, Service } from '@homebridge/hap-nodejs'
5+
import { Access, Categories, Characteristic, Formats, Perms, Service, Units } from '@homebridge/hap-nodejs'
66

77
const __dirname = dirname(fileURLToPath(import.meta.url))
88

9+
// @ts-expect-error - TS7016
10+
const { EveHomeKitTypes } = await import('homebridge-lib/EveHomeKitTypes')
11+
// @ts-expect-error - TS7016
12+
const { MyHomeKitTypes } = await import('homebridge-lib/MyHomeKitTypes')
13+
14+
const homebridge = {
15+
hap: {
16+
Access,
17+
Categories,
18+
Characteristic,
19+
Formats,
20+
Perms,
21+
Service,
22+
Units,
23+
},
24+
}
25+
26+
const customTypes = [
27+
new EveHomeKitTypes(homebridge),
28+
new MyHomeKitTypes(homebridge),
29+
]
30+
931
/** Generate Service Types */
1032

1133
let Services = [
1234
'export const Services = {',
1335
] as any
1436

15-
const uuidMap = new Map()
37+
const serviceUUIDs = new Set()
38+
const serviceNames = new Set()
39+
const serviceEntries = [
40+
...Object.entries(Service),
41+
...customTypes.flatMap(customType => Object.entries(customType.Services)),
42+
]
1643

17-
for (const [name, value] of Object.entries(Service)) {
44+
for (const [name, value] of serviceEntries) {
1845
if (value.UUID) {
19-
if (!uuidMap.has(value.UUID)) {
20-
// If the UUID does not exist, add a new entry
46+
if (!serviceUUIDs.has(value.UUID)) {
2147
Services.push(` '${value.UUID}': '${name}',`)
22-
uuidMap.set(value.UUID, Services.length - 1)
48+
serviceUUIDs.add(value.UUID)
49+
}
50+
if (!serviceNames.has(name)) {
51+
Services.push(` '${name}': '${value.UUID}',`)
52+
serviceNames.add(name)
2353
}
24-
Services.push(` '${name}': '${value.UUID}',`)
2554
}
2655
}
2756

@@ -34,10 +63,24 @@ let Characteristics = [
3463
'export const Characteristics = {',
3564
] as any
3665

37-
for (const [name, value] of Object.entries(Characteristic)) {
66+
const characteristicEntries = [
67+
...Object.entries(Characteristic),
68+
...customTypes.flatMap(customType => Object.entries(customType.Characteristics)),
69+
]
70+
71+
const characteristicUUIDs = new Set()
72+
const characteristicNames = new Set()
73+
74+
for (const [name, value] of characteristicEntries) {
3875
if (value.UUID) {
39-
Characteristics.push(` '${value.UUID}': '${name}',`)
40-
Characteristics.push(` '${name}': '${value.UUID}',`)
76+
if (!characteristicUUIDs.has(value.UUID)) {
77+
Characteristics.push(` '${value.UUID}': '${name}',`)
78+
characteristicUUIDs.add(value.UUID)
79+
}
80+
if (!characteristicNames.has(name)) {
81+
Characteristics.push(` '${name}': '${value.UUID}',`)
82+
characteristicNames.add(name)
83+
}
4184
}
4285
}
4386

src/hap-types.test.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,35 @@ import { describe, expect, it } from 'vitest'
22

33
import { Categories, Characteristics, Enums, Services } from './hap-types.js'
44

5+
const UUID_REGEX = /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/
6+
57
describe('services', () => {
68
it('should be exported', () => {
79
expect(Services).toBeDefined()
810
})
911

10-
it('should have symmetric mappings for all keys except TelevisionSpeaker', () => {
11-
Object.keys(Services)
12-
.filter(key => key !== 'TelevisionSpeaker')
13-
.forEach((key) => {
14-
const value = Services[key]
15-
expect(Services[value]).toBe(key)
12+
it('should have symmetric mappings for UUID keys', () => {
13+
Object.entries(Services)
14+
.filter(([key]) => UUID_REGEX.test(key))
15+
.forEach(([, value]) => {
16+
expect(typeof value).toBe('string')
17+
})
18+
Object.entries(Services)
19+
.filter(([key]) => !UUID_REGEX.test(key))
20+
.forEach(([, value]) => {
21+
expect(UUID_REGEX.test(value)).toBe(true)
22+
expect(Services[value]).toBeDefined()
1623
})
1724
})
1825

1926
it('00000113-0000-1000-8000-0026BB765291 should be Speaker', () => {
2027
expect(Services['00000113-0000-1000-8000-0026BB765291']).toBe('Speaker')
2128
})
2229

30+
it('should include custom homebridge-lib services', () => {
31+
expect(Services.History).toBe('E863F007-079E-48FF-8F27-9C2605A29F52')
32+
})
33+
2334
it('length should be 145 or greater', () => {
2435
expect(Object.keys(Services).length).toBeGreaterThanOrEqual(145)
2536
})
@@ -30,11 +41,22 @@ describe('characteristics', () => {
3041
expect(Characteristics).toBeDefined()
3142
})
3243

33-
it('should have symmetric mappings for all keys', () => {
34-
Object.keys(Characteristics).forEach((key) => {
35-
const value = Characteristics[key]
36-
expect(Characteristics[value]).toBe(key)
37-
})
44+
it('should have symmetric mappings for UUID keys', () => {
45+
Object.entries(Characteristics)
46+
.filter(([key]) => UUID_REGEX.test(key))
47+
.forEach(([, value]) => {
48+
expect(typeof value).toBe('string')
49+
})
50+
Object.entries(Characteristics)
51+
.filter(([key]) => !UUID_REGEX.test(key))
52+
.forEach(([, value]) => {
53+
expect(UUID_REGEX.test(value)).toBe(true)
54+
expect(Characteristics[value]).toBeDefined()
55+
})
56+
})
57+
58+
it('should include custom homebridge-lib characteristics', () => {
59+
expect(Characteristics.VOCLevel).toBe('E863F10B-079E-48FF-8F27-9C2605A29F52')
3860
})
3961

4062
it('length should be 488 or greater', () => {

0 commit comments

Comments
 (0)