daikin-local-control 0.2.1 → 0.3.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 +52 -0
- package/README.md +39 -2
- package/UNSUPPORTED.md +10 -1
- package/dist/clients/classic.d.ts +22 -4
- package/dist/clients/classic.d.ts.map +1 -1
- package/dist/clients/classic.js +55 -8
- package/dist/clients/classic.js.map +1 -1
- package/dist/clients/client.d.ts +11 -2
- package/dist/clients/client.d.ts.map +1 -1
- package/dist/clients/dsiot.d.ts +10 -3
- package/dist/clients/dsiot.d.ts.map +1 -1
- package/dist/clients/dsiot.js +40 -5
- package/dist/clients/dsiot.js.map +1 -1
- package/dist/clients/setStateCache.d.ts +15 -0
- package/dist/clients/setStateCache.d.ts.map +1 -0
- package/dist/clients/setStateCache.js +26 -0
- package/dist/clients/setStateCache.js.map +1 -0
- package/dist/codecs/classic/capabilities.d.ts.map +1 -1
- package/dist/codecs/classic/capabilities.js +28 -1
- package/dist/codecs/classic/capabilities.js.map +1 -1
- package/dist/codecs/classic/extendedControl.d.ts +14 -1
- package/dist/codecs/classic/extendedControl.d.ts.map +1 -1
- package/dist/codecs/classic/extendedControl.js +39 -4
- package/dist/codecs/classic/extendedControl.js.map +1 -1
- package/dist/codecs/classic/keyValue.d.ts +3 -0
- package/dist/codecs/classic/keyValue.d.ts.map +1 -1
- package/dist/codecs/classic/keyValue.js +10 -0
- package/dist/codecs/classic/keyValue.js.map +1 -1
- package/dist/codecs/classic/state.d.ts +3 -2
- package/dist/codecs/classic/state.d.ts.map +1 -1
- package/dist/codecs/classic/state.js +14 -1
- package/dist/codecs/classic/state.js.map +1 -1
- package/dist/codecs/device.d.ts +10 -1
- package/dist/codecs/device.d.ts.map +1 -1
- package/dist/codecs/device.js +25 -10
- package/dist/codecs/device.js.map +1 -1
- package/dist/codecs/dsiot/capabilities.d.ts +4 -2
- package/dist/codecs/dsiot/capabilities.d.ts.map +1 -1
- package/dist/codecs/dsiot/capabilities.js +20 -4
- package/dist/codecs/dsiot/capabilities.js.map +1 -1
- package/dist/codecs/dsiot/device.d.ts +5 -0
- package/dist/codecs/dsiot/device.d.ts.map +1 -0
- package/dist/codecs/dsiot/device.js +14 -0
- package/dist/codecs/dsiot/device.js.map +1 -0
- package/dist/codecs/dsiot/multireq.d.ts +6 -4
- package/dist/codecs/dsiot/multireq.d.ts.map +1 -1
- package/dist/codecs/dsiot/multireq.js +9 -4
- package/dist/codecs/dsiot/multireq.js.map +1 -1
- package/dist/codecs/dsiot/state.d.ts +6 -2
- package/dist/codecs/dsiot/state.d.ts.map +1 -1
- package/dist/codecs/dsiot/state.js +12 -2
- package/dist/codecs/dsiot/state.js.map +1 -1
- package/dist/factory.d.ts +7 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +2 -2
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/models/capabilities.d.ts +5 -1
- package/dist/models/capabilities.d.ts.map +1 -1
- package/dist/models/device.d.ts +12 -1
- package/dist/models/device.d.ts.map +1 -1
- package/dist/models/extendedControl.d.ts +2 -3
- package/dist/models/extendedControl.d.ts.map +1 -1
- package/dist/models/extendedControl.js +0 -1
- package/dist/models/extendedControl.js.map +1 -1
- package/dist/models/state.d.ts +15 -4
- package/dist/models/state.d.ts.map +1 -1
- package/dist/models/state.js +1 -0
- package/dist/models/state.js.map +1 -1
- package/package.json +1 -1
- package/src/clients/classic.ts +76 -10
- package/src/clients/client.ts +13 -1
- package/src/clients/dsiot.ts +52 -9
- package/src/clients/setStateCache.ts +28 -0
- package/src/codecs/classic/capabilities.ts +39 -0
- package/src/codecs/classic/extendedControl.ts +56 -5
- package/src/codecs/classic/keyValue.ts +10 -0
- package/src/codecs/classic/state.ts +30 -12
- package/src/codecs/device.ts +35 -10
- package/src/codecs/dsiot/capabilities.ts +27 -2
- package/src/codecs/dsiot/device.ts +20 -0
- package/src/codecs/dsiot/multireq.ts +9 -4
- package/src/codecs/dsiot/state.ts +16 -1
- package/src/factory.ts +17 -2
- package/src/index.ts +1 -0
- package/src/models/capabilities.ts +5 -0
- package/src/models/device.ts +13 -1
- package/src/models/extendedControl.ts +2 -5
- package/src/models/state.ts +18 -4
package/src/clients/classic.ts
CHANGED
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
encodeClassicSpecialMode,
|
|
16
16
|
encodeClassicTerminalKey,
|
|
17
17
|
encodeClassicTerminalScope,
|
|
18
|
+
parseClassicHolidayMode,
|
|
19
|
+
parseClassicLed,
|
|
20
|
+
parseClassicMetadata,
|
|
21
|
+
parseClassicOutOfHomeAccess,
|
|
18
22
|
} from "../codecs/classic/extendedControl.js";
|
|
19
23
|
import {
|
|
20
24
|
assertClassicOk,
|
|
@@ -25,17 +29,20 @@ import {
|
|
|
25
29
|
parseClassicSensors,
|
|
26
30
|
parseClassicState,
|
|
27
31
|
} from "../codecs/classic/state.js";
|
|
32
|
+
import { parseDeviceInfo } from "../codecs/device.js";
|
|
28
33
|
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
34
|
+
import type { DaikinDeviceInfo } from "../models/device.js";
|
|
29
35
|
import type {
|
|
30
36
|
DemandControl,
|
|
31
37
|
DeviceMetadata,
|
|
32
|
-
SpecialMode,
|
|
33
38
|
TerminalScope,
|
|
34
39
|
} from "../models/extendedControl.js";
|
|
35
40
|
import type {
|
|
36
41
|
DaikinControl,
|
|
37
42
|
DaikinSensors,
|
|
38
43
|
DaikinState,
|
|
44
|
+
DaikinStateAndSensors,
|
|
45
|
+
SpecialMode,
|
|
39
46
|
} from "../models/state.js";
|
|
40
47
|
import {
|
|
41
48
|
type DaikinRequestOptions,
|
|
@@ -44,6 +51,7 @@ import {
|
|
|
44
51
|
withRetry,
|
|
45
52
|
} from "../transport.js";
|
|
46
53
|
import type { DaikinClient } from "./client.js";
|
|
54
|
+
import { SetStateCache } from "./setStateCache.js";
|
|
47
55
|
|
|
48
56
|
// Classic CGI endpoints (BRP069-style). Reads and writes are both HTTP GET with
|
|
49
57
|
// query parameters; the reply is `key=value,key=value`.
|
|
@@ -75,28 +83,40 @@ export class ClassicDaikinClient implements DaikinClient {
|
|
|
75
83
|
// A unit's capabilities don't change, and `setState` needs them, so they are
|
|
76
84
|
// read once.
|
|
77
85
|
private capabilityFields: Promise<Record<string, string>> | undefined;
|
|
86
|
+
private readonly setStateCache: SetStateCache;
|
|
78
87
|
|
|
79
|
-
/** `close` closes `transport`, so give each client a transport of its own.
|
|
88
|
+
/** `close` closes `transport`, so give each client a transport of its own.
|
|
89
|
+
* `maxStateAgeForSetStateInMs` defaults to 0: see
|
|
90
|
+
* `CreateDaikinClientOptions.maxStateAgeForSetStateInMs`. */
|
|
80
91
|
constructor(
|
|
81
92
|
private readonly transport: DaikinTransport,
|
|
82
93
|
private readonly retryOptions?: DaikinRetryOptions,
|
|
83
|
-
|
|
94
|
+
maxStateAgeForSetStateInMs = 0,
|
|
95
|
+
) {
|
|
96
|
+
this.setStateCache = new SetStateCache(maxStateAgeForSetStateInMs);
|
|
97
|
+
}
|
|
84
98
|
|
|
85
99
|
close(): Promise<void> {
|
|
86
100
|
return this.transport.close();
|
|
87
101
|
}
|
|
88
102
|
|
|
103
|
+
async getDeviceInfo(): Promise<DaikinDeviceInfo> {
|
|
104
|
+
return parseDeviceInfo(await this.command(CLASSIC_ROUTES.basicInfo));
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
async getCapabilities(): Promise<DaikinCapabilities> {
|
|
90
108
|
return parseClassicCapabilities(await this.readCapabilityFields());
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
async getState(): Promise<DaikinState> {
|
|
94
|
-
return parseClassicState(await this.
|
|
112
|
+
return parseClassicState(await this.readControlInfo());
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
async setState(control: DaikinControl): Promise<void> {
|
|
98
116
|
const capabilityFields = await this.readCapabilityFields();
|
|
99
|
-
const current =
|
|
117
|
+
const current =
|
|
118
|
+
this.setStateCache.get() ??
|
|
119
|
+
(await this.command(CLASSIC_ROUTES.getControlInfo));
|
|
100
120
|
await this.command(
|
|
101
121
|
CLASSIC_ROUTES.setControlInfo,
|
|
102
122
|
applyClassicControl(current, control, capabilityFields),
|
|
@@ -109,14 +129,32 @@ export class ClassicDaikinClient implements DaikinClient {
|
|
|
109
129
|
);
|
|
110
130
|
}
|
|
111
131
|
|
|
132
|
+
async getStateAndSensors(): Promise<DaikinStateAndSensors> {
|
|
133
|
+
return { state: await this.getState(), sensors: await this.getSensors() };
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
// --- extended controls ---------------------------------------------------
|
|
113
|
-
// Each
|
|
137
|
+
// Each write goes to its own endpoint, not `set_control_info`.
|
|
138
|
+
|
|
139
|
+
/** Whether the adapter's status LED is on (`basic_info`): `null` when the
|
|
140
|
+
* adapter does not report it. */
|
|
141
|
+
async getLed(): Promise<boolean | null> {
|
|
142
|
+
return parseClassicLed(await this.command(CLASSIC_ROUTES.basicInfo));
|
|
143
|
+
}
|
|
114
144
|
|
|
115
145
|
/** Adapter status LED on/off (`set_led`). */
|
|
116
146
|
async setLed(on: boolean): Promise<void> {
|
|
117
147
|
await this.command(CLASSIC_ROUTES.setLed, encodeClassicLed(on));
|
|
118
148
|
}
|
|
119
149
|
|
|
150
|
+
/** Whether holiday mode is on (`basic_info`): `null` when the adapter does
|
|
151
|
+
* not report it. */
|
|
152
|
+
async getHolidayMode(): Promise<boolean | null> {
|
|
153
|
+
return parseClassicHolidayMode(
|
|
154
|
+
await this.command(CLASSIC_ROUTES.basicInfo),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
120
158
|
/** Holiday / away mode (`set_holiday`). */
|
|
121
159
|
async setHolidayMode(enabled: boolean): Promise<void> {
|
|
122
160
|
await this.command(
|
|
@@ -141,6 +179,14 @@ export class ClassicDaikinClient implements DaikinClient {
|
|
|
141
179
|
);
|
|
142
180
|
}
|
|
143
181
|
|
|
182
|
+
/** Whether the unit can be operated from outside the home network
|
|
183
|
+
* (`basic_info`): `null` when the adapter does not report it. */
|
|
184
|
+
async getOutOfHomeAccess(): Promise<boolean | null> {
|
|
185
|
+
return parseClassicOutOfHomeAccess(
|
|
186
|
+
await this.command(CLASSIC_ROUTES.basicInfo),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
144
190
|
/** Whether the unit can be operated from outside the home network
|
|
145
191
|
* (`set_remote_method`). */
|
|
146
192
|
async setOutOfHomeAccess(enabled: boolean): Promise<void> {
|
|
@@ -168,6 +214,11 @@ export class ClassicDaikinClient implements DaikinClient {
|
|
|
168
214
|
);
|
|
169
215
|
}
|
|
170
216
|
|
|
217
|
+
/** Name / icon / group metadata (`basic_info`). */
|
|
218
|
+
async getMetadata(): Promise<DeviceMetadata> {
|
|
219
|
+
return parseClassicMetadata(await this.command(CLASSIC_ROUTES.basicInfo));
|
|
220
|
+
}
|
|
221
|
+
|
|
171
222
|
/** Name / icon / group metadata (`set_name` etc.). */
|
|
172
223
|
async setMetadata(metadata: DeviceMetadata): Promise<void> {
|
|
173
224
|
if (metadata.name !== undefined) {
|
|
@@ -229,10 +280,25 @@ export class ClassicDaikinClient implements DaikinClient {
|
|
|
229
280
|
path,
|
|
230
281
|
...(parameters ? { query: parameters } : {}),
|
|
231
282
|
});
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
283
|
+
try {
|
|
284
|
+
const fields = parseClassicKeyValue(
|
|
285
|
+
await (retry ? withRetry(send, this.retryOptions) : send()),
|
|
286
|
+
);
|
|
287
|
+
assertClassicOk(fields);
|
|
288
|
+
return fields;
|
|
289
|
+
} finally {
|
|
290
|
+
// Every request that changes the unit takes parameters, and any of them
|
|
291
|
+
// may change what `get_control_info` answers, even one that failed: a
|
|
292
|
+
// request that timed out may still have reached the unit.
|
|
293
|
+
if (parameters) {
|
|
294
|
+
this.setStateCache.clear();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private async readControlInfo(): Promise<Record<string, string>> {
|
|
300
|
+
const fields = await this.command(CLASSIC_ROUTES.getControlInfo);
|
|
301
|
+
this.setStateCache.set(fields);
|
|
236
302
|
return fields;
|
|
237
303
|
}
|
|
238
304
|
|
package/src/clients/client.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
2
|
+
import type { DaikinDeviceInfo } from "../models/device.js";
|
|
2
3
|
import type {
|
|
3
4
|
DaikinControl,
|
|
4
5
|
DaikinSensors,
|
|
5
6
|
DaikinState,
|
|
7
|
+
DaikinStateAndSensors,
|
|
6
8
|
} from "../models/state.js";
|
|
7
9
|
|
|
8
10
|
/** The controls every adapter offers, whatever its wire protocol or
|
|
@@ -11,6 +13,9 @@ import type {
|
|
|
11
13
|
* `ClassicDaikinClient.registerTerminal`. Every method rejects with a
|
|
12
14
|
* `DaikinError` when its request fails. */
|
|
13
15
|
export interface DaikinClient {
|
|
16
|
+
/** The adapter's MAC address and firmware version. */
|
|
17
|
+
getDeviceInfo(): Promise<DaikinDeviceInfo>;
|
|
18
|
+
|
|
14
19
|
/** The unit's supported features. */
|
|
15
20
|
getCapabilities(): Promise<DaikinCapabilities>;
|
|
16
21
|
|
|
@@ -18,12 +23,19 @@ export interface DaikinClient {
|
|
|
18
23
|
getState(): Promise<DaikinState>;
|
|
19
24
|
|
|
20
25
|
/** Change part of the state. The client reads the current state, overlays the
|
|
21
|
-
* control, and writes the whole thing back.
|
|
26
|
+
* control, and writes the whole thing back. It skips the read when it holds
|
|
27
|
+
* a reply to `getState` or `getStateAndSensors` younger than
|
|
28
|
+
* `CreateDaikinClientOptions.maxStateAgeForSetStateInMs`. */
|
|
22
29
|
setState(control: DaikinControl): Promise<void>;
|
|
23
30
|
|
|
24
31
|
/** Read-only temperature / humidity readings. */
|
|
25
32
|
getSensors(): Promise<DaikinSensors>;
|
|
26
33
|
|
|
34
|
+
/** What `getState` and `getSensors` resolve with, together. A DSIOT adapter
|
|
35
|
+
* answers both in one request, where calling the two sends the same request
|
|
36
|
+
* twice. A classic adapter takes one request for each. */
|
|
37
|
+
getStateAndSensors(): Promise<DaikinStateAndSensors>;
|
|
38
|
+
|
|
27
39
|
/** Close the connection to the adapter. */
|
|
28
40
|
close(): Promise<void>;
|
|
29
41
|
}
|
package/src/clients/dsiot.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseDsiotCapabilities } from "../codecs/dsiot/capabilities.js";
|
|
2
|
+
import { parseDsiotDeviceInfo } from "../codecs/dsiot/device.js";
|
|
2
3
|
import {
|
|
3
4
|
buildDsiotMultireqGet,
|
|
4
5
|
buildDsiotMultireqUpdate,
|
|
@@ -12,10 +13,12 @@ import {
|
|
|
12
13
|
parseDsiotState,
|
|
13
14
|
} from "../codecs/dsiot/state.js";
|
|
14
15
|
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
16
|
+
import type { DaikinDeviceInfo } from "../models/device.js";
|
|
15
17
|
import type {
|
|
16
18
|
DaikinControl,
|
|
17
19
|
DaikinSensors,
|
|
18
20
|
DaikinState,
|
|
21
|
+
DaikinStateAndSensors,
|
|
19
22
|
} from "../models/state.js";
|
|
20
23
|
import {
|
|
21
24
|
type DaikinRequestOptions,
|
|
@@ -24,6 +27,7 @@ import {
|
|
|
24
27
|
withRetry,
|
|
25
28
|
} from "../transport.js";
|
|
26
29
|
import type { DaikinClient } from "./client.js";
|
|
30
|
+
import { SetStateCache } from "./setStateCache.js";
|
|
27
31
|
|
|
28
32
|
// DSIOT endpoints (BRP084-style). Climate state is a POST (read) or PUT (write)
|
|
29
33
|
// of a JSON property tree to `multireq`.
|
|
@@ -35,33 +39,47 @@ const DSIOT_ROUTES = {
|
|
|
35
39
|
/** DSIOT adapter (BRP084-style). One endpoint, `/dsiot/multireq`: POST to read
|
|
36
40
|
* whole `dgc_status` trees, PUT to write one merged tree. */
|
|
37
41
|
export class DsiotDaikinClient implements DaikinClient {
|
|
38
|
-
|
|
42
|
+
private readonly setStateCache: SetStateCache;
|
|
43
|
+
|
|
44
|
+
/** `close` closes `transport`, so give each client a transport of its own.
|
|
45
|
+
* `maxStateAgeForSetStateInMs` defaults to 0: see
|
|
46
|
+
* `CreateDaikinClientOptions.maxStateAgeForSetStateInMs`. */
|
|
39
47
|
constructor(
|
|
40
48
|
private readonly transport: DaikinTransport,
|
|
41
49
|
private readonly retryOptions?: DaikinRetryOptions,
|
|
42
|
-
|
|
50
|
+
maxStateAgeForSetStateInMs = 0,
|
|
51
|
+
) {
|
|
52
|
+
this.setStateCache = new SetStateCache(maxStateAgeForSetStateInMs);
|
|
53
|
+
}
|
|
43
54
|
|
|
44
55
|
close(): Promise<void> {
|
|
45
56
|
return this.transport.close();
|
|
46
57
|
}
|
|
47
58
|
|
|
59
|
+
async getDeviceInfo(): Promise<DaikinDeviceInfo> {
|
|
60
|
+
return parseDsiotDeviceInfo(
|
|
61
|
+
await this.multireqGet([DSIOT_TARGETS.adapterInfo]),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
async getCapabilities(): Promise<DaikinCapabilities> {
|
|
49
66
|
return parseDsiotCapabilities(
|
|
50
67
|
await this.multireqGet([
|
|
51
68
|
DSIOT_TARGETS.adapterInfo,
|
|
52
69
|
DSIOT_TARGETS.indoorStatus,
|
|
70
|
+
DSIOT_TARGETS.outdoorStatus,
|
|
53
71
|
]),
|
|
54
72
|
);
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
async getState(): Promise<DaikinState> {
|
|
58
|
-
return parseDsiotState(
|
|
59
|
-
await this.multireqGet([DSIOT_TARGETS.indoorStatus]),
|
|
60
|
-
);
|
|
76
|
+
return parseDsiotState(await this.readStatus());
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
async setState(control: DaikinControl): Promise<void> {
|
|
64
|
-
const values =
|
|
80
|
+
const values =
|
|
81
|
+
this.setStateCache.get() ??
|
|
82
|
+
(await this.multireqGet([DSIOT_TARGETS.indoorStatus]));
|
|
65
83
|
await this.multireqUpdate(applyDsiotControl(values, control));
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -74,6 +92,14 @@ export class DsiotDaikinClient implements DaikinClient {
|
|
|
74
92
|
);
|
|
75
93
|
}
|
|
76
94
|
|
|
95
|
+
async getStateAndSensors(): Promise<DaikinStateAndSensors> {
|
|
96
|
+
const values = await this.readStatus();
|
|
97
|
+
return {
|
|
98
|
+
state: parseDsiotState(values),
|
|
99
|
+
sensors: parseDsiotSensors(values),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
// --- extended controls ---------------------------------------------------
|
|
78
104
|
|
|
79
105
|
/** Reboot the adapter (`/config/system/reboot`). */
|
|
@@ -109,8 +135,25 @@ export class DsiotDaikinClient implements DaikinClient {
|
|
|
109
135
|
): Promise<Record<string, string>> {
|
|
110
136
|
const send = () =>
|
|
111
137
|
this.transport.send({ method, path: DSIOT_ROUTES.multireq, jsonBody });
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
try {
|
|
139
|
+
return parseDsiotMultireqReply(
|
|
140
|
+
await (retry ? withRetry(send, this.retryOptions) : send()),
|
|
141
|
+
);
|
|
142
|
+
} finally {
|
|
143
|
+
// Even a write that failed: one that timed out may still have reached
|
|
144
|
+
// the unit.
|
|
145
|
+
if (method === "PUT") {
|
|
146
|
+
this.setStateCache.clear();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async readStatus(): Promise<Record<string, string>> {
|
|
152
|
+
const values = await this.multireqGet([
|
|
153
|
+
DSIOT_TARGETS.indoorStatus,
|
|
154
|
+
DSIOT_TARGETS.outdoorStatus,
|
|
155
|
+
]);
|
|
156
|
+
this.setStateCache.set(values);
|
|
157
|
+
return values;
|
|
115
158
|
}
|
|
116
159
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Caches the reply a client last read a unit's state from, for `setState`
|
|
2
|
+
* alone. A write carries more than the fields a control changes, so
|
|
3
|
+
* `setState` builds it from what the unit holds: from this reply while it is
|
|
4
|
+
* fresh, and from a read of its own otherwise. No read is answered from the
|
|
5
|
+
* cache: see DESIGN.md#the-read-before-a-write. */
|
|
6
|
+
export class SetStateCache {
|
|
7
|
+
private kept:
|
|
8
|
+
| { reply: Record<string, string>; readAtInMs: number }
|
|
9
|
+
| undefined;
|
|
10
|
+
|
|
11
|
+
/** `get` answers a reply younger than `maxAgeInMs`, so 0 answers none. */
|
|
12
|
+
constructor(private readonly maxAgeInMs: number) {}
|
|
13
|
+
|
|
14
|
+
set(reply: Record<string, string>): void {
|
|
15
|
+
this.kept = { reply, readAtInMs: performance.now() };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(): Record<string, string> | undefined {
|
|
19
|
+
return this.kept !== undefined &&
|
|
20
|
+
performance.now() - this.kept.readAtInMs < this.maxAgeInMs
|
|
21
|
+
? this.kept.reply
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear(): void {
|
|
26
|
+
this.kept = undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
HUMIDITY_SETTINGS,
|
|
10
10
|
OPERATING_MODES,
|
|
11
11
|
type OperatingMode,
|
|
12
|
+
SPECIAL_MODES,
|
|
13
|
+
type SpecialMode,
|
|
12
14
|
SWING_MODES,
|
|
13
15
|
type SwingMode,
|
|
14
16
|
type TargetHumidity,
|
|
@@ -16,6 +18,14 @@ import {
|
|
|
16
18
|
} from "../../models/state.js";
|
|
17
19
|
|
|
18
20
|
const CAPABILITY_VERSIONS = ["0", "1", "2", "3"];
|
|
21
|
+
// `en_spmode` sets one bit for each special mode the unit takes. Bit 8 is a
|
|
22
|
+
// mode outside `SpecialMode`. The powerful and econo bits count only at
|
|
23
|
+
// `cpv=3` with a `cpv_minor` other than `00`: see UNSUPPORTED.md.
|
|
24
|
+
const SPECIAL_MODE_BITS: Record<SpecialMode, number> = {
|
|
25
|
+
powerful: 1,
|
|
26
|
+
econo: 2,
|
|
27
|
+
streamer: 4,
|
|
28
|
+
};
|
|
19
29
|
const COOLING_ONLY_MODES: readonly OperatingMode[] = [
|
|
20
30
|
"cool",
|
|
21
31
|
"dehumidify",
|
|
@@ -100,6 +110,7 @@ export function parseClassicCapabilities(
|
|
|
100
110
|
fan: null,
|
|
101
111
|
},
|
|
102
112
|
targetHumidities: targetHumidities(fields, modern, offeredModes),
|
|
113
|
+
specialModes: specialModes(fields, cpv, offeredModes),
|
|
103
114
|
specialOutlet: cpv === "3" && fields.airoutlet_pv === "0",
|
|
104
115
|
energyMonitoring: fields.elec === "1",
|
|
105
116
|
demandControl: fields.adp_kind === "4" && fields.dmnd === "1",
|
|
@@ -108,6 +119,34 @@ export function parseClassicCapabilities(
|
|
|
108
119
|
};
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
// Lists none for a unit that reports no `en_spmode`, and econo in every mode
|
|
123
|
+
// but fan: see UNSUPPORTED.md.
|
|
124
|
+
function specialModes(
|
|
125
|
+
fields: Record<string, string>,
|
|
126
|
+
cpv: string,
|
|
127
|
+
offeredModes: OperatingMode[],
|
|
128
|
+
): Record<OperatingMode, SpecialMode[]> {
|
|
129
|
+
const bits = Number.parseInt(fields.en_spmode ?? "", 10);
|
|
130
|
+
const followsPowerfulAndEcono =
|
|
131
|
+
cpv === "3" && (fields.cpv_minor ?? "00") !== "00";
|
|
132
|
+
const taken = SPECIAL_MODES.filter(
|
|
133
|
+
(specialMode) =>
|
|
134
|
+
(bits & SPECIAL_MODE_BITS[specialMode]) !== 0 &&
|
|
135
|
+
(specialMode === "streamer" || followsPowerfulAndEcono),
|
|
136
|
+
);
|
|
137
|
+
const inMode = (mode: OperatingMode) =>
|
|
138
|
+
offeredModes.includes(mode)
|
|
139
|
+
? taken.filter((specialMode) => mode !== "fan" || specialMode !== "econo")
|
|
140
|
+
: [];
|
|
141
|
+
return {
|
|
142
|
+
auto: inMode("auto"),
|
|
143
|
+
dehumidify: inMode("dehumidify"),
|
|
144
|
+
cool: inMode("cool"),
|
|
145
|
+
heat: inMode("heat"),
|
|
146
|
+
fan: inMode("fan"),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
111
150
|
function present(fields: Record<string, string>, key: string): boolean {
|
|
112
151
|
return (fields[key] ?? "") !== "";
|
|
113
152
|
}
|
|
@@ -2,12 +2,14 @@ import type {
|
|
|
2
2
|
DemandControl,
|
|
3
3
|
DemandMode,
|
|
4
4
|
DeviceGroup,
|
|
5
|
-
|
|
5
|
+
DeviceMetadata,
|
|
6
6
|
TerminalScope,
|
|
7
7
|
} from "../../models/extendedControl.js";
|
|
8
|
+
import type { SpecialMode } from "../../models/state.js";
|
|
9
|
+
import { decodeClassicText } from "./keyValue.js";
|
|
8
10
|
|
|
9
|
-
// Encodes each control outside `
|
|
10
|
-
// endpoint takes.
|
|
11
|
+
// Encodes each control outside `DaikinState` as the query its classic `set_*`
|
|
12
|
+
// endpoint takes, and reads each one back from the reply that reports it.
|
|
11
13
|
|
|
12
14
|
const TERMINAL_SCOPE_CODES: Record<TerminalScope, string> = {
|
|
13
15
|
self: "0",
|
|
@@ -20,8 +22,8 @@ const DEMAND_MODE_CODES: Record<DemandMode, string> = {
|
|
|
20
22
|
auto: "2",
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
// `spmode_kind` on the wire
|
|
24
|
-
//
|
|
25
|
+
// `spmode_kind` on the wire, which numbers the modes differently from the
|
|
26
|
+
// `adv` that `parseClassicState` reads.
|
|
25
27
|
const SPECIAL_MODE_KIND_CODES: Record<
|
|
26
28
|
Exclude<SpecialMode, "streamer">,
|
|
27
29
|
string
|
|
@@ -41,12 +43,27 @@ export function encodeClassicLed(on: boolean): Record<string, string> {
|
|
|
41
43
|
return { led: flag(on) };
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** Reads the LED from a `basic_info` reply: `null` when it carries no `led`. */
|
|
47
|
+
export function parseClassicLed(
|
|
48
|
+
fields: Record<string, string>,
|
|
49
|
+
): boolean | null {
|
|
50
|
+
return parseFlag(fields.led);
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
export function encodeClassicHolidayMode(
|
|
45
54
|
enabled: boolean,
|
|
46
55
|
): Record<string, string> {
|
|
47
56
|
return { en_hol: flag(enabled) };
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
/** Reads holiday mode from a `basic_info` reply: `null` when it carries no
|
|
60
|
+
* `en_hol`. */
|
|
61
|
+
export function parseClassicHolidayMode(
|
|
62
|
+
fields: Record<string, string>,
|
|
63
|
+
): boolean | null {
|
|
64
|
+
return parseFlag(fields.en_hol);
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
export function encodeClassicDemandControl(
|
|
51
68
|
demand: DemandControl,
|
|
52
69
|
): Record<string, string> {
|
|
@@ -67,6 +84,16 @@ export function encodeClassicSpecialMode(
|
|
|
67
84
|
: { set_spmode: flag(enabled), spmode_kind: SPECIAL_MODE_KIND_CODES[mode] };
|
|
68
85
|
}
|
|
69
86
|
|
|
87
|
+
/** Reads out-of-home access from a `basic_info` reply: `null` when it carries
|
|
88
|
+
* no `method`. */
|
|
89
|
+
export function parseClassicOutOfHomeAccess(
|
|
90
|
+
fields: Record<string, string>,
|
|
91
|
+
): boolean | null {
|
|
92
|
+
return fields.method === undefined
|
|
93
|
+
? null
|
|
94
|
+
: fields.method === REMOTE_METHOD_POLLING;
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
/** Turns `current`, the `get_remote_method` reply, and `enabled` into the
|
|
71
98
|
* `set_remote_method` query. The endpoint takes the intervals back unchanged,
|
|
72
99
|
* so the reply's `notice_ip_int` and `notice_sync_int` are copied in. */
|
|
@@ -117,6 +144,26 @@ export function encodeClassicGroup(group: DeviceGroup): Record<string, string> {
|
|
|
117
144
|
return { grp_name: group.name, en_grp: flag(group.enabled) };
|
|
118
145
|
}
|
|
119
146
|
|
|
147
|
+
/** Reads the name, icon and group from a `basic_info` reply, leaving out each
|
|
148
|
+
* one the reply does not carry. The name and the group's name arrive
|
|
149
|
+
* form-encoded. */
|
|
150
|
+
export function parseClassicMetadata(
|
|
151
|
+
fields: Record<string, string>,
|
|
152
|
+
): DeviceMetadata {
|
|
153
|
+
return {
|
|
154
|
+
name:
|
|
155
|
+
fields.name === undefined ? undefined : decodeClassicText(fields.name),
|
|
156
|
+
icon: fields.icon,
|
|
157
|
+
group:
|
|
158
|
+
fields.en_grp === undefined
|
|
159
|
+
? undefined
|
|
160
|
+
: {
|
|
161
|
+
name: decodeClassicText(fields.grp_name ?? ""),
|
|
162
|
+
enabled: fields.en_grp === "1",
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
export function encodeClassicTerminalKey(key: string): Record<string, string> {
|
|
121
168
|
return { key };
|
|
122
169
|
}
|
|
@@ -130,3 +177,7 @@ export function encodeClassicTerminalScope(
|
|
|
130
177
|
function flag(on: boolean): string {
|
|
131
178
|
return on ? "1" : "0";
|
|
132
179
|
}
|
|
180
|
+
|
|
181
|
+
function parseFlag(value: string | undefined): boolean | null {
|
|
182
|
+
return value === undefined ? null : value === "1";
|
|
183
|
+
}
|
|
@@ -17,6 +17,16 @@ export function parseClassicKeyValue(body: string): Record<string, string> {
|
|
|
17
17
|
return result;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Decodes a form-encoded value, such as a name: `+` is a space. A value that
|
|
21
|
+
* is not valid percent-encoding is returned as the adapter sent it. */
|
|
22
|
+
export function decodeClassicText(value: string): string {
|
|
23
|
+
try {
|
|
24
|
+
return decodeURIComponent(value.replaceAll("+", " "));
|
|
25
|
+
} catch {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export function isClassicOk(fields: Record<string, string>): boolean {
|
|
21
31
|
return fields.ret === "OK";
|
|
22
32
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
DaikinControl,
|
|
3
|
-
DaikinSensors,
|
|
4
|
-
DaikinState,
|
|
5
|
-
FanSpeed,
|
|
6
|
-
HorizontalPosition,
|
|
7
|
-
HumiditySetting,
|
|
8
|
-
OperatingMode,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import {
|
|
2
|
+
type DaikinControl,
|
|
3
|
+
type DaikinSensors,
|
|
4
|
+
type DaikinState,
|
|
5
|
+
type FanSpeed,
|
|
6
|
+
type HorizontalPosition,
|
|
7
|
+
type HumiditySetting,
|
|
8
|
+
type OperatingMode,
|
|
9
|
+
SPECIAL_MODES,
|
|
10
|
+
type SpecialMode,
|
|
11
|
+
type SwingMode,
|
|
12
|
+
type TargetHumidity,
|
|
13
|
+
type VerticalPosition,
|
|
12
14
|
} from "../../models/state.js";
|
|
13
15
|
import { takesClassicDirection, takesClassicHumidity } from "./capabilities.js";
|
|
14
16
|
|
|
@@ -123,6 +125,13 @@ const HUMIDITY_SETTINGS_BY_CODE: Record<string, HumiditySetting> =
|
|
|
123
125
|
]),
|
|
124
126
|
);
|
|
125
127
|
|
|
128
|
+
// `adv` holds one code for each special mode that is on, separated by `/`.
|
|
129
|
+
const SPECIAL_MODE_ADV_CODES: Record<SpecialMode, string> = {
|
|
130
|
+
powerful: "2",
|
|
131
|
+
econo: "12",
|
|
132
|
+
streamer: "13",
|
|
133
|
+
};
|
|
134
|
+
|
|
126
135
|
// Fan mode keeps no setpoint memory, and dehumidify runs the fan on auto only.
|
|
127
136
|
const NO_SETPOINT = "--";
|
|
128
137
|
const DEHUMIDIFY_FAN_SPEED = "A";
|
|
@@ -131,7 +140,8 @@ const NO_LOUVER = "-";
|
|
|
131
140
|
|
|
132
141
|
/** Turns a `get_control_info` reply into a `DaikinState`. A `mode` or `f_rate`
|
|
133
142
|
* outside the model reads as `auto`, an `f_dir` outside it as `off`, and an
|
|
134
|
-
* `f_dir_ud` or `f_dir_lr` outside it as `null`.
|
|
143
|
+
* `f_dir_ud` or `f_dir_lr` outside it as `null`. A code in `adv` outside
|
|
144
|
+
* `SpecialMode` is left out. */
|
|
135
145
|
export function parseClassicState(fields: Record<string, string>): DaikinState {
|
|
136
146
|
return {
|
|
137
147
|
power: fields.pow === "1",
|
|
@@ -143,6 +153,7 @@ export function parseClassicState(fields: Record<string, string>): DaikinState {
|
|
|
143
153
|
verticalPosition: VERTICAL_POSITIONS_BY_CODE[fields.f_dir_ud ?? ""] ?? null,
|
|
144
154
|
horizontalPosition:
|
|
145
155
|
HORIZONTAL_POSITIONS_BY_CODE[fields.f_dir_lr ?? ""] ?? null,
|
|
156
|
+
specialModes: parseSpecialModes(fields.adv),
|
|
146
157
|
};
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -269,6 +280,13 @@ export function applyClassicControl(
|
|
|
269
280
|
return next;
|
|
270
281
|
}
|
|
271
282
|
|
|
283
|
+
function parseSpecialModes(adv: string | undefined): SpecialMode[] {
|
|
284
|
+
const codes = (adv ?? "").split("/");
|
|
285
|
+
return SPECIAL_MODES.filter((mode) =>
|
|
286
|
+
codes.includes(SPECIAL_MODE_ADV_CODES[mode]),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
function parseTargetHumidity(shum: string | undefined): TargetHumidity | null {
|
|
273
291
|
const setting = HUMIDITY_SETTINGS_BY_CODE[shum ?? ""];
|
|
274
292
|
if (setting !== undefined) {
|