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.
- package/DESIGN.md +98 -0
- package/LICENSE +674 -0
- package/README.md +317 -0
- package/UNSUPPORTED.md +43 -0
- package/dist/authentication.d.ts +24 -0
- package/dist/authentication.d.ts.map +1 -0
- package/dist/authentication.js +30 -0
- package/dist/authentication.js.map +1 -0
- package/dist/clients/classic.d.ts +53 -0
- package/dist/clients/classic.d.ts.map +1 -0
- package/dist/clients/classic.js +142 -0
- package/dist/clients/classic.js.map +1 -0
- package/dist/clients/client.d.ts +21 -0
- package/dist/clients/client.d.ts.map +1 -0
- package/dist/clients/client.js +2 -0
- package/dist/clients/client.js.map +1 -0
- package/dist/clients/dsiot.d.ts +28 -0
- package/dist/clients/dsiot.d.ts.map +1 -0
- package/dist/clients/dsiot.js +66 -0
- package/dist/clients/dsiot.js.map +1 -0
- package/dist/codecs/classic/capabilities.d.ts +19 -0
- package/dist/codecs/classic/capabilities.d.ts.map +1 -0
- package/dist/codecs/classic/capabilities.js +208 -0
- package/dist/codecs/classic/capabilities.js.map +1 -0
- package/dist/codecs/classic/extendedControl.d.ts +20 -0
- package/dist/codecs/classic/extendedControl.d.ts.map +1 -0
- package/dist/codecs/classic/extendedControl.js +88 -0
- package/dist/codecs/classic/extendedControl.js.map +1 -0
- package/dist/codecs/classic/keyValue.d.ts +10 -0
- package/dist/codecs/classic/keyValue.d.ts.map +1 -0
- package/dist/codecs/classic/keyValue.js +36 -0
- package/dist/codecs/classic/keyValue.js.map +1 -0
- package/dist/codecs/classic/state.d.ts +18 -0
- package/dist/codecs/classic/state.d.ts.map +1 -0
- package/dist/codecs/classic/state.js +235 -0
- package/dist/codecs/classic/state.js.map +1 -0
- package/dist/codecs/credentials.d.ts +8 -0
- package/dist/codecs/credentials.d.ts.map +1 -0
- package/dist/codecs/credentials.js +14 -0
- package/dist/codecs/credentials.js.map +1 -0
- package/dist/codecs/device.d.ts +6 -0
- package/dist/codecs/device.d.ts.map +1 -0
- package/dist/codecs/device.js +35 -0
- package/dist/codecs/device.js.map +1 -0
- package/dist/codecs/dsiot/capabilities.d.ts +5 -0
- package/dist/codecs/dsiot/capabilities.d.ts.map +1 -0
- package/dist/codecs/dsiot/capabilities.js +139 -0
- package/dist/codecs/dsiot/capabilities.js.map +1 -0
- package/dist/codecs/dsiot/multireq.d.ts +37 -0
- package/dist/codecs/dsiot/multireq.d.ts.map +1 -0
- package/dist/codecs/dsiot/multireq.js +122 -0
- package/dist/codecs/dsiot/multireq.js.map +1 -0
- package/dist/codecs/dsiot/state.d.ts +92 -0
- package/dist/codecs/dsiot/state.d.ts.map +1 -0
- package/dist/codecs/dsiot/state.js +461 -0
- package/dist/codecs/dsiot/state.js.map +1 -0
- package/dist/discovery.d.ts +17 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +67 -0
- package/dist/discovery.js.map +1 -0
- package/dist/error.d.ts +15 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +20 -0
- package/dist/error.js.map +1 -0
- package/dist/factory.d.ts +26 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +31 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/models/capabilities.d.ts +42 -0
- package/dist/models/capabilities.d.ts.map +1 -0
- package/dist/models/capabilities.js +2 -0
- package/dist/models/capabilities.js.map +1 -0
- package/dist/models/device.d.ts +24 -0
- package/dist/models/device.d.ts.map +1 -0
- package/dist/models/device.js +2 -0
- package/dist/models/device.js.map +1 -0
- package/dist/models/extendedControl.d.ts +31 -0
- package/dist/models/extendedControl.d.ts.map +1 -0
- package/dist/models/extendedControl.js +7 -0
- package/dist/models/extendedControl.js.map +1 -0
- package/dist/models/state.d.ts +56 -0
- package/dist/models/state.d.ts.map +1 -0
- package/dist/models/state.js +49 -0
- package/dist/models/state.js.map +1 -0
- package/dist/transport.d.ts +75 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +118 -0
- package/dist/transport.js.map +1 -0
- package/package.json +62 -0
- package/src/authentication.ts +53 -0
- package/src/clients/classic.ts +251 -0
- package/src/clients/client.ts +29 -0
- package/src/clients/dsiot.ts +116 -0
- package/src/codecs/classic/capabilities.ts +272 -0
- package/src/codecs/classic/extendedControl.ts +132 -0
- package/src/codecs/classic/keyValue.ts +39 -0
- package/src/codecs/classic/state.ts +302 -0
- package/src/codecs/credentials.ts +19 -0
- package/src/codecs/device.ts +41 -0
- package/src/codecs/dsiot/capabilities.ts +209 -0
- package/src/codecs/dsiot/multireq.ts +162 -0
- package/src/codecs/dsiot/state.ts +644 -0
- package/src/discovery.ts +85 -0
- package/src/error.ts +20 -0
- package/src/factory.ts +66 -0
- package/src/index.ts +21 -0
- package/src/models/capabilities.ts +52 -0
- package/src/models/device.ts +25 -0
- package/src/models/extendedControl.ts +41 -0
- package/src/models/state.ts +110 -0
- 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
|
+
}
|