@kusinta/iot-schema 0.1.0-beta.2 → 0.1.0-beta.21
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/LICENSE +21 -0
- package/README.md +49 -0
- package/package.json +10 -2
- package/src/kusinta/iot/access/v1/acl_pb.d.ts +3 -3
- package/src/kusinta/iot/access/v1/acl_pb.js +3 -3
- package/src/kusinta/iot/access/v1/roles_pb.d.ts +1 -1
- package/src/kusinta/iot/access/v1/roles_pb.js +1 -1
- package/src/kusinta/iot/common/v1/types_pb.d.ts +1 -1
- package/src/kusinta/iot/common/v1/types_pb.js +1 -1
- package/src/kusinta/iot/connector/v1/connector_pb.d.ts +31 -24
- package/src/kusinta/iot/connector/v1/connector_pb.js +16 -15
- package/src/kusinta/iot/connector/v1/connector_service_pb.d.ts +6 -6
- package/src/kusinta/iot/connector/v1/connector_service_pb.js +3 -3
- package/src/kusinta/iot/device/v1/descriptor_pb.d.ts +3 -3
- package/src/kusinta/iot/device/v1/descriptor_pb.js +3 -3
- package/src/kusinta/iot/device/v1/device_pb.d.ts +36 -4
- package/src/kusinta/iot/device/v1/device_pb.js +4 -4
- package/src/kusinta/iot/device/v1/matter_options_pb.d.ts +46 -0
- package/src/kusinta/iot/device/v1/matter_options_pb.js +50 -0
- package/src/kusinta/iot/device/v1/properties_pb.d.ts +175 -208
- package/src/kusinta/iot/device/v1/properties_pb.js +3 -2
- package/src/kusinta/iot/device/v1/property_update_pb.d.ts +38 -5
- package/src/kusinta/iot/device/v1/property_update_pb.js +2 -2
- package/src/kusinta/iot/identity/v1/identity_pb.d.ts +1 -1
- package/src/kusinta/iot/identity/v1/identity_pb.js +1 -1
- package/src/kusinta/iot/registration/v1/registration_pb.d.ts +170 -0
- package/src/kusinta/iot/registration/v1/registration_pb.js +61 -0
- package/src/kusinta/iot/registration/v1/registration_service_pb.d.ts +34 -0
- package/src/kusinta/iot/registration/v1/registration_service_pb.js +19 -0
- package/src/kusinta/iot/signaling/v1/signaling_pb.d.ts +320 -0
- package/src/kusinta/iot/signaling/v1/signaling_pb.js +83 -0
- package/src/kusinta/iot/signaling/v1/signaling_service_pb.d.ts +34 -0
- package/src/kusinta/iot/signaling/v1/signaling_service_pb.js +19 -0
- package/src/kusinta/iot/space/v1/space_pb.d.ts +3 -3
- package/src/kusinta/iot/space/v1/space_pb.js +3 -3
- package/src/kusinta/iot/vendor/homematic/v1/homematic_pb.d.ts +16 -11
- package/src/kusinta/iot/vendor/homematic/v1/homematic_pb.js +2 -2
- package/src/kusinta/iot/webrtc/v1/app_token_pb.d.ts +152 -0
- package/src/kusinta/iot/webrtc/v1/app_token_pb.js +28 -0
- package/src/kusinta/iot/webrtc/v1/command_pb.d.ts +4 -4
- package/src/kusinta/iot/webrtc/v1/command_pb.js +2 -2
- package/src/kusinta/iot/webrtc/v1/device_state_pb.d.ts +69 -6
- package/src/kusinta/iot/webrtc/v1/device_state_pb.js +20 -5
- package/src/kusinta/iot/webrtc/v1/envelope_pb.d.ts +365 -17
- package/src/kusinta/iot/webrtc/v1/envelope_pb.js +79 -11
- package/src/kusinta/iot/webrtc/v1/management_pb.d.ts +451 -0
- package/src/kusinta/iot/webrtc/v1/management_pb.js +106 -0
- package/src/kusinta/iot/webrtc/v1/permission_push_pb.d.ts +10 -4
- package/src/kusinta/iot/webrtc/v1/permission_push_pb.js +3 -3
- package/src/__tests__/access.test.js +0 -134
- package/src/__tests__/common.test.js +0 -29
- package/src/__tests__/connector.test.js +0 -144
- package/src/__tests__/device.test.js +0 -194
- package/src/__tests__/identity.test.js +0 -43
- package/src/__tests__/space.test.js +0 -54
- package/src/__tests__/vendor.test.js +0 -60
- package/src/__tests__/webrtc.test.js +0 -268
|
@@ -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
|
-
})
|