mclimate-payload-helper 1.2.2 → 1.2.4

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.
Files changed (90) hide show
  1. package/dist/decoders/commandsReadingHelper.d.ts.map +1 -1
  2. package/dist/decoders/commandsReadingHelper.js +182 -41
  3. package/dist/decoders/commandsReadingHelper.js.map +1 -1
  4. package/dist/decoders/payloadParsers/index.d.ts +1 -0
  5. package/dist/decoders/payloadParsers/index.d.ts.map +1 -1
  6. package/dist/decoders/payloadParsers/index.js +1 -0
  7. package/dist/decoders/payloadParsers/index.js.map +1 -1
  8. package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.d.ts.map +1 -1
  9. package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.js +12 -12
  10. package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.js.map +1 -1
  11. package/dist/decoders/payloadParsers/multisensorPayloadParser.d.ts +2 -0
  12. package/dist/decoders/payloadParsers/multisensorPayloadParser.d.ts.map +1 -0
  13. package/dist/decoders/payloadParsers/multisensorPayloadParser.js +96 -0
  14. package/dist/decoders/payloadParsers/multisensorPayloadParser.js.map +1 -0
  15. package/dist/decoders/payloadParsers/types/allDevices.d.ts +2 -1
  16. package/dist/decoders/payloadParsers/types/allDevices.d.ts.map +1 -1
  17. package/dist/decoders/payloadParsers/types/allDevices.js +1 -0
  18. package/dist/decoders/payloadParsers/types/allDevices.js.map +1 -1
  19. package/dist/encoders/Co2DisplayCommonCommands.d.ts +0 -4
  20. package/dist/encoders/Co2DisplayCommonCommands.d.ts.map +1 -1
  21. package/dist/encoders/Co2DisplayCommonCommands.js +0 -38
  22. package/dist/encoders/Co2DisplayCommonCommands.js.map +1 -1
  23. package/dist/encoders/CommandBuilder.d.ts.map +1 -1
  24. package/dist/encoders/CommandBuilder.js +2 -0
  25. package/dist/encoders/CommandBuilder.js.map +1 -1
  26. package/dist/encoders/FanCoilThermostat.d.ts +61 -0
  27. package/dist/encoders/FanCoilThermostat.d.ts.map +1 -0
  28. package/dist/encoders/FanCoilThermostat.js +719 -0
  29. package/dist/encoders/FanCoilThermostat.js.map +1 -0
  30. package/dist/encoders/MelissaCommands.d.ts +6 -0
  31. package/dist/encoders/MelissaCommands.d.ts.map +1 -0
  32. package/dist/encoders/MelissaCommands.js +35 -0
  33. package/dist/encoders/MelissaCommands.js.map +1 -0
  34. package/dist/encoders/MultiSensorCommands.d.ts +25 -0
  35. package/dist/encoders/MultiSensorCommands.d.ts.map +1 -0
  36. package/dist/encoders/MultiSensorCommands.js +256 -0
  37. package/dist/encoders/MultiSensorCommands.js.map +1 -0
  38. package/dist/encoders/SetAqiLed.d.ts +6 -0
  39. package/dist/encoders/SetAqiLed.d.ts.map +1 -0
  40. package/dist/encoders/SetAqiLed.js +46 -0
  41. package/dist/encoders/SetAqiLed.js.map +1 -0
  42. package/dist/encoders/index.d.ts +2 -0
  43. package/dist/encoders/index.d.ts.map +1 -1
  44. package/dist/encoders/index.js +2 -0
  45. package/dist/encoders/index.js.map +1 -1
  46. package/dist/encoders/types/enumValidation.d.ts +6 -0
  47. package/dist/encoders/types/enumValidation.d.ts.map +1 -0
  48. package/dist/encoders/types/enumValidation.js +50 -0
  49. package/dist/encoders/types/enumValidation.js.map +1 -0
  50. package/dist/encoders/types/schemaValidatedEnums.d.ts +40 -0
  51. package/dist/encoders/types/schemaValidatedEnums.d.ts.map +1 -0
  52. package/dist/encoders/types/schemaValidatedEnums.js +181 -0
  53. package/dist/encoders/types/schemaValidatedEnums.js.map +1 -0
  54. package/dist/encoders/types/schemas.d.ts +352 -0
  55. package/dist/encoders/types/schemas.d.ts.map +1 -1
  56. package/dist/encoders/types/schemas.js +29 -3
  57. package/dist/encoders/types/schemas.js.map +1 -1
  58. package/dist/test/payloadDecoders.test.js +28 -10
  59. package/dist/test/payloadDecoders.test.js.map +1 -1
  60. package/dist/test/payloadEncoders.test.js +6 -0
  61. package/dist/test/payloadEncoders.test.js.map +1 -1
  62. package/dist/types/deviceTypes.d.ts +19 -0
  63. package/dist/types/deviceTypes.d.ts.map +1 -0
  64. package/dist/types/deviceTypes.js +23 -0
  65. package/dist/types/deviceTypes.js.map +1 -0
  66. package/dist/types/index.d.ts +2 -0
  67. package/dist/types/index.d.ts.map +1 -0
  68. package/dist/types/index.js +18 -0
  69. package/dist/types/index.js.map +1 -0
  70. package/dist/types/types.d.ts +13 -0
  71. package/dist/types/types.d.ts.map +1 -0
  72. package/dist/types/types.js +9 -0
  73. package/dist/types/types.js.map +1 -0
  74. package/dist/validation.d.ts +3 -0
  75. package/dist/validation.d.ts.map +1 -0
  76. package/dist/validation.js +8 -0
  77. package/dist/validation.js.map +1 -0
  78. package/package.json +1 -1
  79. package/src/decoders/commandsReadingHelper.ts +327 -186
  80. package/src/decoders/payloadParsers/index.ts +1 -1
  81. package/src/decoders/payloadParsers/melissaLorawanPayloadParser.ts +19 -16
  82. package/src/decoders/payloadParsers/multisensorPayloadParser.ts +97 -0
  83. package/src/decoders/payloadParsers/types/allDevices.ts +1 -0
  84. package/src/encoders/CommandBuilder.ts +4 -0
  85. package/src/encoders/MelissaCommands.ts +33 -0
  86. package/src/encoders/MultiSensorCommands.ts +253 -0
  87. package/src/encoders/index.ts +2 -0
  88. package/src/encoders/types/schemas.ts +74 -4
  89. package/src/test/payloadDecoders.test.ts +30 -11
  90. package/src/test/payloadEncoders.test.ts +8 -0
@@ -19,4 +19,4 @@ export * from './relay16DryPayloadParser'
19
19
  export * from './melissaLorawanPayloadParser'
20
20
  export * from './co2PirLitePayloadParser'
21
21
  export * from './htPirLitePayloadParser'
22
-
22
+ export * from './multisensorPayloadParser'
@@ -1,28 +1,31 @@
1
1
  import { commandsReadingHelper } from '@/decoders/commandsReadingHelper'
2
- import { DeviceType, RelayKeepAliveData } from '@/decoders/payloadParsers/types'
2
+ import { DeviceType } from '@/decoders/payloadParsers/types'
3
3
  import { byteArrayParser } from '@/helpers'
4
4
  import { CustomError } from '@/utils'
5
5
 
6
+ interface MelissaKeepAliveData {
7
+ sensorTemperature?: number
8
+ relativeHumidity?: number
9
+ isIrCodeRecordingRequested?: boolean
10
+ }
6
11
  export const melissaLorawanPayloadParser = (hexData: string) => {
7
12
  let deviceData = {}
8
13
 
9
14
  try {
10
15
  const handleKeepAliveData = (bytes: number[]) => {
11
- let keepaliveData: RelayKeepAliveData = {}
12
-
13
- // Internal temperature sensor
14
- const isNegative = (bytes[1] & 0x80) !== 0; // Check the 7th bit for the sign
15
- let temperature = bytes[1] & 0x7F; // Mask out the 7th bit to get the temperature value
16
- keepaliveData.internalTemperature = isNegative ? -temperature : temperature;
17
-
18
- // Energy data
19
- keepaliveData.energy = ((bytes[2] << 24) | (bytes[3] << 16) | (bytes[4] << 8) | bytes[5]) / 1000
16
+ let keepaliveData: MelissaKeepAliveData = {}
17
+ // Internal temperature sensor data from bytes 1-2 (16 bits)
18
+ // According to the screenshot: t[°C] = (T[15:0] - 400) / 10
19
+ const rawTemperature = (bytes[1] << 8) | bytes[2];
20
+ keepaliveData.sensorTemperature = Number(((rawTemperature - 400) / 10).toFixed(2));
20
21
 
21
- // Power data
22
- keepaliveData.power = (bytes[6] << 8) | bytes[7]
22
+ // Relative Humidity from byte 3
23
+ // According to the screenshot: RH[%] = (XX * 100) / 256
24
+ keepaliveData.relativeHumidity = Number(((bytes[3] * 100) / 256).toFixed(2));
23
25
 
24
- // AC voltage
25
- keepaliveData.acVoltage = bytes[8]
26
+ // Device status information (byte 4)
27
+ // Bit 7 indicates if IR code recording is requested (1) or not (0)
28
+ keepaliveData.isIrCodeRecordingRequested = ((bytes[4] & 0x80) !== 0);
26
29
 
27
30
  Object.assign(deviceData, { ...deviceData }, { ...keepaliveData })
28
31
  }
@@ -35,7 +38,7 @@ export const melissaLorawanPayloadParser = (hexData: string) => {
35
38
  handleKeepAliveData(byteArray)
36
39
  } else {
37
40
  // parse command answers
38
- let data = commandsReadingHelper(hexData, 18, DeviceType.MelissaLorawan)
41
+ let data = commandsReadingHelper(hexData, 10, DeviceType.MelissaLorawan)
39
42
  if (!data) return
40
43
  const shouldKeepAlive = data.hasOwnProperty('decodeKeepalive') ? true : false
41
44
  if ('decodeKeepalive' in data) {
@@ -46,7 +49,7 @@ export const melissaLorawanPayloadParser = (hexData: string) => {
46
49
 
47
50
  // get only keepalive from device response
48
51
  if (shouldKeepAlive) {
49
- let keepaliveData = hexData.slice(-18)
52
+ let keepaliveData = hexData.slice(-10)
50
53
  let dataToPass = byteArrayParser(keepaliveData)
51
54
  if (!dataToPass) return
52
55
  handleKeepAliveData(dataToPass)
@@ -0,0 +1,97 @@
1
+ import { commandsReadingHelper } from '@/decoders'
2
+ import { CustomError } from '@/utils'
3
+ import { DeviceType } from '@/decoders/payloadParsers/types'
4
+ import { byteArrayParser } from '@/helpers'
5
+
6
+ // Multisensor payload parser (uplink). Structure mirrors other parsers in this folder.
7
+ export const multisensorPayloadParser = (hexData: string) => {
8
+ let deviceData: any = {}
9
+
10
+ try {
11
+ if (!hexData) return
12
+
13
+ const bytes = byteArrayParser(hexData)
14
+ if (!bytes || !bytes.length) return
15
+
16
+ const toHex = (arr: number[]) => arr.map((b) => ('0' + b.toString(16)).slice(-2)).join('')
17
+
18
+ const handleKeepAliveData = (barr: number[]) => {
19
+ // Decode according to provided example indexing
20
+ const b = (i: number, d = 0) => (typeof barr[i] === 'number' ? barr[i] : d)
21
+
22
+ // Byte 1..14 mapping per example
23
+ const deltaTMinC = b(1) / 10
24
+ const deltaTMaxC = b(2) / 10
25
+ const avgTempC = Number((b(3) / 10).toFixed(1))
26
+ const relativeHumidity = b(4)
27
+ // Pressure: matches example (A8 -> 956.94): scale = 390
28
+ const pressureMbar = Number(((b(5) * 390) / 255 + 700).toFixed(2))
29
+ const staticAQI = Number(((b(6) * 500) / 255).toFixed(2))
30
+ // eCO2: matches example (06 -> 508.23): scale = 4600
31
+ const eCO2ppm = Number((((b(7) * 4600) / 255) + 400).toFixed(2))
32
+ const tvocRaw = ((b(8) & 0xff) << 8) | (b(9) & 0xff)
33
+ const tvocPpbApprox = tvocRaw
34
+ const lightLux = Math.round((b(10) * 1000) / 255)
35
+ const noiseDb = b(11)
36
+ const pirTriggerCount = b(12)
37
+ const deviceVoltageMv = Math.round((b(13) * 2200) / 255 + 1600)
38
+ const deviceVoltage = Number((deviceVoltageMv / 1000).toFixed(2))
39
+ const powerSourceRaw = b(14)
40
+ const powerSource = powerSourceRaw === 1 ? 'USB' : 'Battery'
41
+
42
+ const keepaliveData = {
43
+ type: 'keep_alive',
44
+ deltaTMinC,
45
+ deltaTMaxC,
46
+ avgTempC,
47
+ relativeHumidity,
48
+ pressureMbar,
49
+ staticAQI,
50
+ eCO2ppm,
51
+ tvocRaw,
52
+ tvocPpbApprox,
53
+ lightLux,
54
+ noiseDb,
55
+ pirTriggerCount,
56
+ deviceVoltageMv,
57
+ deviceVoltage,
58
+ powerSourceRaw,
59
+ powerSource,
60
+ raw: toHex(barr),
61
+ }
62
+ Object.assign(deviceData, { ...deviceData }, { ...keepaliveData })
63
+ }
64
+
65
+ if (hexData.slice(0, 2).toLowerCase() === '82') {
66
+ // keep-alive packet
67
+ handleKeepAliveData(bytes)
68
+ } else {
69
+ // parse command answers via helper
70
+ const data = commandsReadingHelper(hexData, 30, DeviceType.MultiSensor)
71
+ if (!data) return
72
+
73
+ const shouldKeepAlive = data.hasOwnProperty('decodeKeepalive') ? true : false
74
+ if ('decodeKeepalive' in data) {
75
+ delete (data as any).decodeKeepalive
76
+ }
77
+
78
+ Object.assign(deviceData, { ...deviceData }, { ...data })
79
+
80
+ // If device response includes keepalive, decode last 15 bytes (30 hex chars)
81
+ if (shouldKeepAlive) {
82
+ const keepaliveHex = hexData.slice(-30)
83
+ const keepaliveBytes = byteArrayParser(keepaliveHex)
84
+ if (keepaliveBytes) handleKeepAliveData(keepaliveBytes)
85
+ }
86
+ }
87
+
88
+ return deviceData
89
+ } catch (e) {
90
+ throw new CustomError({
91
+ message: `Unhandled data`,
92
+ hexData: hexData,
93
+ deviceType: 'multisensor',
94
+ originalError: e as Error,
95
+ })
96
+ }
97
+ }
@@ -19,4 +19,5 @@ export enum DeviceType {
19
19
  MelissaLorawan = 'melissa_lorawan',
20
20
  CO2PirLite = 'co2_pir_lite',
21
21
  HTPirLite = 'ht_pir_lite',
22
+ MultiSensor = 'multisensor',
22
23
  }
@@ -19,6 +19,8 @@ import {
19
19
  Relay16DryCommands,
20
20
  HTPirLiteCommands,
21
21
  Co2PirLiteCommands,
22
+ MelissaCommands,
23
+ MultiSensorCommands,
22
24
  } from '@/encoders'
23
25
 
24
26
  import { CustomError, toCamelCase } from '@/utils'
@@ -48,6 +50,8 @@ export class CommandBuilder {
48
50
  relay_16_dry: Relay16DryCommands,
49
51
  ht_pir_lite: HTPirLiteCommands,
50
52
  co2_pir_lite: Co2PirLiteCommands,
53
+ melissa_lorawan: MelissaCommands,
54
+ multisensor: MultiSensorCommands,
51
55
  }
52
56
  }
53
57
 
@@ -0,0 +1,33 @@
1
+ import { GeneralCommands, BaseCommand } from '@/encoders'
2
+ import { applyMixins, decToHex, CustomError } from '@/utils'
3
+ import { DeviceCommandSchemas, MelissaCommandTypes } from './types'
4
+ import { ZodError, } from 'zod'
5
+
6
+
7
+ export class MelissaCommands extends GeneralCommands {
8
+
9
+
10
+ static setIrCodeRecording(params: MelissaCommandTypes.SetIrCodeRecordingParams) {
11
+ try {
12
+ DeviceCommandSchemas.MelissaCommandSchemas.setIrCodeRecording.parse(params)
13
+ const { time } = params
14
+ return new BaseCommand('SetIrCodeRecording', 0x09, decToHex(time))
15
+ } catch (e) {
16
+ if (e instanceof ZodError) {
17
+ throw new CustomError({
18
+ message: 'Zod validation error during SetIrCodeRecording execution',
19
+ command: 'SetIrCodeRecording',
20
+ originalError: e,
21
+ })
22
+ } else {
23
+ throw new CustomError({
24
+ message: 'Error during SetIrCodeRecording execution',
25
+ command: 'SetIrCodeRecording',
26
+ originalError: e as Error,
27
+ })
28
+ }
29
+ }
30
+ }
31
+ }
32
+ applyMixins(MelissaCommands, [
33
+ ])
@@ -0,0 +1,253 @@
1
+ import { BaseCommand, GeneralCommands } from '@/encoders'
2
+ import { ZodError } from 'zod'
3
+ import { CustomError, decToHex, dec2hex } from '@/utils'
4
+ import { MultiSensorCommandTypes, DeviceCommandSchemas } from '@/encoders/types'
5
+
6
+ export class MultiSensorCommands extends GeneralCommands {
7
+ static setLightState(params: MultiSensorCommandTypes.SetLightStateParams) {
8
+ try {
9
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setLightState.parse(params)
10
+ const { enabled } = params
11
+ const enabledValue = enabled ? 1 : 0
12
+ return new BaseCommand('SetLightState', 0x1e, decToHex(enabledValue))
13
+ } catch (e) {
14
+ if (e instanceof ZodError) {
15
+ throw new CustomError({
16
+ message: 'Zod validation error during SetLightState execution',
17
+ command: 'SetLightState',
18
+ originalError: e,
19
+ })
20
+ } else {
21
+ throw new CustomError({
22
+ message: 'Error during SetLightState execution',
23
+ command: 'SetLightState',
24
+ originalError: e as Error,
25
+ })
26
+ }
27
+ }
28
+ }
29
+
30
+ static getLightState() {
31
+ return new BaseCommand('GetLightState', 0x1f)
32
+ }
33
+
34
+ static setHallState(params: MultiSensorCommandTypes.SetHallStateParams) {
35
+ try {
36
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setHallState.parse(params)
37
+ const { enabled } = params
38
+ const enabledValue = enabled ? 1 : 0
39
+ return new BaseCommand('SetHallState', 0x20, decToHex(enabledValue))
40
+ } catch (e) {
41
+ if (e instanceof ZodError) {
42
+ throw new CustomError({
43
+ message: 'Zod validation error during SetHallState execution',
44
+ command: 'SetHallState',
45
+ originalError: e,
46
+ })
47
+ } else {
48
+ throw new CustomError({
49
+ message: 'Error during SetHallState execution',
50
+ command: 'SetHallState',
51
+ originalError: e as Error,
52
+ })
53
+ }
54
+ }
55
+ }
56
+
57
+ static getHallState() {
58
+ return new BaseCommand('GetHallState', 0x21)
59
+ }
60
+
61
+ static setHallBlindPeriod(params: MultiSensorCommandTypes.SetHallBlindPeriodParams) {
62
+ try {
63
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setHallBlindPeriod.parse(params)
64
+ const { period } = params
65
+ return new BaseCommand('SetHallBlindPeriod', 0x22, dec2hex(period))
66
+ } catch (e) {
67
+ if (e instanceof ZodError) {
68
+ throw new CustomError({
69
+ message: 'Zod validation error during SetHallBlindPeriod execution',
70
+ command: 'SetHallBlindPeriod',
71
+ originalError: e,
72
+ })
73
+ } else {
74
+ throw new CustomError({
75
+ message: 'Error during SetHallBlindPeriod execution',
76
+ command: 'SetHallBlindPeriod',
77
+ originalError: e as Error,
78
+ })
79
+ }
80
+ }
81
+ }
82
+
83
+ static getHallBlindPeriod() {
84
+ return new BaseCommand('GetHallBlindPeriod', 0x23)
85
+ }
86
+
87
+ static setMicrophoneState(params: MultiSensorCommandTypes.SetMicrophoneStateParams) {
88
+ try {
89
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setMicrophoneState.parse(params)
90
+ const { enabled } = params
91
+ const enabledValue = enabled ? 1 : 0
92
+ return new BaseCommand('SetMicrophoneState', 0x24, decToHex(enabledValue))
93
+ } catch (e) {
94
+ if (e instanceof ZodError) {
95
+ throw new CustomError({
96
+ message: 'Zod validation error during SetMicrophoneState execution',
97
+ command: 'SetMicrophoneState',
98
+ originalError: e,
99
+ })
100
+ } else {
101
+ throw new CustomError({
102
+ message: 'Error during SetMicrophoneState execution',
103
+ command: 'SetMicrophoneState',
104
+ originalError: e as Error,
105
+ })
106
+ }
107
+ }
108
+ }
109
+
110
+ static getMicrophoneState() {
111
+ return new BaseCommand('GetMicrophoneState', 0x25)
112
+ }
113
+
114
+ static setMicrophoneSamplingPeriod(params: MultiSensorCommandTypes.SetMicrophoneSamplingPeriodParams) {
115
+ try {
116
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setMicrophoneSamplingPeriod.parse(params)
117
+ const { period } = params
118
+ return new BaseCommand('SetMicrophoneSamplingPeriod', 0x26, dec2hex(period))
119
+ } catch (e) {
120
+ if (e instanceof ZodError) {
121
+ throw new CustomError({
122
+ message: 'Zod validation error during SetMicrophoneSamplingPeriod execution',
123
+ command: 'SetMicrophoneSamplingPeriod',
124
+ originalError: e,
125
+ })
126
+ } else {
127
+ throw new CustomError({
128
+ message: 'Error during SetMicrophoneSamplingPeriod execution',
129
+ command: 'SetMicrophoneSamplingPeriod',
130
+ originalError: e as Error,
131
+ })
132
+ }
133
+ }
134
+ }
135
+
136
+ static getMicrophoneSamplingPeriod() {
137
+ return new BaseCommand('GetMicrophoneSamplingPeriod', 0x27)
138
+ }
139
+
140
+ static setGasState(params: MultiSensorCommandTypes.SetGasStateParams) {
141
+ try {
142
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setGasState.parse(params)
143
+ const { enabled } = params
144
+ const enabledValue = enabled ? 1 : 0
145
+ return new BaseCommand('SetGasState', 0x28, decToHex(enabledValue))
146
+ } catch (e) {
147
+ if (e instanceof ZodError) {
148
+ throw new CustomError({
149
+ message: 'Zod validation error during SetGasState execution',
150
+ command: 'SetGasState',
151
+ originalError: e,
152
+ })
153
+ } else {
154
+ throw new CustomError({
155
+ message: 'Error during SetGasState execution',
156
+ command: 'SetGasState',
157
+ originalError: e as Error,
158
+ })
159
+ }
160
+ }
161
+ }
162
+
163
+ static getGasState() {
164
+ return new BaseCommand('GetGasState', 0x29)
165
+ }
166
+
167
+ static setGasMeasurementPeriod(params: MultiSensorCommandTypes.SetGasMeasurementPeriodParams) {
168
+ try {
169
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setGasMeasurementPeriod.parse(params)
170
+ const { minutes } = params
171
+ return new BaseCommand('SetGasMeasurementPeriod', 0x2a, decToHex(minutes))
172
+ } catch (e) {
173
+ if (e instanceof ZodError) {
174
+ throw new CustomError({
175
+ message: 'Zod validation error during SetGasMeasurementPeriod execution',
176
+ command: 'SetGasMeasurementPeriod',
177
+ originalError: e,
178
+ })
179
+ } else {
180
+ throw new CustomError({
181
+ message: 'Error during SetGasMeasurementPeriod execution',
182
+ command: 'SetGasMeasurementPeriod',
183
+ originalError: e as Error,
184
+ })
185
+ }
186
+ }
187
+ }
188
+
189
+ static getGasMeasurementPeriod() {
190
+ return new BaseCommand('GetGasMeasurementPeriod', 0x2b)
191
+ }
192
+
193
+ static setPirState(params: MultiSensorCommandTypes.SetPirStateParams) {
194
+ try {
195
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setPirState.parse(params)
196
+ const { enabled } = params
197
+ const enabledValue = enabled ? 1 : 0
198
+ return new BaseCommand('SetPirState', 0x2c, decToHex(enabledValue))
199
+ } catch (e) {
200
+ if (e instanceof ZodError) {
201
+ throw new CustomError({
202
+ message: 'Zod validation error during SetPirState execution',
203
+ command: 'SetPirState',
204
+ originalError: e,
205
+ })
206
+ } else {
207
+ throw new CustomError({
208
+ message: 'Error during SetPirState execution',
209
+ command: 'SetPirState',
210
+ originalError: e as Error,
211
+ })
212
+ }
213
+ }
214
+ }
215
+
216
+ static getPirState() {
217
+ return new BaseCommand('GetPirState', 0x2d)
218
+ }
219
+
220
+ static setPirActiveReportingPeriod(params: MultiSensorCommandTypes.SetPirActiveReportingPeriodParams) {
221
+ try {
222
+ DeviceCommandSchemas.MultiSensorCommandSchemas.setPirActiveReportingPeriod.parse(params)
223
+ const { time } = params
224
+ return new BaseCommand('SetPirActiveReportingPeriod', 0x2e, dec2hex(time))
225
+ } catch (e) {
226
+ if (e instanceof ZodError) {
227
+ throw new CustomError({
228
+ message: 'Zod validation error during SetPirActiveReportingPeriod execution',
229
+ command: 'SetPirActiveReportingPeriod',
230
+ originalError: e,
231
+ })
232
+ } else {
233
+ throw new CustomError({
234
+ message: 'Error during SetPirActiveReportingPeriod execution',
235
+ command: 'SetPirActiveReportingPeriod',
236
+ originalError: e as Error,
237
+ })
238
+ }
239
+ }
240
+ }
241
+
242
+ static getPirActiveReportingPeriod() {
243
+ return new BaseCommand('GetPirActiveReportingPeriod', 0x2f)
244
+ }
245
+
246
+ static getRegion() {
247
+ return new BaseCommand('GetRegion', 0xa4)
248
+ }
249
+
250
+ static restart() {
251
+ return new BaseCommand('Restart', 0xa5)
252
+ }
253
+ }
@@ -24,4 +24,6 @@ export * from './ButtonCommands'
24
24
  export * from './Relay16DryCommands'
25
25
  export * from './HTPirLiteCommands'
26
26
  export * from './Co2PirLiteCommands'
27
+ export * from './MelissaCommands'
28
+ export * from './MultiSensorCommands'
27
29
 
@@ -493,7 +493,7 @@ const VickiCommandSchemas = {
493
493
  targetTemperature: z.union([
494
494
  z.literal(0), // To disable the feature
495
495
  z.number().min(5).max(30).multipleOf(0.1), // Temperature range 5.0-30.0°C with 0.1°C increments
496
- ])
496
+ ]),
497
497
  }),
498
498
  setInternalAlgoTemporaryState: z.object({
499
499
  enabled: z.boolean(), // true = enable algorithm (00), false = disable algorithm temporarily (01)
@@ -504,11 +504,11 @@ const VickiCommandSchemas = {
504
504
  scaleLevel2: z.number().min(5).max(30).multipleOf(0.1),
505
505
  scaleLevel3: z.number().min(5).max(30).multipleOf(0.1),
506
506
  scaleLevel4: z.number().min(5).max(30).multipleOf(0.1),
507
- scaleLevel5: z.number().min(5).max(30).multipleOf(0.1)
507
+ scaleLevel5: z.number().min(5).max(30).multipleOf(0.1),
508
508
  }),
509
509
  setLedIndicationDuration: z.object({
510
- duration: z.number().min(0.5).max(20).multipleOf(0.5) // Duration in range [0.5:20] with 0.5s resolution
511
- })
510
+ duration: z.number().min(0.5).max(20).multipleOf(0.5), // Duration in range [0.5:20] with 0.5s resolution
511
+ }),
512
512
  }
513
513
 
514
514
  export namespace VickiCommandTypes {
@@ -1377,6 +1377,74 @@ export namespace Co2PirLiteCommandTypes {
1377
1377
  >
1378
1378
  export type RestartDeviceParams = z.infer<typeof Co2PirLiteCommandSchemas.restartDevice>
1379
1379
  }
1380
+
1381
+ /* --------------------------------------- Melissa --------------------------------------- */
1382
+ const MelissaCommandSchemas = {
1383
+ ...GeneralCommandSchemas,
1384
+ setIrCodeRecording: z.object({
1385
+ time: z.number().min(0).max(255),
1386
+ }),
1387
+ }
1388
+
1389
+ export namespace MelissaCommandTypes {
1390
+ export type SetIrCodeRecordingParams = z.infer<typeof MelissaCommandSchemas.setIrCodeRecording>
1391
+ }
1392
+
1393
+ /* --------------------------------------- MULTISENSOR COMMANDS --------------------------------------- */
1394
+ const MultiSensorCommandSchemas = {
1395
+ ...GeneralCommandSchemas,
1396
+ setLightState: z.object({
1397
+ enabled: z.boolean(),
1398
+ }),
1399
+ getLightState: z.object({}),
1400
+ setHallState: z.object({
1401
+ enabled: z.boolean(),
1402
+ }),
1403
+ getHallState: z.object({}),
1404
+ setHallBlindPeriod: z.object({
1405
+ period: z.number().min(0).max(65535),
1406
+ }),
1407
+ getHallBlindPeriod: z.object({}),
1408
+ setMicrophoneState: z.object({
1409
+ enabled: z.boolean(),
1410
+ }),
1411
+ getMicrophoneState: z.object({}),
1412
+ setMicrophoneSamplingPeriod: z.object({
1413
+ period: z.number().min(0).max(65535),
1414
+ }),
1415
+ getMicrophoneSamplingPeriod: z.object({}),
1416
+ setGasState: z.object({
1417
+ enabled: z.boolean(),
1418
+ }),
1419
+ getGasState: z.object({}),
1420
+ setGasMeasurementPeriod: z.object({
1421
+ minutes: z.number().min(0).max(255),
1422
+ }),
1423
+ getGasMeasurementPeriod: z.object({}),
1424
+ setPirState: z.object({
1425
+ enabled: z.boolean(),
1426
+ }),
1427
+ getPirState: z.object({}),
1428
+ setPirActiveReportingPeriod: z.object({
1429
+ time: z.number().min(0).max(65535),
1430
+ }),
1431
+ getPirActiveReportingPeriod: z.object({}),
1432
+ getRegion: z.object({}),
1433
+ restart: z.object({}),
1434
+ }
1435
+
1436
+ export namespace MultiSensorCommandTypes {
1437
+ export type SetLightStateParams = z.infer<typeof MultiSensorCommandSchemas.setLightState>
1438
+ export type SetHallStateParams = z.infer<typeof MultiSensorCommandSchemas.setHallState>
1439
+ export type SetHallBlindPeriodParams = z.infer<typeof MultiSensorCommandSchemas.setHallBlindPeriod>
1440
+ export type SetMicrophoneStateParams = z.infer<typeof MultiSensorCommandSchemas.setMicrophoneState>
1441
+ export type SetMicrophoneSamplingPeriodParams = z.infer<typeof MultiSensorCommandSchemas.setMicrophoneSamplingPeriod>
1442
+ export type SetGasStateParams = z.infer<typeof MultiSensorCommandSchemas.setGasState>
1443
+ export type SetGasMeasurementPeriodParams = z.infer<typeof MultiSensorCommandSchemas.setGasMeasurementPeriod>
1444
+ export type SetPirStateParams = z.infer<typeof MultiSensorCommandSchemas.setPirState>
1445
+ export type SetPirActiveReportingPeriodParams = z.infer<typeof MultiSensorCommandSchemas.setPirActiveReportingPeriod>
1446
+ }
1447
+
1380
1448
  /* --------------------------------------- EXPORT ALL SCHEMA GROUPS --------------------------------------- */
1381
1449
  export const DeviceCommandSchemas = {
1382
1450
  GeneralCommandSchemas,
@@ -1402,4 +1470,6 @@ export const DeviceCommandSchemas = {
1402
1470
  Relay16DryCommandSchemas,
1403
1471
  HTPirLiteCommandSchemas,
1404
1472
  Co2PirLiteCommandSchemas,
1473
+ MelissaCommandSchemas,
1474
+ MultiSensorCommandSchemas,
1405
1475
  }
@@ -824,23 +824,42 @@ describe('ADS payload decoder', () => {
824
824
  })
825
825
  describe('Melissa payload decoder', () => {
826
826
  test('simple keepalive', () => {
827
- expect(uplinkPayloadParser('010031010A1D0218A4', DeviceType.MelissaLorawan)).toStrictEqual({
828
- internalTemperature: 0,
829
- energy: 822151.709,
830
- power: 536,
831
- acVoltage: 164
832
- })
827
+ expect(uplinkPayloadParser('01027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
828
+ sensorTemperature: 23.8,
829
+ relativeHumidity: 27.34,
830
+ isIrCodeRecordingRequested: false
831
+ })
833
832
 
834
833
  })
835
834
  test('keepalive with response of preloaded code', () => {
836
- expect(uplinkPayloadParser('060042010031010A1D0218A4', DeviceType.MelissaLorawan)).toStrictEqual({
837
- internalTemperature: 0,
838
- energy: 822151.709,
839
- power: 536,
840
- acVoltage: 164,
835
+ expect(uplinkPayloadParser('06004201027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
836
+ sensorTemperature: 23.8,
837
+ relativeHumidity: 27.34,
838
+ isIrCodeRecordingRequested: false,
841
839
  preloadedCode: { codeAddress: 66, codeAddressRaw: '0042' }
842
840
  })
843
841
  })
842
+ test('keepalive with response of recorded code', () => {
843
+ expect(uplinkPayloadParser('0a0020002001027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
844
+ sensorTemperature: 23.8,
845
+ relativeHumidity: 27.34,
846
+ isIrCodeRecordingRequested: false,
847
+ recordedIrInfo: { recordedIrCodeSize: 32, bytesSent: '0020' }
848
+ })
849
+ })
850
+ test('keepalive with response of transfered code', () => {
851
+ expect(uplinkPayloadParser('0b2200000063320612062e062e000606000508120004040008080005180008081100050c01027E4600', DeviceType.MelissaLorawan)).toStrictEqual({
852
+ sensorTemperature: 23.8,
853
+ relativeHumidity: 27.34,
854
+ isIrCodeRecordingRequested: false,
855
+ irCodeData: {
856
+ bytesCount: 32,
857
+ address: 0,
858
+ data: "0063320612062e062e000606000508120004040008080005180008081100050c"
859
+ }
860
+ })
861
+ })
862
+
844
863
  })
845
864
  describe('CO2PirLite payload decoder', () => {
846
865
  test('simple keepalive', () => {
@@ -816,3 +816,11 @@ describe('HT PIR Lite Commands payload encoder', () => {
816
816
  expect(commandBuilder.build('RestartDevice')).toStrictEqual(new BaseCommand('RestartDevice', 0xa5))
817
817
  })
818
818
  })
819
+ describe('Melissa Commands payload encoder', () => {
820
+ const commandBuilder = new CommandBuilder('melissa_lorawan')
821
+ test('Set IR Code Recording ', () => {
822
+ expect(commandBuilder.build('SetIrCodeRecording', { time: 15 })).toStrictEqual(
823
+ new BaseCommand('SetIrCodeRecording', 0x09, '0F'),
824
+ )
825
+ })
826
+ })