daikin-local-control 0.4.0 → 0.5.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.
Files changed (96) hide show
  1. package/DESIGN.md +47 -4
  2. package/README.md +154 -46
  3. package/UNSUPPORTED.md +27 -2
  4. package/dist/clients/airbase.d.ts +64 -0
  5. package/dist/clients/airbase.d.ts.map +1 -0
  6. package/dist/clients/airbase.js +175 -0
  7. package/dist/clients/airbase.js.map +1 -0
  8. package/dist/clients/client.d.ts +3 -2
  9. package/dist/clients/client.d.ts.map +1 -1
  10. package/dist/codecs/airbase/capabilities.d.ts +23 -0
  11. package/dist/codecs/airbase/capabilities.d.ts.map +1 -0
  12. package/dist/codecs/airbase/capabilities.js +108 -0
  13. package/dist/codecs/airbase/capabilities.js.map +1 -0
  14. package/dist/codecs/airbase/state.d.ts +29 -0
  15. package/dist/codecs/airbase/state.d.ts.map +1 -0
  16. package/dist/codecs/airbase/state.js +166 -0
  17. package/dist/codecs/airbase/state.js.map +1 -0
  18. package/dist/codecs/airbase/tables.d.ts +7 -0
  19. package/dist/codecs/airbase/tables.d.ts.map +1 -0
  20. package/dist/codecs/airbase/tables.js +27 -0
  21. package/dist/codecs/airbase/tables.js.map +1 -0
  22. package/dist/codecs/airbase/zone.d.ts +12 -0
  23. package/dist/codecs/airbase/zone.d.ts.map +1 -0
  24. package/dist/codecs/airbase/zone.js +50 -0
  25. package/dist/codecs/airbase/zone.js.map +1 -0
  26. package/dist/codecs/classic/tables.d.ts +2 -0
  27. package/dist/codecs/classic/tables.d.ts.map +1 -1
  28. package/dist/codecs/classic/tables.js +2 -0
  29. package/dist/codecs/classic/tables.js.map +1 -1
  30. package/dist/codecs/device.d.ts +7 -1
  31. package/dist/codecs/device.d.ts.map +1 -1
  32. package/dist/codecs/device.js +14 -2
  33. package/dist/codecs/device.js.map +1 -1
  34. package/dist/codecs/dsiot/tables.d.ts +3 -0
  35. package/dist/codecs/dsiot/tables.d.ts.map +1 -1
  36. package/dist/codecs/dsiot/tables.js +3 -0
  37. package/dist/codecs/dsiot/tables.js.map +1 -1
  38. package/dist/discovery.d.ts +20 -10
  39. package/dist/discovery.d.ts.map +1 -1
  40. package/dist/discovery.js +96 -30
  41. package/dist/discovery.js.map +1 -1
  42. package/dist/factory.d.ts +8 -5
  43. package/dist/factory.d.ts.map +1 -1
  44. package/dist/factory.js +13 -3
  45. package/dist/factory.js.map +1 -1
  46. package/dist/index.d.ts +6 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +6 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/models/device.d.ts +3 -2
  51. package/dist/models/device.d.ts.map +1 -1
  52. package/dist/models/device.js +1 -1
  53. package/dist/models/device.js.map +1 -1
  54. package/dist/models/zone.d.ts +9 -0
  55. package/dist/models/zone.d.ts.map +1 -0
  56. package/dist/models/zone.js +2 -0
  57. package/dist/models/zone.js.map +1 -0
  58. package/dist/simulators/airbase.d.ts +82 -0
  59. package/dist/simulators/airbase.d.ts.map +1 -0
  60. package/dist/simulators/airbase.js +352 -0
  61. package/dist/simulators/airbase.js.map +1 -0
  62. package/dist/simulators/classic.d.ts.map +1 -1
  63. package/dist/simulators/classic.js +2 -2
  64. package/dist/simulators/classic.js.map +1 -1
  65. package/dist/simulators/index.d.ts +1 -0
  66. package/dist/simulators/index.d.ts.map +1 -1
  67. package/dist/simulators/index.js +1 -0
  68. package/dist/simulators/index.js.map +1 -1
  69. package/dist/simulators/simulator.d.ts +11 -10
  70. package/dist/simulators/simulator.d.ts.map +1 -1
  71. package/dist/simulators/simulator.js +2 -2
  72. package/dist/simulators/simulator.js.map +1 -1
  73. package/dist/transport.d.ts +3 -2
  74. package/dist/transport.d.ts.map +1 -1
  75. package/dist/transport.js +4 -1
  76. package/dist/transport.js.map +1 -1
  77. package/package.json +7 -3
  78. package/src/clients/airbase.ts +274 -0
  79. package/src/clients/client.ts +3 -2
  80. package/src/codecs/airbase/capabilities.ts +150 -0
  81. package/src/codecs/airbase/state.ts +223 -0
  82. package/src/codecs/airbase/tables.ts +32 -0
  83. package/src/codecs/airbase/zone.ts +62 -0
  84. package/src/codecs/classic/tables.ts +3 -0
  85. package/src/codecs/device.ts +18 -2
  86. package/src/codecs/dsiot/tables.ts +4 -0
  87. package/src/discovery.ts +101 -32
  88. package/src/factory.ts +21 -5
  89. package/src/index.ts +6 -0
  90. package/src/models/device.ts +3 -2
  91. package/src/models/zone.ts +8 -0
  92. package/src/simulators/airbase.ts +495 -0
  93. package/src/simulators/classic.ts +2 -1
  94. package/src/simulators/index.ts +1 -0
  95. package/src/simulators/simulator.ts +13 -12
  96. package/src/transport.ts +7 -3
package/DESIGN.md CHANGED
@@ -53,6 +53,46 @@ the read. A `DaikinState` holds the current mode's values alone: it has neither
53
53
  the values a classic adapter remembers for the other modes nor the `md` of a
54
54
  DSIOT leaf, so the client would still need the reply.
55
55
 
56
+ ## Discovery
57
+
58
+ `discoverDevices` sends its probes from the first free UDP port from 30000 to
59
+ 30050. An adapter answers the port a probe came from, so a reply reaches the
60
+ call that sent the probe.
61
+
62
+ Rejected: port 30000 alone, bound with `reuseAddr`.
63
+
64
+ - Two calls at the same time both bound the port, and each reply reached one
65
+ of the two sockets alone. One call resolved with every adapter and the other
66
+ with none, and neither rejected.
67
+
68
+ `discoverDevices` asks each adapter that replies as a classic one, over HTTP,
69
+ whether it is an Airbase adapter: `isAirbaseAdapter` takes HTTP 404 at
70
+ `/common/basic_info` with `ret=OK` at `/skyfi/common/basic_info` for one.
71
+
72
+ Rejected: the protocol from the reply alone.
73
+
74
+ - An Airbase adapter reports `type=aircon` as a classic adapter does, and no
75
+ field of the reply is known to differ. Discovery reported it as `classic`,
76
+ and it answered HTTP 404 to every request of the client built from that.
77
+
78
+ Rejected: asking in `createDaikinClient`, for a device written by hand as
79
+ well.
80
+
81
+ - `createDaikinClient` returns a client at once and sends nothing. Asking
82
+ there would make it asynchronous for every caller, to settle what a caller
83
+ who writes a device by hand already knows.
84
+
85
+ `discoverDevices` resolves with the adapters of air conditioners alone:
86
+ `isAirConditioner` passes a reply whose `type` is `aircon`, or `GPF` with a
87
+ `cdev` that is not an air cleaner's.
88
+
89
+ Rejected: a device for every adapter that answers.
90
+
91
+ - The adapter of an air cleaner, a water heater, a floor heater or a
92
+ circulator answers the probe as well. `parseDevice` read each as a classic
93
+ air conditioner, a `DaikinDevice` had nothing to tell them apart by, and no
94
+ method of the client built from one worked.
95
+
56
96
  ## Capabilities
57
97
 
58
98
  `DaikinCapabilities` lists what a unit accepts in the vocabulary of `DaikinState`:
@@ -154,8 +194,8 @@ Rejected: one list for the whole unit.
154
194
 
155
195
  ## Simulators
156
196
 
157
- `startClassicSimulator` and `startDsiotSimulator` each start an adapter as a
158
- server. A simulator shares the codecs' tables, `src/codecs/*/tables.ts`, and
197
+ `startClassicSimulator`, `startDsiotSimulator` and `startAirbaseSimulator`
198
+ each start an adapter as a server. A simulator shares the codecs' tables, `src/codecs/*/tables.ts`, and
159
199
  none of their functions. A table holds what an adapter calls a thing: its
160
200
  field and leaf names, and the code for each value. A function does something
161
201
  with it: a codec parses a reply and builds a request, and a simulator answers
@@ -198,8 +238,11 @@ reply.
198
238
  - A caller had to know the adapter's encodings to start a unit in a state: a
199
239
  DSIOT setpoint of 24 °C is the leaf value `3000`.
200
240
 
201
- A simulator takes what the unit has as a `DaikinSimulatorUnit`, which says it
202
- in terms both protocols have flags for: `coolingOnly`, `swing`, `fan`.
241
+ A classic or a DSIOT simulator takes what the unit has as a
242
+ `DaikinSimulatorUnit`, which says it in terms both protocols have flags for:
243
+ `coolingOnly`, `swing`, `fan`. An Airbase simulator takes an
244
+ `AirbaseSimulatorUnit` of its own: an Airbase unit has zones and airside, and
245
+ neither a cooling-only kind nor a special mode.
203
246
 
204
247
  Rejected: taking a `DaikinCapabilities`.
205
248
 
package/README.md CHANGED
@@ -1,26 +1,32 @@
1
1
  # daikin-local-control
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/daikin-local-control)](https://www.npmjs.com/package/daikin-local-control)
4
+ [![npm downloads](https://img.shields.io/npm/dm/daikin-local-control)](https://www.npmjs.com/package/daikin-local-control)
4
5
  [![CI](https://github.com/cheziyi/daikin-local-control/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/cheziyi/daikin-local-control/actions/workflows/ci.yml?query=branch%3Amain)
5
6
  [![Node version](https://img.shields.io/node/v/daikin-local-control)](package.json)
7
+ [![Types](https://img.shields.io/npm/types/daikin-local-control)](package.json)
6
8
  [![License](https://img.shields.io/npm/l/daikin-local-control)](LICENSE)
9
+ [![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
7
10
 
8
11
  A TypeScript and Node.js client for controlling Daikin air conditioners **over
9
12
  the local network** (LAN), through their Wi-Fi adapters and with no cloud
10
- dependency. It implements the same local-control protocol as the **DAIKIN Mobile
11
- Controller** app, so it works with the units and Wi-Fi adapters that app
12
- supports — split / multi-split and SkyAir units fitted with adapters such as the
13
- BRP072A42, BRP072C42 and BRP084 — in the
14
- [markets where that app is used](#coverage--limitations).
13
+ dependency. It implements the local-control protocols of the **DAIKIN Mobile
14
+ Controller**, **Daikin Airbase** and **ONECTA** apps, so it works with the units
15
+ and Wi-Fi adapters those apps support — split / multi-split, SkyAir and ducted
16
+ units fitted with adapters such as the BRP072A42, BRP072C42, BRP084, BRP069 and
17
+ BRP15B61 — in the [markets where those apps are used](#coverage--limitations).
15
18
 
16
- Daikin Wi-Fi adapters come in two generations, and the package has a client for
17
- each:
19
+ Daikin Wi-Fi adapters speak one of three protocols, and the package has a
20
+ client for each:
18
21
 
19
22
  - **`ClassicDaikinClient`** — classic adapters (BRP072A42, BRP072C42, BRP069).
20
23
  Full core control plus typed [extended controls](#extended-controls).
21
24
  - **`DsiotDaikinClient`** — newer DSIOT adapters (BRP084). Power, mode,
22
25
  setpoint, humidity, fan, swing, louver positions and special modes;
23
26
  [UNSUPPORTED.md](UNSUPPORTED.md) lists what is not mapped yet.
27
+ - **`AirbaseDaikinClient`** — Airbase adapters (BRP15B61) on ducted units.
28
+ Power, mode, setpoint, fan and direction, plus [zones](#airbase), fan auto
29
+ and airside.
24
30
 
25
31
  You normally don't pick one yourself: `createDaikinClient` chooses the right
26
32
  client for each discovered device.
@@ -47,9 +53,9 @@ if (device === undefined) {
47
53
  throw new Error("No adapter answered");
48
54
  }
49
55
 
50
- // 2. Build the right client: createDaikinClient picks the classic or the DSIOT
51
- // client from the device. Pass localPassword only when the adapter has a
52
- // child lock code (see Credentials below).
56
+ // 2. Build the right client: createDaikinClient picks the classic, the DSIOT
57
+ // or the Airbase client from the device. Pass localPassword only when the
58
+ // adapter has a child lock code (see Credentials below).
53
59
  const client = createDaikinClient(device, {
54
60
  terminalUuid: savedUuid, // generated once and stored (see Credentials below)
55
61
  localPassword: device.requiresLocalPassword ? "<child lock code>" : undefined,
@@ -73,17 +79,19 @@ await client.close();
73
79
 
74
80
  ### Discovery
75
81
 
76
- `discoverDevices()` resolves with one `DaikinDevice` for each adapter that
77
- answered, and with an empty array when none did. Each carries the adapter's
78
- `macAddress`, such as `A4:08:EA:CC:91:D4`, and `firmwareVersion`, such as
79
- `1.2.51`, when the adapter reports them: key a stored device on `macAddress`,
80
- because `host` changes when the adapter is given a new address.
82
+ `discoverDevices()` resolves with one `DaikinDevice` for each air conditioner's
83
+ adapter that answered, and with an empty array when none did. Each carries the
84
+ adapter's `macAddress`, such as `A4:08:EA:CC:91:D4`, and `firmwareVersion`,
85
+ such as `1.2.51`, when the adapter reports them: key a stored device on
86
+ `macAddress`, because `host` changes when the adapter is given a new address.
81
87
 
82
88
  | It | So |
83
89
  | --- | --- |
84
90
  | Resolves after `timeoutInMs`, never earlier | Pass `discoverDevices({ timeoutInMs: 1000 })` to wait less than the default 3000 |
85
91
  | Broadcasts to `255.255.255.255` | It does not find an adapter on another subnet or VLAN |
86
- | Binds UDP port 30000 | It rejects with the socket's error when the socket fails |
92
+ | Keeps the adapters of air conditioners alone | It leaves out the adapter of an air cleaner, a water heater or another appliance, which answers the same broadcast |
93
+ | Asks each classic adapter over HTTP whether it is an Airbase adapter, which replies to the broadcast as a classic one does | It resolves later than `timeoutInMs` while an adapter is slow to answer, by 3000 ms at most for each request after up to 750 ms of retrying a dropped connection, and reports as `classic` one that does not answer. An Airbase device requires no registration and no child lock code, whatever the adapter's reply says |
94
+ | Binds the first free UDP port from 30000 to 30050 | Calls made at the same time each get every reply. It rejects with the socket's error when none of the ports is free, or when the socket fails |
87
95
 
88
96
  Write the device by hand when you already know its address, or when discovery
89
97
  cannot reach it:
@@ -98,8 +106,13 @@ const client = createDaikinClient(device, { terminalUuid: savedUuid });
98
106
  A device written by hand has no `name`, `macAddress` or `firmwareVersion`: read
99
107
  them from the adapter with `await client.getDeviceInfo()`.
100
108
 
101
- Set `protocol` to `dsiot` for a BRP084 and to `classic` for every other
102
- adapter. A device without flags is an adapter that requires neither
109
+ To tell an Airbase adapter from a classic one by its address, request
110
+ `/common/basic_info`: an Airbase adapter answers HTTP 404 there, and `ret=OK`
111
+ at `/skyfi/common/basic_info`.
112
+
113
+ Set `protocol` to `dsiot` for a BRP084, to `airbase` for a BRP15B61 and to
114
+ `classic` for every other adapter: `createDaikinClient()` goes by it and asks
115
+ the adapter nothing. A device without flags is an adapter that requires neither
103
116
  registration nor a child lock code. If yours requires either, set
104
117
  `requiresRegistration: true` / `requiresLocalPassword: true` and set up
105
118
  [credentials](#credentials) to match; add `usesHttps: true` if it also speaks
@@ -109,7 +122,9 @@ for you.
109
122
  ### Credentials
110
123
 
111
124
  An adapter can need up to three things. Discovery tells you which ones apply
112
- (`device.requiresRegistration`, `device.requiresLocalPassword`).
125
+ (`device.requiresRegistration`, `device.requiresLocalPassword`). An Airbase
126
+ adapter takes none of them: `createDaikinClient()` still takes a
127
+ `terminalUuid`, and sends it nothing of the three.
113
128
 
114
129
  **Terminal uuid (always).** It identifies your app to the adapter. Pass 32
115
130
  lowercase hexadecimal digits, a UUID without its dashes: `createDaikinClient()`
@@ -128,8 +143,8 @@ const savedUuid =
128
143
  adapters only answer terminals they have registered. Each adapter's KEY is
129
144
  printed on its label, next to the SSID (also in the operation manual). Register
130
145
  your uuid with it once. Until you do, every call fails with HTTP 401/403.
131
- Each uuid is registered separately, so an adapter already set up in the DAIKIN
132
- Mobile Controller app still needs registering here.
146
+ Each uuid is registered separately, so an adapter already set up in a Daikin
147
+ app still needs registering here.
133
148
 
134
149
  ```ts
135
150
  import { ClassicDaikinClient } from "daikin-local-control";
@@ -149,8 +164,8 @@ Some of these adapters also switch to HTTPS (`device.usesHttps`); see
149
164
 
150
165
  **Child lock code (when `device.requiresLocalPassword` is `true`).** The protocol
151
166
  calls it the local password (`lpw`). It is the 4-digit code set under Child lock
152
- in the DAIKIN Mobile Controller app. It is **not** printed on the label, and it
153
- is not your Wi-Fi or Out-of-Home password. Pass it as `localPassword` to
167
+ in the Daikin app the adapter was set up with. It is **not** printed on the
168
+ label, and it is not your Wi-Fi or Out-of-Home password. Pass it as `localPassword` to
154
169
  `createDaikinClient`. If it's forgotten, the adapter has to be reset.
155
170
 
156
171
  ### State and capabilities
@@ -203,11 +218,16 @@ given, whether or not the unit lists it.
203
218
  | `targetTemperatureInC` | In `fan` and `dehumidify` |
204
219
  | `targetHumidity` | In `fan` |
205
220
  | `fanSpeed` | In `dehumidify`, and on a DSIOT adapter in `cool` while a humidity setting is on: the fan runs on `auto` |
206
- | `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 |
221
+ | `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. On an Airbase adapter, always |
222
+
223
+ An Airbase adapter also drops every `targetHumidity`, and a
224
+ `targetTemperatureInC` in `fan` as well as in `dehumidify`.
207
225
 
208
226
  `setState()` takes no `specialModes`: turn one on or off with
209
227
  `setSpecialMode()`, which sends a request of its own. Check the mode against
210
228
  `getCapabilities().specialModes` first: the client sends any mode as given.
229
+ An Airbase adapter has no request for one, so `AirbaseDaikinClient` rejects
230
+ with a `DaikinError` whose `httpStatusCode` is 501.
211
231
 
212
232
  ```ts
213
233
  await client.setSpecialMode("powerful", true);
@@ -271,6 +291,43 @@ what is not known about each.
271
291
  `DsiotDaikinClient` has `reboot()` and the raw `multireqGet` / `multireqUpdate`
272
292
  hatches for any `dgc_status` leaf.
273
293
 
294
+ ### Airbase
295
+
296
+ `AirbaseDaikinClient` adds the controls a ducted unit has. Narrow to the
297
+ concrete class to reach them:
298
+
299
+ ```ts
300
+ import { AirbaseDaikinClient } from "daikin-local-control";
301
+
302
+ if (client instanceof AirbaseDaikinClient) {
303
+ const zones = await client.getZones(); // [{ name: "Living", enabled: true }, ...]
304
+ await client.setZone(1, true); // open the second zone
305
+
306
+ if ((await client.getFanAuto()) !== null) {
307
+ await client.setFanAuto(true);
308
+ }
309
+ if ((await client.getAirside()) !== null) {
310
+ await client.setAirside(true);
311
+ }
312
+
313
+ // Any other endpoint, by its path below /skyfi.
314
+ const holiday = await client.command("/common/get_holiday");
315
+ }
316
+ ```
317
+
318
+ | Method | Does |
319
+ | --- | --- |
320
+ | `getZones()` | Resolves with `{ name, enabled }` for each zone, in the unit's order, and with an empty array on a unit without zones |
321
+ | `setZone(index, enabled)` | Opens or closes the zone at that place in `getZones()`, counted from 0 |
322
+ | `getFanAuto()`, `getAirside()` | Resolve with `true` or `false`, or `null` on a unit that does not take the setting |
323
+ | `setFanAuto(enabled)` | Lets the unit pick the fan speed itself, up to the `fanSpeed` that is set. It switches airside off |
324
+ | `setAirside(enabled)` | Switches airside on or off. Switching it on switches fan auto off and sets `fanSpeed` to `level1` |
325
+
326
+ A `fanSpeed` in `setState()` switches airside off and keeps fan auto.
327
+
328
+ Only a unit with zones takes fan auto and airside, and it takes no `auto` fan
329
+ speed and no `swingMode`: `getCapabilities()` lists what a unit takes.
330
+
274
331
  ### Errors and retries
275
332
 
276
333
  A client method that fails rejects with a `DaikinError`; branch on
@@ -282,13 +339,15 @@ A client method that fails rejects with a `DaikinError`; branch on
282
339
  | 502 | The adapter answered with an error: a `ret` or `rsc` other than OK, or a reply that does not parse |
283
340
  | 503 | The adapter refused or dropped the connection (a `DaikinConnectionError`), or DSIOT answered `rsc` 503 |
284
341
  | 504 | The request timed out, or the TLS handshake failed |
342
+ | 501 | `AirbaseDaikinClient.setSpecialMode()`: an Airbase adapter has no request for a special mode |
285
343
  | Any other | The adapter's own HTTP status, such as 403 |
286
344
 
287
- Three failures are not a `DaikinError`: `discoverDevices()` rejects with the
345
+ Four failures are not a `DaikinError`: `discoverDevices()` rejects with the
288
346
  socket's error, `createDaikinClient()` throws an `Error` for a
289
- [terminal uuid](#credentials) in the wrong form, and `multireqUpdate()` throws
347
+ [terminal uuid](#credentials) in the wrong form, `multireqUpdate()` throws
290
348
  an `Error` for a path that names a resource without a leaf, such as
291
- `dgc_status`.
349
+ `dgc_status`, and `setZone()` rejects with an `Error` for an index the unit
350
+ has no zone at.
292
351
 
293
352
  Only a `DaikinConnectionError` is retried, up to five times by default.
294
353
  [DESIGN.md](DESIGN.md#retries) says why.
@@ -330,9 +389,10 @@ await simulator.stop();
330
389
  ```
331
390
 
332
391
  `startDsiotSimulator()` starts a DSIOT adapter and takes the same options:
333
- pass `protocol: "dsiot"` to `createDaikinClient`. A simulator speaks plain
334
- HTTP on a free port of `127.0.0.1`, which nothing outside the machine
335
- reaches.
392
+ pass `protocol: "dsiot"` to `createDaikinClient`.
393
+ `startAirbaseSimulator()` starts an [Airbase adapter](#airbase-simulator):
394
+ pass `protocol: "airbase"`. A simulator speaks plain HTTP on a free port of
395
+ `127.0.0.1`, which nothing outside the machine reaches.
336
396
 
337
397
  Describe the adapter and its unit in the types the clients resolve with:
338
398
 
@@ -356,6 +416,40 @@ Describe the adapter and its unit in the types the clients resolve with:
356
416
  `setState()` throws an `Error` for a field the unit has nothing to hold, such
357
417
  as a `verticalPosition` on a unit without fixed louver positions.
358
418
 
419
+ #### Airbase simulator
420
+
421
+ `startAirbaseSimulator()` takes `deviceInfo`, `state` and `sensors` as the
422
+ other two do, and a `unit` of its own. It reports no humidity unless
423
+ `sensors` gives one, and its `setState()` throws an `Error` for a
424
+ `targetHumidity`, a louver position or a special mode.
425
+
426
+ ```ts
427
+ import { startAirbaseSimulator } from "daikin-local-control/simulators";
428
+
429
+ const simulator = await startAirbaseSimulator({
430
+ unit: { zones: ["Living", "Bedrooms"], airside: true },
431
+ state: { power: true, fanSpeed: "level3" },
432
+ });
433
+
434
+ simulator.setZone(1, false); // someone closes a zone on the wall controller
435
+ simulator.controlInfo.f_auto = "1"; // and switches fan auto on
436
+ ```
437
+
438
+ | `unit` field | Holds | Defaults to |
439
+ | --- | --- | --- |
440
+ | `zones` | The names of the unit's zones, each open at the start | None: a unit without zones |
441
+ | `auto`, `dehumidify` | `false` for a unit that does not offer the mode | `true` |
442
+ | `fan` | `false` for a unit whose fan speed cannot be set | `true` |
443
+ | `fanLevels` | 2 or 3 fan levels | 3 |
444
+ | `airside` | `true` for a unit with zones that takes airside | `false` |
445
+ | `swing` | `false` for a unit without zones that takes no direction | `true` |
446
+ | `coolTemperaturesInC`, `heatTemperaturesInC` | `{ min, max }`, in whole degrees | None reported, so 18 to 32 and 10 to 30 |
447
+
448
+ A unit with zones takes fan auto, and no `auto` fan speed and no direction.
449
+ It answers HTTP 404 to a path outside `/skyfi`, as an Airbase adapter does,
450
+ so `discoverDevices()` reports it as `airbase` when it is started with
451
+ `answersDiscovery`.
452
+
359
453
  | To | Do |
360
454
  | --- | --- |
361
455
  | Check what the unit holds | Call `getState()` on a client |
@@ -379,8 +473,9 @@ simulator with `answersDiscovery` rejects with the socket's error.
379
473
  For anything the options above do not cover, such as humidity settings or
380
474
  fixed louver positions, set the adapter's own fields. A classic simulator
381
475
  takes `basicInfo`, `modelInfo`, `controlInfo` and `sensorInfo`, the fields of
382
- each reply, and holds them under the same names. A DSIOT simulator takes
383
- `leaves`, and holds each leaf by its address. A field given this way replaces
476
+ each reply, and holds them under the same names. An Airbase simulator takes
477
+ those four and `zoneSetting`. A DSIOT simulator takes `leaves`, and holds each
478
+ leaf by its address. A field given this way replaces
384
479
  what `unit` sets, and `state` and `sensors` replace it in turn. The package
385
480
  root exports the adapter's codes as tables, such as `CLASSIC_FAN_SPEED_CODES`
386
481
  and `DSIOT_PATHS`.
@@ -411,20 +506,33 @@ clients' codecs, and why no more.
411
506
 
412
507
  ## Coverage & limitations
413
508
 
414
- It supports the same markets and Wi-Fi adapters as the DAIKIN Mobile Controller
415
- app, so it is **not valid for every Daikin region or app**.
416
-
417
- | Region | Markets using the DAIKIN Mobile Controller app |
418
- | -------------- | -------------------------------------------------------------- |
419
- | Southeast Asia | Thailand, Singapore, the Philippines |
420
- | East Asia | Hong Kong, Taiwan |
421
- | Oceania | Australia, New Zealand |
422
-
423
- The app is published by Daikin Thailand and may be available in other countries
424
- too; if your local Daikin site points you to it, this package should apply. Units
425
- sold for other apps likely use a different protocol, e.g. Onecta / Daikin Online
426
- Controller (Europe), GO DAIKIN (Malaysia) or Daikin Comfort Control / Daikin One
427
- (North America).
509
+ It supports the Wi-Fi adapters of three apps, in the markets where each is
510
+ used, so it is **not valid for every Daikin region or app**.
511
+
512
+ | App | Markets | Adapters | Client |
513
+ | --- | --- | --- | --- |
514
+ | DAIKIN Mobile Controller | Thailand, Singapore, the Philippines, Hong Kong, Taiwan, Australia, New Zealand | BRP072A42, BRP072C42, BRP069 | `ClassicDaikinClient` |
515
+ | DAIKIN Mobile Controller | The same | BRP084 | `DsiotDaikinClient` |
516
+ | Daikin Airbase | Australia | BRP15B61, on a ducted unit | `AirbaseDaikinClient` |
517
+ | ONECTA, once Daikin Online Controller | Europe | BRP069A and BRP069B series | `ClassicDaikinClient` |
518
+
519
+ Each app may be available in more countries than the table lists: if your
520
+ local Daikin site points you to one of the three, this package should apply.
521
+
522
+ Three limits apply, and [UNSUPPORTED.md](UNSUPPORTED.md) says what is not known
523
+ about each:
524
+
525
+ - **Europe**: power, mode, setpoint, fan, swing and special modes go to a
526
+ classic adapter there as they do elsewhere. `getCapabilities()` follows the
527
+ rules of the other markets, and which adapters there speak HTTPS is not
528
+ known.
529
+ - **A DSIOT adapter in Europe** (BRP069C series) is not covered: it may not
530
+ answer on the LAN until it is switched on.
531
+ - **An Airbase adapter** is told from a classic one by how it answers over
532
+ HTTP: no field of its discovery reply is known to differ.
533
+
534
+ Units sold for other apps likely use a different protocol, e.g. GO DAIKIN
535
+ (Malaysia) or Daikin Comfort Control / Daikin One (North America).
428
536
 
429
537
  This package is still young, so expect some rough edges. If something doesn't
430
538
  work with your unit, please
package/UNSUPPORTED.md CHANGED
@@ -31,6 +31,20 @@ To add one, pin down the request and reply first, then delete its row.
31
31
  | 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()` |
32
32
  | Classic firmware update: a POST to `/dkac/system/fwupdate` | The format of the upload. | Not reachable |
33
33
  | A terminal uuid in another form: with dashes, uppercase, or not 32 hexadecimal digits | Whether the adapter accepts it. `createDaikinClient` throws an `Error` for one. | Not reachable |
34
+ | Telling an Airbase adapter from a classic one in a discovery reply | Whether any field of the reply differs: both report `type=aircon`. `discoverDevices` asks the adapter over HTTP instead, and takes HTTP 404 at `/common/basic_info` with `ret=OK` at `/skyfi/common/basic_info` for an Airbase adapter. What a classic adapter answers under `/skyfi` is not known, so it is never asked there. | Set `protocol` on the `DaikinDevice` by hand |
35
+ | An Airbase child lock: `lpw_flag` in its `/common/basic_info` | Whether an Airbase adapter has a child lock code, and how a request would carry one. `discoverDevices` reports `requiresLocalPassword: false` for an Airbase adapter whatever its `lpw_flag`, and `AirbaseDaikinClient` sends no `lpw`. | `AirbaseDaikinClient.command()` |
36
+ | Airbase special modes | Whether a unit takes any, and the request. `getState` reports none, and `AirbaseDaikinClient.setSpecialMode` rejects with status 501. | `AirbaseDaikinClient.command()` |
37
+ | Airbase humidity: `shum`, and `dh1` to `dhh` in `/aircon/get_control_info` | Whether a ducted unit has a humidity setting, and what each field holds. `getState` reports `null`, and `setState` sends `shum` back as the unit holds it and none of the `dh` fields. | `AirbaseDaikinClient.command()` |
38
+ | Airbase energy monitoring and demand control | Which field says a unit has either. `getCapabilities` reports `false` for both. | `AirbaseDaikinClient.command()` |
39
+ | Airbase `f_dir` values 4 and 7 | What each stands for. `getState` reports `swingMode: "off"`. | `AirbaseDaikinClient.command()` |
40
+ | Airbase `bk_auto` and `operate` in `/aircon/get_control_info`, on a unit that reports a `bk_auto` other than its mode while it runs in cool or heat | Which of the two modes such a unit remembers a fan speed for. `setState` writes it to the one `bk_auto` names. | `AirbaseDaikinClient.command()` |
41
+ | Airbase `/aircon/set_control_info` without the memory fields: `dt0` to `dt7`, `dfr1`, `dfr2`, `dfd1`, `dfd2`, `auto1`, `auto2`, `airside1` and `airside2` | Whether the adapter takes it, so `setState` sends each one a control changes. | `AirbaseDaikinClient.command()` |
42
+ | Every other Airbase endpoint: timers, programs, schedules, holiday, demand control, energy history, network and firmware | Their parameters and reply fields. | `AirbaseDaikinClient.command()` |
43
+ | A classic adapter sold in Europe: the capability fields of a unit there (`reg=eu`) | Whether such a unit reports them as a unit in the other markets does. `getCapabilities` reads them by the same rules. | `ClassicDaikinClient.command()` |
44
+ | A classic adapter sold in Europe that reports `adp_kind=3` and `en_secure=1` | Whether it speaks HTTPS, as such an adapter does in the other markets. `discoverDevices` sets `usesHttps` for it. | Set `usesHttps: false` on the `DaikinDevice` |
45
+ | A DSIOT adapter sold in Europe (BRP069C series) | Whether its `dgc_status` leaves hold the values a BRP084 holds. Its `adp_i`, `adp_d` and `dgc_status` resources are read with the same request. | `DsiotDaikinClient.multireqGet()` |
46
+ | A DSIOT adapter that answers `/dsiot/multireq` only once it is switched on, by the UDP datagram `DAIKIN_UDP/debug/en_multireq 1` sent to its port 30050 | Which adapters need it, what one answers to the datagram and to `/dsiot/multireq` before it, and how long it stays on. | Not reachable |
47
+ | An adapter whose discovery reply has a `type` other than `aircon` and `GPF`, such as `common_adp`, or none | Whether any is an air conditioner's. `discoverDevices` leaves it out. | Write the `DaikinDevice` by hand |
34
48
 
35
49
  ## Not modelled
36
50
 
@@ -43,6 +57,9 @@ These values are known, but the package's types don't represent them yet:
43
57
  | DSIOT laundry mode (`dgc_status.e_1002.e_3003.p_25` = `01`) | The mode in `dgc_status.e_1002.e_3001.p_01` |
44
58
  | Classic `f_dir=6` (heat with the special air outlet) | `swingMode: "off"` |
45
59
  | Japanese units (`reg=jp`), which follow separate rules for limits, humidity and fan, and take a humidity percentage | The rules for other regions |
60
+ | Airbase zone temperatures: `lztemp_c` and `lztemp_h` in `/aircon/get_zone_setting`, a setpoint for each zone in cool and in heat | Nothing: `getZones` leaves them out, and `setZone` sends them back as the unit holds them |
61
+ | Airbase fan auto and airside | `fanSpeed` holds `f_rate` alone. `AirbaseDaikinClient.getFanAuto` and `getAirside` report the other two |
62
+ | Airbase modes outside `OperatingMode`, such as `HUM` | `mode: "auto"`. `setState` keeps the mode unless the control sets one |
46
63
 
47
64
  These adapter codes pass through a model unchanged, because what each one
48
65
  stands for is not known:
@@ -54,8 +71,9 @@ stands for is not known:
54
71
 
55
72
  ## Simulators
56
73
 
57
- `startClassicSimulator` and `startDsiotSimulator` answer HTTP 501 to every
58
- request this table leaves them without an answer for:
74
+ `startClassicSimulator`, `startDsiotSimulator` and `startAirbaseSimulator`
75
+ answer HTTP 501 to every request this table leaves them without an answer
76
+ for:
59
77
 
60
78
  | Left out | What is unknown |
61
79
  | --- | --- |
@@ -66,6 +84,8 @@ request this table leaves them without an answer for:
66
84
  | A `/dsiot/multireq` body that is not JSON, has no `requests`, or carries an `op` other than 2 in a POST and 3 in a PUT | What an adapter answers. |
67
85
  | `setState` on a DSIOT simulator with a humidity percentage, or a fixed horizontal louver position: it throws an `Error` | The step a percentage is counted in, and which value each position is. Set the leaf in `leaves`. |
68
86
  | HTTPS | The certificate an adapter presents is issued by Daikin's own CA. A simulator started with a `key` reports `adp_kind=4`, an adapter that registers terminals over HTTP. |
87
+ | An Airbase endpoint without a typed method on `AirbaseDaikinClient`, and any method but GET | The reply fields. |
88
+ | Airbase `/aircon/get_zone_setting` and `set_zone_setting` on a unit without zones | What such a unit answers. |
69
89
 
70
90
  These answers are a simulator's own choice, where an adapter's is not known:
71
91
 
@@ -88,3 +108,8 @@ These answers are a simulator's own choice, where an adapter's is not known:
88
108
  | Still answers `f_dir` in `/aircon/get_control_info` on a classic simulator started with a `unit` whose `swing` is `none` | Whether a classic unit without a louver reports the field. |
89
109
  | Makes a request wait while it answers another | Whether an adapter also drops a connection while it is busy. |
90
110
  | Answers a discovery probe from UDP port 30050 of its address | The port an adapter answers from. |
111
+ | Answers HTTP 404 to every path outside `/skyfi`, on an Airbase simulator | Which paths besides `/common/basic_info` an Airbase adapter answers 404. |
112
+ | Answers `ret=PARAM NG` to an Airbase `/aircon/set_control_info` without `pow`, `mode`, `stemp`, `shum`, `f_rate`, `f_auto`, `f_airside` or `f_dir`, and to a `set_zone_setting` without `zone_name` or `zone_onoff` | Which requests an Airbase adapter answers `PARAM NG`, and whether it takes a write without one of them. |
113
+ | Keeps what an Airbase unit remembers for cool or for heat equal to what it holds, and names that mode in `bk_auto` once the unit is set to it | What an Airbase unit remembers on its own, and when it changes `bk_auto`. |
114
+ | Reports no `shum`, no `adp_kind` and no `adv` on an Airbase simulator, and `model=NOTSUPPORT` | Which of them an Airbase adapter reports, and what its `model` holds on each unit. |
115
+ | Holds the lists of an Airbase `get_zone_setting` with `%3B` for each `;`, and as many names as the unit has zones | The letter case an adapter sends, and whether it names more zones than `en_zone` counts. |
@@ -0,0 +1,64 @@
1
+ import type { DaikinCapabilities } from "../models/capabilities.js";
2
+ import type { DaikinDeviceInfo } from "../models/device.js";
3
+ import type { DaikinControl, DaikinSensors, DaikinState, DaikinStateAndSensors, SpecialMode } from "../models/state.js";
4
+ import type { DaikinZone } from "../models/zone.js";
5
+ import { type DaikinRequestOptions, type DaikinRetryOptions, type DaikinTransport } from "../transport.js";
6
+ import type { DaikinClient } from "./client.js";
7
+ /** Whether the adapter `transport` reaches is an Airbase adapter: one that
8
+ * answers HTTP 404 at `/common/basic_info`, where a classic adapter answers,
9
+ * and `ret=OK` at `/skyfi/common/basic_info`. It sends the second request
10
+ * only after that 404, and retries a refused or dropped connection as
11
+ * `withRetry` does by default: an adapter drops one while it answers another
12
+ * caller. It resolves with `false` when either request fails any other way.
13
+ * `discoverDevices` calls it, and `src/index.ts` does not export it. */
14
+ export declare function isAirbaseAdapter(transport: DaikinTransport): Promise<boolean>;
15
+ /** Airbase adapter (BRP15B61), on a ducted unit. Reads and writes are HTTP GET
16
+ * with query parameters, as on a classic adapter, at paths under `/skyfi`.
17
+ * It takes no terminal uuid and no local password. */
18
+ export declare class AirbaseDaikinClient implements DaikinClient {
19
+ private readonly transport;
20
+ private readonly retryOptions?;
21
+ private capabilityFields;
22
+ private readonly setStateCache;
23
+ /** `close` closes `transport`, so give each client a transport of its own.
24
+ * `maxStateAgeForSetStateInMs` defaults to 0: see
25
+ * `CreateDaikinClientOptions.maxStateAgeForSetStateInMs`. */
26
+ constructor(transport: DaikinTransport, retryOptions?: DaikinRetryOptions | undefined, maxStateAgeForSetStateInMs?: number);
27
+ close(): Promise<void>;
28
+ getDeviceInfo(): Promise<DaikinDeviceInfo>;
29
+ getCapabilities(): Promise<DaikinCapabilities>;
30
+ getState(): Promise<DaikinState>;
31
+ setState(control: DaikinControl): Promise<void>;
32
+ getSensors(): Promise<DaikinSensors>;
33
+ getStateAndSensors(): Promise<DaikinStateAndSensors>;
34
+ /** Rejects with a `DaikinError` whose status is 501: an Airbase adapter has
35
+ * no known request for a special mode. */
36
+ setSpecialMode(_mode: SpecialMode, _enabled: boolean): Promise<void>;
37
+ /** Whether fan auto is on: the unit then picks the fan speed itself, up to
38
+ * the one that is set. `null` on a unit that takes no fan auto. */
39
+ getFanAuto(): Promise<boolean | null>;
40
+ /** Switches fan auto on or off. It switches airside off, and keeps the fan
41
+ * speed. Check `getFanAuto` first: the client sends it to any unit. */
42
+ setFanAuto(enabled: boolean): Promise<void>;
43
+ /** Whether airside is on. `null` on a unit that takes no airside. */
44
+ getAirside(): Promise<boolean | null>;
45
+ /** Switches airside on or off. Switching it on switches fan auto off and
46
+ * sets the lowest fan speed. Check `getAirside` first: the client sends it
47
+ * to any unit. */
48
+ setAirside(enabled: boolean): Promise<void>;
49
+ /** The unit's zones, in the unit's order; empty on a unit without zones,
50
+ * which is not asked for any. */
51
+ getZones(): Promise<DaikinZone[]>;
52
+ /** Opens or closes the zone at `index` in `getZones`, counted from 0. It
53
+ * rejects with an `Error` for an index the unit has no zone at. */
54
+ setZone(index: number, enabled: boolean): Promise<void>;
55
+ /** Calls any endpoint by its path below `/skyfi`, such as
56
+ * `/aircon/get_control_info`, with query parameters, and returns the reply's
57
+ * `key=value` fields. It throws a `DaikinError` when the reply's `ret` is
58
+ * not `OK`. */
59
+ command(path: string, parameters?: Record<string, string>, { retry }?: DaikinRequestOptions): Promise<Record<string, string>>;
60
+ private readZoneSetting;
61
+ private readControlInfo;
62
+ private readCapabilityFields;
63
+ }
64
+ //# sourceMappingURL=airbase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"airbase.d.ts","sourceRoot":"","sources":["../../src/clients/airbase.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,WAAW,EACX,qBAAqB,EACrB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAErB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmBhD;;;;;;wEAMwE;AACxE,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,eAAe,GACzB,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;sDAEsD;AACtD,qBAAa,mBAAoB,YAAW,YAAY;IASpD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IARhC,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAE9C;;iEAE6D;IAC7D,YACmB,SAAS,EAAE,eAAe,EAC1B,YAAY,CAAC,EAAE,kBAAkB,YAAA,EAClD,0BAA0B,SAAI,EAG/B;IAED,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAErB;IAEK,aAAa,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAE/C;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,CAQpD;IAEK,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC,CAIzC;IAEK,kBAAkB,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAEzD;IAED;8CAC0C;IACpC,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzE;IAID;uEACmE;IAC7D,UAAU,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAI1C;IAED;2EACuE;IACjE,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhD;IAED,qEAAqE;IAC/D,UAAU,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAI1C;IAED;;sBAEkB;IACZ,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhD;IAED;qCACiC;IAC3B,QAAQ,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAMtC;IAED;uEACmE;IAC7D,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5D;IAED;;;mBAGe;IACT,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,CAqBjC;YAIa,eAAe;YAQf,eAAe;IAM7B,OAAO,CAAC,oBAAoB;CAY7B"}