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.
- package/DESIGN.md +113 -2
- package/README.md +131 -13
- package/UNSUPPORTED.md +42 -4
- package/dist/clients/classic.d.ts +2 -2
- package/dist/clients/classic.d.ts.map +1 -1
- package/dist/clients/classic.js +4 -4
- package/dist/clients/classic.js.map +1 -1
- package/dist/clients/client.d.ts +6 -2
- package/dist/clients/client.d.ts.map +1 -1
- package/dist/clients/dsiot.d.ts +3 -1
- package/dist/clients/dsiot.d.ts.map +1 -1
- package/dist/clients/dsiot.js +11 -3
- package/dist/clients/dsiot.js.map +1 -1
- package/dist/codecs/classic/capabilities.d.ts.map +1 -1
- package/dist/codecs/classic/capabilities.js +5 -11
- package/dist/codecs/classic/capabilities.js.map +1 -1
- package/dist/codecs/classic/extendedControl.d.ts.map +1 -1
- package/dist/codecs/classic/extendedControl.js +10 -19
- package/dist/codecs/classic/extendedControl.js.map +1 -1
- package/dist/codecs/classic/state.d.ts.map +1 -1
- package/dist/codecs/classic/state.js +36 -95
- package/dist/codecs/classic/state.js.map +1 -1
- package/dist/codecs/classic/tables.d.ts +32 -0
- package/dist/codecs/classic/tables.d.ts.map +1 -0
- package/dist/codecs/classic/tables.js +94 -0
- package/dist/codecs/classic/tables.js.map +1 -0
- package/dist/codecs/credentials.d.ts +3 -2
- package/dist/codecs/credentials.d.ts.map +1 -1
- package/dist/codecs/credentials.js +8 -3
- package/dist/codecs/credentials.js.map +1 -1
- package/dist/codecs/device.d.ts +2 -1
- package/dist/codecs/device.d.ts.map +1 -1
- package/dist/codecs/device.js +7 -6
- package/dist/codecs/device.js.map +1 -1
- package/dist/codecs/dsiot/capabilities.d.ts.map +1 -1
- package/dist/codecs/dsiot/capabilities.js +8 -15
- package/dist/codecs/dsiot/capabilities.js.map +1 -1
- package/dist/codecs/dsiot/device.d.ts +4 -2
- package/dist/codecs/dsiot/device.d.ts.map +1 -1
- package/dist/codecs/dsiot/device.js +8 -8
- package/dist/codecs/dsiot/device.js.map +1 -1
- package/dist/codecs/dsiot/multireq.d.ts +0 -18
- package/dist/codecs/dsiot/multireq.d.ts.map +1 -1
- package/dist/codecs/dsiot/multireq.js +2 -24
- package/dist/codecs/dsiot/multireq.js.map +1 -1
- package/dist/codecs/dsiot/state.d.ts +6 -62
- package/dist/codecs/dsiot/state.d.ts.map +1 -1
- package/dist/codecs/dsiot/state.js +29 -148
- package/dist/codecs/dsiot/state.js.map +1 -1
- package/dist/codecs/dsiot/tables.d.ts +116 -0
- package/dist/codecs/dsiot/tables.d.ts.map +1 -0
- package/dist/codecs/dsiot/tables.js +185 -0
- package/dist/codecs/dsiot/tables.js.map +1 -0
- package/dist/discovery.d.ts +2 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +9 -5
- package/dist/discovery.js.map +1 -1
- package/dist/factory.d.ts +7 -4
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +4 -2
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/models/device.d.ts +3 -1
- package/dist/models/device.d.ts.map +1 -1
- package/dist/models/state.d.ts +2 -3
- package/dist/models/state.d.ts.map +1 -1
- package/dist/simulators/classic.d.ts +61 -0
- package/dist/simulators/classic.d.ts.map +1 -0
- package/dist/simulators/classic.js +473 -0
- package/dist/simulators/classic.js.map +1 -0
- package/dist/simulators/dsiot.d.ts +49 -0
- package/dist/simulators/dsiot.d.ts.map +1 -0
- package/dist/simulators/dsiot.js +519 -0
- package/dist/simulators/dsiot.js.map +1 -0
- package/dist/simulators/index.d.ts +4 -0
- package/dist/simulators/index.d.ts.map +1 -0
- package/dist/simulators/index.js +3 -0
- package/dist/simulators/index.js.map +1 -0
- package/dist/simulators/simulator.d.ts +116 -0
- package/dist/simulators/simulator.d.ts.map +1 -0
- package/dist/simulators/simulator.js +141 -0
- package/dist/simulators/simulator.js.map +1 -0
- package/dist/transport.d.ts +6 -2
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +7 -2
- package/dist/transport.js.map +1 -1
- package/package.json +5 -2
- package/src/clients/classic.ts +8 -8
- package/src/clients/client.ts +7 -1
- package/src/clients/dsiot.ts +12 -2
- package/src/codecs/classic/capabilities.ts +8 -11
- package/src/codecs/classic/extendedControl.ts +14 -25
- package/src/codecs/classic/state.ts +49 -101
- package/src/codecs/classic/tables.ts +123 -0
- package/src/codecs/credentials.ts +11 -3
- package/src/codecs/device.ts +8 -7
- package/src/codecs/dsiot/capabilities.ts +10 -17
- package/src/codecs/dsiot/device.ts +8 -9
- package/src/codecs/dsiot/multireq.ts +2 -24
- package/src/codecs/dsiot/state.ts +57 -193
- package/src/codecs/dsiot/tables.ts +249 -0
- package/src/discovery.ts +9 -5
- package/src/factory.ts +7 -4
- package/src/index.ts +2 -0
- package/src/models/device.ts +3 -1
- package/src/models/state.ts +2 -3
- package/src/simulators/classic.ts +635 -0
- package/src/simulators/dsiot.ts +736 -0
- package/src/simulators/index.ts +9 -0
- package/src/simulators/simulator.ts +281 -0
- 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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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 ===
|
|
284
|
+
if (flag === DSIOT_HUMIDITY_OFF || flag === DSIOT_HUMIDITY_OFF_KEPT) {
|
|
425
285
|
return "off";
|
|
426
286
|
}
|
|
427
|
-
if (humidity === undefined || flag !==
|
|
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] ===
|
|
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 [
|
|
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:
|
|
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 !==
|
|
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]] ===
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
18
|
-
*
|
|
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.
|
|
44
|
-
*
|
|
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";
|