@kusinta/iot-schema 0.1.0-beta.5 → 0.1.0-beta.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kusinta/iot-schema",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "files": ["src"],
@@ -1,134 +0,0 @@
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
- })
@@ -1,29 +0,0 @@
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
- })
@@ -1,144 +0,0 @@
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
- })
@@ -1,194 +0,0 @@
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
- })
@@ -1,43 +0,0 @@
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
- })
@@ -1,54 +0,0 @@
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
- })
@@ -1,60 +0,0 @@
1
- // @ts-check
2
- import { describe, it, expect } from 'vitest'
3
- import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
4
- import {
5
- HomematicVendorExtensionSchema,
6
- HmThermostatPropsSchema,
7
- } from '../kusinta/iot/vendor/homematic/v1/homematic_pb.js'
8
- import { DeviceSchema } from '../kusinta/iot/device/v1/device_pb.js'
9
-
10
- describe('HmThermostatProps', () => {
11
- it('round-trips boost_mode and control_mode', () => {
12
- const p = create(HmThermostatPropsSchema, {
13
- boostMode: true, boostTimePeriod: 15.5, controlMode: 2, frostProtection: false,
14
- })
15
- const decoded = fromBinary(HmThermostatPropsSchema, toBinary(HmThermostatPropsSchema, p))
16
- expect(decoded.boostMode).toBe(true)
17
- expect(decoded.boostTimePeriod).toBeCloseTo(15.5, 3)
18
- expect(decoded.controlMode).toBe(2)
19
- expect(decoded.frostProtection).toBe(false)
20
- })
21
- })
22
-
23
- describe('HomematicVendorExtension', () => {
24
- it('round-trips homematic_address and thermostat props', () => {
25
- const ext = create(HomematicVendorExtensionSchema, {
26
- homematicAddress: 'MEQ1234567',
27
- homematicType: 'HmIP-eTRV-2',
28
- homematicProps: { case: 'hmThermostat', value: { boostMode: false, controlMode: 1, currentProfilePeriod: 3.0 } },
29
- })
30
- const decoded = fromBinary(HomematicVendorExtensionSchema, toBinary(HomematicVendorExtensionSchema, ext))
31
- expect(decoded.homematicAddress).toBe('MEQ1234567')
32
- expect(decoded.homematicType).toBe('HmIP-eTRV-2')
33
- expect(decoded.homematicProps?.case).toBe('hmThermostat')
34
- if (decoded.homematicProps?.case === 'hmThermostat') {
35
- expect(decoded.homematicProps.value.controlMode).toBe(1)
36
- }
37
- })
38
-
39
- })
40
-
41
- describe('Device with HomematicVendorExtension (field 50)', () => {
42
- it('stores homematic extension in the oneof', () => {
43
- const d = create(DeviceSchema, {
44
- descriptor: { deviceId: { value: 'hm-therm-1' }, matterDeviceTypeId: 0x0301 },
45
- properties: {
46
- case: 'homematic',
47
- value: {
48
- homematicAddress: 'MEQ1234567',
49
- homematicType: 'HmIP-eTRV-2',
50
- homematicProps: { case: 'hmThermostat', value: { controlMode: 2 } },
51
- },
52
- },
53
- })
54
- const decoded = fromBinary(DeviceSchema, toBinary(DeviceSchema, d))
55
- expect(decoded.properties?.case).toBe('homematic')
56
- if (decoded.properties?.case === 'homematic') {
57
- expect(decoded.properties.value.homematicAddress).toBe('MEQ1234567')
58
- }
59
- })
60
- })
@@ -1,268 +0,0 @@
1
- // @ts-check
2
- import { describe, it, expect } from 'vitest'
3
- import { create, toBinary, fromBinary } from '@bufbuild/protobuf'
4
- import {
5
- GatewayMessageSchema,
6
- AppMessageSchema,
7
- AppHandshakeSchema,
8
- } from '../kusinta/iot/webrtc/v1/envelope_pb.js'
9
- import { DeviceCommandSchema, CommandResultSchema } from '../kusinta/iot/webrtc/v1/command_pb.js'
10
- import { DeviceStateSnapshotSchema, DevicePropertyEventSchema } from '../kusinta/iot/webrtc/v1/device_state_pb.js'
11
- import { LivePermissionUpdateSchema } from '../kusinta/iot/webrtc/v1/permission_push_pb.js'
12
-
13
- describe('AppHandshake', () => {
14
- it('round-trips jwt and subscribe_device_ids', () => {
15
- const h = create(AppHandshakeSchema, {
16
- jwt: 'eyJhbGciOiJSUzI1NiJ9.test.sig',
17
- subscribeDeviceIds: [{ value: 'dev-1' }, { value: 'dev-2' }],
18
- })
19
- const decoded = fromBinary(AppHandshakeSchema, toBinary(AppHandshakeSchema, h))
20
- expect(decoded.jwt).toBe('eyJhbGciOiJSUzI1NiJ9.test.sig')
21
- expect(decoded.subscribeDeviceIds).toHaveLength(2)
22
- expect(decoded.subscribeDeviceIds[0].value).toBe('dev-1')
23
- })
24
- })
25
-
26
- describe('DeviceCommand', () => {
27
- it('round-trips thermostat setpoint command', () => {
28
- const cmd = create(DeviceCommandSchema, {
29
- commandId: 'cmd-uuid-1',
30
- deviceId: { value: 'therm-1' },
31
- clusterIdHex: '0201',
32
- commandName: 'SetpointRaiseLower',
33
- parameters: { case: 'thermostatSetpoint', value: { mode: 4, amount: 50 } },
34
- })
35
- const decoded = fromBinary(DeviceCommandSchema, toBinary(DeviceCommandSchema, cmd))
36
- expect(decoded.commandId).toBe('cmd-uuid-1')
37
- expect(decoded.deviceId?.value).toBe('therm-1')
38
- expect(decoded.commandName).toBe('SetpointRaiseLower')
39
- expect(decoded.parameters?.case).toBe('thermostatSetpoint')
40
- if (decoded.parameters?.case === 'thermostatSetpoint') {
41
- expect(decoded.parameters.value.mode).toBe(4)
42
- expect(decoded.parameters.value.amount).toBe(50)
43
- }
44
- })
45
-
46
- it('round-trips on/off command', () => {
47
- const cmd = create(DeviceCommandSchema, {
48
- commandId: 'cmd-uuid-2',
49
- deviceId: { value: 'light-1' },
50
- clusterIdHex: '0006',
51
- commandName: 'Toggle',
52
- parameters: { case: 'onOff', value: { toggle: true } },
53
- })
54
- const decoded = fromBinary(DeviceCommandSchema, toBinary(DeviceCommandSchema, cmd))
55
- expect(decoded.parameters?.case).toBe('onOff')
56
- if (decoded.parameters?.case === 'onOff') {
57
- expect(decoded.parameters.value.toggle).toBe(true)
58
- }
59
- })
60
-
61
- it('round-trips level control command', () => {
62
- const cmd = create(DeviceCommandSchema, {
63
- commandId: 'cmd-uuid-3',
64
- deviceId: { value: 'light-2' },
65
- clusterIdHex: '0008',
66
- commandName: 'MoveToLevel',
67
- parameters: { case: 'levelControl', value: { level: 127, transitionTime: 10 } },
68
- })
69
- const decoded = fromBinary(DeviceCommandSchema, toBinary(DeviceCommandSchema, cmd))
70
- expect(decoded.parameters?.case).toBe('levelControl')
71
- if (decoded.parameters?.case === 'levelControl') {
72
- expect(decoded.parameters.value.level).toBe(127)
73
- expect(decoded.parameters.value.transitionTime).toBe(10)
74
- }
75
- })
76
-
77
- it('round-trips window covering lift command', () => {
78
- const cmd = create(DeviceCommandSchema, {
79
- commandId: 'cmd-uuid-4',
80
- deviceId: { value: 'blind-1' },
81
- clusterIdHex: '0102',
82
- commandName: 'GoToLiftPercentage',
83
- parameters: { case: 'windowCoveringLift', value: { liftPercent100ths: 5000 } },
84
- })
85
- const decoded = fromBinary(DeviceCommandSchema, toBinary(DeviceCommandSchema, cmd))
86
- expect(decoded.parameters?.case).toBe('windowCoveringLift')
87
- if (decoded.parameters?.case === 'windowCoveringLift') {
88
- expect(decoded.parameters.value.liftPercent100ths).toBe(5000)
89
- }
90
- })
91
-
92
- it('round-trips door lock command', () => {
93
- const cmd = create(DeviceCommandSchema, {
94
- commandId: 'cmd-uuid-5',
95
- deviceId: { value: 'lock-1' },
96
- clusterIdHex: '0101',
97
- commandName: 'LockDoor',
98
- parameters: { case: 'doorLock', value: { lockState: 1 } },
99
- })
100
- const decoded = fromBinary(DeviceCommandSchema, toBinary(DeviceCommandSchema, cmd))
101
- expect(decoded.parameters?.case).toBe('doorLock')
102
- if (decoded.parameters?.case === 'doorLock') {
103
- expect(decoded.parameters.value.lockState).toBe(1)
104
- }
105
- })
106
- })
107
-
108
- describe('CommandResult', () => {
109
- it('round-trips successful result', () => {
110
- const result = create(CommandResultSchema, { commandId: 'cmd-uuid-1', success: true })
111
- const decoded = fromBinary(CommandResultSchema, toBinary(CommandResultSchema, result))
112
- expect(decoded.commandId).toBe('cmd-uuid-1')
113
- expect(decoded.success).toBe(true)
114
- })
115
-
116
- it('round-trips failed result with error', () => {
117
- const result = create(CommandResultSchema, {
118
- commandId: 'cmd-uuid-2',
119
- success: false,
120
- error: { code: 'PERMISSION_DENIED', message: 'Setpoint exceeds property-owner limit' },
121
- })
122
- const decoded = fromBinary(CommandResultSchema, toBinary(CommandResultSchema, result))
123
- expect(decoded.success).toBe(false)
124
- expect(decoded.error?.code).toBe('PERMISSION_DENIED')
125
- })
126
- })
127
-
128
- describe('DeviceStateSnapshot', () => {
129
- it('round-trips with devices and permissions', () => {
130
- const snap = create(DeviceStateSnapshotSchema, {
131
- devices: [{
132
- descriptor: { deviceId: { value: 'therm-1' }, matterDeviceTypeId: 0x0301 },
133
- properties: { case: 'thermostat', value: { occupiedHeatingSetpoint: 2100 } },
134
- }],
135
- permissions: { userId: { value: 'user-1' }, gatewayId: { value: 'gw-1' }, deviceAcls: [] },
136
- })
137
- const decoded = fromBinary(DeviceStateSnapshotSchema, toBinary(DeviceStateSnapshotSchema, snap))
138
- expect(decoded.devices).toHaveLength(1)
139
- expect(decoded.devices[0].descriptor?.deviceId?.value).toBe('therm-1')
140
- expect(decoded.permissions?.userId?.value).toBe('user-1')
141
- })
142
- })
143
-
144
- describe('DevicePropertyEvent', () => {
145
- it('round-trips a property update event', () => {
146
- const event = create(DevicePropertyEventSchema, {
147
- update: {
148
- deviceId: { value: 'therm-1' },
149
- attributeName: 'LocalTemperature',
150
- value: { case: 'intValue', value: 2050 },
151
- clusterIdHex: '0402',
152
- },
153
- })
154
- const decoded = fromBinary(DevicePropertyEventSchema, toBinary(DevicePropertyEventSchema, event))
155
- expect(decoded.update?.attributeName).toBe('LocalTemperature')
156
- expect(decoded.update?.value?.case).toBe('intValue')
157
- })
158
- })
159
-
160
- describe('LivePermissionUpdate', () => {
161
- it('round-trips added and removed devices', () => {
162
- const update = create(LivePermissionUpdateSchema, {
163
- newPermissions: { userId: { value: 'user-1' }, gatewayId: { value: 'gw-1' }, deviceAcls: [] },
164
- addedDevices: [{ value: 'dev-new-1' }],
165
- removedDevices: [{ value: 'dev-old-1' }],
166
- changeReason: 'DEVICE_ASSIGNED',
167
- })
168
- const decoded = fromBinary(LivePermissionUpdateSchema, toBinary(LivePermissionUpdateSchema, update))
169
- expect(decoded.addedDevices).toHaveLength(1)
170
- expect(decoded.addedDevices[0].value).toBe('dev-new-1')
171
- expect(decoded.removedDevices).toHaveLength(1)
172
- expect(decoded.changeReason).toBe('DEVICE_ASSIGNED')
173
- })
174
- })
175
-
176
- describe('GatewayMessage oneof payload', () => {
177
- it('round-trips state_snapshot payload', () => {
178
- const msg = create(GatewayMessageSchema, {
179
- messageId: 'gw-msg-001',
180
- payload: {
181
- case: 'stateSnapshot',
182
- value: {
183
- devices: [],
184
- permissions: { userId: { value: 'user-1' }, gatewayId: { value: 'gw-1' }, deviceAcls: [] },
185
- },
186
- },
187
- })
188
- const decoded = fromBinary(GatewayMessageSchema, toBinary(GatewayMessageSchema, msg))
189
- expect(decoded.messageId).toBe('gw-msg-001')
190
- expect(decoded.payload?.case).toBe('stateSnapshot')
191
- })
192
-
193
- it('round-trips property_event payload', () => {
194
- const msg = create(GatewayMessageSchema, {
195
- messageId: 'gw-msg-002',
196
- payload: {
197
- case: 'propertyEvent',
198
- value: {
199
- update: {
200
- deviceId: { value: 'therm-1' },
201
- attributeName: 'OccupiedHeatingSetpoint',
202
- value: { case: 'intValue', value: 2100 },
203
- clusterIdHex: '0201',
204
- },
205
- },
206
- },
207
- })
208
- const decoded = fromBinary(GatewayMessageSchema, toBinary(GatewayMessageSchema, msg))
209
- expect(decoded.payload?.case).toBe('propertyEvent')
210
- if (decoded.payload?.case === 'propertyEvent') {
211
- expect(decoded.payload.value.update?.attributeName).toBe('OccupiedHeatingSetpoint')
212
- }
213
- })
214
-
215
- it('round-trips pong payload', () => {
216
- const msg = create(GatewayMessageSchema, {
217
- messageId: 'gw-msg-003',
218
- payload: { case: 'pong', value: {} },
219
- })
220
- const decoded = fromBinary(GatewayMessageSchema, toBinary(GatewayMessageSchema, msg))
221
- expect(decoded.payload?.case).toBe('pong')
222
- })
223
- })
224
-
225
- describe('AppMessage oneof payload', () => {
226
- it('round-trips handshake payload', () => {
227
- const msg = create(AppMessageSchema, {
228
- messageId: 'app-msg-001',
229
- payload: {
230
- case: 'handshake',
231
- value: { jwt: 'token.payload.sig', subscribeDeviceIds: [{ value: 'dev-1' }] },
232
- },
233
- })
234
- const decoded = fromBinary(AppMessageSchema, toBinary(AppMessageSchema, msg))
235
- expect(decoded.messageId).toBe('app-msg-001')
236
- expect(decoded.payload?.case).toBe('handshake')
237
- if (decoded.payload?.case === 'handshake') {
238
- expect(decoded.payload.value.jwt).toBe('token.payload.sig')
239
- }
240
- })
241
-
242
- it('round-trips command payload', () => {
243
- const msg = create(AppMessageSchema, {
244
- messageId: 'app-msg-002',
245
- payload: {
246
- case: 'command',
247
- value: {
248
- commandId: 'cmd-1',
249
- deviceId: { value: 'light-1' },
250
- clusterIdHex: '0006',
251
- commandName: 'Toggle',
252
- parameters: { case: 'onOff', value: { toggle: true } },
253
- },
254
- },
255
- })
256
- const decoded = fromBinary(AppMessageSchema, toBinary(AppMessageSchema, msg))
257
- expect(decoded.payload?.case).toBe('command')
258
- })
259
-
260
- it('round-trips ping payload', () => {
261
- const msg = create(AppMessageSchema, {
262
- messageId: 'app-msg-003',
263
- payload: { case: 'ping', value: {} },
264
- })
265
- const decoded = fromBinary(AppMessageSchema, toBinary(AppMessageSchema, msg))
266
- expect(decoded.payload?.case).toBe('ping')
267
- })
268
- })