daikin-local-control 0.1.1

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 (115) hide show
  1. package/DESIGN.md +98 -0
  2. package/LICENSE +674 -0
  3. package/README.md +317 -0
  4. package/UNSUPPORTED.md +43 -0
  5. package/dist/authentication.d.ts +24 -0
  6. package/dist/authentication.d.ts.map +1 -0
  7. package/dist/authentication.js +30 -0
  8. package/dist/authentication.js.map +1 -0
  9. package/dist/clients/classic.d.ts +53 -0
  10. package/dist/clients/classic.d.ts.map +1 -0
  11. package/dist/clients/classic.js +142 -0
  12. package/dist/clients/classic.js.map +1 -0
  13. package/dist/clients/client.d.ts +21 -0
  14. package/dist/clients/client.d.ts.map +1 -0
  15. package/dist/clients/client.js +2 -0
  16. package/dist/clients/client.js.map +1 -0
  17. package/dist/clients/dsiot.d.ts +28 -0
  18. package/dist/clients/dsiot.d.ts.map +1 -0
  19. package/dist/clients/dsiot.js +66 -0
  20. package/dist/clients/dsiot.js.map +1 -0
  21. package/dist/codecs/classic/capabilities.d.ts +19 -0
  22. package/dist/codecs/classic/capabilities.d.ts.map +1 -0
  23. package/dist/codecs/classic/capabilities.js +208 -0
  24. package/dist/codecs/classic/capabilities.js.map +1 -0
  25. package/dist/codecs/classic/extendedControl.d.ts +20 -0
  26. package/dist/codecs/classic/extendedControl.d.ts.map +1 -0
  27. package/dist/codecs/classic/extendedControl.js +88 -0
  28. package/dist/codecs/classic/extendedControl.js.map +1 -0
  29. package/dist/codecs/classic/keyValue.d.ts +10 -0
  30. package/dist/codecs/classic/keyValue.d.ts.map +1 -0
  31. package/dist/codecs/classic/keyValue.js +36 -0
  32. package/dist/codecs/classic/keyValue.js.map +1 -0
  33. package/dist/codecs/classic/state.d.ts +18 -0
  34. package/dist/codecs/classic/state.d.ts.map +1 -0
  35. package/dist/codecs/classic/state.js +235 -0
  36. package/dist/codecs/classic/state.js.map +1 -0
  37. package/dist/codecs/credentials.d.ts +8 -0
  38. package/dist/codecs/credentials.d.ts.map +1 -0
  39. package/dist/codecs/credentials.js +14 -0
  40. package/dist/codecs/credentials.js.map +1 -0
  41. package/dist/codecs/device.d.ts +6 -0
  42. package/dist/codecs/device.d.ts.map +1 -0
  43. package/dist/codecs/device.js +35 -0
  44. package/dist/codecs/device.js.map +1 -0
  45. package/dist/codecs/dsiot/capabilities.d.ts +5 -0
  46. package/dist/codecs/dsiot/capabilities.d.ts.map +1 -0
  47. package/dist/codecs/dsiot/capabilities.js +139 -0
  48. package/dist/codecs/dsiot/capabilities.js.map +1 -0
  49. package/dist/codecs/dsiot/multireq.d.ts +37 -0
  50. package/dist/codecs/dsiot/multireq.d.ts.map +1 -0
  51. package/dist/codecs/dsiot/multireq.js +122 -0
  52. package/dist/codecs/dsiot/multireq.js.map +1 -0
  53. package/dist/codecs/dsiot/state.d.ts +92 -0
  54. package/dist/codecs/dsiot/state.d.ts.map +1 -0
  55. package/dist/codecs/dsiot/state.js +461 -0
  56. package/dist/codecs/dsiot/state.js.map +1 -0
  57. package/dist/discovery.d.ts +17 -0
  58. package/dist/discovery.d.ts.map +1 -0
  59. package/dist/discovery.js +67 -0
  60. package/dist/discovery.js.map +1 -0
  61. package/dist/error.d.ts +15 -0
  62. package/dist/error.d.ts.map +1 -0
  63. package/dist/error.js +20 -0
  64. package/dist/error.js.map +1 -0
  65. package/dist/factory.d.ts +26 -0
  66. package/dist/factory.d.ts.map +1 -0
  67. package/dist/factory.js +31 -0
  68. package/dist/factory.js.map +1 -0
  69. package/dist/index.d.ts +22 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +21 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/models/capabilities.d.ts +42 -0
  74. package/dist/models/capabilities.d.ts.map +1 -0
  75. package/dist/models/capabilities.js +2 -0
  76. package/dist/models/capabilities.js.map +1 -0
  77. package/dist/models/device.d.ts +24 -0
  78. package/dist/models/device.d.ts.map +1 -0
  79. package/dist/models/device.js +2 -0
  80. package/dist/models/device.js.map +1 -0
  81. package/dist/models/extendedControl.d.ts +31 -0
  82. package/dist/models/extendedControl.d.ts.map +1 -0
  83. package/dist/models/extendedControl.js +7 -0
  84. package/dist/models/extendedControl.js.map +1 -0
  85. package/dist/models/state.d.ts +56 -0
  86. package/dist/models/state.d.ts.map +1 -0
  87. package/dist/models/state.js +49 -0
  88. package/dist/models/state.js.map +1 -0
  89. package/dist/transport.d.ts +75 -0
  90. package/dist/transport.d.ts.map +1 -0
  91. package/dist/transport.js +118 -0
  92. package/dist/transport.js.map +1 -0
  93. package/package.json +62 -0
  94. package/src/authentication.ts +53 -0
  95. package/src/clients/classic.ts +251 -0
  96. package/src/clients/client.ts +29 -0
  97. package/src/clients/dsiot.ts +116 -0
  98. package/src/codecs/classic/capabilities.ts +272 -0
  99. package/src/codecs/classic/extendedControl.ts +132 -0
  100. package/src/codecs/classic/keyValue.ts +39 -0
  101. package/src/codecs/classic/state.ts +302 -0
  102. package/src/codecs/credentials.ts +19 -0
  103. package/src/codecs/device.ts +41 -0
  104. package/src/codecs/dsiot/capabilities.ts +209 -0
  105. package/src/codecs/dsiot/multireq.ts +162 -0
  106. package/src/codecs/dsiot/state.ts +644 -0
  107. package/src/discovery.ts +85 -0
  108. package/src/error.ts +20 -0
  109. package/src/factory.ts +66 -0
  110. package/src/index.ts +21 -0
  111. package/src/models/capabilities.ts +52 -0
  112. package/src/models/device.ts +25 -0
  113. package/src/models/extendedControl.ts +41 -0
  114. package/src/models/state.ts +110 -0
  115. package/src/transport.ts +191 -0
@@ -0,0 +1,272 @@
1
+ import type {
2
+ DaikinCapabilities,
3
+ TemperatureRange,
4
+ } from "../../models/capabilities.js";
5
+ import {
6
+ type FanSpeed,
7
+ FIXED_LOUVER_POSITIONS,
8
+ type HorizontalPosition,
9
+ HUMIDITY_SETTINGS,
10
+ OPERATING_MODES,
11
+ type OperatingMode,
12
+ SWING_MODES,
13
+ type SwingMode,
14
+ type TargetHumidity,
15
+ type VerticalPosition,
16
+ } from "../../models/state.js";
17
+
18
+ const CAPABILITY_VERSIONS = ["0", "1", "2", "3"];
19
+ const COOLING_ONLY_MODES: readonly OperatingMode[] = [
20
+ "cool",
21
+ "dehumidify",
22
+ "fan",
23
+ ];
24
+ const VERTICAL_SWING_MODES: SwingMode[] = ["off", "vertical"];
25
+ const ALL_FAN_LEVELS: FanSpeed[] = [
26
+ "level1",
27
+ "level2",
28
+ "level3",
29
+ "level4",
30
+ "level5",
31
+ ];
32
+ const FAN_LEVELS_BY_STEPS: Record<string, FanSpeed[]> = {
33
+ "2": ["level1", "level5"],
34
+ "3": ["level1", "level3", "level5"],
35
+ "5": ALL_FAN_LEVELS,
36
+ };
37
+ // `s_humd` is a decimal mask with one bit for each mode that takes a humidity
38
+ // setting.
39
+ const HUMIDITY_MODE_BITS = {
40
+ auto: 0x01,
41
+ cool: 0x02,
42
+ heat: 0x04,
43
+ dehumidify: 0x10,
44
+ } as const;
45
+ // Dehumidify takes no off.
46
+ const DEHUMIDIFY_HUMIDITY_SETTINGS = HUMIDITY_SETTINGS.filter(
47
+ (setting) => setting !== "off",
48
+ );
49
+
50
+ /** Merges the `basic_info` and `get_model_info` replies into the one field map
51
+ * `parseClassicCapabilities` and `applyClassicControl` read. Both replies
52
+ * carry a `type` (the adapter's and the unit's), so they are renamed
53
+ * to `basic_type` and `model_type` before merging; cooling-only detection
54
+ * reads `model_type`. */
55
+ export function mergeClassicCapabilityFields(
56
+ basicInfo: Record<string, string>,
57
+ modelInfo: Record<string, string>,
58
+ ): Record<string, string> {
59
+ const { type: basicType, ...basic } = basicInfo;
60
+ const { type: modelType, ...model } = modelInfo;
61
+ return {
62
+ ...basic,
63
+ ...model,
64
+ ...(basicType === undefined ? {} : { basic_type: basicType }),
65
+ ...(modelType === undefined ? {} : { model_type: modelType }),
66
+ };
67
+ }
68
+
69
+ /** Turns the field map from `mergeClassicCapabilityFields` into
70
+ * `DaikinCapabilities`. `cpv` (capability-protocol version) is the master
71
+ * branch. Units for Japan (`reg=jp`) follow other rules, which this package
72
+ * does not model. */
73
+ export function parseClassicCapabilities(
74
+ fields: Record<string, string>,
75
+ ): DaikinCapabilities {
76
+ const cpv = fields.cpv ?? "";
77
+ const modern = cpv === "2" || cpv === "3";
78
+ const threeD = fields.en_fdir2 === "1";
79
+ const offeredModes = modes(fields, cpv);
80
+ const offered = (mode: OperatingMode, range: TemperatureRange) =>
81
+ offeredModes.includes(mode) ? range : null;
82
+
83
+ return {
84
+ modes: offeredModes,
85
+ fanSpeeds: fanSpeeds(fields, modern),
86
+ swingModes: swingModes(fields),
87
+ verticalPositions: threeD ? verticalPositions(fields) : [],
88
+ horizontalPositions: threeD ? horizontalPositions(fields) : [],
89
+ targetTemperatureInC: {
90
+ // Only `subtype=qa` units report an auto range; the rest are fixed.
91
+ auto: offered(
92
+ "auto",
93
+ fields.subtype === "qa"
94
+ ? range(fields.hmlmt_l, 18, fields.cmlmt_h, 30)
95
+ : range(undefined, 18, undefined, 30),
96
+ ),
97
+ cool: offered("cool", range(fields.cmlmt_l, 18, fields.cmlmt_h, 32)),
98
+ heat: offered("heat", range(fields.hmlmt_l, 10, fields.hmlmt_h, 30)),
99
+ dehumidify: null,
100
+ fan: null,
101
+ },
102
+ targetHumidity: targetHumidity(fields, modern, offeredModes),
103
+ specialOutlet: cpv === "3" && fields.airoutlet_pv === "0",
104
+ energyMonitoring: fields.elec === "1",
105
+ demandControl: fields.adp_kind === "4" && fields.dmnd === "1",
106
+ scheduleTimer: ["1", "2"].includes(fields.en_scdltmr ?? ""),
107
+ model: fields.model,
108
+ };
109
+ }
110
+
111
+ function present(fields: Record<string, string>, key: string): boolean {
112
+ return (fields[key] ?? "") !== "";
113
+ }
114
+
115
+ function modes(fields: Record<string, string>, cpv: string): OperatingMode[] {
116
+ if (!CAPABILITY_VERSIONS.includes(cpv)) {
117
+ return [];
118
+ }
119
+ return fields.model_type === "C"
120
+ ? OPERATING_MODES.filter((mode) => COOLING_ONLY_MODES.includes(mode))
121
+ : [...OPERATING_MODES];
122
+ }
123
+
124
+ function fanSpeeds(
125
+ fields: Record<string, string>,
126
+ modern: boolean,
127
+ ): FanSpeed[] {
128
+ if (!present(fields, "reg") || fields.reg === "jp") {
129
+ return [];
130
+ }
131
+ // A `reg=th`, `subtype=qa` unit is controllable by the speeds it lists, not
132
+ // by `en_frate`.
133
+ const listsItsSpeeds = fields.reg === "th" && fields.subtype === "qa";
134
+ if (!listsItsSpeeds && fields.en_frate !== "1") {
135
+ return [];
136
+ }
137
+ const speeds: FanSpeed[] = [];
138
+ if (!("en_frate_auto" in fields) || fields.en_frate_auto === "1") {
139
+ speeds.push("auto");
140
+ }
141
+ if (
142
+ modern &&
143
+ (!present(fields, "en_frate_silent") || fields.en_frate_silent === "1")
144
+ ) {
145
+ speeds.push("silent");
146
+ }
147
+ const levels =
148
+ "frate_steps" in fields
149
+ ? (FAN_LEVELS_BY_STEPS[fields.frate_steps ?? ""] ?? [])
150
+ : ALL_FAN_LEVELS;
151
+ return [...speeds, ...levels];
152
+ }
153
+
154
+ /** Whether a unit's `set_control_info` takes `shum`, read from the field map
155
+ * of `mergeClassicCapabilityFields`. A `subtype=qa` unit takes none. */
156
+ export function takesClassicHumidity(fields: Record<string, string>): boolean {
157
+ return fields.subtype !== "qa";
158
+ }
159
+
160
+ function targetHumidity(
161
+ fields: Record<string, string>,
162
+ modern: boolean,
163
+ offeredModes: OperatingMode[],
164
+ ): Record<OperatingMode, TargetHumidity[]> {
165
+ const mask =
166
+ modern && takesClassicHumidity(fields)
167
+ ? Number.parseInt(fields.s_humd ?? "", 10)
168
+ : 0;
169
+ const settings = (
170
+ mode: keyof typeof HUMIDITY_MODE_BITS,
171
+ values: TargetHumidity[],
172
+ ) =>
173
+ offeredModes.includes(mode) && mask & HUMIDITY_MODE_BITS[mode]
174
+ ? values
175
+ : [];
176
+ return {
177
+ auto: settings("auto", [...HUMIDITY_SETTINGS]),
178
+ dehumidify: settings("dehumidify", DEHUMIDIFY_HUMIDITY_SETTINGS),
179
+ cool: settings("cool", [...HUMIDITY_SETTINGS]),
180
+ heat: settings("heat", [...HUMIDITY_SETTINGS]),
181
+ fan: [],
182
+ };
183
+ }
184
+
185
+ // A unit that takes `f_dir` swings vertically only when `s_fdir=1`; a 3D unit
186
+ // without `f_dir` swings through `f_dir_ud`/`f_dir_lr` on both axes.
187
+ function swingModes(fields: Record<string, string>): SwingMode[] {
188
+ if (takesClassicDirection(fields)) {
189
+ return fields.s_fdir === "1" ? VERTICAL_SWING_MODES : [...SWING_MODES];
190
+ }
191
+ return fields.en_fdir2 === "1" ? [...SWING_MODES] : [];
192
+ }
193
+
194
+ /** Whether a unit's `set_control_info` takes `f_dir`, read from the field map
195
+ * of `mergeClassicCapabilityFields`. */
196
+ export function takesClassicDirection(fields: Record<string, string>): boolean {
197
+ if (!present(fields, "reg")) {
198
+ return false;
199
+ }
200
+ if (fields.subtype === "qa") {
201
+ return fields.s_fdir === "1";
202
+ }
203
+ const cpv = fields.cpv;
204
+ if (cpv === "0" || cpv === "1") {
205
+ return fields.s_fdir === "1" || fields.s_fdir === "3";
206
+ }
207
+ return (cpv === "2" || cpv === "3") && fields.en_fdir === "1";
208
+ }
209
+
210
+ // `s_fdir_ud` is a hex mask: 0x01 swing, 0x10 floor warming, 0x80 the five
211
+ // fixed positions (only when `s_fdir_ud_step=5`). Stop is offered unless a
212
+ // fixed position is.
213
+ function verticalPositions(fields: Record<string, string>): VerticalPosition[] {
214
+ const stopOrSwing: VerticalPosition[] = ["swing", "stop"];
215
+ if (!present(fields, "s_fdir_ud")) {
216
+ return stopOrSwing;
217
+ }
218
+ const mask = Number.parseInt(fields.s_fdir_ud ?? "", 16);
219
+ if (!Number.isFinite(mask)) {
220
+ return stopOrSwing;
221
+ }
222
+ const positions: VerticalPosition[] = [];
223
+ if (mask & 0x01) {
224
+ positions.push("swing");
225
+ }
226
+ if (mask & 0x10) {
227
+ positions.push("floorWarming");
228
+ }
229
+ if (mask & 0x80) {
230
+ const steps = Number.parseInt(fields.s_fdir_ud_step ?? "", 10);
231
+ if (!(steps >= 0 && steps <= 5)) {
232
+ return stopOrSwing;
233
+ }
234
+ if (steps === 5) {
235
+ positions.push(...FIXED_LOUVER_POSITIONS);
236
+ }
237
+ }
238
+ if (
239
+ positions.length === 0 ||
240
+ (positions.length === 1 && positions[0] === "swing")
241
+ ) {
242
+ positions.push("stop");
243
+ }
244
+ return positions;
245
+ }
246
+
247
+ // `s_fdir_lr` bit 0x01 is swing; stop is always offered.
248
+ function horizontalPositions(
249
+ fields: Record<string, string>,
250
+ ): HorizontalPosition[] {
251
+ const mask = Number.parseInt(fields.s_fdir_lr ?? "", 16);
252
+ return Number.isFinite(mask) && mask & 0x01 ? ["stop", "swing"] : ["stop"];
253
+ }
254
+
255
+ // Setpoint limits are whole °C on the wire; any fraction is dropped.
256
+ function range(
257
+ low: string | undefined,
258
+ fallbackMin: number,
259
+ high: string | undefined,
260
+ fallbackMax: number,
261
+ ): TemperatureRange {
262
+ return {
263
+ min: wholeCelsius(low, fallbackMin),
264
+ max: wholeCelsius(high, fallbackMax),
265
+ step: 1,
266
+ };
267
+ }
268
+
269
+ function wholeCelsius(value: string | undefined, fallback: number): number {
270
+ const parsed = Number.parseFloat(value ?? "");
271
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
272
+ }
@@ -0,0 +1,132 @@
1
+ import type {
2
+ DemandControl,
3
+ DemandMode,
4
+ DeviceGroup,
5
+ SpecialMode,
6
+ TerminalScope,
7
+ } from "../../models/extendedControl.js";
8
+
9
+ // Encodes each control outside `control_info` as the query its classic `set_*`
10
+ // endpoint takes.
11
+
12
+ const TERMINAL_SCOPE_CODES: Record<TerminalScope, string> = {
13
+ self: "0",
14
+ others: "1",
15
+ all: "2",
16
+ };
17
+
18
+ const DEMAND_MODE_CODES: Record<DemandMode, string> = {
19
+ manual: "0",
20
+ auto: "2",
21
+ };
22
+
23
+ // `spmode_kind` on the wire; control_info's `adv` reports powerful as 2 and
24
+ // econo as 12.
25
+ const SPECIAL_MODE_KIND_CODES: Record<
26
+ Exclude<SpecialMode, "streamer">,
27
+ string
28
+ > = {
29
+ powerful: "1",
30
+ econo: "2",
31
+ };
32
+
33
+ // `method`: the adapter is reached from outside the home network while it
34
+ // polls, and from inside only otherwise.
35
+ const REMOTE_METHOD_POLLING = "polling";
36
+ const REMOTE_METHOD_HOME_ONLY = "home only";
37
+
38
+ const REMOTE_METHOD_INTERVALS = ["notice_ip_int", "notice_sync_int"];
39
+
40
+ export function encodeClassicLed(on: boolean): Record<string, string> {
41
+ return { led: flag(on) };
42
+ }
43
+
44
+ export function encodeClassicHolidayMode(
45
+ enabled: boolean,
46
+ ): Record<string, string> {
47
+ return { en_hol: flag(enabled) };
48
+ }
49
+
50
+ export function encodeClassicDemandControl(
51
+ demand: DemandControl,
52
+ ): Record<string, string> {
53
+ return {
54
+ demand_type: "1",
55
+ en_demand: flag(demand.enabled),
56
+ mode: DEMAND_MODE_CODES[demand.mode],
57
+ max_pow: String(demand.maxPowerInPercent),
58
+ };
59
+ }
60
+
61
+ export function encodeClassicSpecialMode(
62
+ mode: SpecialMode,
63
+ enabled: boolean,
64
+ ): Record<string, string> {
65
+ return mode === "streamer"
66
+ ? { en_streamer: flag(enabled) }
67
+ : { set_spmode: flag(enabled), spmode_kind: SPECIAL_MODE_KIND_CODES[mode] };
68
+ }
69
+
70
+ /** Turns `current`, the `get_remote_method` reply, and `enabled` into the
71
+ * `set_remote_method` query. The endpoint takes the intervals back unchanged,
72
+ * so the reply's `notice_ip_int` and `notice_sync_int` are copied in. */
73
+ export function applyClassicOutOfHomeAccess(
74
+ current: Record<string, string>,
75
+ enabled: boolean,
76
+ ): Record<string, string> {
77
+ const query: Record<string, string> = {
78
+ method: enabled ? REMOTE_METHOD_POLLING : REMOTE_METHOD_HOME_ONLY,
79
+ };
80
+ for (const key of REMOTE_METHOD_INTERVALS) {
81
+ const value = current[key];
82
+ if (value !== undefined) {
83
+ query[key] = value;
84
+ }
85
+ }
86
+ return query;
87
+ }
88
+
89
+ /** Encodes the price as the `set_price` query takes it: whole units
90
+ * (`price_int`) and hundredths (`price_dec`). The price is rounded to two
91
+ * decimal places. */
92
+ export function encodeClassicEnergyPrice(
93
+ price: number,
94
+ ): Record<string, string> {
95
+ const hundredths = Math.round(price * 100);
96
+ return {
97
+ price_int: String(Math.trunc(hundredths / 100)),
98
+ price_dec: String(hundredths % 100),
99
+ };
100
+ }
101
+
102
+ export function encodeClassicEnergyTarget(
103
+ target: number,
104
+ ): Record<string, string> {
105
+ return { target: String(target) };
106
+ }
107
+
108
+ export function encodeClassicName(name: string): Record<string, string> {
109
+ return { name };
110
+ }
111
+
112
+ export function encodeClassicIcon(icon: string): Record<string, string> {
113
+ return { icon };
114
+ }
115
+
116
+ export function encodeClassicGroup(group: DeviceGroup): Record<string, string> {
117
+ return { grp_name: group.name, en_grp: flag(group.enabled) };
118
+ }
119
+
120
+ export function encodeClassicTerminalKey(key: string): Record<string, string> {
121
+ return { key };
122
+ }
123
+
124
+ export function encodeClassicTerminalScope(
125
+ scope: TerminalScope,
126
+ ): Record<string, string> {
127
+ return { type: TERMINAL_SCOPE_CODES[scope] };
128
+ }
129
+
130
+ function flag(on: boolean): string {
131
+ return on ? "1" : "0";
132
+ }
@@ -0,0 +1,39 @@
1
+ import { DaikinError } from "../../error.js";
2
+
3
+ // The classic CGI protocol answers a flat, comma-separated `key=value` string,
4
+ // e.g. `ret=OK,pow=1,mode=3,stemp=24,...`, and takes its writes as query parameters.
5
+
6
+ /** Splits a classic reply into its fields. A pair without `=` is dropped, and
7
+ * a value is kept as the adapter sent it, undecoded. */
8
+ export function parseClassicKeyValue(body: string): Record<string, string> {
9
+ const result: Record<string, string> = {};
10
+ for (const pair of body.split(",")) {
11
+ const index = pair.indexOf("=");
12
+ if (index === -1) {
13
+ continue;
14
+ }
15
+ result[pair.slice(0, index)] = pair.slice(index + 1);
16
+ }
17
+ return result;
18
+ }
19
+
20
+ export function isClassicOk(fields: Record<string, string>): boolean {
21
+ return fields.ret === "OK";
22
+ }
23
+
24
+ /** Throws a `DaikinError` unless the reply's `ret` is `OK`: status 401 for
25
+ * `AUTH NG` or `LPW NG`, 502 for anything else. Every reply carries a `ret`,
26
+ * and the adapter rejects a call with HTTP 200, so the rejection has to be
27
+ * read here rather than from the status code. */
28
+ export function assertClassicOk(fields: Record<string, string>): void {
29
+ if (isClassicOk(fields)) {
30
+ return;
31
+ }
32
+ const ret = fields.ret;
33
+ // AUTH NG: the terminal is not registered. LPW NG: the child lock code is
34
+ // wrong.
35
+ if (ret === "AUTH NG" || ret === "LPW NG") {
36
+ throw new DaikinError(`Daikin rejected the credentials (ret=${ret})`, 401);
37
+ }
38
+ throw new DaikinError(`Daikin command failed (ret=${ret ?? "missing"})`, 502);
39
+ }