@kusinta/iot-schema 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -0
- package/src/__tests__/access.test.js +134 -0
- package/src/__tests__/common.test.js +29 -0
- package/src/__tests__/connector.test.js +144 -0
- package/src/__tests__/device.test.js +194 -0
- package/src/__tests__/identity.test.js +43 -0
- package/src/__tests__/space.test.js +54 -0
- package/src/__tests__/vendor.test.js +60 -0
- package/src/__tests__/webrtc.test.js +268 -0
- package/src/kusinta/iot/access/v1/acl_pb.d.ts +144 -0
- package/src/kusinta/iot/access/v1/acl_pb.js +36 -0
- package/src/kusinta/iot/access/v1/roles_pb.d.ts +81 -0
- package/src/kusinta/iot/access/v1/roles_pb.js +36 -0
- package/src/kusinta/iot/common/v1/types_pb.d.ts +163 -0
- package/src/kusinta/iot/common/v1/types_pb.js +67 -0
- package/src/kusinta/iot/connector/v1/connector_pb.d.ts +359 -0
- package/src/kusinta/iot/connector/v1/connector_pb.js +101 -0
- package/src/kusinta/iot/connector/v1/connector_service_pb.d.ts +29 -0
- package/src/kusinta/iot/connector/v1/connector_service_pb.js +22 -0
- package/src/kusinta/iot/device/v1/descriptor_pb.d.ts +115 -0
- package/src/kusinta/iot/device/v1/descriptor_pb.js +22 -0
- package/src/kusinta/iot/device/v1/device_pb.d.ts +131 -0
- package/src/kusinta/iot/device/v1/device_pb.js +23 -0
- package/src/kusinta/iot/device/v1/properties_pb.d.ts +580 -0
- package/src/kusinta/iot/device/v1/properties_pb.js +96 -0
- package/src/kusinta/iot/device/v1/property_update_pb.d.ts +112 -0
- package/src/kusinta/iot/device/v1/property_update_pb.js +28 -0
- package/src/kusinta/iot/identity/v1/identity_pb.d.ts +108 -0
- package/src/kusinta/iot/identity/v1/identity_pb.js +54 -0
- package/src/kusinta/iot/space/v1/space_pb.d.ts +86 -0
- package/src/kusinta/iot/space/v1/space_pb.js +21 -0
- package/src/kusinta/iot/vendor/homematic/v1/homematic_pb.d.ts +94 -0
- package/src/kusinta/iot/vendor/homematic/v1/homematic_pb.js +26 -0
- package/src/kusinta/iot/webrtc/v1/command_pb.d.ts +271 -0
- package/src/kusinta/iot/webrtc/v1/command_pb.js +70 -0
- package/src/kusinta/iot/webrtc/v1/device_state_pb.d.ts +67 -0
- package/src/kusinta/iot/webrtc/v1/device_state_pb.js +30 -0
- package/src/kusinta/iot/webrtc/v1/envelope_pb.d.ts +210 -0
- package/src/kusinta/iot/webrtc/v1/envelope_pb.js +59 -0
- package/src/kusinta/iot/webrtc/v1/permission_push_pb.d.ts +48 -0
- package/src/kusinta/iot/webrtc/v1/permission_push_pb.js +21 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kusinta/iot-schema",
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": ["src"],
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"test:watch": "vitest"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./common": "./src/kusinta/iot/common/v1/types_pb.js",
|
|
12
|
+
"./identity": "./src/kusinta/iot/identity/v1/identity_pb.js",
|
|
13
|
+
"./device": "./src/kusinta/iot/device/v1/device_pb.js",
|
|
14
|
+
"./device/descriptor": "./src/kusinta/iot/device/v1/descriptor_pb.js",
|
|
15
|
+
"./device/properties": "./src/kusinta/iot/device/v1/properties_pb.js",
|
|
16
|
+
"./device/property-update": "./src/kusinta/iot/device/v1/property_update_pb.js",
|
|
17
|
+
"./space": "./src/kusinta/iot/space/v1/space_pb.js",
|
|
18
|
+
"./access": "./src/kusinta/iot/access/v1/acl_pb.js",
|
|
19
|
+
"./access/roles": "./src/kusinta/iot/access/v1/roles_pb.js",
|
|
20
|
+
"./connector": "./src/kusinta/iot/connector/v1/connector_pb.js",
|
|
21
|
+
"./connector/service": "./src/kusinta/iot/connector/v1/connector_service_pb.js",
|
|
22
|
+
"./webrtc": "./src/kusinta/iot/webrtc/v1/envelope_pb.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@bufbuild/protobuf": "^2.12.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
|
|
4
|
+
import { Role, PermissionAction } from '../kusinta/iot/access/v1/roles_pb.js'
|
|
5
|
+
import {
|
|
6
|
+
PropertyConstraintSchema,
|
|
7
|
+
DeviceAclSchema,
|
|
8
|
+
EffectivePermissionsSchema,
|
|
9
|
+
} from '../kusinta/iot/access/v1/acl_pb.js'
|
|
10
|
+
|
|
11
|
+
describe('Role and PermissionAction enums', () => {
|
|
12
|
+
it('Role.RESIDENT is defined', () => {
|
|
13
|
+
expect(Role.RESIDENT).toBeDefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('Role.PROPERTY_OWNER is defined', () => {
|
|
17
|
+
expect(Role.PROPERTY_OWNER).toBeDefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('PermissionAction.WRITE is defined', () => {
|
|
21
|
+
expect(PermissionAction.WRITE).toBeDefined()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('PropertyConstraint — property-owner setpoint limits', () => {
|
|
26
|
+
it('round-trips int_max constraint for MaxHeatSetpointLimit', () => {
|
|
27
|
+
const c = create(PropertyConstraintSchema, {
|
|
28
|
+
attributeName: 'MaxHeatSetpointLimit',
|
|
29
|
+
constraint: { case: 'intMax', value: 2200 },
|
|
30
|
+
clusterIdHex: '0201',
|
|
31
|
+
})
|
|
32
|
+
const decoded = fromBinary(PropertyConstraintSchema, toBinary(PropertyConstraintSchema, c))
|
|
33
|
+
expect(decoded.attributeName).toBe('MaxHeatSetpointLimit')
|
|
34
|
+
expect(decoded.constraint?.case).toBe('intMax')
|
|
35
|
+
if (decoded.constraint?.case === 'intMax') {
|
|
36
|
+
expect(decoded.constraint.value).toBe(2200)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('round-trips int_min constraint for MinHeatSetpointLimit', () => {
|
|
41
|
+
const c = create(PropertyConstraintSchema, {
|
|
42
|
+
attributeName: 'MinHeatSetpointLimit',
|
|
43
|
+
constraint: { case: 'intMin', value: 1600 },
|
|
44
|
+
clusterIdHex: '0201',
|
|
45
|
+
})
|
|
46
|
+
const decoded = fromBinary(PropertyConstraintSchema, toBinary(PropertyConstraintSchema, c))
|
|
47
|
+
expect(decoded.constraint?.case).toBe('intMin')
|
|
48
|
+
if (decoded.constraint?.case === 'intMin') {
|
|
49
|
+
expect(decoded.constraint.value).toBe(1600)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('round-trips uint_max constraint', () => {
|
|
54
|
+
const c = create(PropertyConstraintSchema, {
|
|
55
|
+
attributeName: 'CurrentLevel',
|
|
56
|
+
constraint: { case: 'uintMax', value: 200 },
|
|
57
|
+
clusterIdHex: '0008',
|
|
58
|
+
})
|
|
59
|
+
const decoded = fromBinary(PropertyConstraintSchema, toBinary(PropertyConstraintSchema, c))
|
|
60
|
+
expect(decoded.constraint?.case).toBe('uintMax')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('DeviceAcl', () => {
|
|
65
|
+
it('round-trips resident acl with allowed actions and constraints', () => {
|
|
66
|
+
const acl = create(DeviceAclSchema, {
|
|
67
|
+
deviceId: { value: 'therm-1' },
|
|
68
|
+
userId: { value: 'user-resident-1' },
|
|
69
|
+
role: Role.RESIDENT,
|
|
70
|
+
allowedActions: [PermissionAction.READ, PermissionAction.WRITE],
|
|
71
|
+
allowedAttributes: ['OccupiedHeatingSetpoint', 'LocalTemperature'],
|
|
72
|
+
propertyConstraints: [
|
|
73
|
+
{
|
|
74
|
+
attributeName: 'OccupiedHeatingSetpoint',
|
|
75
|
+
constraint: { case: 'intMax', value: 2200 },
|
|
76
|
+
clusterIdHex: '0201',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
attributeName: 'OccupiedHeatingSetpoint',
|
|
80
|
+
constraint: { case: 'intMin', value: 1600 },
|
|
81
|
+
clusterIdHex: '0201',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
})
|
|
85
|
+
const decoded = fromBinary(DeviceAclSchema, toBinary(DeviceAclSchema, acl))
|
|
86
|
+
expect(decoded.deviceId?.value).toBe('therm-1')
|
|
87
|
+
expect(decoded.userId?.value).toBe('user-resident-1')
|
|
88
|
+
expect(decoded.role).toBe(Role.RESIDENT)
|
|
89
|
+
expect(decoded.allowedActions).toEqual([PermissionAction.READ, PermissionAction.WRITE])
|
|
90
|
+
expect(decoded.allowedAttributes).toEqual(['OccupiedHeatingSetpoint', 'LocalTemperature'])
|
|
91
|
+
expect(decoded.propertyConstraints).toHaveLength(2)
|
|
92
|
+
expect(decoded.propertyConstraints[0].constraint?.case).toBe('intMax')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('round-trips property-owner acl', () => {
|
|
96
|
+
const acl = create(DeviceAclSchema, {
|
|
97
|
+
deviceId: { value: 'therm-1' },
|
|
98
|
+
userId: { value: 'user-owner-1' },
|
|
99
|
+
role: Role.PROPERTY_OWNER,
|
|
100
|
+
allowedActions: [PermissionAction.READ, PermissionAction.WRITE, PermissionAction.OBSERVE],
|
|
101
|
+
})
|
|
102
|
+
const decoded = fromBinary(DeviceAclSchema, toBinary(DeviceAclSchema, acl))
|
|
103
|
+
expect(decoded.role).toBe(Role.PROPERTY_OWNER)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('EffectivePermissions', () => {
|
|
108
|
+
it('round-trips with multiple device acls', () => {
|
|
109
|
+
const perms = create(EffectivePermissionsSchema, {
|
|
110
|
+
userId: { value: 'user-1' },
|
|
111
|
+
gatewayId: { value: 'gw-1' },
|
|
112
|
+
deviceAcls: [
|
|
113
|
+
{
|
|
114
|
+
deviceId: { value: 'therm-1' },
|
|
115
|
+
userId: { value: 'user-1' },
|
|
116
|
+
role: Role.RESIDENT,
|
|
117
|
+
allowedActions: [PermissionAction.READ, PermissionAction.WRITE],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
deviceId: { value: 'light-1' },
|
|
121
|
+
userId: { value: 'user-1' },
|
|
122
|
+
role: Role.RESIDENT,
|
|
123
|
+
allowedActions: [PermissionAction.READ, PermissionAction.WRITE],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
})
|
|
127
|
+
const decoded = fromBinary(EffectivePermissionsSchema, toBinary(EffectivePermissionsSchema, perms))
|
|
128
|
+
expect(decoded.userId?.value).toBe('user-1')
|
|
129
|
+
expect(decoded.gatewayId?.value).toBe('gw-1')
|
|
130
|
+
expect(decoded.deviceAcls).toHaveLength(2)
|
|
131
|
+
expect(decoded.deviceAcls[0].deviceId?.value).toBe('therm-1')
|
|
132
|
+
expect(decoded.deviceAcls[1].deviceId?.value).toBe('light-1')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
|
|
4
|
+
import { ErrorDetailSchema } from '../kusinta/iot/common/v1/types_pb.js'
|
|
5
|
+
|
|
6
|
+
describe('ErrorDetail', () => {
|
|
7
|
+
it('round-trips code and message', () => {
|
|
8
|
+
const original = create(ErrorDetailSchema, { code: 'NOT_FOUND', message: 'device not found', metadata: {} })
|
|
9
|
+
const decoded = fromBinary(ErrorDetailSchema, toBinary(ErrorDetailSchema, original))
|
|
10
|
+
expect(decoded.code).toBe('NOT_FOUND')
|
|
11
|
+
expect(decoded.message).toBe('device not found')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('round-trips metadata map entries', () => {
|
|
15
|
+
const original = create(ErrorDetailSchema, {
|
|
16
|
+
code: 'PERMISSION_DENIED',
|
|
17
|
+
message: 'write not allowed',
|
|
18
|
+
metadata: { deviceId: 'abc-123', attribute: 'OccupiedHeatingSetpoint' },
|
|
19
|
+
})
|
|
20
|
+
const decoded = fromBinary(ErrorDetailSchema, toBinary(ErrorDetailSchema, original))
|
|
21
|
+
expect(decoded.metadata['deviceId']).toBe('abc-123')
|
|
22
|
+
expect(decoded.metadata['attribute']).toBe('OccupiedHeatingSetpoint')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('produces an empty byte array for a default instance', () => {
|
|
26
|
+
const bytes = toBinary(ErrorDetailSchema, create(ErrorDetailSchema))
|
|
27
|
+
expect(bytes.length).toBe(0)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
|
|
4
|
+
import {
|
|
5
|
+
ConnectRequestSchema,
|
|
6
|
+
ConnectResponseSchema,
|
|
7
|
+
ConnectorHandshakeSchema,
|
|
8
|
+
HandshakeAckSchema,
|
|
9
|
+
DeviceAnnouncementSchema,
|
|
10
|
+
DeviceRemovalSchema,
|
|
11
|
+
} from '../kusinta/iot/connector/v1/connector_pb.js'
|
|
12
|
+
import { ConnectorTransport } from '../kusinta/iot/common/v1/types_pb.js'
|
|
13
|
+
|
|
14
|
+
describe('ConnectorHandshake', () => {
|
|
15
|
+
it('round-trips with known_devices', () => {
|
|
16
|
+
const h = create(ConnectorHandshakeSchema, {
|
|
17
|
+
info: {
|
|
18
|
+
connectorId: { value: 'homematic-ccu3' },
|
|
19
|
+
displayName: 'HomeMatic CCU3',
|
|
20
|
+
version: '1.0.0',
|
|
21
|
+
transport: ConnectorTransport.UNIX_SOCKET,
|
|
22
|
+
endpoint: '/run/kusinta/connectors/homematic.sock',
|
|
23
|
+
supportedDeviceTypeIds: [0x0301, 0x0302],
|
|
24
|
+
},
|
|
25
|
+
knownDevices: [{ deviceId: { value: 'dev-1' }, matterDeviceTypeId: 0x0301 }],
|
|
26
|
+
})
|
|
27
|
+
const decoded = fromBinary(ConnectorHandshakeSchema, toBinary(ConnectorHandshakeSchema, h))
|
|
28
|
+
expect(decoded.info?.connectorId?.value).toBe('homematic-ccu3')
|
|
29
|
+
expect(decoded.info?.displayName).toBe('HomeMatic CCU3')
|
|
30
|
+
expect(decoded.info?.transport).toBe(ConnectorTransport.UNIX_SOCKET)
|
|
31
|
+
expect(decoded.info?.supportedDeviceTypeIds).toEqual([0x0301, 0x0302])
|
|
32
|
+
expect(decoded.knownDevices).toHaveLength(1)
|
|
33
|
+
expect(decoded.knownDevices[0].deviceId?.value).toBe('dev-1')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('HandshakeAck', () => {
|
|
38
|
+
it('round-trips accepted ack', () => {
|
|
39
|
+
const ack = create(HandshakeAckSchema, { accepted: true, reason: '', gatewayId: { value: 'gw-abc' } })
|
|
40
|
+
const decoded = fromBinary(HandshakeAckSchema, toBinary(HandshakeAckSchema, ack))
|
|
41
|
+
expect(decoded.accepted).toBe(true)
|
|
42
|
+
expect(decoded.gatewayId?.value).toBe('gw-abc')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('round-trips rejected ack with reason', () => {
|
|
46
|
+
const ack = create(HandshakeAckSchema, { accepted: false, reason: 'Unknown connector ID' })
|
|
47
|
+
const decoded = fromBinary(HandshakeAckSchema, toBinary(HandshakeAckSchema, ack))
|
|
48
|
+
expect(decoded.accepted).toBe(false)
|
|
49
|
+
expect(decoded.reason).toBe('Unknown connector ID')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('DeviceAnnouncement', () => {
|
|
54
|
+
it('round-trips descriptor', () => {
|
|
55
|
+
const ann = create(DeviceAnnouncementSchema, {
|
|
56
|
+
descriptor: { deviceId: { value: 'dev-new-1' }, matterDeviceTypeId: 0x0307, vendorName: 'eQ-3', productName: 'HmIP-STH' },
|
|
57
|
+
})
|
|
58
|
+
const decoded = fromBinary(DeviceAnnouncementSchema, toBinary(DeviceAnnouncementSchema, ann))
|
|
59
|
+
expect(decoded.descriptor?.deviceId?.value).toBe('dev-new-1')
|
|
60
|
+
expect(decoded.descriptor?.matterDeviceTypeId).toBe(0x0307)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('DeviceRemoval', () => {
|
|
65
|
+
it('round-trips device_id and reason', () => {
|
|
66
|
+
const rem = create(DeviceRemovalSchema, { deviceId: { value: 'dev-1' }, reason: 'Device unpaired from CCU3' })
|
|
67
|
+
const decoded = fromBinary(DeviceRemovalSchema, toBinary(DeviceRemovalSchema, rem))
|
|
68
|
+
expect(decoded.deviceId?.value).toBe('dev-1')
|
|
69
|
+
expect(decoded.reason).toBe('Device unpaired from CCU3')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('ConnectRequest oneof payload', () => {
|
|
74
|
+
it('round-trips handshake payload', () => {
|
|
75
|
+
const msg = create(ConnectRequestSchema, {
|
|
76
|
+
messageId: 'msg-001',
|
|
77
|
+
payload: {
|
|
78
|
+
case: 'handshake',
|
|
79
|
+
value: {
|
|
80
|
+
info: {
|
|
81
|
+
connectorId: { value: 'conn-1' },
|
|
82
|
+
displayName: 'Test Connector',
|
|
83
|
+
version: '0.1.0',
|
|
84
|
+
transport: ConnectorTransport.UNIX_SOCKET,
|
|
85
|
+
endpoint: '/run/test.sock',
|
|
86
|
+
},
|
|
87
|
+
knownDevices: [],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
const decoded = fromBinary(ConnectRequestSchema, toBinary(ConnectRequestSchema, msg))
|
|
92
|
+
expect(decoded.messageId).toBe('msg-001')
|
|
93
|
+
expect(decoded.payload?.case).toBe('handshake')
|
|
94
|
+
if (decoded.payload?.case === 'handshake') {
|
|
95
|
+
expect(decoded.payload.value.info?.connectorId?.value).toBe('conn-1')
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('round-trips property_update payload', () => {
|
|
100
|
+
const msg = create(ConnectRequestSchema, {
|
|
101
|
+
messageId: 'msg-002',
|
|
102
|
+
payload: {
|
|
103
|
+
case: 'propertyUpdate',
|
|
104
|
+
value: {
|
|
105
|
+
updates: [{
|
|
106
|
+
deviceId: { value: 'dev-1' },
|
|
107
|
+
attributeName: 'OccupiedHeatingSetpoint',
|
|
108
|
+
value: { case: 'intValue', value: 2150 },
|
|
109
|
+
clusterIdHex: '0201',
|
|
110
|
+
}],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
const decoded = fromBinary(ConnectRequestSchema, toBinary(ConnectRequestSchema, msg))
|
|
115
|
+
expect(decoded.payload?.case).toBe('propertyUpdate')
|
|
116
|
+
if (decoded.payload?.case === 'propertyUpdate') {
|
|
117
|
+
expect(decoded.payload.value.updates).toHaveLength(1)
|
|
118
|
+
expect(decoded.payload.value.updates[0].attributeName).toBe('OccupiedHeatingSetpoint')
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('round-trips heartbeat payload', () => {
|
|
123
|
+
const msg = create(ConnectRequestSchema, {
|
|
124
|
+
messageId: 'msg-hb-1',
|
|
125
|
+
payload: { case: 'heartbeat', value: {} },
|
|
126
|
+
})
|
|
127
|
+
const decoded = fromBinary(ConnectRequestSchema, toBinary(ConnectRequestSchema, msg))
|
|
128
|
+
expect(decoded.payload?.case).toBe('heartbeat')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('ConnectResponse oneof payload', () => {
|
|
133
|
+
it('round-trips handshake_ack payload', () => {
|
|
134
|
+
const msg = create(ConnectResponseSchema, {
|
|
135
|
+
messageId: 'gw-msg-001',
|
|
136
|
+
payload: { case: 'handshakeAck', value: { accepted: true, gatewayId: { value: 'gw-1' } } },
|
|
137
|
+
})
|
|
138
|
+
const decoded = fromBinary(ConnectResponseSchema, toBinary(ConnectResponseSchema, msg))
|
|
139
|
+
expect(decoded.payload?.case).toBe('handshakeAck')
|
|
140
|
+
if (decoded.payload?.case === 'handshakeAck') {
|
|
141
|
+
expect(decoded.payload.value.accepted).toBe(true)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
|
|
4
|
+
import { DeviceDescriptorSchema } from '../kusinta/iot/device/v1/descriptor_pb.js'
|
|
5
|
+
import { DeviceOwnershipType, DeviceLifecycleState } from '../kusinta/iot/common/v1/types_pb.js'
|
|
6
|
+
import {
|
|
7
|
+
ThermostatPropertiesSchema,
|
|
8
|
+
TemperatureSensorPropertiesSchema,
|
|
9
|
+
WindowCoveringPropertiesSchema,
|
|
10
|
+
DoorLockPropertiesSchema,
|
|
11
|
+
ContactSensorPropertiesSchema,
|
|
12
|
+
OnOffLightPropertiesSchema,
|
|
13
|
+
DimmableLightPropertiesSchema,
|
|
14
|
+
} from '../kusinta/iot/device/v1/properties_pb.js'
|
|
15
|
+
import { DeviceSchema } from '../kusinta/iot/device/v1/device_pb.js'
|
|
16
|
+
import { PropertyUpdateSchema, PropertyUpdateBatchSchema } from '../kusinta/iot/device/v1/property_update_pb.js'
|
|
17
|
+
|
|
18
|
+
describe('DeviceDescriptor', () => {
|
|
19
|
+
it('round-trips matter_device_type_id for thermostat (0x0301 = 769)', () => {
|
|
20
|
+
const d = create(DeviceDescriptorSchema, {
|
|
21
|
+
deviceId: { value: 'dev-001' },
|
|
22
|
+
matterDeviceTypeId: 0x0301,
|
|
23
|
+
vendorName: 'eQ-3',
|
|
24
|
+
productName: 'HM-CC-RT-DN',
|
|
25
|
+
serialNumber: 'MEQ1234567',
|
|
26
|
+
nodeLabel: 'Living room thermostat',
|
|
27
|
+
vendorId: 0x0135,
|
|
28
|
+
productId: 0x0095,
|
|
29
|
+
hardwareVersionString: '1.4',
|
|
30
|
+
softwareVersionString: '2.8.0',
|
|
31
|
+
connectorId: { value: 'homematic-ccu3' },
|
|
32
|
+
spaceId: { value: 'room-uuid' },
|
|
33
|
+
ownership: DeviceOwnershipType.COMPANY,
|
|
34
|
+
lifecycle: DeviceLifecycleState.OWNED,
|
|
35
|
+
})
|
|
36
|
+
const decoded = fromBinary(DeviceDescriptorSchema, toBinary(DeviceDescriptorSchema, d))
|
|
37
|
+
expect(decoded.matterDeviceTypeId).toBe(0x0301)
|
|
38
|
+
expect(decoded.vendorName).toBe('eQ-3')
|
|
39
|
+
expect(decoded.ownership).toBe(DeviceOwnershipType.COMPANY)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('round-trips resident-owned lifecycle', () => {
|
|
43
|
+
const d = create(DeviceDescriptorSchema, {
|
|
44
|
+
deviceId: { value: 'dev-resident-001' },
|
|
45
|
+
matterDeviceTypeId: 0x0100,
|
|
46
|
+
ownership: DeviceOwnershipType.RESIDENT,
|
|
47
|
+
lifecycle: DeviceLifecycleState.PENDING_CLAIM,
|
|
48
|
+
ownerUserId: { value: 'user-abc' },
|
|
49
|
+
})
|
|
50
|
+
const decoded = fromBinary(DeviceDescriptorSchema, toBinary(DeviceDescriptorSchema, d))
|
|
51
|
+
expect(decoded.ownership).toBe(DeviceOwnershipType.RESIDENT)
|
|
52
|
+
expect(decoded.lifecycle).toBe(DeviceLifecycleState.PENDING_CLAIM)
|
|
53
|
+
expect(decoded.ownerUserId?.value).toBe('user-abc')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('ThermostatProperties — Matter Thermostat cluster 0x0201', () => {
|
|
58
|
+
it('round-trips occupied_heating_setpoint (21.50°C = 2150 centidegrees)', () => {
|
|
59
|
+
const p = create(ThermostatPropertiesSchema, { occupiedHeatingSetpoint: 2150 })
|
|
60
|
+
const decoded = fromBinary(ThermostatPropertiesSchema, toBinary(ThermostatPropertiesSchema, p))
|
|
61
|
+
expect(decoded.occupiedHeatingSetpoint).toBe(2150)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('preserves negative local_temperature (frost: -5.00°C = -500)', () => {
|
|
65
|
+
const p = create(ThermostatPropertiesSchema, { localTemperature: -500 })
|
|
66
|
+
const decoded = fromBinary(ThermostatPropertiesSchema, toBinary(ThermostatPropertiesSchema, p))
|
|
67
|
+
expect(decoded.localTemperature).toBe(-500)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('round-trips min/max setpoint limits (property-owner constraints)', () => {
|
|
71
|
+
const p = create(ThermostatPropertiesSchema, { minHeatSetpointLimit: 1600, maxHeatSetpointLimit: 2200 })
|
|
72
|
+
const decoded = fromBinary(ThermostatPropertiesSchema, toBinary(ThermostatPropertiesSchema, p))
|
|
73
|
+
expect(decoded.minHeatSetpointLimit).toBe(1600)
|
|
74
|
+
expect(decoded.maxHeatSetpointLimit).toBe(2200)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('round-trips system_mode enum values', () => {
|
|
78
|
+
const p = create(ThermostatPropertiesSchema, { systemMode: 4 }) // Heat=4
|
|
79
|
+
const decoded = fromBinary(ThermostatPropertiesSchema, toBinary(ThermostatPropertiesSchema, p))
|
|
80
|
+
expect(decoded.systemMode).toBe(4)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('TemperatureSensorProperties — Matter Temperature Measurement 0x0402', () => {
|
|
85
|
+
it('round-trips measured_value', () => {
|
|
86
|
+
const p = create(TemperatureSensorPropertiesSchema, { measuredValue: 2100 })
|
|
87
|
+
const decoded = fromBinary(TemperatureSensorPropertiesSchema, toBinary(TemperatureSensorPropertiesSchema, p))
|
|
88
|
+
expect(decoded.measuredValue).toBe(2100)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('WindowCoveringProperties — Matter Window Covering 0x0102', () => {
|
|
93
|
+
it('round-trips position as percent100ths (50% open = 5000)', () => {
|
|
94
|
+
const p = create(WindowCoveringPropertiesSchema, { currentPositionLiftPercent100ths: 5000 })
|
|
95
|
+
const decoded = fromBinary(WindowCoveringPropertiesSchema, toBinary(WindowCoveringPropertiesSchema, p))
|
|
96
|
+
expect(decoded.currentPositionLiftPercent100ths).toBe(5000)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('DoorLockProperties — Matter Door Lock 0x0101', () => {
|
|
101
|
+
it('round-trips lock_state (Locked=1)', () => {
|
|
102
|
+
const p = create(DoorLockPropertiesSchema, { lockState: 1 })
|
|
103
|
+
const decoded = fromBinary(DoorLockPropertiesSchema, toBinary(DoorLockPropertiesSchema, p))
|
|
104
|
+
expect(decoded.lockState).toBe(1)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('ContactSensorProperties — Matter Boolean State 0x0045', () => {
|
|
109
|
+
it('round-trips state_value false (contact open = alarm)', () => {
|
|
110
|
+
const p = create(ContactSensorPropertiesSchema, { stateValue: false })
|
|
111
|
+
const decoded = fromBinary(ContactSensorPropertiesSchema, toBinary(ContactSensorPropertiesSchema, p))
|
|
112
|
+
expect(decoded.stateValue).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('OnOffLightProperties — Matter On/Off 0x0006', () => {
|
|
117
|
+
it('round-trips on_off true', () => {
|
|
118
|
+
const p = create(OnOffLightPropertiesSchema, { onOff: true })
|
|
119
|
+
const decoded = fromBinary(OnOffLightPropertiesSchema, toBinary(OnOffLightPropertiesSchema, p))
|
|
120
|
+
expect(decoded.onOff).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('DimmableLightProperties — Matter Level Control 0x0008', () => {
|
|
125
|
+
it('round-trips current_level (0–254)', () => {
|
|
126
|
+
const p = create(DimmableLightPropertiesSchema, { onOff: true, currentLevel: 127 })
|
|
127
|
+
const decoded = fromBinary(DimmableLightPropertiesSchema, toBinary(DimmableLightPropertiesSchema, p))
|
|
128
|
+
expect(decoded.currentLevel).toBe(127)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('Device oneof properties', () => {
|
|
133
|
+
it('stores thermostat properties in the oneof', () => {
|
|
134
|
+
const d = create(DeviceSchema, {
|
|
135
|
+
descriptor: { deviceId: { value: 'therm-1' }, matterDeviceTypeId: 0x0301 },
|
|
136
|
+
properties: { case: 'thermostat', value: { occupiedHeatingSetpoint: 2000 } },
|
|
137
|
+
})
|
|
138
|
+
const decoded = fromBinary(DeviceSchema, toBinary(DeviceSchema, d))
|
|
139
|
+
expect(decoded.properties?.case).toBe('thermostat')
|
|
140
|
+
if (decoded.properties?.case === 'thermostat') {
|
|
141
|
+
expect(decoded.properties.value.occupiedHeatingSetpoint).toBe(2000)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('stores temperature sensor properties in the oneof', () => {
|
|
146
|
+
const d = create(DeviceSchema, {
|
|
147
|
+
descriptor: { deviceId: { value: 'temp-1' }, matterDeviceTypeId: 0x0302 },
|
|
148
|
+
properties: { case: 'temperatureSensor', value: { measuredValue: 1950 } },
|
|
149
|
+
})
|
|
150
|
+
const decoded = fromBinary(DeviceSchema, toBinary(DeviceSchema, d))
|
|
151
|
+
expect(decoded.properties?.case).toBe('temperatureSensor')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('PropertyUpdate', () => {
|
|
156
|
+
it('round-trips int_value for thermostat setpoint', () => {
|
|
157
|
+
const u = create(PropertyUpdateSchema, {
|
|
158
|
+
deviceId: { value: 'therm-1' },
|
|
159
|
+
attributeName: 'OccupiedHeatingSetpoint',
|
|
160
|
+
value: { case: 'intValue', value: 2150 },
|
|
161
|
+
clusterIdHex: '0201',
|
|
162
|
+
})
|
|
163
|
+
const decoded = fromBinary(PropertyUpdateSchema, toBinary(PropertyUpdateSchema, u))
|
|
164
|
+
expect(decoded.attributeName).toBe('OccupiedHeatingSetpoint')
|
|
165
|
+
expect(decoded.value?.case).toBe('intValue')
|
|
166
|
+
if (decoded.value?.case === 'intValue') expect(decoded.value.value).toBe(2150)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('round-trips bool_value for on/off light', () => {
|
|
170
|
+
const u = create(PropertyUpdateSchema, {
|
|
171
|
+
deviceId: { value: 'light-1' },
|
|
172
|
+
attributeName: 'OnOff',
|
|
173
|
+
value: { case: 'boolValue', value: true },
|
|
174
|
+
clusterIdHex: '0006',
|
|
175
|
+
})
|
|
176
|
+
const decoded = fromBinary(PropertyUpdateSchema, toBinary(PropertyUpdateSchema, u))
|
|
177
|
+
expect(decoded.value?.case).toBe('boolValue')
|
|
178
|
+
if (decoded.value?.case === 'boolValue') expect(decoded.value.value).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('PropertyUpdateBatch', () => {
|
|
183
|
+
it('round-trips multiple updates', () => {
|
|
184
|
+
const batch = create(PropertyUpdateBatchSchema, {
|
|
185
|
+
updates: [
|
|
186
|
+
{ deviceId: { value: 'd1' }, attributeName: 'LocalTemperature', value: { case: 'intValue', value: 1900 } },
|
|
187
|
+
{ deviceId: { value: 'd1' }, attributeName: 'OccupiedHeatingSetpoint', value: { case: 'intValue', value: 2100 } },
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
const decoded = fromBinary(PropertyUpdateBatchSchema, toBinary(PropertyUpdateBatchSchema, batch))
|
|
191
|
+
expect(decoded.updates.length).toBe(2)
|
|
192
|
+
expect(decoded.updates[0].attributeName).toBe('LocalTemperature')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
|
|
4
|
+
import {
|
|
5
|
+
DeviceIdSchema,
|
|
6
|
+
GatewayIdSchema,
|
|
7
|
+
UserIdSchema,
|
|
8
|
+
SpaceIdSchema,
|
|
9
|
+
ConnectorIdSchema,
|
|
10
|
+
TenantIdSchema,
|
|
11
|
+
} from '../kusinta/iot/identity/v1/identity_pb.js'
|
|
12
|
+
|
|
13
|
+
describe('Identity wrapper types', () => {
|
|
14
|
+
it('DeviceId round-trips value', () => {
|
|
15
|
+
const id = create(DeviceIdSchema, { value: 'device-abc-123' })
|
|
16
|
+
expect(fromBinary(DeviceIdSchema, toBinary(DeviceIdSchema, id)).value).toBe('device-abc-123')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('GatewayId round-trips value', () => {
|
|
20
|
+
const id = create(GatewayIdSchema, { value: 'gw-hmac-sha256-result' })
|
|
21
|
+
expect(fromBinary(GatewayIdSchema, toBinary(GatewayIdSchema, id)).value).toBe('gw-hmac-sha256-result')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('UserId round-trips value', () => {
|
|
25
|
+
const id = create(UserIdSchema, { value: 'user-keycloak-sub' })
|
|
26
|
+
expect(fromBinary(UserIdSchema, toBinary(UserIdSchema, id)).value).toBe('user-keycloak-sub')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('SpaceId round-trips value', () => {
|
|
30
|
+
const id = create(SpaceIdSchema, { value: 'space-uuid-v4' })
|
|
31
|
+
expect(fromBinary(SpaceIdSchema, toBinary(SpaceIdSchema, id)).value).toBe('space-uuid-v4')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('ConnectorId round-trips value', () => {
|
|
35
|
+
const id = create(ConnectorIdSchema, { value: 'homematic-ccu3' })
|
|
36
|
+
expect(fromBinary(ConnectorIdSchema, toBinary(ConnectorIdSchema, id)).value).toBe('homematic-ccu3')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('TenantId round-trips value', () => {
|
|
40
|
+
const id = create(TenantIdSchema, { value: 'tenant-property-co' })
|
|
41
|
+
expect(fromBinary(TenantIdSchema, toBinary(TenantIdSchema, id)).value).toBe('tenant-property-co')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
|
|
4
|
+
import { SpaceType } from '../kusinta/iot/common/v1/types_pb.js'
|
|
5
|
+
import { SpaceSchema } from '../kusinta/iot/space/v1/space_pb.js'
|
|
6
|
+
|
|
7
|
+
describe('Space', () => {
|
|
8
|
+
it('round-trips a building with sub-spaces', () => {
|
|
9
|
+
const building = create(SpaceSchema, {
|
|
10
|
+
spaceId: { value: 'building-1' },
|
|
11
|
+
spaceType: SpaceType.BUILDING,
|
|
12
|
+
name: 'Storgatan 5',
|
|
13
|
+
tenantId: { value: 'tenant-1' },
|
|
14
|
+
gatewayId: { value: 'gw-1' },
|
|
15
|
+
subSpaceIds: [{ value: 'floor-1' }, { value: 'floor-2' }],
|
|
16
|
+
})
|
|
17
|
+
const decoded = fromBinary(SpaceSchema, toBinary(SpaceSchema, building))
|
|
18
|
+
expect(decoded.spaceId?.value).toBe('building-1')
|
|
19
|
+
expect(decoded.spaceType).toBe(SpaceType.BUILDING)
|
|
20
|
+
expect(decoded.name).toBe('Storgatan 5')
|
|
21
|
+
expect(decoded.subSpaceIds).toHaveLength(2)
|
|
22
|
+
expect(decoded.subSpaceIds[0].value).toBe('floor-1')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('round-trips an apartment with resident', () => {
|
|
26
|
+
const apt = create(SpaceSchema, {
|
|
27
|
+
spaceId: { value: 'apt-101' },
|
|
28
|
+
spaceType: SpaceType.APARTMENT,
|
|
29
|
+
name: 'Lgh 101',
|
|
30
|
+
floor: 1,
|
|
31
|
+
parentSpaceId: { value: 'floor-1' },
|
|
32
|
+
residentUserId: { value: 'user-resident-1' },
|
|
33
|
+
deviceIds: [{ value: 'therm-1' }, { value: 'temp-sensor-1' }],
|
|
34
|
+
})
|
|
35
|
+
const decoded = fromBinary(SpaceSchema, toBinary(SpaceSchema, apt))
|
|
36
|
+
expect(decoded.spaceType).toBe(SpaceType.APARTMENT)
|
|
37
|
+
expect(decoded.floor).toBe(1)
|
|
38
|
+
expect(decoded.parentSpaceId?.value).toBe('floor-1')
|
|
39
|
+
expect(decoded.residentUserId?.value).toBe('user-resident-1')
|
|
40
|
+
expect(decoded.deviceIds).toHaveLength(2)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('round-trips a room', () => {
|
|
44
|
+
const room = create(SpaceSchema, {
|
|
45
|
+
spaceId: { value: 'room-living' },
|
|
46
|
+
spaceType: SpaceType.ROOM,
|
|
47
|
+
name: 'Living Room',
|
|
48
|
+
parentSpaceId: { value: 'apt-101' },
|
|
49
|
+
})
|
|
50
|
+
const decoded = fromBinary(SpaceSchema, toBinary(SpaceSchema, room))
|
|
51
|
+
expect(decoded.spaceType).toBe(SpaceType.ROOM)
|
|
52
|
+
expect(decoded.name).toBe('Living Room')
|
|
53
|
+
})
|
|
54
|
+
})
|