mclimate-payload-helper 1.2.1 → 1.2.3

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 (70) hide show
  1. package/dist/decoders/commandsReadingHelper.d.ts.map +1 -1
  2. package/dist/decoders/commandsReadingHelper.js +214 -121
  3. package/dist/decoders/commandsReadingHelper.js.map +1 -1
  4. package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.d.ts.map +1 -1
  5. package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.js +12 -12
  6. package/dist/decoders/payloadParsers/melissaLorawanPayloadParser.js.map +1 -1
  7. package/dist/encoders/Co2DisplayCommonCommands.d.ts +4 -0
  8. package/dist/encoders/Co2DisplayCommonCommands.d.ts.map +1 -1
  9. package/dist/encoders/Co2DisplayCommonCommands.js +38 -0
  10. package/dist/encoders/Co2DisplayCommonCommands.js.map +1 -1
  11. package/dist/encoders/CommandBuilder.d.ts.map +1 -1
  12. package/dist/encoders/CommandBuilder.js +1 -0
  13. package/dist/encoders/CommandBuilder.js.map +1 -1
  14. package/dist/encoders/MelissaCommands.d.ts +6 -0
  15. package/dist/encoders/MelissaCommands.d.ts.map +1 -0
  16. package/dist/encoders/MelissaCommands.js +35 -0
  17. package/dist/encoders/MelissaCommands.js.map +1 -0
  18. package/dist/encoders/index.d.ts +1 -0
  19. package/dist/encoders/index.d.ts.map +1 -1
  20. package/dist/encoders/index.js +1 -0
  21. package/dist/encoders/index.js.map +1 -1
  22. package/dist/encoders/types/schemas.d.ts +105 -0
  23. package/dist/encoders/types/schemas.d.ts.map +1 -1
  24. package/dist/encoders/types/schemas.js +5 -0
  25. package/dist/encoders/types/schemas.js.map +1 -1
  26. package/dist/test/payloadDecoders.test.js +28 -10
  27. package/dist/test/payloadDecoders.test.js.map +1 -1
  28. package/dist/test/payloadEncoders.test.js +6 -0
  29. package/dist/test/payloadEncoders.test.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/decoders/commandsReadingHelper.ts +204 -122
  32. package/src/decoders/payloadParsers/melissaLorawanPayloadParser.ts +19 -16
  33. package/src/encoders/CommandBuilder.ts +2 -0
  34. package/src/encoders/MelissaCommands.ts +33 -0
  35. package/src/encoders/index.ts +1 -0
  36. package/src/encoders/types/schemas.ts +17 -0
  37. package/src/test/payloadDecoders.test.ts +30 -11
  38. package/src/test/payloadEncoders.test.ts +8 -0
  39. package/dist/encoders/FanCoilThermostat.d.ts +0 -61
  40. package/dist/encoders/FanCoilThermostat.d.ts.map +0 -1
  41. package/dist/encoders/FanCoilThermostat.js +0 -719
  42. package/dist/encoders/FanCoilThermostat.js.map +0 -1
  43. package/dist/encoders/SetAqiLed.d.ts +0 -6
  44. package/dist/encoders/SetAqiLed.d.ts.map +0 -1
  45. package/dist/encoders/SetAqiLed.js +0 -46
  46. package/dist/encoders/SetAqiLed.js.map +0 -1
  47. package/dist/encoders/types/enumValidation.d.ts +0 -6
  48. package/dist/encoders/types/enumValidation.d.ts.map +0 -1
  49. package/dist/encoders/types/enumValidation.js +0 -50
  50. package/dist/encoders/types/enumValidation.js.map +0 -1
  51. package/dist/encoders/types/schemaValidatedEnums.d.ts +0 -40
  52. package/dist/encoders/types/schemaValidatedEnums.d.ts.map +0 -1
  53. package/dist/encoders/types/schemaValidatedEnums.js +0 -181
  54. package/dist/encoders/types/schemaValidatedEnums.js.map +0 -1
  55. package/dist/types/deviceTypes.d.ts +0 -19
  56. package/dist/types/deviceTypes.d.ts.map +0 -1
  57. package/dist/types/deviceTypes.js +0 -23
  58. package/dist/types/deviceTypes.js.map +0 -1
  59. package/dist/types/index.d.ts +0 -2
  60. package/dist/types/index.d.ts.map +0 -1
  61. package/dist/types/index.js +0 -18
  62. package/dist/types/index.js.map +0 -1
  63. package/dist/types/types.d.ts +0 -13
  64. package/dist/types/types.d.ts.map +0 -1
  65. package/dist/types/types.js +0 -9
  66. package/dist/types/types.js.map +0 -1
  67. package/dist/validation.d.ts +0 -3
  68. package/dist/validation.d.ts.map +0 -1
  69. package/dist/validation.js +0 -8
  70. package/dist/validation.js.map +0 -1
@@ -198,6 +198,68 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
198
198
  }
199
199
  }
200
200
  break
201
+ case '0a':
202
+ {
203
+ try {
204
+ let data;
205
+ if (deviceType === DeviceType.MelissaLorawan) {
206
+ command_len = 4;
207
+ let recordedIrCodeSize = (parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16)
208
+ let bytesSent = `${commands[i + 3]}${commands[i + 4]}`
209
+ data = { recordedIrInfo: { recordedIrCodeSize, bytesSent } }
210
+ Object.assign(resultToPass, { ...resultToPass }, { ...data })
211
+ }
212
+ } catch (e) {
213
+ throw new CustomError({
214
+ message: `Failed to process command '0a'`,
215
+ hexData,
216
+ command,
217
+ deviceType,
218
+ originalError: e as Error,
219
+ })
220
+ }
221
+ }
222
+ break;
223
+ case '0b':
224
+ {
225
+ try {
226
+ let data;
227
+ if (deviceType === DeviceType.MelissaLorawan) {
228
+ // First byte after command indicates how many bytes follow
229
+ const bytesCount = parseInt(commands[i + 1], 16);
230
+ command_len = 1 + bytesCount; // 1 for bytesCount + the actual bytes
231
+
232
+ // Next two bytes are buffer offset
233
+ const bufferOffsetMSB = parseInt(commands[i + 2], 16);
234
+ const bufferOffsetLSB = parseInt(commands[i + 3], 16);
235
+ const address = (bufferOffsetMSB << 8) | bufferOffsetLSB;
236
+
237
+ // The rest is IR code data
238
+ let irCodeDataString = '';
239
+ for (let j = 4; j < 4 + bytesCount - 2; j++) { // -2 to account for the offset bytes
240
+ irCodeDataString += commands[i + j];
241
+ }
242
+
243
+ data = {
244
+ irCodeData: {
245
+ bytesCount: bytesCount - 2,
246
+ address,
247
+ data: irCodeDataString
248
+ }
249
+ };
250
+ Object.assign(resultToPass, { ...resultToPass }, { ...data });
251
+ }
252
+ } catch (e) {
253
+ throw new CustomError({
254
+ message: `Failed to process command '0b'`,
255
+ hexData,
256
+ command,
257
+ deviceType,
258
+ originalError: e as Error,
259
+ })
260
+ }
261
+ }
262
+ break;
201
263
  case '0e':
202
264
  {
203
265
  try {
@@ -1588,10 +1650,24 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
1588
1650
  {
1589
1651
  try {
1590
1652
  let data
1591
- command_len = 3
1592
- let state = parseInt(commands[i + 1], 16)
1593
- let time = (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16)
1594
- data = { relayTimerInSeconds: { state, time } }
1653
+ if (deviceType === DeviceType.Vicki) {
1654
+ command_len = 2;
1655
+ let notificationByte = parseInt(commands[i + 1], 16);
1656
+ // Extract notification flags from bits
1657
+ let temperatureRestoredAfterManualBoost = !!(notificationByte & 0x01); // Bit 0
1658
+ let temperatureChangedByHeatingSchedule = !!(notificationByte & 0x02); // Bit 1
1659
+ data = {
1660
+ notifications: {
1661
+ temperatureRestoredAfterManualBoost: temperatureRestoredAfterManualBoost,
1662
+ temperatureChangedByHeatingSchedule: temperatureChangedByHeatingSchedule
1663
+ }
1664
+ };
1665
+ }else{
1666
+ command_len = 3
1667
+ let state = parseInt(commands[i + 1], 16)
1668
+ let time = (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16)
1669
+ data = { relayTimerInSeconds: { state, time } }
1670
+ }
1595
1671
  Object.assign(resultToPass, { ...resultToPass }, { ...data })
1596
1672
  } catch (e) {
1597
1673
  throw new CustomError({
@@ -1656,6 +1732,9 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
1656
1732
  if (deviceType === DeviceType.FanCoilThermostat) {
1657
1733
  command_len = 1
1658
1734
  data = { fanSpeedNotOccupied: parseInt(commands[i + 1], 16) }
1735
+ }else{
1736
+ command_len = 1
1737
+ data = { manualChangeRelayState: parseInt(commands[i + 1], 16) }
1659
1738
  }
1660
1739
  Object.assign(resultToPass, { ...resultToPass }, { ...data })
1661
1740
  } catch (e) {
@@ -1701,6 +1780,11 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
1701
1780
  data = {
1702
1781
  overheatingEvents: { events: parseInt(commands[i + 1], 16), temperature: parseInt(commands[i + 2], 16) },
1703
1782
  }
1783
+ } else if (deviceType === DeviceType.Vicki) {
1784
+ command_len = 1;
1785
+ let offsetByte = parseInt(commands[i + 1], 16);
1786
+ let offsetHours = (offsetByte & 0x80) ? (offsetByte - 256) : offsetByte;
1787
+ data = { deviceTimeZone: offsetHours };
1704
1788
  }
1705
1789
  Object.assign(resultToPass, { ...resultToPass }, { ...data })
1706
1790
  } catch (e) {
@@ -1754,6 +1838,10 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
1754
1838
  current: (parseInt(commands[i + 2], 16) << 8) | parseInt(commands[i + 3], 16),
1755
1839
  },
1756
1840
  }
1841
+ }else{
1842
+ command_len = 1;
1843
+ let timeValue = parseInt(commands[i + 1], 16);
1844
+ data = { autoSetpointRestoreStatus: timeValue === 0 ? 0 : timeValue * 10 };
1757
1845
  }
1758
1846
  Object.assign(resultToPass, { ...resultToPass }, { ...data })
1759
1847
  } catch (e) {
@@ -1893,6 +1981,9 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
1893
1981
  if (deviceType === DeviceType.FanCoilThermostat) {
1894
1982
  command_len = 1
1895
1983
  data = { frostProtectionStatus: parseInt(commands[i + 1], 16) }
1984
+ }else{
1985
+ command_len = 1;
1986
+ data = { timeRequestByMACcommand: parseInt(commands[i + 1], 16) };
1896
1987
  }
1897
1988
  Object.assign(resultToPass, { ...resultToPass }, { ...data })
1898
1989
  } catch (e) {
@@ -2307,23 +2398,6 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
2307
2398
  }
2308
2399
  }
2309
2400
  break
2310
- case '58': {
2311
- command_len = 1;
2312
- let notificationByte = parseInt(commands[i + 1], 16);
2313
-
2314
- // Extract notification flags from bits
2315
- let temperatureRestoredAfterManualBoost = !!(notificationByte & 0x01); // Bit 0
2316
- let temperatureChangedByHeatingSchedule = !!(notificationByte & 0x02); // Bit 1
2317
-
2318
- let data = {
2319
- notifications: {
2320
- temperatureRestoredAfterManualBoost: temperatureRestoredAfterManualBoost,
2321
- temperatureChangedByHeatingSchedule: temperatureChangedByHeatingSchedule
2322
- }
2323
- };
2324
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2325
- }
2326
- break;
2327
2401
  case '5a':
2328
2402
  {
2329
2403
  try {
@@ -2441,24 +2515,6 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
2441
2515
  }
2442
2516
  }
2443
2517
  break
2444
- case '5d':
2445
- {
2446
- try {
2447
- command_len = 1
2448
- let data = { manualChangeRelayState: parseInt(commands[i + 1], 16) }
2449
-
2450
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2451
- } catch (e) {
2452
- throw new CustomError({
2453
- message: `Failed to process command '5d'`,
2454
- hexData,
2455
- command,
2456
- deviceType,
2457
- originalError: e as Error,
2458
- })
2459
- }
2460
- }
2461
- break
2462
2518
  case '5e':
2463
2519
  {
2464
2520
  try {
@@ -2487,116 +2543,142 @@ export const commandsReadingHelper = (hexData: string, payloadLength: number, de
2487
2543
  })
2488
2544
  }
2489
2545
  }
2490
- break
2491
- case '60': {
2492
- command_len = 1;
2493
- let offsetByte = parseInt(commands[i + 1], 16);
2494
- let offsetHours = (offsetByte & 0x80) ? (offsetByte - 256) : offsetByte;
2495
- let data = { deviceTimeZone: offsetHours };
2496
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2497
- }
2498
2546
  break;
2499
- case '62': {
2500
- command_len = 1;
2501
- let timeValue = parseInt(commands[i + 1], 16);
2502
- let data = { autoSetpointRestoreStatus: timeValue === 0 ? 0 : timeValue * 10 };
2503
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2504
- }
2505
- break;
2506
-
2507
2547
  case '64': {
2508
- command_len = 1;
2509
- let ledDurationValue = parseInt(commands[i + 1], 16);
2510
- let durationInSeconds = ledDurationValue / 2; // As per the docs, value is divided by 2 to get seconds
2548
+ try {
2549
+ command_len = 1;
2550
+ let ledDurationValue = parseInt(commands[i + 1], 16);
2551
+ let durationInSeconds = ledDurationValue / 2; // As per the docs, value is divided by 2 to get seconds
2511
2552
 
2512
- let data = {
2513
- ledIndicationDuration: durationInSeconds
2514
- };
2515
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2553
+ let data = {
2554
+ ledIndicationDuration: durationInSeconds
2555
+ };
2556
+ Object.assign(resultToPass, { ...resultToPass }, { ...data })
2557
+ } catch (e) {
2558
+ throw new CustomError({
2559
+ message: `Failed to process command '64'`,
2560
+ hexData,
2561
+ command,
2562
+ deviceType,
2563
+ originalError: e as Error,
2564
+ })
2565
+ }
2516
2566
  }
2517
2567
  break;
2518
-
2519
2568
  case '66': {
2520
- command_len = 2;
2569
+ try {
2570
+ command_len = 2;
2521
2571
 
2522
- let tempHigh = parseInt(commands[i + 1], 16);
2523
- let tempLow = parseInt(commands[i + 2], 16);
2524
- let targetTemp = (tempHigh << 8) | tempLow;
2525
- let data = {
2526
- offlineTargetTemperature: targetTemp === 0 ? 0 : targetTemp / 10
2527
- };
2528
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2572
+ let tempHigh = parseInt(commands[i + 1], 16);
2573
+ let tempLow = parseInt(commands[i + 2], 16);
2574
+ let targetTemp = (tempHigh << 8) | tempLow;
2575
+ let data = {
2576
+ offlineTargetTemperature: targetTemp === 0 ? 0 : targetTemp / 10
2577
+ };
2578
+ Object.assign(resultToPass, { ...resultToPass }, { ...data })
2579
+ } catch (e) {
2580
+ throw new CustomError({
2581
+ message: `Failed to process command '66'`,
2582
+ hexData,
2583
+ command,
2584
+ deviceType,
2585
+ originalError: e as Error,
2586
+ })
2587
+ }
2529
2588
  }
2530
2589
  break;
2531
2590
  case '68': {
2532
- command_len = 1;
2533
- let data = { internalAlgoTemporaryState: parseInt(commands[i + 1], 16) === 0 ? true : false };
2534
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2591
+ try {
2592
+ command_len = 1;
2593
+ let data = { internalAlgoTemporaryState: parseInt(commands[i + 1], 16) === 0 ? true : false };
2594
+ Object.assign(resultToPass, { ...resultToPass }, { ...data })
2595
+ } catch (e) {
2596
+ throw new CustomError({
2597
+ message: `Failed to process command '68'`,
2598
+ hexData,
2599
+ command,
2600
+ deviceType,
2601
+ originalError: e as Error,
2602
+ })
2603
+ }
2535
2604
  }
2536
2605
  break;
2537
2606
  case '6a': {
2538
- command_len = 12;
2539
- let temperatureLevels: { [key: string]: number } = {};
2607
+ try {
2608
+ command_len = 12;
2609
+ let temperatureLevels: { [key: string]: number } = {};
2540
2610
 
2541
- // Process 6 scale levels (0-5), each with a 2-byte temperature value
2542
- for (let level = 0; level < 6; level++) {
2543
- let tempHighByte = parseInt(commands[i + 1 + (level * 2)], 16);
2544
- let tempLowByte = parseInt(commands[i + 2 + (level * 2)], 16);
2545
- let tempValue = (tempHighByte << 8) | tempLowByte;
2611
+ // Process 6 scale levels (0-5), each with a 2-byte temperature value
2612
+ for (let level = 0; level < 6; level++) {
2613
+ let tempHighByte = parseInt(commands[i + 1 + (level * 2)], 16);
2614
+ let tempLowByte = parseInt(commands[i + 2 + (level * 2)], 16);
2615
+ let tempValue = (tempHighByte << 8) | tempLowByte;
2546
2616
 
2547
- // The temperature values are pre-multiplied by 10
2548
- temperatureLevels['level' + level] = tempValue / 10;
2549
- }
2617
+ // The temperature values are pre-multiplied by 10
2618
+ temperatureLevels['level' + level] = tempValue / 10;
2619
+ }
2550
2620
 
2551
- let data = {
2552
- temperatureLevels: temperatureLevels
2553
- };
2621
+ let data = {
2622
+ temperatureLevels: temperatureLevels
2623
+ };
2554
2624
 
2555
- Object.assign(resultToPass, { ...resultToPass }, { ...data })
2625
+ Object.assign(resultToPass, { ...resultToPass }, { ...data })
2626
+ } catch (e) {
2627
+ throw new CustomError({
2628
+ message: `Failed to process command '6a'`,
2629
+ hexData,
2630
+ command,
2631
+ deviceType,
2632
+ originalError: e as Error,
2633
+ })
2634
+ }
2556
2635
  }
2557
2636
  break;
2558
2637
  case '6c': {
2559
- command_len = 4;
2560
- let eventsHighByte = parseInt(commands[i + 2], 16); // Events 19-16
2561
- let eventsMidByte = parseInt(commands[i + 3], 16); // Events 15-8
2562
- let eventsLowByte = parseInt(commands[i + 4], 16); // Events 7-0
2638
+ try {
2639
+ command_len = 4;
2640
+ let eventsHighByte = parseInt(commands[i + 2], 16); // Events 19-16
2641
+ let eventsMidByte = parseInt(commands[i + 3], 16); // Events 15-8
2642
+ let eventsLowByte = parseInt(commands[i + 4], 16); // Events 7-0
2563
2643
 
2564
- // Create a more structured and readable format for heating events
2565
- let heatingEventStates: { [key: string]: boolean } = {};
2644
+ // Create a more structured and readable format for heating events
2645
+ let heatingEventStates: { [key: string]: boolean } = {};
2566
2646
 
2567
- // Process all 20 events (0-19) in a single loop
2568
- for (let eventIdx = 0; eventIdx < 20; eventIdx++) {
2569
- // Calculate which bit to check
2570
- let bitPosition;
2571
- if (eventIdx >= 16) {
2572
- // Events 16-19 in high byte
2573
- bitPosition = eventIdx - 16;
2574
- heatingEventStates[eventIdx] = !!(eventsHighByte & (1 << bitPosition));
2575
- } else if (eventIdx >= 8) {
2576
- // Events 8-15 in mid byte
2577
- bitPosition = eventIdx - 8;
2578
- heatingEventStates[eventIdx] = !!(eventsMidByte & (1 << bitPosition));
2579
- } else {
2580
- // Events 0-7 in low byte
2581
- bitPosition = eventIdx;
2582
- heatingEventStates[eventIdx] = !!(eventsLowByte & (1 << bitPosition));
2647
+ // Process all 20 events (0-19) in a single loop
2648
+ for (let eventIdx = 0; eventIdx < 20; eventIdx++) {
2649
+ // Calculate which bit to check
2650
+ let bitPosition;
2651
+ if (eventIdx >= 16) {
2652
+ // Events 16-19 in high byte
2653
+ bitPosition = eventIdx - 16;
2654
+ heatingEventStates[eventIdx] = !!(eventsHighByte & (1 << bitPosition));
2655
+ } else if (eventIdx >= 8) {
2656
+ // Events 8-15 in mid byte
2657
+ bitPosition = eventIdx - 8;
2658
+ heatingEventStates[eventIdx] = !!(eventsMidByte & (1 << bitPosition));
2659
+ } else {
2660
+ // Events 0-7 in low byte
2661
+ bitPosition = eventIdx;
2662
+ heatingEventStates[eventIdx] = !!(eventsLowByte & (1 << bitPosition));
2663
+ }
2583
2664
  }
2584
- }
2585
2665
 
2586
- let data = {
2587
- heatingEventStates: heatingEventStates
2588
- };
2666
+ let data = {
2667
+ heatingEventStates: heatingEventStates
2668
+ };
2589
2669
 
2590
- Object.assign(resultToPass, { ...resultToPass }, { ...data });
2591
- break;
2592
- }
2593
- case '6e': {
2594
- command_len = 1;
2595
- let data = { timeRequestByMACcommand: parseInt(commands[i + 1], 16) };
2596
- Object.assign(resultToPass, { ...resultToPass }, { ...data });
2670
+ Object.assign(resultToPass, { ...resultToPass }, { ...data });
2671
+ } catch (e) {
2672
+ throw new CustomError({
2673
+ message: `Failed to process command '6c'`,
2674
+ hexData,
2675
+ command,
2676
+ deviceType,
2677
+ originalError: e as Error,
2678
+ })
2679
+ }
2597
2680
  break;
2598
2681
  }
2599
-
2600
2682
  case '9b':
2601
2683
  {
2602
2684
  try {
@@ -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)
@@ -19,6 +19,7 @@ import {
19
19
  Relay16DryCommands,
20
20
  HTPirLiteCommands,
21
21
  Co2PirLiteCommands,
22
+ MelissaCommands,
22
23
  } from '@/encoders'
23
24
 
24
25
  import { CustomError, toCamelCase } from '@/utils'
@@ -48,6 +49,7 @@ export class CommandBuilder {
48
49
  relay_16_dry: Relay16DryCommands,
49
50
  ht_pir_lite: HTPirLiteCommands,
50
51
  co2_pir_lite: Co2PirLiteCommands,
52
+ melissa_lorawan: MelissaCommands,
51
53
  }
52
54
  }
53
55
 
@@ -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
+ ])
@@ -24,4 +24,5 @@ export * from './ButtonCommands'
24
24
  export * from './Relay16DryCommands'
25
25
  export * from './HTPirLiteCommands'
26
26
  export * from './Co2PirLiteCommands'
27
+ export * from './MelissaCommands'
27
28
 
@@ -1377,6 +1377,22 @@ export namespace Co2PirLiteCommandTypes {
1377
1377
  >
1378
1378
  export type RestartDeviceParams = z.infer<typeof Co2PirLiteCommandSchemas.restartDevice>
1379
1379
  }
1380
+
1381
+
1382
+
1383
+ /* --------------------------------------- Melissa --------------------------------------- */
1384
+ const MelissaCommandSchemas = {
1385
+ ...GeneralCommandSchemas,
1386
+ setIrCodeRecording: z.object({
1387
+ time: z.number().min(0).max(255),
1388
+ }),
1389
+ }
1390
+
1391
+ export namespace MelissaCommandTypes {
1392
+ export type SetIrCodeRecordingParams = z.infer<
1393
+ typeof MelissaCommandSchemas.setIrCodeRecording
1394
+ >
1395
+ }
1380
1396
  /* --------------------------------------- EXPORT ALL SCHEMA GROUPS --------------------------------------- */
1381
1397
  export const DeviceCommandSchemas = {
1382
1398
  GeneralCommandSchemas,
@@ -1402,4 +1418,5 @@ export const DeviceCommandSchemas = {
1402
1418
  Relay16DryCommandSchemas,
1403
1419
  HTPirLiteCommandSchemas,
1404
1420
  Co2PirLiteCommandSchemas,
1421
+ MelissaCommandSchemas,
1405
1422
  }
@@ -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
+ })