mclimate-payload-helper 1.3.4 → 1.3.5

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/CONTEXT.md ADDED
@@ -0,0 +1,162 @@
1
+ # CONTEXT.md
2
+
3
+ This file is the canonical, high-level reference for the `mclimate-payload-helper` repository. It is intended for AI assistants and new contributors so they can orient quickly without re-reading the entire codebase.
4
+
5
+ ## 1. Project Overview
6
+
7
+ - **Name:** `mclimate-payload-helper` (npm package)
8
+ - **Version:** `1.3.4` (see `package.json`)
9
+ - **Language / Runtime:** TypeScript, compiled to CommonJS for Node.js consumers
10
+ - **Purpose:** Encode LoRaWAN downlink commands and decode uplink payloads for MClimate IoT devices (Vicki, Relay 16, T-Valve, T-Flood, CO₂ sensors/displays, Fan-Coil thermostat, PIR Mini, etc.).
11
+ - **Distribution:** Published to npm; consumers import `uplinkPayloadParser`, `CommandBuilder`, `DeviceType`, schemas and per-device command classes.
12
+ - **License:** ISC. **Author:** MClimate.
13
+
14
+ ## 2. Tech Stack and Dependencies
15
+
16
+ - **Runtime dep:** `zod` (^3.23.8) — schema validation for command parameters.
17
+ - **Build:** `typescript` (^5.5.4) + `tsc-alias` (resolves `@/*` path aliases in emitted JS) + `typescript-transform-paths` (rewrites paths inside `.d.ts`).
18
+ - **Test:** `jest` (^29) + `ts-jest`.
19
+ - **Lint/Format:** `eslint` (with `@typescript-eslint`), `prettier`, `eslint-config-prettier`, `eslint-plugin-jest`.
20
+ - **Git hooks:** `husky` runs format/type-check/lint/test on pre-commit (see `.husky/pre-commit`).
21
+ - **TS Config:** `target: ES2016`, `module: commonjs`, `strict: true`, `rootDir: ./src`, `outDir: ./dist`, path alias `@/* -> ./src/*` (see `tsconfig.json`).
22
+
23
+ ## 3. Project Structure
24
+
25
+ ```
26
+ .
27
+ ├── src/
28
+ │ ├── index.ts # Public package entry — re-exports parser, CommandBuilder, device commands, schemas, enums, CustomError
29
+ │ ├── decoders/
30
+ │ │ ├── index.ts
31
+ │ │ ├── commandsReadingHelper.ts # Large helper that decodes command-response bytes embedded in keepalives
32
+ │ │ └── payloadParsers/
33
+ │ │ ├── index.ts
34
+ │ │ ├── uplinkPayloadParser.ts # Switch on DeviceType → device-specific parser
35
+ │ │ ├── <Device>PayloadParser.ts # One file per device (Vicki, HTSensor, TFlood, TValve, CO2*, Relay16*, PirMini, MultiSensor, Melissa, etc.)
36
+ │ │ └── types/ # DeviceType enum + Vicki/Relay enum types
37
+ │ ├── encoders/
38
+ │ │ ├── index.ts # Re-exports BaseCommand, CommandBuilder, all device command classes & mixins
39
+ │ │ ├── BaseCommand.ts # Hex serialization (cmdId + params → hex string)
40
+ │ │ ├── CommandBuilder.ts # Registry: device_type → command class; build(command, params), combine([...])
41
+ │ │ ├── GeneralCommands.ts # Shared commands (keepalive, uplinkType, watchdog, region, customHex, joinRetry, deviceVersion)
42
+ │ │ ├── DisplayCommands.ts # Mixin
43
+ │ │ ├── TemperatureCommonCommands.ts # Mixin
44
+ │ │ ├── PIRCommands.ts # Mixin
45
+ │ │ ├── ChildLockCommands.ts # Mixin
46
+ │ │ ├── <Device>Commands.ts # One per device, extends GeneralCommands (+ mixins via applyMixins)
47
+ │ │ ├── <Device>Commands.test.ts # Jest tests colocated with each command class
48
+ │ │ └── types/
49
+ │ │ ├── index.ts
50
+ │ │ └── schemas.ts # All Zod schemas + per-device enums (large, ~82KB)
51
+ │ ├── helpers/ # byteArrayParser, decbin, toBool
52
+ │ ├── utils/ # caseConverter (toCamelCase), customErrorHandler (CustomError), decToHex, delMethods, mixin (applyMixins)
53
+ │ └── test/
54
+ │ └── payloadDecoders.test.ts # Consolidated decoder tests (all devices)
55
+ ├── docs/ # External LoRaWAN API docs (PDFs + how-to-add-new-device.md, PIR Mini API)
56
+ ├── add-encoder-command-existing.md # Step-by-step guide for adding a command to an existing device
57
+ ├── add-encoder-command-new.md # Step-by-step guide for adding a brand-new device
58
+ ├── README.md # Public-facing usage doc
59
+ ├── CLAUDE.md # AI assistant guidance (overlap with this file)
60
+ ├── package.json / package-lock.json
61
+ ├── tsconfig.json
62
+ ├── jest.config.ts
63
+ ├── .eslintrc.cjs / .prettierrc
64
+ └── .husky/pre-commit
65
+ ```
66
+
67
+ ## 4. Core Flows
68
+
69
+ ### 4.1 Uplink decode flow
70
+
71
+ 1. Consumer calls `uplinkPayloadParser(hexData, DeviceType)` (`src/decoders/payloadParsers/uplinkPayloadParser.ts`).
72
+ 2. Switch dispatches to a device-specific parser function (e.g. `vickiPayloadParser`, `pirMiniPayloadParser`).
73
+ 3. The parser reads bytes (hex pairs) from the head, identifies the frame type by the first byte (e.g. keepalive command id), and returns a typed object of decoded fields (temperatures, humidity, battery, status flags, etc.).
74
+ 4. If the frame contains a response to a previously-sent command, the parser delegates remaining bytes to `commandsReadingHelper.ts`, which switches on the response cmdId to populate the appropriate fields.
75
+ 5. **Default fallback:** if `DeviceType` does not match any case, `vickiPayloadParser` is used (see TODO comment in `uplinkPayloadParser.ts`).
76
+
77
+ ### 4.2 Downlink encode flow
78
+
79
+ 1. Consumer constructs `new CommandBuilder(device_type)` (string matching the `DeviceType` enum value, e.g. `'vicki'`).
80
+ 2. Calls `.build(commandName, params?)`.
81
+ - `commandName` is converted via `toCamelCase` (snake_case or kebab → camelCase), then looked up as a static method on the registered command class.
82
+ - The static method validates `params` with a Zod schema from `src/encoders/types/schemas.ts`; on failure, throws a `CustomError` wrapping the `ZodError`.
83
+ - On success it returns a `BaseCommand(cmdName, cmdId, ...hexParams)`.
84
+ 3. `BaseCommand.toHex()` produces the wire hex (cmdId padded to 2 chars + each param padded to 2 chars). Special case for `SetOpenWindow` skips param padding.
85
+ 4. Multiple commands can be concatenated for a single downlink via `commandBuilder.combine([cmd1, cmd2, ...])`.
86
+
87
+ ### 4.3 Mixin / inheritance pattern
88
+
89
+ - `GeneralCommands` is the base class — every device class extends it.
90
+ - Reusable command sets live in mixin classes (`DisplayCommands`, `TemperatureCommonCommands`, `PIRCommands`, `ChildLockCommands`).
91
+ - Mixins are applied via `applyMixins` in `src/utils/mixin.ts`. `delMethods` is used to remove inherited methods that don't apply to a particular device.
92
+
93
+ ## 5. Data Stores
94
+
95
+ None. This package is a pure library — no database, no cache, no I/O at runtime. All inputs are hex strings / JS objects; outputs are decoded objects / hex strings.
96
+
97
+ ## 6. Environment Variables
98
+
99
+ None required by the library or its tests.
100
+
101
+ ## 7. How to Run and Test
102
+
103
+ ```bash
104
+ npm install
105
+ npm run build # tsc + tsc-alias → ./dist
106
+ npm test # jest
107
+ npm test -- VickiCommands # filter by file/pattern
108
+ npm run type-check # tsc --noEmit
109
+ npm run lint # eslint
110
+ npm run format # prettier --write .
111
+ npm run format:check # prettier --check .
112
+ ```
113
+
114
+ Publishing:
115
+
116
+ ```bash
117
+ npm version patch|minor|major # bump + git tag
118
+ npm publish # prepublishOnly runs build
119
+ ```
120
+
121
+ ## 8. Key Patterns and Conventions
122
+
123
+ - **Path alias `@/`** maps to `src/` everywhere (imports, tests, build output).
124
+ - **Public entry** is `src/index.ts` only — keep new exports there.
125
+ - **Device registration is three-sided.** When adding a device, all three sides must be updated:
126
+ 1. `DeviceType` enum in `src/decoders/payloadParsers/types/allDevices.ts`.
127
+ 2. Decoder: new `xxxPayloadParser.ts` exported via `src/decoders/payloadParsers/index.ts` and wired into the switch in `uplinkPayloadParser.ts`.
128
+ 3. Encoder: new `XxxCommands.ts` (extends `GeneralCommands`, optional mixins) exported via `src/encoders/index.ts`, registered in `CommandBuilder.commandRegistry`, and re-exported in `src/index.ts`. Add Zod schemas + enums under `src/encoders/types/schemas.ts`.
129
+ - **Command class style:** all commands are `static` methods on the class. Each set/get pair returns `new BaseCommand(name, cmdId, ...params)`. `decToHex` from `src/utils/decToHex.ts` is the standard numeric-to-hex helper.
130
+ - **Validation:** every command with parameters parses with Zod first, wrapping failures in `CustomError` (`src/utils/customErrorHandler.ts`).
131
+ - **Error type:** `CustomError` is the only error type the library throws publicly.
132
+ - **Tests live next to encoders** (`<Class>.test.ts`); decoder tests are consolidated in `src/test/payloadDecoders.test.ts`.
133
+ - **Encoding helpers:** keep numeric → hex conversions in `src/utils/decToHex.ts`. Bit/byte parsing helpers live in `src/helpers/` (`byteArrayParser`, `decbin`, `toBool`).
134
+ - **Naming:** Device class files are PascalCase (`VickiCommands.ts`); decoder parser files are mixed (`vickiPayloadParser.ts`, `htSensorPayloadParser.ts`, `CO2SensorPayloadParser.ts`) — match the existing neighbours when adding new ones.
135
+
136
+ ## 9. Known Gotchas
137
+
138
+ - **`uplinkPayloadParser` defaults to Vicki** when an unknown `DeviceType` is passed (see comment `// Q: is this OK?`). New devices that aren't wired into the switch will silently be parsed as Vicki.
139
+ - **`BaseCommand.toHex` has a Vicki-specific branch** for `SetOpenWindow` (skips per-param padding). Don't reuse that command name elsewhere.
140
+ - **`CommandBuilder.build` uses `toCamelCase`** on the command string; consumers can pass `"set_target_temperature"` or `"setTargetTemperature"` and both resolve. Method names on command classes must therefore be camelCase.
141
+ - **`commandsReadingHelper.ts` is shared across devices.** The same response cmdId can mean different things on different devices (e.g. `0x3d` → `pirDemoMode` for PirMini, `integralGain` for Vicki, `pirSensorStatus` elsewhere). When adding a new response, check existing case branches before reusing a byte.
142
+ - **Husky pre-commit is strict:** format-check, type-check, lint, and tests must all pass. CI/local commits will fail otherwise.
143
+ - **`prepublishOnly` runs `npm run build`.** Always bump the version in `package.json` before `npm publish` or it will be rejected by the registry.
144
+ - **Mixins + `delMethods`:** if a device inherits a command via a mixin that it should not expose, it must explicitly delete that method (pattern used in several command classes).
145
+
146
+ ## 10. Key Files to Read First
147
+
148
+ When starting a new task, read these in order:
149
+
150
+ 1. `src/index.ts` — public API surface.
151
+ 2. `src/decoders/payloadParsers/uplinkPayloadParser.ts` and `src/decoders/payloadParsers/types/allDevices.ts` — supported devices.
152
+ 3. `src/encoders/CommandBuilder.ts` — device → command-class registry.
153
+ 4. `src/encoders/BaseCommand.ts` and `src/encoders/GeneralCommands.ts` — base command shape and shared commands.
154
+ 5. `src/encoders/types/schemas.ts` — Zod schemas + device enums (large, scan by section).
155
+ 6. `src/decoders/commandsReadingHelper.ts` — shared response decoder switch (large).
156
+ 7. The specific `<Device>PayloadParser.ts` and `<Device>Commands.ts` for the device you are touching.
157
+ 8. `add-encoder-command-existing.md` / `add-encoder-command-new.md` / `docs/how-to-add-new-device.md` — official extension checklists.
158
+
159
+ ## 11. Changelog
160
+
161
+ - **2026-05-14** — Initial `CONTEXT.md` created. Captures architecture as of `package.json` v1.3.4: 22 supported `DeviceType`s, mixin-based encoder hierarchy, single shared `commandsReadingHelper`, Zod-validated command params, colocated encoder tests + consolidated decoder tests.
162
+ - **2026-05-14** — Added `FanCoilThermostatCommands.getFctOperationalMode` (cmdId `0x53`) in `src/encoders/FanCoilThermostatCommands.ts`. Added a `DeviceType.FanCoilThermostat` branch to case `'53'` in `src/decoders/commandsReadingHelper.ts` that decodes the byte as `fctOperationalMode` (raw int, 0=Vent / 1=Heat / 2=Cool) while leaving the default `targetTemperatureStep` decoding intact for other devices. Encoder + decoder tests added (`FanCoilThermostatCommands.test.ts`, `payloadDecoders.test.ts`). Reinforces the existing pattern of guarding shared response cmdIds by `deviceType` (gotcha §9).
@@ -1 +1 @@
1
- {"version":3,"file":"commandsReadingHelper.d.ts","sourceRoot":"","sources":["../../src/decoders/commandsReadingHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAG5D,eAAO,MAAM,qBAAqB,YAAa,MAAM,iBAAiB,MAAM,cAAc,UAAU,mBAqzFnG,CAAA"}
1
+ {"version":3,"file":"commandsReadingHelper.d.ts","sourceRoot":"","sources":["../../src/decoders/commandsReadingHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAG5D,eAAO,MAAM,qBAAqB,YAAa,MAAM,iBAAiB,MAAM,cAAc,UAAU,mBA2zFnG,CAAA"}
@@ -1801,8 +1801,15 @@ const commandsReadingHelper = (hexData, payloadLength, deviceType) => {
1801
1801
  case '53':
1802
1802
  {
1803
1803
  try {
1804
- command_len = 1;
1805
- const data = { targetTemperatureStep: parseInt(commands[i + 1], 16) / 10 };
1804
+ let data;
1805
+ if (deviceType === types_1.DeviceType.FanCoilThermostat) {
1806
+ command_len = 1;
1807
+ data = { fctOperationalMode: parseInt(commands[i + 1], 16) };
1808
+ }
1809
+ else {
1810
+ command_len = 1;
1811
+ data = { targetTemperatureStep: parseInt(commands[i + 1], 16) / 10 };
1812
+ }
1806
1813
  Object.assign(resultToPass, Object.assign({}, resultToPass), Object.assign({}, data));
1807
1814
  }
1808
1815
  catch (e) {