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,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
encodeClassicLocalPassword,
|
|
3
|
+
encodeDsiotLocalPassword,
|
|
4
|
+
} from "./codecs/credentials.js";
|
|
5
|
+
|
|
6
|
+
// Authentication is orthogonal to the wire protocol, so it is injected rather
|
|
7
|
+
// than subclassed. A strategy contributes both headers and query, because the
|
|
8
|
+
// classic protocol carries its local password as a query parameter while
|
|
9
|
+
// DSIOT carries it as a Basic header.
|
|
10
|
+
//
|
|
11
|
+
// An adapter without a local password (basic_info `lpw_flag=0`) takes no
|
|
12
|
+
// strategy: the uuid header alone is accepted. `en_secure` is not
|
|
13
|
+
// authentication here: it means the terminal uuid must be registered
|
|
14
|
+
// (`registerTerminal`), plus HTTPS on a Marvell adapter, which is the
|
|
15
|
+
// transport's scheme.
|
|
16
|
+
|
|
17
|
+
/** `DaikinTransport.send` calls both methods on every request and adds what
|
|
18
|
+
* they return. */
|
|
19
|
+
export interface DaikinAuthentication {
|
|
20
|
+
headers(): Record<string, string> | Promise<Record<string, string>>;
|
|
21
|
+
/** Replaces a request parameter of the same name. */
|
|
22
|
+
query(): Record<string, string> | Promise<Record<string, string>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Classic adapter that requires the local password: sent as
|
|
26
|
+
* `?lpw=<value>`. */
|
|
27
|
+
export class ClassicLocalPasswordAuthentication
|
|
28
|
+
implements DaikinAuthentication
|
|
29
|
+
{
|
|
30
|
+
constructor(private readonly localPassword: string) {}
|
|
31
|
+
|
|
32
|
+
headers(): Record<string, string> {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
query(): Record<string, string> {
|
|
37
|
+
return encodeClassicLocalPassword(this.localPassword);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** DSIOT adapter that requires the local password: sent as
|
|
42
|
+
* `Authorization: Basic <lpw>`. */
|
|
43
|
+
export class DsiotLocalPasswordAuthentication implements DaikinAuthentication {
|
|
44
|
+
constructor(private readonly localPassword: string) {}
|
|
45
|
+
|
|
46
|
+
headers(): Record<string, string> {
|
|
47
|
+
return encodeDsiotLocalPassword(this.localPassword);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
query(): Record<string, string> {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mergeClassicCapabilityFields,
|
|
3
|
+
parseClassicCapabilities,
|
|
4
|
+
} from "../codecs/classic/capabilities.js";
|
|
5
|
+
import {
|
|
6
|
+
applyClassicOutOfHomeAccess,
|
|
7
|
+
encodeClassicDemandControl,
|
|
8
|
+
encodeClassicEnergyPrice,
|
|
9
|
+
encodeClassicEnergyTarget,
|
|
10
|
+
encodeClassicGroup,
|
|
11
|
+
encodeClassicHolidayMode,
|
|
12
|
+
encodeClassicIcon,
|
|
13
|
+
encodeClassicLed,
|
|
14
|
+
encodeClassicName,
|
|
15
|
+
encodeClassicSpecialMode,
|
|
16
|
+
encodeClassicTerminalKey,
|
|
17
|
+
encodeClassicTerminalScope,
|
|
18
|
+
} from "../codecs/classic/extendedControl.js";
|
|
19
|
+
import {
|
|
20
|
+
assertClassicOk,
|
|
21
|
+
parseClassicKeyValue,
|
|
22
|
+
} from "../codecs/classic/keyValue.js";
|
|
23
|
+
import {
|
|
24
|
+
applyClassicControl,
|
|
25
|
+
parseClassicSensors,
|
|
26
|
+
parseClassicState,
|
|
27
|
+
} from "../codecs/classic/state.js";
|
|
28
|
+
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
29
|
+
import type {
|
|
30
|
+
DemandControl,
|
|
31
|
+
DeviceMetadata,
|
|
32
|
+
SpecialMode,
|
|
33
|
+
TerminalScope,
|
|
34
|
+
} from "../models/extendedControl.js";
|
|
35
|
+
import type {
|
|
36
|
+
DaikinControl,
|
|
37
|
+
DaikinSensors,
|
|
38
|
+
DaikinState,
|
|
39
|
+
} from "../models/state.js";
|
|
40
|
+
import {
|
|
41
|
+
type DaikinRequestOptions,
|
|
42
|
+
type DaikinRetryOptions,
|
|
43
|
+
type DaikinTransport,
|
|
44
|
+
withRetry,
|
|
45
|
+
} from "../transport.js";
|
|
46
|
+
import type { DaikinClient } from "./client.js";
|
|
47
|
+
|
|
48
|
+
// Classic CGI endpoints (BRP069-style). Reads and writes are both HTTP GET with
|
|
49
|
+
// query parameters; the reply is `key=value,key=value`.
|
|
50
|
+
const CLASSIC_ROUTES = {
|
|
51
|
+
basicInfo: "/common/basic_info",
|
|
52
|
+
getModelInfo: "/aircon/get_model_info",
|
|
53
|
+
getControlInfo: "/aircon/get_control_info",
|
|
54
|
+
setControlInfo: "/aircon/set_control_info",
|
|
55
|
+
getSensorInfo: "/aircon/get_sensor_info",
|
|
56
|
+
setSpecialMode: "/aircon/set_special_mode",
|
|
57
|
+
setDemandControl: "/aircon/set_demand_control",
|
|
58
|
+
setPrice: "/aircon/set_price",
|
|
59
|
+
setTarget: "/aircon/set_target",
|
|
60
|
+
setLed: "/common/set_led",
|
|
61
|
+
setHoliday: "/common/set_holiday",
|
|
62
|
+
setName: "/common/set_name",
|
|
63
|
+
setIcon: "/common/set_icon",
|
|
64
|
+
setGroup: "/common/set_group",
|
|
65
|
+
getRemoteMethod: "/common/get_remote_method",
|
|
66
|
+
setRemoteMethod: "/common/set_remote_method",
|
|
67
|
+
reboot: "/common/reboot",
|
|
68
|
+
registerTerminal: "/common/register_terminal",
|
|
69
|
+
unregisterTerminal: "/common/unregister_terminal",
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
/** Classic CGI adapter (BRP069-style). Reads and writes are HTTP GET with query
|
|
73
|
+
* parameters; replies are `key=value,key=value`. */
|
|
74
|
+
export class ClassicDaikinClient implements DaikinClient {
|
|
75
|
+
// A unit's capabilities don't change, and `setState` needs them, so they are
|
|
76
|
+
// read once.
|
|
77
|
+
private capabilityFields: Promise<Record<string, string>> | undefined;
|
|
78
|
+
|
|
79
|
+
/** `close` closes `transport`, so give each client a transport of its own. */
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly transport: DaikinTransport,
|
|
82
|
+
private readonly retryOptions?: DaikinRetryOptions,
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
close(): Promise<void> {
|
|
86
|
+
return this.transport.close();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getCapabilities(): Promise<DaikinCapabilities> {
|
|
90
|
+
return parseClassicCapabilities(await this.readCapabilityFields());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getState(): Promise<DaikinState> {
|
|
94
|
+
return parseClassicState(await this.command(CLASSIC_ROUTES.getControlInfo));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async setState(control: DaikinControl): Promise<void> {
|
|
98
|
+
const capabilityFields = await this.readCapabilityFields();
|
|
99
|
+
const current = await this.command(CLASSIC_ROUTES.getControlInfo);
|
|
100
|
+
await this.command(
|
|
101
|
+
CLASSIC_ROUTES.setControlInfo,
|
|
102
|
+
applyClassicControl(current, control, capabilityFields),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getSensors(): Promise<DaikinSensors> {
|
|
107
|
+
return parseClassicSensors(
|
|
108
|
+
await this.command(CLASSIC_ROUTES.getSensorInfo),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- extended controls ---------------------------------------------------
|
|
113
|
+
// Each maps to its own endpoint (not control_info).
|
|
114
|
+
|
|
115
|
+
/** Adapter status LED on/off (`set_led`). */
|
|
116
|
+
async setLed(on: boolean): Promise<void> {
|
|
117
|
+
await this.command(CLASSIC_ROUTES.setLed, encodeClassicLed(on));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Holiday / away mode (`set_holiday`). */
|
|
121
|
+
async setHolidayMode(enabled: boolean): Promise<void> {
|
|
122
|
+
await this.command(
|
|
123
|
+
CLASSIC_ROUTES.setHoliday,
|
|
124
|
+
encodeClassicHolidayMode(enabled),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Demand control — cap the unit's power draw (`set_demand_control`). */
|
|
129
|
+
async setDemandControl(demand: DemandControl): Promise<void> {
|
|
130
|
+
await this.command(
|
|
131
|
+
CLASSIC_ROUTES.setDemandControl,
|
|
132
|
+
encodeClassicDemandControl(demand),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Powerful, econo or streamer (`set_special_mode`). */
|
|
137
|
+
async setSpecialMode(mode: SpecialMode, enabled: boolean): Promise<void> {
|
|
138
|
+
await this.command(
|
|
139
|
+
CLASSIC_ROUTES.setSpecialMode,
|
|
140
|
+
encodeClassicSpecialMode(mode, enabled),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Whether the unit can be operated from outside the home network
|
|
145
|
+
* (`set_remote_method`). */
|
|
146
|
+
async setOutOfHomeAccess(enabled: boolean): Promise<void> {
|
|
147
|
+
const current = await this.command(CLASSIC_ROUTES.getRemoteMethod);
|
|
148
|
+
await this.command(
|
|
149
|
+
CLASSIC_ROUTES.setRemoteMethod,
|
|
150
|
+
applyClassicOutOfHomeAccess(current, enabled),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Electricity price per kWh for the consumption estimates (`set_price`),
|
|
155
|
+
* to two decimal places. */
|
|
156
|
+
async setEnergyPrice(price: number): Promise<void> {
|
|
157
|
+
await this.command(
|
|
158
|
+
CLASSIC_ROUTES.setPrice,
|
|
159
|
+
encodeClassicEnergyPrice(price),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Energy target for the "target" feature (`set_target`). */
|
|
164
|
+
async setEnergyTarget(target: number): Promise<void> {
|
|
165
|
+
await this.command(
|
|
166
|
+
CLASSIC_ROUTES.setTarget,
|
|
167
|
+
encodeClassicEnergyTarget(target),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Name / icon / group metadata (`set_name` etc.). */
|
|
172
|
+
async setMetadata(metadata: DeviceMetadata): Promise<void> {
|
|
173
|
+
if (metadata.name !== undefined) {
|
|
174
|
+
await this.command(
|
|
175
|
+
CLASSIC_ROUTES.setName,
|
|
176
|
+
encodeClassicName(metadata.name),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (metadata.icon !== undefined) {
|
|
180
|
+
await this.command(
|
|
181
|
+
CLASSIC_ROUTES.setIcon,
|
|
182
|
+
encodeClassicIcon(metadata.icon),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (metadata.group !== undefined) {
|
|
186
|
+
await this.command(
|
|
187
|
+
CLASSIC_ROUTES.setGroup,
|
|
188
|
+
encodeClassicGroup(metadata.group),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Register this client's uuid with an adapter whose
|
|
194
|
+
* `requiresRegistration` is true, using the KEY printed on the adapter's
|
|
195
|
+
* label (`register_terminal`). Once per uuid; the adapter remembers it. */
|
|
196
|
+
async registerTerminal(key: string): Promise<void> {
|
|
197
|
+
await this.command(
|
|
198
|
+
CLASSIC_ROUTES.registerTerminal,
|
|
199
|
+
encodeClassicTerminalKey(key),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Unregister terminals (`unregister_terminal`): this uuid, every other
|
|
204
|
+
* uuid, or all of them. */
|
|
205
|
+
async unregisterTerminal(scope: TerminalScope = "self"): Promise<void> {
|
|
206
|
+
await this.command(
|
|
207
|
+
CLASSIC_ROUTES.unregisterTerminal,
|
|
208
|
+
encodeClassicTerminalScope(scope),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Reboot the adapter (`reboot`). */
|
|
213
|
+
async reboot(): Promise<void> {
|
|
214
|
+
await this.command(CLASSIC_ROUTES.reboot, undefined, { retry: false });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Calls any classic CGI endpoint with query parameters and returns the reply's
|
|
218
|
+
* `key=value` fields. It reaches the controls without a typed method:
|
|
219
|
+
* timers, schedules, programs, network, credentials and firmware. It throws
|
|
220
|
+
* a `DaikinError` when the reply's `ret` is not `OK`. */
|
|
221
|
+
async command(
|
|
222
|
+
path: string,
|
|
223
|
+
parameters?: Record<string, string>,
|
|
224
|
+
{ retry = true }: DaikinRequestOptions = {},
|
|
225
|
+
): Promise<Record<string, string>> {
|
|
226
|
+
const send = () =>
|
|
227
|
+
this.transport.send({
|
|
228
|
+
method: "GET",
|
|
229
|
+
path,
|
|
230
|
+
...(parameters ? { query: parameters } : {}),
|
|
231
|
+
});
|
|
232
|
+
const fields = parseClassicKeyValue(
|
|
233
|
+
await (retry ? withRetry(send, this.retryOptions) : send()),
|
|
234
|
+
);
|
|
235
|
+
assertClassicOk(fields);
|
|
236
|
+
return fields;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private readCapabilityFields(): Promise<Record<string, string>> {
|
|
240
|
+
this.capabilityFields ??= (async () =>
|
|
241
|
+
mergeClassicCapabilityFields(
|
|
242
|
+
await this.command(CLASSIC_ROUTES.basicInfo),
|
|
243
|
+
await this.command(CLASSIC_ROUTES.getModelInfo),
|
|
244
|
+
))();
|
|
245
|
+
// A failed read is not cached, so the next call tries again.
|
|
246
|
+
this.capabilityFields.catch(() => {
|
|
247
|
+
this.capabilityFields = undefined;
|
|
248
|
+
});
|
|
249
|
+
return this.capabilityFields;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
2
|
+
import type {
|
|
3
|
+
DaikinControl,
|
|
4
|
+
DaikinSensors,
|
|
5
|
+
DaikinState,
|
|
6
|
+
} from "../models/state.js";
|
|
7
|
+
|
|
8
|
+
/** The controls every adapter offers, whatever its wire protocol or
|
|
9
|
+
* authentication. `createDaikinClient` picks the implementation, so a caller
|
|
10
|
+
* needs the concrete class only for what one protocol adds, such as
|
|
11
|
+
* `ClassicDaikinClient.registerTerminal`. Every method rejects with a
|
|
12
|
+
* `DaikinError` when its request fails. */
|
|
13
|
+
export interface DaikinClient {
|
|
14
|
+
/** The unit's supported features. */
|
|
15
|
+
getCapabilities(): Promise<DaikinCapabilities>;
|
|
16
|
+
|
|
17
|
+
/** Current controllable state. */
|
|
18
|
+
getState(): Promise<DaikinState>;
|
|
19
|
+
|
|
20
|
+
/** Change part of the state. The client reads the current state, overlays the
|
|
21
|
+
* control, and writes the whole thing back. */
|
|
22
|
+
setState(control: DaikinControl): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/** Read-only temperature / humidity readings. */
|
|
25
|
+
getSensors(): Promise<DaikinSensors>;
|
|
26
|
+
|
|
27
|
+
/** Close the connection to the adapter. */
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { parseDsiotCapabilities } from "../codecs/dsiot/capabilities.js";
|
|
2
|
+
import {
|
|
3
|
+
buildDsiotMultireqGet,
|
|
4
|
+
buildDsiotMultireqUpdate,
|
|
5
|
+
DSIOT_TARGETS,
|
|
6
|
+
type DsiotUpdate,
|
|
7
|
+
parseDsiotMultireqReply,
|
|
8
|
+
} from "../codecs/dsiot/multireq.js";
|
|
9
|
+
import {
|
|
10
|
+
applyDsiotControl,
|
|
11
|
+
parseDsiotSensors,
|
|
12
|
+
parseDsiotState,
|
|
13
|
+
} from "../codecs/dsiot/state.js";
|
|
14
|
+
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
15
|
+
import type {
|
|
16
|
+
DaikinControl,
|
|
17
|
+
DaikinSensors,
|
|
18
|
+
DaikinState,
|
|
19
|
+
} from "../models/state.js";
|
|
20
|
+
import {
|
|
21
|
+
type DaikinRequestOptions,
|
|
22
|
+
type DaikinRetryOptions,
|
|
23
|
+
type DaikinTransport,
|
|
24
|
+
withRetry,
|
|
25
|
+
} from "../transport.js";
|
|
26
|
+
import type { DaikinClient } from "./client.js";
|
|
27
|
+
|
|
28
|
+
// DSIOT endpoints (BRP084-style). Climate state is a POST (read) or PUT (write)
|
|
29
|
+
// of a JSON property tree to `multireq`.
|
|
30
|
+
const DSIOT_ROUTES = {
|
|
31
|
+
multireq: "/dsiot/multireq",
|
|
32
|
+
reboot: "/config/system/reboot",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
/** DSIOT adapter (BRP084-style). One endpoint, `/dsiot/multireq`: POST to read
|
|
36
|
+
* whole `dgc_status` trees, PUT to write one merged tree. */
|
|
37
|
+
export class DsiotDaikinClient implements DaikinClient {
|
|
38
|
+
/** `close` closes `transport`, so give each client a transport of its own. */
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly transport: DaikinTransport,
|
|
41
|
+
private readonly retryOptions?: DaikinRetryOptions,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
close(): Promise<void> {
|
|
45
|
+
return this.transport.close();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getCapabilities(): Promise<DaikinCapabilities> {
|
|
49
|
+
return parseDsiotCapabilities(
|
|
50
|
+
await this.multireqGet([
|
|
51
|
+
DSIOT_TARGETS.adapterInfo,
|
|
52
|
+
DSIOT_TARGETS.indoorStatus,
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getState(): Promise<DaikinState> {
|
|
58
|
+
return parseDsiotState(
|
|
59
|
+
await this.multireqGet([DSIOT_TARGETS.indoorStatus]),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async setState(control: DaikinControl): Promise<void> {
|
|
64
|
+
const values = await this.multireqGet([DSIOT_TARGETS.indoorStatus]);
|
|
65
|
+
await this.multireqUpdate(applyDsiotControl(values, control));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getSensors(): Promise<DaikinSensors> {
|
|
69
|
+
return parseDsiotSensors(
|
|
70
|
+
await this.multireqGet([
|
|
71
|
+
DSIOT_TARGETS.indoorStatus,
|
|
72
|
+
DSIOT_TARGETS.outdoorStatus,
|
|
73
|
+
]),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- extended controls ---------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/** Reboot the adapter (`/config/system/reboot`). */
|
|
80
|
+
async reboot(): Promise<void> {
|
|
81
|
+
await this.transport.send({
|
|
82
|
+
method: "POST",
|
|
83
|
+
path: DSIOT_ROUTES.reboot,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Raw multireq read of whole resources (`DSIOT_TARGETS` values or any `to`),
|
|
88
|
+
* flattened to dotted path -> value. */
|
|
89
|
+
async multireqGet(
|
|
90
|
+
targets: string[],
|
|
91
|
+
options?: DaikinRequestOptions,
|
|
92
|
+
): Promise<Record<string, string>> {
|
|
93
|
+
return this.multireq("POST", buildDsiotMultireqGet(targets), options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Raw multireq write: update any leaves by dotted path, merged into one tree
|
|
97
|
+
* on the indoor edge. Returns the reply, flattened as `multireqGet` does. */
|
|
98
|
+
async multireqUpdate(
|
|
99
|
+
updates: DsiotUpdate[],
|
|
100
|
+
options?: DaikinRequestOptions,
|
|
101
|
+
): Promise<Record<string, string>> {
|
|
102
|
+
return this.multireq("PUT", buildDsiotMultireqUpdate(updates), options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async multireq(
|
|
106
|
+
method: "POST" | "PUT",
|
|
107
|
+
jsonBody: unknown,
|
|
108
|
+
{ retry = true }: DaikinRequestOptions = {},
|
|
109
|
+
): Promise<Record<string, string>> {
|
|
110
|
+
const send = () =>
|
|
111
|
+
this.transport.send({ method, path: DSIOT_ROUTES.multireq, jsonBody });
|
|
112
|
+
return parseDsiotMultireqReply(
|
|
113
|
+
await (retry ? withRetry(send, this.retryOptions) : send()),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|