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
package/README.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# daikin-local-control
|
|
2
|
+
|
|
3
|
+
A TypeScript client for controlling Daikin air-conditioner Wi-Fi adapters **over
|
|
4
|
+
the local network**, with no cloud dependency. It implements the same
|
|
5
|
+
local-control protocol as the **DAIKIN Mobile Controller** app, so it works with
|
|
6
|
+
the units and Wi-Fi adapters that app supports — split / multi-split and SkyAir
|
|
7
|
+
units fitted with adapters such as the BRP072A42, BRP072C42 and BRP084 — in the
|
|
8
|
+
[markets where that app is used](#coverage--limitations).
|
|
9
|
+
|
|
10
|
+
Daikin Wi-Fi adapters come in two generations, and the package has a client for
|
|
11
|
+
each:
|
|
12
|
+
|
|
13
|
+
- **`ClassicDaikinClient`** — classic adapters (BRP072A42, BRP072C42, BRP069).
|
|
14
|
+
Full core control plus typed [extended controls](#extended-controls).
|
|
15
|
+
- **`DsiotDaikinClient`** — newer DSIOT adapters (BRP084). Power, mode,
|
|
16
|
+
setpoint, humidity, fan, swing and louver positions;
|
|
17
|
+
[UNSUPPORTED.md](UNSUPPORTED.md) lists what is not mapped yet.
|
|
18
|
+
|
|
19
|
+
You normally don't pick one yourself: `createDaikinClient` chooses the right
|
|
20
|
+
client for each discovered device.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { discoverDevices, createDaikinClient } from "daikin-local-control";
|
|
26
|
+
|
|
27
|
+
// 1. Find adapters on the LAN (UDP broadcast DAIKIN_UDP/common/basic_info :30050).
|
|
28
|
+
const [device] = await discoverDevices();
|
|
29
|
+
if (device === undefined) {
|
|
30
|
+
throw new Error("No adapter answered");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Build the right client: createDaikinClient picks the classic or the DSIOT
|
|
34
|
+
// client from the device. Pass localPassword only when the adapter has a
|
|
35
|
+
// child lock code (see Credentials below).
|
|
36
|
+
const client = createDaikinClient(device, {
|
|
37
|
+
terminalUuid: savedUuid, // generated once and stored (see Credentials below)
|
|
38
|
+
localPassword: device.requiresLocalPassword ? "<child lock code>" : undefined,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 3. Capabilities are data — branch on them, don't special-case device types.
|
|
42
|
+
const capabilities = await client.getCapabilities();
|
|
43
|
+
if (capabilities.swingModes.includes("both")) {
|
|
44
|
+
/* this unit swings on both axes */
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 4. Read and control.
|
|
48
|
+
const state = await client.getState();
|
|
49
|
+
await client.setState({ power: true, mode: "cool", targetTemperatureInC: 24 });
|
|
50
|
+
|
|
51
|
+
const { indoorTemperatureInC } = await client.getSensors();
|
|
52
|
+
|
|
53
|
+
// 5. Close the connection to the adapter when you're done with the unit.
|
|
54
|
+
await client.close();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Discovery
|
|
58
|
+
|
|
59
|
+
`discoverDevices()` resolves with one `DaikinDevice` for each adapter that
|
|
60
|
+
answered, and with an empty array when none did.
|
|
61
|
+
|
|
62
|
+
| It | So |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Resolves after `timeoutInMs`, never earlier | Pass `discoverDevices({ timeoutInMs: 1000 })` to wait less than the default 3000 |
|
|
65
|
+
| Broadcasts to `255.255.255.255` | It does not find an adapter on another subnet or VLAN |
|
|
66
|
+
| Binds UDP port 30000 | It rejects with the socket's error when the socket fails |
|
|
67
|
+
|
|
68
|
+
Write the device by hand when you already know its address, or when discovery
|
|
69
|
+
cannot reach it:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createDaikinClient, type DaikinDevice } from "daikin-local-control";
|
|
73
|
+
|
|
74
|
+
const device: DaikinDevice = { host: "192.168.1.50", protocol: "classic" };
|
|
75
|
+
const client = createDaikinClient(device, { terminalUuid: savedUuid });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Set `protocol` to `dsiot` for a BRP084 and to `classic` for every other
|
|
79
|
+
adapter. A device without flags is an adapter that requires neither
|
|
80
|
+
registration nor a child lock code. If yours requires either, set
|
|
81
|
+
`requiresRegistration: true` / `requiresLocalPassword: true` and set up
|
|
82
|
+
[credentials](#credentials) to match; add `usesHttps: true` if it also speaks
|
|
83
|
+
HTTPS. The flags are independent, and discovery reads them all from the adapter
|
|
84
|
+
for you.
|
|
85
|
+
|
|
86
|
+
### Credentials
|
|
87
|
+
|
|
88
|
+
An adapter can need up to three things. Discovery tells you which ones apply
|
|
89
|
+
(`device.requiresRegistration`, `device.requiresLocalPassword`).
|
|
90
|
+
|
|
91
|
+
**Terminal uuid (always).** It identifies your app to the adapter. Any random
|
|
92
|
+
UUID works. Generate it **once**, store it, and reuse it on every run: an
|
|
93
|
+
adapter that requires registration remembers it, so a new uuid means registering
|
|
94
|
+
again.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { randomUUID } from "node:crypto";
|
|
98
|
+
|
|
99
|
+
const savedUuid = loadFromConfiguration() ?? saveToConfiguration(randomUUID());
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**KEY (when `device.requiresRegistration` is `true`).** These
|
|
103
|
+
adapters only answer terminals they have registered. Each adapter's KEY is
|
|
104
|
+
printed on its label, next to the SSID (also in the operation manual). Register
|
|
105
|
+
your uuid with it once. Until you do, every call fails with HTTP 401/403.
|
|
106
|
+
Each uuid is registered separately, so an adapter already set up in the DAIKIN
|
|
107
|
+
Mobile Controller app still needs registering here.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { ClassicDaikinClient } from "daikin-local-control";
|
|
111
|
+
|
|
112
|
+
if (device.requiresRegistration && client instanceof ClassicDaikinClient) {
|
|
113
|
+
await client.registerTerminal("<KEY from the label>");
|
|
114
|
+
|
|
115
|
+
// To unregister later: this uuid (default), every other uuid, or all of them.
|
|
116
|
+
// await client.unregisterTerminal("others");
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Registration applies to classic adapters only; DSIOT adapters don't use it.
|
|
121
|
+
|
|
122
|
+
Some of these adapters also switch to HTTPS (`device.usesHttps`); see
|
|
123
|
+
[Local HTTPS](#local-https).
|
|
124
|
+
|
|
125
|
+
**Child lock code (when `device.requiresLocalPassword` is `true`).** The protocol
|
|
126
|
+
calls it the local password (`lpw`). It is the 4-digit code set under Child lock
|
|
127
|
+
in the DAIKIN Mobile Controller app. It is **not** printed on the label, and it
|
|
128
|
+
is not your Wi-Fi or Out-of-Home password. Pass it as `localPassword` to
|
|
129
|
+
`createDaikinClient`. If it's forgotten, the adapter has to be reset.
|
|
130
|
+
|
|
131
|
+
### State and capabilities
|
|
132
|
+
|
|
133
|
+
`getState()` resolves with a `DaikinState`:
|
|
134
|
+
|
|
135
|
+
| Field | Holds |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `power` | `true` or `false` |
|
|
138
|
+
| `mode` | `auto`, `dehumidify`, `cool`, `heat` or `fan` |
|
|
139
|
+
| `targetTemperatureInC` | A number, or `null` in a mode without a setpoint |
|
|
140
|
+
| `targetHumidity` | `off`, `autoLow`, `auto`, `autoHigh` or `continuous`, or `null` when the unit has no humidity setting. A Japanese unit set to a percentage reads as `{ percent: 50 }`; `getCapabilities()` never lists a percentage, so `setState()` sends one unchecked: see [UNSUPPORTED.md](UNSUPPORTED.md) |
|
|
141
|
+
| `fanSpeed` | `auto`, `silent`, or `level1` to `level5` |
|
|
142
|
+
| `swingMode` | `off`, `vertical`, `horizontal` or `both`: `vertical` moves the air up and down, `horizontal` left and right |
|
|
143
|
+
| `verticalPosition` | `stop`, `position1` to `position5`, `swing`, `naturalWind`, `auto` or `floorWarming`, or `null` where the unit reports none |
|
|
144
|
+
| `horizontalPosition` | `stop`, `position1` to `position5`, `swing` or `auto`, or `null` where the unit reports none |
|
|
145
|
+
|
|
146
|
+
`getCapabilities()` resolves with a `DaikinCapabilities`, which lists the
|
|
147
|
+
values this unit takes under the name of the state field each one limits:
|
|
148
|
+
|
|
149
|
+
| Field | Holds |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `modes` | The modes the unit offers |
|
|
152
|
+
| `fanSpeeds`, `swingModes`, `verticalPositions`, `horizontalPositions` | The values the unit takes, empty when it has no such control |
|
|
153
|
+
| `targetTemperatureInC` | For each mode, `{ min, max, step }` in °C, or `null` where the mode has no setpoint |
|
|
154
|
+
| `targetHumidity` | For each mode, the humidity settings it takes, empty where it takes none |
|
|
155
|
+
| `specialOutlet`, `energyMonitoring`, `demandControl`, `scheduleTimer` | `true` when the unit has the feature |
|
|
156
|
+
| `model` | The adapter's own code for the unit's model |
|
|
157
|
+
|
|
158
|
+
`getSensors()` resolves with `indoorTemperatureInC`, `outdoorTemperatureInC`
|
|
159
|
+
and `indoorHumidityInPercent`, each `null` when the unit does not report it.
|
|
160
|
+
|
|
161
|
+
### Changing state
|
|
162
|
+
|
|
163
|
+
Pass `setState()` only the fields to change: a field left out keeps the value
|
|
164
|
+
the unit holds for the target mode.
|
|
165
|
+
|
|
166
|
+
Check a value against `getCapabilities()` before sending it: `setState()`
|
|
167
|
+
resolves without an error when it drops a field, and sends any other value as
|
|
168
|
+
given, whether or not the unit lists it.
|
|
169
|
+
|
|
170
|
+
| Field | `setState()` drops it |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `targetTemperatureInC` | In `fan` and `dehumidify` |
|
|
173
|
+
| `targetHumidity` | In `fan` |
|
|
174
|
+
| `fanSpeed` | In `dehumidify`, and on a DSIOT adapter in `cool` while a humidity setting is on: the fan runs on `auto` |
|
|
175
|
+
| `verticalPosition`, `horizontalPosition` | On a classic adapter, when the unit has no 3D airflow. On a DSIOT adapter, a fixed vertical position the unit lacks, and every fixed horizontal position |
|
|
176
|
+
|
|
177
|
+
### Extended controls
|
|
178
|
+
|
|
179
|
+
Beyond core climate, `ClassicDaikinClient` exposes the adapter's other `set_*`
|
|
180
|
+
endpoints as typed methods, plus a raw `command()` hatch for anything without a
|
|
181
|
+
wrapper. Narrow to the concrete class to reach them:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { ClassicDaikinClient } from "daikin-local-control";
|
|
185
|
+
|
|
186
|
+
if (client instanceof ClassicDaikinClient) {
|
|
187
|
+
await client.setLed(false);
|
|
188
|
+
await client.setHolidayMode(true);
|
|
189
|
+
await client.setDemandControl({
|
|
190
|
+
enabled: true,
|
|
191
|
+
maxPowerInPercent: 70,
|
|
192
|
+
mode: "manual",
|
|
193
|
+
});
|
|
194
|
+
await client.setSpecialMode("powerful", true);
|
|
195
|
+
await client.setOutOfHomeAccess(false); // home network only
|
|
196
|
+
await client.setEnergyPrice(0.3); // per kWh
|
|
197
|
+
await client.setMetadata({
|
|
198
|
+
name: "Living Room",
|
|
199
|
+
group: { name: "Upstairs", enabled: true },
|
|
200
|
+
});
|
|
201
|
+
await client.reboot();
|
|
202
|
+
|
|
203
|
+
// Any other endpoint, by its path: the reply's key=value fields come back.
|
|
204
|
+
const basicInfo = await client.command("/common/basic_info");
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
[UNSUPPORTED.md](UNSUPPORTED.md) lists the endpoints without a typed method and
|
|
209
|
+
what is not known about each.
|
|
210
|
+
|
|
211
|
+
`DsiotDaikinClient` has `reboot()` and the raw `multireqGet` / `multireqUpdate`
|
|
212
|
+
hatches for any `dgc_status` leaf.
|
|
213
|
+
|
|
214
|
+
### Errors and retries
|
|
215
|
+
|
|
216
|
+
A client method that fails rejects with a `DaikinError`; branch on
|
|
217
|
+
`httpStatusCode`:
|
|
218
|
+
|
|
219
|
+
| `httpStatusCode` | Meaning |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| 401 | The adapter rejected the credentials: `ret=AUTH NG` or `LPW NG`, or DSIOT `rsc` 401 or 4001 |
|
|
222
|
+
| 502 | The adapter answered with an error: a `ret` or `rsc` other than OK, or a reply that does not parse |
|
|
223
|
+
| 503 | The adapter refused or dropped the connection (a `DaikinConnectionError`), or DSIOT answered `rsc` 503 |
|
|
224
|
+
| 504 | The request timed out, or the TLS handshake failed |
|
|
225
|
+
| Any other | The adapter's own HTTP status, such as 403 |
|
|
226
|
+
|
|
227
|
+
Two failures are not a `DaikinError`: `discoverDevices()` rejects with the
|
|
228
|
+
socket's error, and `multireqUpdate()` throws an `Error` for a path that names
|
|
229
|
+
a resource without a leaf, such as `dgc_status`.
|
|
230
|
+
|
|
231
|
+
Only a `DaikinConnectionError` is retried, up to five times by default.
|
|
232
|
+
[DESIGN.md](DESIGN.md#retries) says why.
|
|
233
|
+
|
|
234
|
+
| To | Pass |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| Change how often and how long a client retries | `retry: { delaysInMs: [0, 500] }` to `createDaikinClient`: one wait, in milliseconds, before each retry |
|
|
237
|
+
| Stop a client retrying | `retry: { delaysInMs: [] }` to `createDaikinClient` |
|
|
238
|
+
| Send one request that is not safe to repeat | `{ retry: false }` as the last argument of `command()`, `multireqGet()` or `multireqUpdate()` |
|
|
239
|
+
|
|
240
|
+
## Coverage & limitations
|
|
241
|
+
|
|
242
|
+
It supports the same markets and Wi-Fi adapters as the DAIKIN Mobile Controller
|
|
243
|
+
app, so it is **not valid for every Daikin region or app**.
|
|
244
|
+
|
|
245
|
+
| Region | Markets using the DAIKIN Mobile Controller app |
|
|
246
|
+
| -------------- | -------------------------------------------------------------- |
|
|
247
|
+
| Southeast Asia | Thailand, Singapore, the Philippines |
|
|
248
|
+
| East Asia | Hong Kong, Taiwan |
|
|
249
|
+
| Oceania | Australia, New Zealand |
|
|
250
|
+
|
|
251
|
+
The app is published by Daikin Thailand and may be available in other countries
|
|
252
|
+
too; if your local Daikin site points you to it, this package should apply. Units
|
|
253
|
+
sold for other apps likely use a different protocol, e.g. Onecta / Daikin Online
|
|
254
|
+
Controller (Europe), GO DAIKIN (Malaysia) or Daikin Comfort Control / Daikin One
|
|
255
|
+
(North America).
|
|
256
|
+
|
|
257
|
+
This package is still young, so expect some rough edges. If something doesn't
|
|
258
|
+
work with your unit, please
|
|
259
|
+
[open an issue](https://github.com/cheziyi/daikin-local-control/issues).
|
|
260
|
+
[UNSUPPORTED.md](UNSUPPORTED.md) lists what isn't supported yet.
|
|
261
|
+
|
|
262
|
+
## Local HTTPS
|
|
263
|
+
|
|
264
|
+
Only some classic adapters use HTTPS: those reporting `adp_kind=3` with
|
|
265
|
+
`en_secure=1`. Discovery sets `device.usesHttps` for them. Every other adapter,
|
|
266
|
+
DSIOT included, talks plain HTTP on the LAN, even when it requires registration.
|
|
267
|
+
|
|
268
|
+
Over HTTPS the adapter presents a certificate issued by Daikin's own CA, which
|
|
269
|
+
this package can't ship. So by default **the certificate is not verified**: the
|
|
270
|
+
connection is encrypted, but another device on your LAN could impersonate the
|
|
271
|
+
adapter. If you have the CA certificate, pass it to turn verification on:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { readFileSync } from "node:fs";
|
|
275
|
+
|
|
276
|
+
const client = createDaikinClient(device, {
|
|
277
|
+
terminalUuid: savedUuid,
|
|
278
|
+
tls: { ca: readFileSync("daikin-ca.pem") },
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
With a CA, the certificate chain is verified but the hostname isn't, because the
|
|
283
|
+
adapter's certificate doesn't name its LAN address.
|
|
284
|
+
|
|
285
|
+
## Development
|
|
286
|
+
|
|
287
|
+
Uses [pnpm](https://pnpm.io) and Node 22.19 or later (see `.nvmrc`; `fnm use` / `nvm use` picks it up).
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
pnpm build # tsc -> dist
|
|
291
|
+
pnpm test # typecheck + vitest
|
|
292
|
+
pnpm check # biome lint + format + import sorting
|
|
293
|
+
pnpm fix # check, applying safe fixes
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`pnpm install` sets up a [lefthook](https://lefthook.dev) pre-commit hook that runs `pnpm fix` on staged files.
|
|
297
|
+
|
|
298
|
+
## Legal disclaimer
|
|
299
|
+
|
|
300
|
+
An independent, community project for local interoperability and home automation.
|
|
301
|
+
It is not affiliated with, authorized, or endorsed by Daikin Industries, Ltd. All
|
|
302
|
+
product names and trademarks are the property of their respective owners; their use
|
|
303
|
+
here is for identification and compatibility only.
|
|
304
|
+
|
|
305
|
+
**Local-only.** The package talks only to the adapter over the LAN — it never contacts
|
|
306
|
+
any manufacturer cloud, remote server, or online API — and acts as an ordinary
|
|
307
|
+
client of the adapter's own endpoints.
|
|
308
|
+
|
|
309
|
+
**No proprietary material.** It ships no manufacturer keys, tokens, certificates, or
|
|
310
|
+
binaries. The protocol was documented independently, for interoperability.
|
|
311
|
+
|
|
312
|
+
Provided "as is", without warranty of any kind (see [LICENSE](LICENSE)); use it at
|
|
313
|
+
your own risk.
|
|
314
|
+
|
|
315
|
+
## License
|
|
316
|
+
|
|
317
|
+
[GPL-3.0-or-later](LICENSE). Copyright © 2026 the contributors.
|
package/UNSUPPORTED.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Unsupported
|
|
2
|
+
|
|
3
|
+
Everything below is left out because its exact request or reply is not known.
|
|
4
|
+
To add one, pin down the request and reply first, then delete its row.
|
|
5
|
+
|
|
6
|
+
## Left out
|
|
7
|
+
|
|
8
|
+
| Feature | What is unknown | Reach it with |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| DSIOT special air outlet | Which leaf reports it. `getCapabilities` reports `specialOutlet: false`. | `DsiotDaikinClient.multireqGet()` |
|
|
11
|
+
| Classic `/common/set_location` | The parameter name. | `ClassicDaikinClient.command()` |
|
|
12
|
+
| DSIOT `/config/*` endpoints: time zone, date and time, network settings, Wi-Fi scan, firmware update | The request bodies. | Not reachable |
|
|
13
|
+
| DSIOT fixed horizontal louver positions | Which position each value 1-14 stands for. `getState` reports `stop` for any of them, and a control that sets `stop` keeps the position. | `DsiotDaikinClient.multireqUpdate()` |
|
|
14
|
+
| DSIOT dehumidify setpoint write | Whether `p_1E` accepts a write; it is treated as read-only, so `setState` drops a `targetTemperatureInC` in dehumidify. | `DsiotDaikinClient.multireqUpdate()` |
|
|
15
|
+
| Classic `shum_asean`, the humidity setting of a `subtype=qa` unit | What each value stands for. `setState` sends such a unit no humidity setting, and `getCapabilities` lists none. | `ClassicDaikinClient.command()` |
|
|
16
|
+
| Classic vertical louver value `f_dir_ud=C` | What it stands for. `getState` reports `null`. | `ClassicDaikinClient.command()` |
|
|
17
|
+
| Classic schedule demand mode (`mode=1`) | The full format of the weekly table it needs (`moc`..`suc`, `<day><n>_en`/`_t`/`_p`). | `ClassicDaikinClient.command()` |
|
|
18
|
+
| Classic special mode `spmode_kind=4` (heat plus) | Which units take it. | `ClassicDaikinClient.command()` |
|
|
19
|
+
| Timers, weekly programs, schedule timers (`/aircon/get_timer`, `set_timer`, `get_program`, `set_program`, `get_scdltimer_info`, `get_scdltimer_body`, `set_scdltimer_info`, `set_scdltimer_body`) | Their parameters. | `ClassicDaikinClient.command()` |
|
|
20
|
+
| Classic setting reads: `/aircon/get_demand_control`, `/aircon/get_price`, `/aircon/get_target`, `/common/get_holiday` | The reply fields. | `ClassicDaikinClient.command()` |
|
|
21
|
+
| Classic notifications, clock and account: `/common/get_notify`, `set_notify`, `set_push_notice`, `set_timezone`, `notify_date_time`, `set_account` | Their parameters. | `ClassicDaikinClient.command()` |
|
|
22
|
+
| Classic network: `/common/get_network_setting`, `set_network_setting`, `start_wifi_scan`, `get_wifi_scan_result`, `start_wifi_connection`, `permit_wifi_connection`, `set_lpw` | Their parameters. | `ClassicDaikinClient.command()` |
|
|
23
|
+
| Classic firmware update: a POST to `/dkac/system/fwupdate` | The format of the upload. | Not reachable |
|
|
24
|
+
|
|
25
|
+
## Not modelled
|
|
26
|
+
|
|
27
|
+
These values are known, but the package's types don't represent them yet:
|
|
28
|
+
|
|
29
|
+
| Value | What `getState` reports instead |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| Humidify mode (classic `HUM`, DSIOT `0800`) and laundry mode (classic `LND`) | `mode: "auto"`. `setState` keeps the mode unless the control sets one, and on a DSIOT adapter writes power alone while it is kept. |
|
|
32
|
+
| DSIOT vertical louver values other than stop, the first five fixed positions, swing, auto, natural wind and floor warming | `verticalPosition: null` |
|
|
33
|
+
| DSIOT laundry mode (`dgc_status.e_1002.e_3003.p_25` = `01`) | The mode in `dgc_status.e_1002.e_3001.p_01` |
|
|
34
|
+
| Classic `f_dir=6` (heat with the special air outlet) | `swingMode: "off"` |
|
|
35
|
+
| Japanese units (`reg=jp`), which follow separate rules for limits, humidity and fan, and take a humidity percentage | The rules for other regions |
|
|
36
|
+
|
|
37
|
+
These adapter codes pass through a model unchanged, because what each one
|
|
38
|
+
stands for is not known:
|
|
39
|
+
|
|
40
|
+
| Model field | Holds |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `DeviceMetadata.icon` | The `icon` that classic `/common/set_icon` takes |
|
|
43
|
+
| `DaikinCapabilities.model` | The classic `model` field, or DSIOT `dgc_status.e_1002.e_A001.p_0D`, such as `0ABB` |
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** `DaikinTransport.send` calls both methods on every request and adds what
|
|
2
|
+
* they return. */
|
|
3
|
+
export interface DaikinAuthentication {
|
|
4
|
+
headers(): Record<string, string> | Promise<Record<string, string>>;
|
|
5
|
+
/** Replaces a request parameter of the same name. */
|
|
6
|
+
query(): Record<string, string> | Promise<Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
/** Classic adapter that requires the local password: sent as
|
|
9
|
+
* `?lpw=<value>`. */
|
|
10
|
+
export declare class ClassicLocalPasswordAuthentication implements DaikinAuthentication {
|
|
11
|
+
private readonly localPassword;
|
|
12
|
+
constructor(localPassword: string);
|
|
13
|
+
headers(): Record<string, string>;
|
|
14
|
+
query(): Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/** DSIOT adapter that requires the local password: sent as
|
|
17
|
+
* `Authorization: Basic <lpw>`. */
|
|
18
|
+
export declare class DsiotLocalPasswordAuthentication implements DaikinAuthentication {
|
|
19
|
+
private readonly localPassword;
|
|
20
|
+
constructor(localPassword: string);
|
|
21
|
+
headers(): Record<string, string>;
|
|
22
|
+
query(): Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=authentication.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authentication.d.ts","sourceRoot":"","sources":["../src/authentication.ts"],"names":[],"mappings":"AAgBA;kBACkB;AAClB,MAAM,WAAW,oBAAoB;IACnC,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE,qDAAqD;IACrD,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnE;AAED;qBACqB;AACrB,qBAAa,kCACX,YAAW,oBAAoB;IAEnB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAA1C,YAA6B,aAAa,EAAE,MAAM,EAAI;IAEtD,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEhC;IAED,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE9B;CACF;AAED;mCACmC;AACnC,qBAAa,gCAAiC,YAAW,oBAAoB;IAC/D,OAAO,CAAC,QAAQ,CAAC,aAAa;IAA1C,YAA6B,aAAa,EAAE,MAAM,EAAI;IAEtD,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEhC;IAED,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE9B;CACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { encodeClassicLocalPassword, encodeDsiotLocalPassword, } from "./codecs/credentials.js";
|
|
2
|
+
/** Classic adapter that requires the local password: sent as
|
|
3
|
+
* `?lpw=<value>`. */
|
|
4
|
+
export class ClassicLocalPasswordAuthentication {
|
|
5
|
+
localPassword;
|
|
6
|
+
constructor(localPassword) {
|
|
7
|
+
this.localPassword = localPassword;
|
|
8
|
+
}
|
|
9
|
+
headers() {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
query() {
|
|
13
|
+
return encodeClassicLocalPassword(this.localPassword);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** DSIOT adapter that requires the local password: sent as
|
|
17
|
+
* `Authorization: Basic <lpw>`. */
|
|
18
|
+
export class DsiotLocalPasswordAuthentication {
|
|
19
|
+
localPassword;
|
|
20
|
+
constructor(localPassword) {
|
|
21
|
+
this.localPassword = localPassword;
|
|
22
|
+
}
|
|
23
|
+
headers() {
|
|
24
|
+
return encodeDsiotLocalPassword(this.localPassword);
|
|
25
|
+
}
|
|
26
|
+
query() {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=authentication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authentication.js","sourceRoot":"","sources":["../src/authentication.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAqBjC;qBACqB;AACrB,MAAM,OAAO,kCAAkC;IAGhB,aAAa;IAA1C,YAA6B,aAAqB;6BAArB,aAAa;IAAW,CAAC;IAEtD,OAAO;QACL,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK;QACH,OAAO,0BAA0B,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACxD,CAAC;CACF;AAED;mCACmC;AACnC,MAAM,OAAO,gCAAgC;IACd,aAAa;IAA1C,YAA6B,aAAqB;6BAArB,aAAa;IAAW,CAAC;IAEtD,OAAO;QACL,OAAO,wBAAwB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACtD,CAAC;IAED,KAAK;QACH,OAAO,EAAE,CAAC;IACZ,CAAC;CACF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
2
|
+
import type { DemandControl, DeviceMetadata, SpecialMode, TerminalScope } from "../models/extendedControl.js";
|
|
3
|
+
import type { DaikinControl, DaikinSensors, DaikinState } from "../models/state.js";
|
|
4
|
+
import { type DaikinRequestOptions, type DaikinRetryOptions, type DaikinTransport } from "../transport.js";
|
|
5
|
+
import type { DaikinClient } from "./client.js";
|
|
6
|
+
/** Classic CGI adapter (BRP069-style). Reads and writes are HTTP GET with query
|
|
7
|
+
* parameters; replies are `key=value,key=value`. */
|
|
8
|
+
export declare class ClassicDaikinClient implements DaikinClient {
|
|
9
|
+
private readonly transport;
|
|
10
|
+
private readonly retryOptions?;
|
|
11
|
+
private capabilityFields;
|
|
12
|
+
/** `close` closes `transport`, so give each client a transport of its own. */
|
|
13
|
+
constructor(transport: DaikinTransport, retryOptions?: DaikinRetryOptions | undefined);
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
getCapabilities(): Promise<DaikinCapabilities>;
|
|
16
|
+
getState(): Promise<DaikinState>;
|
|
17
|
+
setState(control: DaikinControl): Promise<void>;
|
|
18
|
+
getSensors(): Promise<DaikinSensors>;
|
|
19
|
+
/** Adapter status LED on/off (`set_led`). */
|
|
20
|
+
setLed(on: boolean): Promise<void>;
|
|
21
|
+
/** Holiday / away mode (`set_holiday`). */
|
|
22
|
+
setHolidayMode(enabled: boolean): Promise<void>;
|
|
23
|
+
/** Demand control — cap the unit's power draw (`set_demand_control`). */
|
|
24
|
+
setDemandControl(demand: DemandControl): Promise<void>;
|
|
25
|
+
/** Powerful, econo or streamer (`set_special_mode`). */
|
|
26
|
+
setSpecialMode(mode: SpecialMode, enabled: boolean): Promise<void>;
|
|
27
|
+
/** Whether the unit can be operated from outside the home network
|
|
28
|
+
* (`set_remote_method`). */
|
|
29
|
+
setOutOfHomeAccess(enabled: boolean): Promise<void>;
|
|
30
|
+
/** Electricity price per kWh for the consumption estimates (`set_price`),
|
|
31
|
+
* to two decimal places. */
|
|
32
|
+
setEnergyPrice(price: number): Promise<void>;
|
|
33
|
+
/** Energy target for the "target" feature (`set_target`). */
|
|
34
|
+
setEnergyTarget(target: number): Promise<void>;
|
|
35
|
+
/** Name / icon / group metadata (`set_name` etc.). */
|
|
36
|
+
setMetadata(metadata: DeviceMetadata): Promise<void>;
|
|
37
|
+
/** Register this client's uuid with an adapter whose
|
|
38
|
+
* `requiresRegistration` is true, using the KEY printed on the adapter's
|
|
39
|
+
* label (`register_terminal`). Once per uuid; the adapter remembers it. */
|
|
40
|
+
registerTerminal(key: string): Promise<void>;
|
|
41
|
+
/** Unregister terminals (`unregister_terminal`): this uuid, every other
|
|
42
|
+
* uuid, or all of them. */
|
|
43
|
+
unregisterTerminal(scope?: TerminalScope): Promise<void>;
|
|
44
|
+
/** Reboot the adapter (`reboot`). */
|
|
45
|
+
reboot(): Promise<void>;
|
|
46
|
+
/** Calls any classic CGI endpoint with query parameters and returns the reply's
|
|
47
|
+
* `key=value` fields. It reaches the controls without a typed method:
|
|
48
|
+
* timers, schedules, programs, network, credentials and firmware. It throws
|
|
49
|
+
* a `DaikinError` when the reply's `ret` is not `OK`. */
|
|
50
|
+
command(path: string, parameters?: Record<string, string>, { retry }?: DaikinRequestOptions): Promise<Record<string, string>>;
|
|
51
|
+
private readCapabilityFields;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=classic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classic.d.ts","sourceRoot":"","sources":["../../src/clients/classic.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,WAAW,EACX,aAAa,EACd,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAErB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA0BhD;oDACoD;AACpD,qBAAa,mBAAoB,YAAW,YAAY;IAOpD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IALhC,OAAO,CAAC,gBAAgB,CAA8C;IAEtE,8EAA8E;IAC9E,YACmB,SAAS,EAAE,eAAe,EAC1B,YAAY,CAAC,EAAE,kBAAkB,YAAA,EAChD;IAEJ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAErB;IAEK,eAAe,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAEnD;IAEK,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAErC;IAEK,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAOpD;IAEK,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC,CAIzC;IAKD,6CAA6C;IACvC,MAAM,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvC;IAED,2CAA2C;IACrC,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAKpD;IAED,yEAAyE;IACnE,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAK3D;IAED,wDAAwD;IAClD,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvE;IAED;gCAC4B;IACtB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxD;IAED;gCAC4B;IACtB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjD;IAED,6DAA6D;IACvD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnD;IAED,sDAAsD;IAChD,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBzD;IAED;;+EAE2E;IACrE,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjD;IAED;+BAC2B;IACrB,kBAAkB,CAAC,KAAK,GAAE,aAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrE;IAED,qCAAqC;IAC/B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5B;IAED;;;6DAGyD;IACnD,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,EAAE,KAAY,EAAE,GAAE,oBAAyB,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAYjC;IAED,OAAO,CAAC,oBAAoB;CAY7B"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { mergeClassicCapabilityFields, parseClassicCapabilities, } from "../codecs/classic/capabilities.js";
|
|
2
|
+
import { applyClassicOutOfHomeAccess, encodeClassicDemandControl, encodeClassicEnergyPrice, encodeClassicEnergyTarget, encodeClassicGroup, encodeClassicHolidayMode, encodeClassicIcon, encodeClassicLed, encodeClassicName, encodeClassicSpecialMode, encodeClassicTerminalKey, encodeClassicTerminalScope, } from "../codecs/classic/extendedControl.js";
|
|
3
|
+
import { assertClassicOk, parseClassicKeyValue, } from "../codecs/classic/keyValue.js";
|
|
4
|
+
import { applyClassicControl, parseClassicSensors, parseClassicState, } from "../codecs/classic/state.js";
|
|
5
|
+
import { withRetry, } from "../transport.js";
|
|
6
|
+
// Classic CGI endpoints (BRP069-style). Reads and writes are both HTTP GET with
|
|
7
|
+
// query parameters; the reply is `key=value,key=value`.
|
|
8
|
+
const CLASSIC_ROUTES = {
|
|
9
|
+
basicInfo: "/common/basic_info",
|
|
10
|
+
getModelInfo: "/aircon/get_model_info",
|
|
11
|
+
getControlInfo: "/aircon/get_control_info",
|
|
12
|
+
setControlInfo: "/aircon/set_control_info",
|
|
13
|
+
getSensorInfo: "/aircon/get_sensor_info",
|
|
14
|
+
setSpecialMode: "/aircon/set_special_mode",
|
|
15
|
+
setDemandControl: "/aircon/set_demand_control",
|
|
16
|
+
setPrice: "/aircon/set_price",
|
|
17
|
+
setTarget: "/aircon/set_target",
|
|
18
|
+
setLed: "/common/set_led",
|
|
19
|
+
setHoliday: "/common/set_holiday",
|
|
20
|
+
setName: "/common/set_name",
|
|
21
|
+
setIcon: "/common/set_icon",
|
|
22
|
+
setGroup: "/common/set_group",
|
|
23
|
+
getRemoteMethod: "/common/get_remote_method",
|
|
24
|
+
setRemoteMethod: "/common/set_remote_method",
|
|
25
|
+
reboot: "/common/reboot",
|
|
26
|
+
registerTerminal: "/common/register_terminal",
|
|
27
|
+
unregisterTerminal: "/common/unregister_terminal",
|
|
28
|
+
};
|
|
29
|
+
/** Classic CGI adapter (BRP069-style). Reads and writes are HTTP GET with query
|
|
30
|
+
* parameters; replies are `key=value,key=value`. */
|
|
31
|
+
export class ClassicDaikinClient {
|
|
32
|
+
transport;
|
|
33
|
+
retryOptions;
|
|
34
|
+
// A unit's capabilities don't change, and `setState` needs them, so they are
|
|
35
|
+
// read once.
|
|
36
|
+
capabilityFields;
|
|
37
|
+
/** `close` closes `transport`, so give each client a transport of its own. */
|
|
38
|
+
constructor(transport, retryOptions) {
|
|
39
|
+
this.transport = transport;
|
|
40
|
+
this.retryOptions = retryOptions;
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
return this.transport.close();
|
|
44
|
+
}
|
|
45
|
+
async getCapabilities() {
|
|
46
|
+
return parseClassicCapabilities(await this.readCapabilityFields());
|
|
47
|
+
}
|
|
48
|
+
async getState() {
|
|
49
|
+
return parseClassicState(await this.command(CLASSIC_ROUTES.getControlInfo));
|
|
50
|
+
}
|
|
51
|
+
async setState(control) {
|
|
52
|
+
const capabilityFields = await this.readCapabilityFields();
|
|
53
|
+
const current = await this.command(CLASSIC_ROUTES.getControlInfo);
|
|
54
|
+
await this.command(CLASSIC_ROUTES.setControlInfo, applyClassicControl(current, control, capabilityFields));
|
|
55
|
+
}
|
|
56
|
+
async getSensors() {
|
|
57
|
+
return parseClassicSensors(await this.command(CLASSIC_ROUTES.getSensorInfo));
|
|
58
|
+
}
|
|
59
|
+
// --- extended controls ---------------------------------------------------
|
|
60
|
+
// Each maps to its own endpoint (not control_info).
|
|
61
|
+
/** Adapter status LED on/off (`set_led`). */
|
|
62
|
+
async setLed(on) {
|
|
63
|
+
await this.command(CLASSIC_ROUTES.setLed, encodeClassicLed(on));
|
|
64
|
+
}
|
|
65
|
+
/** Holiday / away mode (`set_holiday`). */
|
|
66
|
+
async setHolidayMode(enabled) {
|
|
67
|
+
await this.command(CLASSIC_ROUTES.setHoliday, encodeClassicHolidayMode(enabled));
|
|
68
|
+
}
|
|
69
|
+
/** Demand control — cap the unit's power draw (`set_demand_control`). */
|
|
70
|
+
async setDemandControl(demand) {
|
|
71
|
+
await this.command(CLASSIC_ROUTES.setDemandControl, encodeClassicDemandControl(demand));
|
|
72
|
+
}
|
|
73
|
+
/** Powerful, econo or streamer (`set_special_mode`). */
|
|
74
|
+
async setSpecialMode(mode, enabled) {
|
|
75
|
+
await this.command(CLASSIC_ROUTES.setSpecialMode, encodeClassicSpecialMode(mode, enabled));
|
|
76
|
+
}
|
|
77
|
+
/** Whether the unit can be operated from outside the home network
|
|
78
|
+
* (`set_remote_method`). */
|
|
79
|
+
async setOutOfHomeAccess(enabled) {
|
|
80
|
+
const current = await this.command(CLASSIC_ROUTES.getRemoteMethod);
|
|
81
|
+
await this.command(CLASSIC_ROUTES.setRemoteMethod, applyClassicOutOfHomeAccess(current, enabled));
|
|
82
|
+
}
|
|
83
|
+
/** Electricity price per kWh for the consumption estimates (`set_price`),
|
|
84
|
+
* to two decimal places. */
|
|
85
|
+
async setEnergyPrice(price) {
|
|
86
|
+
await this.command(CLASSIC_ROUTES.setPrice, encodeClassicEnergyPrice(price));
|
|
87
|
+
}
|
|
88
|
+
/** Energy target for the "target" feature (`set_target`). */
|
|
89
|
+
async setEnergyTarget(target) {
|
|
90
|
+
await this.command(CLASSIC_ROUTES.setTarget, encodeClassicEnergyTarget(target));
|
|
91
|
+
}
|
|
92
|
+
/** Name / icon / group metadata (`set_name` etc.). */
|
|
93
|
+
async setMetadata(metadata) {
|
|
94
|
+
if (metadata.name !== undefined) {
|
|
95
|
+
await this.command(CLASSIC_ROUTES.setName, encodeClassicName(metadata.name));
|
|
96
|
+
}
|
|
97
|
+
if (metadata.icon !== undefined) {
|
|
98
|
+
await this.command(CLASSIC_ROUTES.setIcon, encodeClassicIcon(metadata.icon));
|
|
99
|
+
}
|
|
100
|
+
if (metadata.group !== undefined) {
|
|
101
|
+
await this.command(CLASSIC_ROUTES.setGroup, encodeClassicGroup(metadata.group));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Register this client's uuid with an adapter whose
|
|
105
|
+
* `requiresRegistration` is true, using the KEY printed on the adapter's
|
|
106
|
+
* label (`register_terminal`). Once per uuid; the adapter remembers it. */
|
|
107
|
+
async registerTerminal(key) {
|
|
108
|
+
await this.command(CLASSIC_ROUTES.registerTerminal, encodeClassicTerminalKey(key));
|
|
109
|
+
}
|
|
110
|
+
/** Unregister terminals (`unregister_terminal`): this uuid, every other
|
|
111
|
+
* uuid, or all of them. */
|
|
112
|
+
async unregisterTerminal(scope = "self") {
|
|
113
|
+
await this.command(CLASSIC_ROUTES.unregisterTerminal, encodeClassicTerminalScope(scope));
|
|
114
|
+
}
|
|
115
|
+
/** Reboot the adapter (`reboot`). */
|
|
116
|
+
async reboot() {
|
|
117
|
+
await this.command(CLASSIC_ROUTES.reboot, undefined, { retry: false });
|
|
118
|
+
}
|
|
119
|
+
/** Calls any classic CGI endpoint with query parameters and returns the reply's
|
|
120
|
+
* `key=value` fields. It reaches the controls without a typed method:
|
|
121
|
+
* timers, schedules, programs, network, credentials and firmware. It throws
|
|
122
|
+
* a `DaikinError` when the reply's `ret` is not `OK`. */
|
|
123
|
+
async command(path, parameters, { retry = true } = {}) {
|
|
124
|
+
const send = () => this.transport.send({
|
|
125
|
+
method: "GET",
|
|
126
|
+
path,
|
|
127
|
+
...(parameters ? { query: parameters } : {}),
|
|
128
|
+
});
|
|
129
|
+
const fields = parseClassicKeyValue(await (retry ? withRetry(send, this.retryOptions) : send()));
|
|
130
|
+
assertClassicOk(fields);
|
|
131
|
+
return fields;
|
|
132
|
+
}
|
|
133
|
+
readCapabilityFields() {
|
|
134
|
+
this.capabilityFields ??= (async () => mergeClassicCapabilityFields(await this.command(CLASSIC_ROUTES.basicInfo), await this.command(CLASSIC_ROUTES.getModelInfo)))();
|
|
135
|
+
// A failed read is not cached, so the next call tries again.
|
|
136
|
+
this.capabilityFields.catch(() => {
|
|
137
|
+
this.capabilityFields = undefined;
|
|
138
|
+
});
|
|
139
|
+
return this.capabilityFields;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=classic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classic.js","sourceRoot":"","sources":["../../src/clients/classic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DaikinCapabilities } from "../models/capabilities.js";
|
|
2
|
+
import type { DaikinControl, DaikinSensors, DaikinState } from "../models/state.js";
|
|
3
|
+
/** The controls every adapter offers, whatever its wire protocol or
|
|
4
|
+
* authentication. `createDaikinClient` picks the implementation, so a caller
|
|
5
|
+
* needs the concrete class only for what one protocol adds, such as
|
|
6
|
+
* `ClassicDaikinClient.registerTerminal`. Every method rejects with a
|
|
7
|
+
* `DaikinError` when its request fails. */
|
|
8
|
+
export interface DaikinClient {
|
|
9
|
+
/** The unit's supported features. */
|
|
10
|
+
getCapabilities(): Promise<DaikinCapabilities>;
|
|
11
|
+
/** Current controllable state. */
|
|
12
|
+
getState(): Promise<DaikinState>;
|
|
13
|
+
/** Change part of the state. The client reads the current state, overlays the
|
|
14
|
+
* control, and writes the whole thing back. */
|
|
15
|
+
setState(control: DaikinControl): Promise<void>;
|
|
16
|
+
/** Read-only temperature / humidity readings. */
|
|
17
|
+
getSensors(): Promise<DaikinSensors>;
|
|
18
|
+
/** Close the connection to the adapter. */
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=client.d.ts.map
|