daikin-local-control 0.3.0 → 0.4.0

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 (114) hide show
  1. package/DESIGN.md +113 -2
  2. package/README.md +131 -13
  3. package/UNSUPPORTED.md +42 -4
  4. package/dist/clients/classic.d.ts +2 -2
  5. package/dist/clients/classic.d.ts.map +1 -1
  6. package/dist/clients/classic.js +4 -4
  7. package/dist/clients/classic.js.map +1 -1
  8. package/dist/clients/client.d.ts +6 -2
  9. package/dist/clients/client.d.ts.map +1 -1
  10. package/dist/clients/dsiot.d.ts +3 -1
  11. package/dist/clients/dsiot.d.ts.map +1 -1
  12. package/dist/clients/dsiot.js +11 -3
  13. package/dist/clients/dsiot.js.map +1 -1
  14. package/dist/codecs/classic/capabilities.d.ts.map +1 -1
  15. package/dist/codecs/classic/capabilities.js +5 -11
  16. package/dist/codecs/classic/capabilities.js.map +1 -1
  17. package/dist/codecs/classic/extendedControl.d.ts.map +1 -1
  18. package/dist/codecs/classic/extendedControl.js +10 -19
  19. package/dist/codecs/classic/extendedControl.js.map +1 -1
  20. package/dist/codecs/classic/state.d.ts.map +1 -1
  21. package/dist/codecs/classic/state.js +36 -95
  22. package/dist/codecs/classic/state.js.map +1 -1
  23. package/dist/codecs/classic/tables.d.ts +32 -0
  24. package/dist/codecs/classic/tables.d.ts.map +1 -0
  25. package/dist/codecs/classic/tables.js +94 -0
  26. package/dist/codecs/classic/tables.js.map +1 -0
  27. package/dist/codecs/credentials.d.ts +3 -2
  28. package/dist/codecs/credentials.d.ts.map +1 -1
  29. package/dist/codecs/credentials.js +8 -3
  30. package/dist/codecs/credentials.js.map +1 -1
  31. package/dist/codecs/device.d.ts +2 -1
  32. package/dist/codecs/device.d.ts.map +1 -1
  33. package/dist/codecs/device.js +7 -6
  34. package/dist/codecs/device.js.map +1 -1
  35. package/dist/codecs/dsiot/capabilities.d.ts.map +1 -1
  36. package/dist/codecs/dsiot/capabilities.js +8 -15
  37. package/dist/codecs/dsiot/capabilities.js.map +1 -1
  38. package/dist/codecs/dsiot/device.d.ts +4 -2
  39. package/dist/codecs/dsiot/device.d.ts.map +1 -1
  40. package/dist/codecs/dsiot/device.js +8 -8
  41. package/dist/codecs/dsiot/device.js.map +1 -1
  42. package/dist/codecs/dsiot/multireq.d.ts +0 -18
  43. package/dist/codecs/dsiot/multireq.d.ts.map +1 -1
  44. package/dist/codecs/dsiot/multireq.js +2 -24
  45. package/dist/codecs/dsiot/multireq.js.map +1 -1
  46. package/dist/codecs/dsiot/state.d.ts +6 -62
  47. package/dist/codecs/dsiot/state.d.ts.map +1 -1
  48. package/dist/codecs/dsiot/state.js +29 -148
  49. package/dist/codecs/dsiot/state.js.map +1 -1
  50. package/dist/codecs/dsiot/tables.d.ts +116 -0
  51. package/dist/codecs/dsiot/tables.d.ts.map +1 -0
  52. package/dist/codecs/dsiot/tables.js +185 -0
  53. package/dist/codecs/dsiot/tables.js.map +1 -0
  54. package/dist/discovery.d.ts +2 -1
  55. package/dist/discovery.d.ts.map +1 -1
  56. package/dist/discovery.js +9 -5
  57. package/dist/discovery.js.map +1 -1
  58. package/dist/factory.d.ts +7 -4
  59. package/dist/factory.d.ts.map +1 -1
  60. package/dist/factory.js +4 -2
  61. package/dist/factory.js.map +1 -1
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +2 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/models/device.d.ts +3 -1
  67. package/dist/models/device.d.ts.map +1 -1
  68. package/dist/models/state.d.ts +2 -3
  69. package/dist/models/state.d.ts.map +1 -1
  70. package/dist/simulators/classic.d.ts +61 -0
  71. package/dist/simulators/classic.d.ts.map +1 -0
  72. package/dist/simulators/classic.js +473 -0
  73. package/dist/simulators/classic.js.map +1 -0
  74. package/dist/simulators/dsiot.d.ts +49 -0
  75. package/dist/simulators/dsiot.d.ts.map +1 -0
  76. package/dist/simulators/dsiot.js +519 -0
  77. package/dist/simulators/dsiot.js.map +1 -0
  78. package/dist/simulators/index.d.ts +4 -0
  79. package/dist/simulators/index.d.ts.map +1 -0
  80. package/dist/simulators/index.js +3 -0
  81. package/dist/simulators/index.js.map +1 -0
  82. package/dist/simulators/simulator.d.ts +116 -0
  83. package/dist/simulators/simulator.d.ts.map +1 -0
  84. package/dist/simulators/simulator.js +141 -0
  85. package/dist/simulators/simulator.js.map +1 -0
  86. package/dist/transport.d.ts +6 -2
  87. package/dist/transport.d.ts.map +1 -1
  88. package/dist/transport.js +7 -2
  89. package/dist/transport.js.map +1 -1
  90. package/package.json +5 -2
  91. package/src/clients/classic.ts +8 -8
  92. package/src/clients/client.ts +7 -1
  93. package/src/clients/dsiot.ts +12 -2
  94. package/src/codecs/classic/capabilities.ts +8 -11
  95. package/src/codecs/classic/extendedControl.ts +14 -25
  96. package/src/codecs/classic/state.ts +49 -101
  97. package/src/codecs/classic/tables.ts +123 -0
  98. package/src/codecs/credentials.ts +11 -3
  99. package/src/codecs/device.ts +8 -7
  100. package/src/codecs/dsiot/capabilities.ts +10 -17
  101. package/src/codecs/dsiot/device.ts +8 -9
  102. package/src/codecs/dsiot/multireq.ts +2 -24
  103. package/src/codecs/dsiot/state.ts +57 -193
  104. package/src/codecs/dsiot/tables.ts +249 -0
  105. package/src/discovery.ts +9 -5
  106. package/src/factory.ts +7 -4
  107. package/src/index.ts +2 -0
  108. package/src/models/device.ts +3 -1
  109. package/src/models/state.ts +2 -3
  110. package/src/simulators/classic.ts +635 -0
  111. package/src/simulators/dsiot.ts +736 -0
  112. package/src/simulators/index.ts +9 -0
  113. package/src/simulators/simulator.ts +281 -0
  114. package/src/transport.ts +10 -3
@@ -15,126 +15,53 @@ import {
15
15
  VERTICAL_POSITIONS,
16
16
  type VerticalPosition,
17
17
  } from "../../models/state.js";
18
- import type { DsiotUpdate } from "./multireq.js";
19
-
20
- /** Full dotted paths into the `dgc_status` trees. `e_1002` lives on the indoor
21
- * edge and `e_1003` on the outdoor edge. */
22
- export const DSIOT_PATHS = {
23
- power: "dgc_status.e_1002.e_A002.p_01",
24
- /** Sent with every control. */
25
- powerTransition: "dgc_status.e_1002.e_3003.p_2D",
26
- mode: "dgc_status.e_1002.e_3001.p_01",
27
- indoorTemperature: "dgc_status.e_1002.e_A00B.p_01",
28
- indoorHumidity: "dgc_status.e_1002.e_A00B.p_02",
29
- outdoorTemperature: "dgc_status.e_1003.e_A00D.p_01",
30
- model: "dgc_status.e_1002.e_A001.p_0D",
31
- modelType: "dgc_status.e_1002.e_A001.p_07",
32
- energyMonitoring: "dgc_status.e_1002.e_A011.p_01",
33
- } as const;
34
-
35
- /** Each leaf holds `01` while its mode is on. Powerful is on the outdoor
36
- * edge. */
37
- export const DSIOT_SPECIAL_MODE_PATHS: Record<SpecialMode, string> = {
38
- powerful: "dgc_status.e_1003.e_3002.p_44",
39
- econo: "dgc_status.e_1002.e_3003.p_24",
40
- streamer: "dgc_status.e_1002.e_3003.p_20",
41
- };
42
- const SPECIAL_MODE_ON = "01";
43
-
44
- export interface DsiotSetpoint {
45
- path: string;
46
- /** Auto and dehumidify are a signed byte; cool and heat are little-endian. */
47
- signed: boolean;
48
- /** The dehumidify setpoint is read-only. */
49
- writable: boolean;
50
- }
51
-
52
- export interface DsiotHumidity {
53
- /** Counted in steps of the leaf's `md.st`. */
54
- value: string;
55
- /** The setting: `00` or `05` off, `01` the percentage in `value`, and one
56
- * value for each `HumiditySetting`. */
57
- flag: string;
58
- }
59
-
60
- export interface DsiotModePaths {
61
- /** Absent in fan mode. */
62
- setpoint?: DsiotSetpoint;
63
- /** Absent in fan mode. */
64
- humidity?: DsiotHumidity;
65
- fanSpeed: string;
66
- vertical: string;
67
- horizontal: string;
68
- }
69
-
70
- export const DSIOT_MODE_NODE = "dgc_status.e_1002.e_3001";
71
-
72
- export const DSIOT_MODE_PATHS: Record<OperatingMode, DsiotModePaths> = {
73
- auto: {
74
- setpoint: { path: `${DSIOT_MODE_NODE}.p_1D`, signed: true, writable: true },
75
- humidity: {
76
- value: `${DSIOT_MODE_NODE}.p_2E`,
77
- flag: `${DSIOT_MODE_NODE}.p_2F`,
78
- },
79
- fanSpeed: `${DSIOT_MODE_NODE}.p_26`,
80
- vertical: `${DSIOT_MODE_NODE}.p_20`,
81
- horizontal: `${DSIOT_MODE_NODE}.p_21`,
82
- },
83
- dehumidify: {
84
- setpoint: {
85
- path: `${DSIOT_MODE_NODE}.p_1E`,
86
- signed: true,
87
- writable: false,
88
- },
89
- humidity: {
90
- value: `${DSIOT_MODE_NODE}.p_30`,
91
- flag: `${DSIOT_MODE_NODE}.p_31`,
92
- },
93
- fanSpeed: `${DSIOT_MODE_NODE}.p_27`,
94
- vertical: `${DSIOT_MODE_NODE}.p_22`,
95
- horizontal: `${DSIOT_MODE_NODE}.p_23`,
96
- },
97
- cool: {
98
- setpoint: {
99
- path: `${DSIOT_MODE_NODE}.p_02`,
100
- signed: false,
101
- writable: true,
102
- },
103
- humidity: {
104
- value: `${DSIOT_MODE_NODE}.p_0B`,
105
- flag: `${DSIOT_MODE_NODE}.p_0C`,
106
- },
107
- fanSpeed: `${DSIOT_MODE_NODE}.p_09`,
108
- vertical: `${DSIOT_MODE_NODE}.p_05`,
109
- horizontal: `${DSIOT_MODE_NODE}.p_06`,
110
- },
111
- heat: {
112
- setpoint: {
113
- path: `${DSIOT_MODE_NODE}.p_03`,
114
- signed: false,
115
- writable: true,
116
- },
117
- humidity: {
118
- value: `${DSIOT_MODE_NODE}.p_2C`,
119
- flag: `${DSIOT_MODE_NODE}.p_2D`,
120
- },
121
- fanSpeed: `${DSIOT_MODE_NODE}.p_0A`,
122
- vertical: `${DSIOT_MODE_NODE}.p_07`,
123
- horizontal: `${DSIOT_MODE_NODE}.p_08`,
124
- },
125
- fan: {
126
- fanSpeed: `${DSIOT_MODE_NODE}.p_28`,
127
- vertical: `${DSIOT_MODE_NODE}.p_24`,
128
- horizontal: `${DSIOT_MODE_NODE}.p_25`,
129
- },
130
- };
18
+ import { buildDsiotMultireqUpdate, type DsiotUpdate } from "./multireq.js";
19
+ import {
20
+ DSIOT_EDGES,
21
+ DSIOT_FAN_SPEED_CODES,
22
+ DSIOT_FIXED_LOUVER_VALUES,
23
+ DSIOT_HORIZONTAL_POSITION_VALUES,
24
+ DSIOT_HUMIDITY_OFF,
25
+ DSIOT_HUMIDITY_OFF_KEPT,
26
+ DSIOT_HUMIDITY_PERCENTAGE,
27
+ DSIOT_HUMIDITY_SETTING_FLAGS,
28
+ DSIOT_LOUVER_STOP,
29
+ DSIOT_LOUVER_SWING,
30
+ DSIOT_LOUVER_VALUES,
31
+ DSIOT_MODE_PATHS,
32
+ DSIOT_OFF,
33
+ DSIOT_ON,
34
+ DSIOT_OPERATING_MODE_CODES,
35
+ DSIOT_PATHS,
36
+ DSIOT_SPECIAL_MODE_PATHS,
37
+ DSIOT_VERTICAL_POSITION_VALUES,
38
+ type DsiotHumidity,
39
+ } from "./tables.js";
40
+
41
+ /** Builds the body of the multireq write that turns a special mode on or off:
42
+ * its one leaf, on the outdoor edge for powerful and on the indoor edge for
43
+ * the rest. */
44
+ export function buildDsiotSpecialModeUpdate(
45
+ mode: SpecialMode,
46
+ enabled: boolean,
47
+ ): object {
48
+ return buildDsiotMultireqUpdate(
49
+ [
50
+ {
51
+ path: DSIOT_SPECIAL_MODE_PATHS[mode],
52
+ value: encodeDsiotBoolean(enabled),
53
+ },
54
+ ],
55
+ mode === "powerful" ? DSIOT_EDGES.outdoor : DSIOT_EDGES.indoor,
56
+ );
57
+ }
131
58
 
132
59
  // --- value encodings -------------------------------------------------------
133
60
  // Leaf values are hex strings. Multi-byte values are little-endian, and
134
61
  // temperatures count half degrees.
135
62
 
136
63
  export function encodeDsiotBoolean(on: boolean): string {
137
- return on ? "01" : "00";
64
+ return on ? DSIOT_ON : DSIOT_OFF;
138
65
  }
139
66
 
140
67
  export function decodeDsiotBoolean(pv: string | undefined): boolean {
@@ -196,14 +123,6 @@ function nullIfNaN(value: number): number | null {
196
123
  return Number.isFinite(value) ? value : null;
197
124
  }
198
125
 
199
- const OPERATING_MODE_CODES: Record<OperatingMode, string> = {
200
- fan: "0000",
201
- heat: "0100",
202
- cool: "0200",
203
- auto: "0300",
204
- dehumidify: "0500",
205
- };
206
-
207
126
  const OPERATING_MODES_BY_CODE: Record<string, OperatingMode> = {
208
127
  "0000": "fan",
209
128
  "0100": "heat",
@@ -215,7 +134,7 @@ const OPERATING_MODES_BY_CODE: Record<string, OperatingMode> = {
215
134
  };
216
135
 
217
136
  export function encodeDsiotMode(mode: OperatingMode): string {
218
- return OPERATING_MODE_CODES[mode];
137
+ return DSIOT_OPERATING_MODE_CODES[mode];
219
138
  }
220
139
 
221
140
  export function decodeDsiotMode(
@@ -224,25 +143,15 @@ export function decodeDsiotMode(
224
143
  return OPERATING_MODES_BY_CODE[(pv ?? "").toUpperCase()];
225
144
  }
226
145
 
227
- const FAN_SPEED_CODES: Record<FanSpeed, string> = {
228
- auto: "0A00",
229
- silent: "0B00",
230
- level1: "0300",
231
- level2: "0400",
232
- level3: "0500",
233
- level4: "0600",
234
- level5: "0700",
235
- };
236
-
237
146
  const FAN_SPEEDS_BY_CODE: Record<string, FanSpeed> = Object.fromEntries(
238
- Object.entries(FAN_SPEED_CODES).map(([speed, code]) => [
147
+ Object.entries(DSIOT_FAN_SPEED_CODES).map(([speed, code]) => [
239
148
  code,
240
149
  speed as FanSpeed,
241
150
  ]),
242
151
  );
243
152
 
244
153
  export function encodeDsiotFanSpeed(speed: FanSpeed): string {
245
- return FAN_SPEED_CODES[speed];
154
+ return DSIOT_FAN_SPEED_CODES[speed];
246
155
  }
247
156
 
248
157
  export function decodeDsiotFanSpeed(
@@ -251,48 +160,12 @@ export function decodeDsiotFanSpeed(
251
160
  return FAN_SPEEDS_BY_CODE[(pv ?? "").toUpperCase()];
252
161
  }
253
162
 
254
- /** The values a louver leaf holds besides a fixed position, which is 1-14.
255
- * The leaf is three bytes, little-endian, and bit n of its `md.mx` is set
256
- * when the unit takes value n. */
257
- export const DSIOT_LOUVER_VALUES = {
258
- stop: 0,
259
- swing: 15,
260
- auto: 16,
261
- naturalWind: 19,
262
- floorWarming: 25,
263
- } as const;
264
-
265
- const FIXED_LOUVER_VALUES = Array.from({ length: 14 }, (_, index) => index + 1);
266
-
267
- export const DSIOT_LOUVER_STOP = "000000";
268
- export const DSIOT_LOUVER_SWING = "0F0000";
269
-
270
163
  export function isDsiotLouverSwinging(pv: string | undefined): boolean {
271
164
  return (pv ?? "").toUpperCase() === DSIOT_LOUVER_SWING;
272
165
  }
273
166
 
274
- /** The value each `VerticalPosition` is held as. A fixed position has none: it
275
- * is held as the value at its rank among the fixed values the unit takes. */
276
- export const DSIOT_VERTICAL_POSITION_VALUES: Partial<
277
- Record<VerticalPosition, number>
278
- > = {
279
- stop: DSIOT_LOUVER_VALUES.stop,
280
- swing: DSIOT_LOUVER_VALUES.swing,
281
- naturalWind: DSIOT_LOUVER_VALUES.naturalWind,
282
- auto: DSIOT_LOUVER_VALUES.auto,
283
- floorWarming: DSIOT_LOUVER_VALUES.floorWarming,
284
- };
285
167
  const FIXED_VERTICAL_POSITIONS: readonly VerticalPosition[] =
286
168
  FIXED_LOUVER_POSITIONS;
287
- /** The value each `HorizontalPosition` is held as. `stop` and a fixed position
288
- * have none: see UNSUPPORTED.md. */
289
- export const DSIOT_HORIZONTAL_POSITION_VALUES: Partial<
290
- Record<HorizontalPosition, number>
291
- > = {
292
- swing: DSIOT_LOUVER_VALUES.swing,
293
- auto: DSIOT_LOUVER_VALUES.auto,
294
- };
295
-
296
169
  function positionOf<PositionT extends string>(
297
170
  positions: readonly PositionT[],
298
171
  held: Partial<Record<PositionT, number>>,
@@ -310,7 +183,7 @@ function fixedLouverValues(
310
183
  path: string,
311
184
  ): number[] {
312
185
  const mask = decodeDsiotLittleEndian(values[`${path}.md.mx`] ?? "");
313
- return FIXED_LOUVER_VALUES.filter((value) => mask & (1 << value));
186
+ return DSIOT_FIXED_LOUVER_VALUES.filter((value) => mask & (1 << value));
314
187
  }
315
188
 
316
189
  function encodeLouver(value: number): string {
@@ -383,22 +256,9 @@ function encodeHorizontalPosition(
383
256
  return value === undefined ? undefined : encodeLouver(value);
384
257
  }
385
258
 
386
- const HUMIDITY_OFF = "00";
387
- const HUMIDITY_ON = "01";
388
- // Also reads as off, and a control that switches humidity off leaves it.
389
- const HUMIDITY_OFF_KEPT = "05";
390
- const HUMIDITY_SETTING_FLAGS: Record<
391
- Exclude<HumiditySetting, "off">,
392
- string
393
- > = {
394
- auto: "02",
395
- autoHigh: "03",
396
- autoLow: "04",
397
- continuous: "06",
398
- };
399
259
  const HUMIDITY_SETTINGS_BY_FLAG: Record<string, HumiditySetting> =
400
260
  Object.fromEntries(
401
- Object.entries(HUMIDITY_SETTING_FLAGS).map(([setting, flag]) => [
261
+ Object.entries(DSIOT_HUMIDITY_SETTING_FLAGS).map(([setting, flag]) => [
402
262
  flag,
403
263
  setting as HumiditySetting,
404
264
  ]),
@@ -421,10 +281,10 @@ function parseTargetHumidity(
421
281
  humidity: DsiotHumidity | undefined,
422
282
  ): TargetHumidity | null {
423
283
  const flag = humidity && values[humidity.flag];
424
- if (flag === HUMIDITY_OFF || flag === HUMIDITY_OFF_KEPT) {
284
+ if (flag === DSIOT_HUMIDITY_OFF || flag === DSIOT_HUMIDITY_OFF_KEPT) {
425
285
  return "off";
426
286
  }
427
- if (humidity === undefined || flag !== HUMIDITY_ON) {
287
+ if (humidity === undefined || flag !== DSIOT_HUMIDITY_PERCENTAGE) {
428
288
  return HUMIDITY_SETTINGS_BY_FLAG[flag ?? ""] ?? null;
429
289
  }
430
290
  const count = Number.parseInt(values[humidity.value] ?? "", 16);
@@ -447,18 +307,22 @@ function applyTargetHumidity(
447
307
  {
448
308
  path: flag,
449
309
  value:
450
- values[flag] === HUMIDITY_OFF_KEPT ? HUMIDITY_OFF_KEPT : HUMIDITY_OFF,
310
+ values[flag] === DSIOT_HUMIDITY_OFF_KEPT
311
+ ? DSIOT_HUMIDITY_OFF_KEPT
312
+ : DSIOT_HUMIDITY_OFF,
451
313
  },
452
314
  ];
453
315
  }
454
316
  if (typeof targetHumidity === "string") {
455
- return [{ path: flag, value: HUMIDITY_SETTING_FLAGS[targetHumidity] }];
317
+ return [
318
+ { path: flag, value: DSIOT_HUMIDITY_SETTING_FLAGS[targetHumidity] },
319
+ ];
456
320
  }
457
321
  const count = Math.trunc(
458
322
  targetHumidity.percent / decodeStep(values[`${value}.md.st`]),
459
323
  );
460
324
  return [
461
- { path: flag, value: HUMIDITY_ON },
325
+ { path: flag, value: DSIOT_HUMIDITY_PERCENTAGE },
462
326
  { path: value, value: count.toString(16).toUpperCase().padStart(2, "0") },
463
327
  ];
464
328
  }
@@ -474,7 +338,7 @@ function isHumidityOn(
474
338
  return false;
475
339
  }
476
340
  return targetHumidity == null
477
- ? flag !== HUMIDITY_OFF && flag !== HUMIDITY_OFF_KEPT
341
+ ? flag !== DSIOT_HUMIDITY_OFF && flag !== DSIOT_HUMIDITY_OFF_KEPT
478
342
  : targetHumidity !== "off";
479
343
  }
480
344
 
@@ -504,7 +368,7 @@ export function parseDsiotState(values: Record<string, string>): DaikinState {
504
368
  verticalPosition: parseVerticalPosition(values, paths.vertical),
505
369
  horizontalPosition: parseHorizontalPosition(values[paths.horizontal]),
506
370
  specialModes: SPECIAL_MODES.filter(
507
- (mode) => values[DSIOT_SPECIAL_MODE_PATHS[mode]] === SPECIAL_MODE_ON,
371
+ (mode) => values[DSIOT_SPECIAL_MODE_PATHS[mode]] === DSIOT_ON,
508
372
  ),
509
373
  };
510
374
  }
@@ -0,0 +1,249 @@
1
+ import type {
2
+ FanSpeed,
3
+ HorizontalPosition,
4
+ HumiditySetting,
5
+ OperatingMode,
6
+ SpecialMode,
7
+ VerticalPosition,
8
+ } from "../../models/state.js";
9
+
10
+ // The resources, leaves and codes a DSIOT adapter holds each value of a model
11
+ // as. The codecs and the simulators both read them from here: see
12
+ // DESIGN.md#simulators.
13
+
14
+ export const DSIOT_OPERATIONS = {
15
+ get: 2,
16
+ update: 3,
17
+ delete: 4,
18
+ } as const;
19
+
20
+ /** The indoor and the outdoor unit are separate edges on the adapter. */
21
+ export const DSIOT_EDGES = {
22
+ indoor: "adr_0100",
23
+ outdoor: "adr_0200",
24
+ } as const;
25
+
26
+ /** What the `to` of a request starts with: an adapter resource follows as
27
+ * `.adp_i`, and a unit's as `/adr_0100.dgc_status`. */
28
+ export const DSIOT_EDGE_ROOT = "/dsiot/edge";
29
+
30
+ /** What a read fetches, as the `to` of a request: whole trees, never single
31
+ * leaves. The status trees come with `pt`, which `parseDsiotCapabilities`
32
+ * reads. */
33
+ export const DSIOT_TARGETS = {
34
+ adapterInfo: `${DSIOT_EDGE_ROOT}.adp_i?filter=pv`,
35
+ adapterSettings: `${DSIOT_EDGE_ROOT}.adp_d?filter=pv`,
36
+ indoorStatus: `${DSIOT_EDGE_ROOT}/${DSIOT_EDGES.indoor}.dgc_status?filter=pv,pt,md`,
37
+ outdoorStatus: `${DSIOT_EDGE_ROOT}/${DSIOT_EDGES.outdoor}.dgc_status?filter=pv,pt,md`,
38
+ } as const;
39
+
40
+ /** `type` in a discovery reply from a DSIOT adapter. */
41
+ export const DSIOT_ADAPTER_TYPE = "GPF";
42
+
43
+ /** Dotted paths into the `adp_i` and `adp_d` trees. The MAC address and the
44
+ * version hold the value a classic adapter sends in `basic_info`, and the
45
+ * name is plain text, where a classic adapter form-encodes it. */
46
+ export const DSIOT_ADAPTER_PATHS = {
47
+ name: "adp_d.name",
48
+ macAddress: "adp_i.mac",
49
+ firmwareVersion: "adp_i.ver",
50
+ region: "adp_i.reg",
51
+ kind: "adp_i.kind",
52
+ energyMonitoring: "adp_i.func.en_ipower",
53
+ demandControl: "adp_i.func.en_demand",
54
+ scheduleTimer: "adp_i.func.en_scdltmr",
55
+ } as const;
56
+
57
+ /** Full dotted paths into the `dgc_status` trees. `e_1002` lives on the indoor
58
+ * edge and `e_1003` on the outdoor edge. */
59
+ export const DSIOT_PATHS = {
60
+ power: "dgc_status.e_1002.e_A002.p_01",
61
+ /** Sent with every control. */
62
+ powerTransition: "dgc_status.e_1002.e_3003.p_2D",
63
+ mode: "dgc_status.e_1002.e_3001.p_01",
64
+ indoorTemperature: "dgc_status.e_1002.e_A00B.p_01",
65
+ indoorHumidity: "dgc_status.e_1002.e_A00B.p_02",
66
+ outdoorTemperature: "dgc_status.e_1003.e_A00D.p_01",
67
+ model: "dgc_status.e_1002.e_A001.p_0D",
68
+ modelType: "dgc_status.e_1002.e_A001.p_07",
69
+ energyMonitoring: "dgc_status.e_1002.e_A011.p_01",
70
+ } as const;
71
+
72
+ /** What a leaf that is on or off holds, such as the power. */
73
+ export const DSIOT_ON = "01";
74
+ export const DSIOT_OFF = "00";
75
+
76
+ /** `DSIOT_PATHS.modelType` on a unit that only cools. */
77
+ export const DSIOT_COOLING_ONLY_MODEL_TYPE = "01";
78
+
79
+ /** Each leaf holds `01` while its mode is on. Powerful is on the outdoor
80
+ * edge. */
81
+ export const DSIOT_SPECIAL_MODE_PATHS: Record<SpecialMode, string> = {
82
+ powerful: "dgc_status.e_1003.e_3002.p_44",
83
+ econo: "dgc_status.e_1002.e_3003.p_24",
84
+ streamer: "dgc_status.e_1002.e_3003.p_20",
85
+ };
86
+
87
+ export interface DsiotSetpoint {
88
+ path: string;
89
+ /** Auto and dehumidify are a signed byte; cool and heat are little-endian. */
90
+ signed: boolean;
91
+ /** The dehumidify setpoint is read-only. */
92
+ writable: boolean;
93
+ }
94
+
95
+ export interface DsiotHumidity {
96
+ /** Counted in steps of the leaf's `md.st`. */
97
+ value: string;
98
+ /** The setting: `00` or `05` off, `01` the percentage in `value`, and one
99
+ * value for each `HumiditySetting`. */
100
+ flag: string;
101
+ }
102
+
103
+ export interface DsiotModePaths {
104
+ /** Absent in fan mode. */
105
+ setpoint?: DsiotSetpoint;
106
+ /** Absent in fan mode. */
107
+ humidity?: DsiotHumidity;
108
+ fanSpeed: string;
109
+ vertical: string;
110
+ horizontal: string;
111
+ }
112
+
113
+ export const DSIOT_MODE_NODE = "dgc_status.e_1002.e_3001";
114
+
115
+ export const DSIOT_MODE_PATHS: Record<OperatingMode, DsiotModePaths> = {
116
+ auto: {
117
+ setpoint: { path: `${DSIOT_MODE_NODE}.p_1D`, signed: true, writable: true },
118
+ humidity: {
119
+ value: `${DSIOT_MODE_NODE}.p_2E`,
120
+ flag: `${DSIOT_MODE_NODE}.p_2F`,
121
+ },
122
+ fanSpeed: `${DSIOT_MODE_NODE}.p_26`,
123
+ vertical: `${DSIOT_MODE_NODE}.p_20`,
124
+ horizontal: `${DSIOT_MODE_NODE}.p_21`,
125
+ },
126
+ dehumidify: {
127
+ setpoint: {
128
+ path: `${DSIOT_MODE_NODE}.p_1E`,
129
+ signed: true,
130
+ writable: false,
131
+ },
132
+ humidity: {
133
+ value: `${DSIOT_MODE_NODE}.p_30`,
134
+ flag: `${DSIOT_MODE_NODE}.p_31`,
135
+ },
136
+ fanSpeed: `${DSIOT_MODE_NODE}.p_27`,
137
+ vertical: `${DSIOT_MODE_NODE}.p_22`,
138
+ horizontal: `${DSIOT_MODE_NODE}.p_23`,
139
+ },
140
+ cool: {
141
+ setpoint: {
142
+ path: `${DSIOT_MODE_NODE}.p_02`,
143
+ signed: false,
144
+ writable: true,
145
+ },
146
+ humidity: {
147
+ value: `${DSIOT_MODE_NODE}.p_0B`,
148
+ flag: `${DSIOT_MODE_NODE}.p_0C`,
149
+ },
150
+ fanSpeed: `${DSIOT_MODE_NODE}.p_09`,
151
+ vertical: `${DSIOT_MODE_NODE}.p_05`,
152
+ horizontal: `${DSIOT_MODE_NODE}.p_06`,
153
+ },
154
+ heat: {
155
+ setpoint: {
156
+ path: `${DSIOT_MODE_NODE}.p_03`,
157
+ signed: false,
158
+ writable: true,
159
+ },
160
+ humidity: {
161
+ value: `${DSIOT_MODE_NODE}.p_2C`,
162
+ flag: `${DSIOT_MODE_NODE}.p_2D`,
163
+ },
164
+ fanSpeed: `${DSIOT_MODE_NODE}.p_0A`,
165
+ vertical: `${DSIOT_MODE_NODE}.p_07`,
166
+ horizontal: `${DSIOT_MODE_NODE}.p_08`,
167
+ },
168
+ fan: {
169
+ fanSpeed: `${DSIOT_MODE_NODE}.p_28`,
170
+ vertical: `${DSIOT_MODE_NODE}.p_24`,
171
+ horizontal: `${DSIOT_MODE_NODE}.p_25`,
172
+ },
173
+ };
174
+
175
+ export const DSIOT_OPERATING_MODE_CODES: Record<OperatingMode, string> = {
176
+ fan: "0000",
177
+ heat: "0100",
178
+ cool: "0200",
179
+ auto: "0300",
180
+ dehumidify: "0500",
181
+ };
182
+
183
+ export const DSIOT_FAN_SPEED_CODES: Record<FanSpeed, string> = {
184
+ auto: "0A00",
185
+ silent: "0B00",
186
+ level1: "0300",
187
+ level2: "0400",
188
+ level3: "0500",
189
+ level4: "0600",
190
+ level5: "0700",
191
+ };
192
+
193
+ /** The values a louver leaf holds besides a fixed position, which is 1-14.
194
+ * The leaf is three bytes, little-endian, and bit n of its `md.mx` is set
195
+ * when the unit takes value n. */
196
+ export const DSIOT_LOUVER_VALUES = {
197
+ stop: 0,
198
+ swing: 15,
199
+ auto: 16,
200
+ naturalWind: 19,
201
+ floorWarming: 25,
202
+ } as const;
203
+
204
+ /** The values that are fixed positions: 1 to 14. */
205
+ export const DSIOT_FIXED_LOUVER_VALUES = Array.from(
206
+ { length: 14 },
207
+ (_, index) => index + 1,
208
+ );
209
+
210
+ export const DSIOT_LOUVER_STOP = "000000";
211
+ export const DSIOT_LOUVER_SWING = "0F0000";
212
+
213
+ /** The value each `VerticalPosition` is held as. A fixed position has none: it
214
+ * is held as the value at its rank among the fixed values the unit takes. */
215
+ export const DSIOT_VERTICAL_POSITION_VALUES: Partial<
216
+ Record<VerticalPosition, number>
217
+ > = {
218
+ stop: DSIOT_LOUVER_VALUES.stop,
219
+ swing: DSIOT_LOUVER_VALUES.swing,
220
+ naturalWind: DSIOT_LOUVER_VALUES.naturalWind,
221
+ auto: DSIOT_LOUVER_VALUES.auto,
222
+ floorWarming: DSIOT_LOUVER_VALUES.floorWarming,
223
+ };
224
+
225
+ /** The value each `HorizontalPosition` is held as. `stop` and a fixed position
226
+ * have none: see UNSUPPORTED.md. */
227
+ export const DSIOT_HORIZONTAL_POSITION_VALUES: Partial<
228
+ Record<HorizontalPosition, number>
229
+ > = {
230
+ swing: DSIOT_LOUVER_VALUES.swing,
231
+ auto: DSIOT_LOUVER_VALUES.auto,
232
+ };
233
+
234
+ /** What a humidity flag holds while the setting is off. */
235
+ export const DSIOT_HUMIDITY_OFF = "00";
236
+ /** What a humidity flag holds while the setting is a percentage, held in the
237
+ * mode's `value` leaf. */
238
+ export const DSIOT_HUMIDITY_PERCENTAGE = "01";
239
+ /** Also reads as off, and a control that switches humidity off leaves it. */
240
+ export const DSIOT_HUMIDITY_OFF_KEPT = "05";
241
+ export const DSIOT_HUMIDITY_SETTING_FLAGS: Record<
242
+ Exclude<HumiditySetting, "off">,
243
+ string
244
+ > = {
245
+ auto: "02",
246
+ autoHigh: "03",
247
+ autoLow: "04",
248
+ continuous: "06",
249
+ };
package/src/discovery.ts CHANGED
@@ -31,7 +31,8 @@ export interface DiscoverDevicesOptions {
31
31
  * datagram whose `ret` is not `OK`.
32
32
  *
33
33
  * It resolves once `timeoutInMs` has passed, never earlier, with one device
34
- * per replying address. It binds UDP port 30000, and rejects with the socket's
34
+ * for each `host` that replied: the address a reply came from, with the port
35
+ * the reply announces. It binds UDP port 30000, and rejects with the socket's
35
36
  * error when the socket fails. */
36
37
  export async function discoverDevices(
37
38
  options: DiscoverDevicesOptions = {},
@@ -58,14 +59,17 @@ export async function discoverDevices(
58
59
  });
59
60
 
60
61
  socket.on("message", (message, remote) => {
61
- if (devices.has(remote.address)) {
62
- return;
63
- }
64
62
  const fields = parseClassicKeyValue(message.toString("utf8"));
65
63
  if (!isClassicOk(fields)) {
66
64
  return;
67
65
  }
68
- devices.set(remote.address, parseDevice(remote.address, fields));
66
+ // An adapter answers each of the probes, so the first reply from a
67
+ // host is kept. Adapters at one address, as simulators on one machine
68
+ // are, differ by the port they announce.
69
+ const device = parseDevice(remote.address, fields);
70
+ if (!devices.has(device.host)) {
71
+ devices.set(device.host, device);
72
+ }
69
73
  });
70
74
 
71
75
  socket.bind(DISCOVERY_SEND_PORT, () => {
package/src/factory.ts CHANGED
@@ -14,8 +14,9 @@ import {
14
14
  } from "./transport.js";
15
15
 
16
16
  export interface CreateDaikinClientOptions {
17
- /** The terminal uuid, sent as `X-Daikin-uuid`. Generate one and reuse it:
18
- * an adapter that requires registration remembers it. */
17
+ /** The terminal uuid, sent as `X-Daikin-uuid`: 32 lowercase hexadecimal
18
+ * digits, a UUID without its dashes. Generate one and reuse it: an adapter
19
+ * that requires registration remembers it. */
19
20
  terminalUuid: string;
20
21
  /** The child lock code, for a device whose `requiresLocalPassword` is true. */
21
22
  localPassword?: string;
@@ -40,8 +41,10 @@ export interface CreateDaikinClientOptions {
40
41
  /** Builds the client for a device: a `DsiotDaikinClient` when
41
42
  * `device.protocol` is `dsiot`, a `ClassicDaikinClient` otherwise. The client
42
43
  * sends over HTTPS when `device.usesHttps` is set, and sends the local
43
- * password when `options.localPassword` is given. Call `close` on the client
44
- * when done with the unit. */
44
+ * password when `options.localPassword` is given. It throws an `Error` when
45
+ * `options.terminalUuid` is not in the form
46
+ * `CreateDaikinClientOptions.terminalUuid` describes. Call `close` on the
47
+ * client when done with the unit. */
45
48
  export function createDaikinClient(
46
49
  device: DaikinDevice,
47
50
  options: CreateDaikinClientOptions,
package/src/index.ts CHANGED
@@ -6,12 +6,14 @@ export * from "./codecs/classic/capabilities.js";
6
6
  export * from "./codecs/classic/extendedControl.js";
7
7
  export * from "./codecs/classic/keyValue.js";
8
8
  export * from "./codecs/classic/state.js";
9
+ export * from "./codecs/classic/tables.js";
9
10
  export * from "./codecs/credentials.js";
10
11
  export * from "./codecs/device.js";
11
12
  export * from "./codecs/dsiot/capabilities.js";
12
13
  export * from "./codecs/dsiot/device.js";
13
14
  export * from "./codecs/dsiot/multireq.js";
14
15
  export * from "./codecs/dsiot/state.js";
16
+ export * from "./codecs/dsiot/tables.js";
15
17
  export * from "./discovery.js";
16
18
  export * from "./error.js";
17
19
  export * from "./factory.js";