daikin-local-control 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/DESIGN.md +52 -0
  2. package/LICENSE +21 -674
  3. package/README.md +45 -3
  4. package/UNSUPPORTED.md +10 -1
  5. package/dist/clients/classic.d.ts +22 -4
  6. package/dist/clients/classic.d.ts.map +1 -1
  7. package/dist/clients/classic.js +55 -8
  8. package/dist/clients/classic.js.map +1 -1
  9. package/dist/clients/client.d.ts +11 -2
  10. package/dist/clients/client.d.ts.map +1 -1
  11. package/dist/clients/dsiot.d.ts +10 -3
  12. package/dist/clients/dsiot.d.ts.map +1 -1
  13. package/dist/clients/dsiot.js +40 -5
  14. package/dist/clients/dsiot.js.map +1 -1
  15. package/dist/clients/setStateCache.d.ts +15 -0
  16. package/dist/clients/setStateCache.d.ts.map +1 -0
  17. package/dist/clients/setStateCache.js +26 -0
  18. package/dist/clients/setStateCache.js.map +1 -0
  19. package/dist/codecs/classic/capabilities.d.ts.map +1 -1
  20. package/dist/codecs/classic/capabilities.js +28 -1
  21. package/dist/codecs/classic/capabilities.js.map +1 -1
  22. package/dist/codecs/classic/extendedControl.d.ts +14 -1
  23. package/dist/codecs/classic/extendedControl.d.ts.map +1 -1
  24. package/dist/codecs/classic/extendedControl.js +39 -4
  25. package/dist/codecs/classic/extendedControl.js.map +1 -1
  26. package/dist/codecs/classic/keyValue.d.ts +3 -0
  27. package/dist/codecs/classic/keyValue.d.ts.map +1 -1
  28. package/dist/codecs/classic/keyValue.js +10 -0
  29. package/dist/codecs/classic/keyValue.js.map +1 -1
  30. package/dist/codecs/classic/state.d.ts +3 -2
  31. package/dist/codecs/classic/state.d.ts.map +1 -1
  32. package/dist/codecs/classic/state.js +14 -1
  33. package/dist/codecs/classic/state.js.map +1 -1
  34. package/dist/codecs/device.d.ts +10 -1
  35. package/dist/codecs/device.d.ts.map +1 -1
  36. package/dist/codecs/device.js +25 -10
  37. package/dist/codecs/device.js.map +1 -1
  38. package/dist/codecs/dsiot/capabilities.d.ts +4 -2
  39. package/dist/codecs/dsiot/capabilities.d.ts.map +1 -1
  40. package/dist/codecs/dsiot/capabilities.js +20 -4
  41. package/dist/codecs/dsiot/capabilities.js.map +1 -1
  42. package/dist/codecs/dsiot/device.d.ts +5 -0
  43. package/dist/codecs/dsiot/device.d.ts.map +1 -0
  44. package/dist/codecs/dsiot/device.js +14 -0
  45. package/dist/codecs/dsiot/device.js.map +1 -0
  46. package/dist/codecs/dsiot/multireq.d.ts +6 -4
  47. package/dist/codecs/dsiot/multireq.d.ts.map +1 -1
  48. package/dist/codecs/dsiot/multireq.js +9 -4
  49. package/dist/codecs/dsiot/multireq.js.map +1 -1
  50. package/dist/codecs/dsiot/state.d.ts +6 -2
  51. package/dist/codecs/dsiot/state.d.ts.map +1 -1
  52. package/dist/codecs/dsiot/state.js +12 -2
  53. package/dist/codecs/dsiot/state.js.map +1 -1
  54. package/dist/factory.d.ts +7 -0
  55. package/dist/factory.d.ts.map +1 -1
  56. package/dist/factory.js +2 -2
  57. package/dist/factory.js.map +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/models/capabilities.d.ts +5 -1
  63. package/dist/models/capabilities.d.ts.map +1 -1
  64. package/dist/models/device.d.ts +12 -1
  65. package/dist/models/device.d.ts.map +1 -1
  66. package/dist/models/extendedControl.d.ts +2 -3
  67. package/dist/models/extendedControl.d.ts.map +1 -1
  68. package/dist/models/extendedControl.js +0 -1
  69. package/dist/models/extendedControl.js.map +1 -1
  70. package/dist/models/state.d.ts +15 -4
  71. package/dist/models/state.d.ts.map +1 -1
  72. package/dist/models/state.js +1 -0
  73. package/dist/models/state.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/clients/classic.ts +76 -10
  76. package/src/clients/client.ts +13 -1
  77. package/src/clients/dsiot.ts +52 -9
  78. package/src/clients/setStateCache.ts +28 -0
  79. package/src/codecs/classic/capabilities.ts +39 -0
  80. package/src/codecs/classic/extendedControl.ts +56 -5
  81. package/src/codecs/classic/keyValue.ts +10 -0
  82. package/src/codecs/classic/state.ts +30 -12
  83. package/src/codecs/device.ts +35 -10
  84. package/src/codecs/dsiot/capabilities.ts +27 -2
  85. package/src/codecs/dsiot/device.ts +20 -0
  86. package/src/codecs/dsiot/multireq.ts +9 -4
  87. package/src/codecs/dsiot/state.ts +16 -1
  88. package/src/factory.ts +17 -2
  89. package/src/index.ts +1 -0
  90. package/src/models/capabilities.ts +5 -0
  91. package/src/models/device.ts +13 -1
  92. package/src/models/extendedControl.ts +2 -5
  93. package/src/models/state.ts +18 -4
@@ -1,4 +1,5 @@
1
- import type { DaikinDevice } from "../models/device.js";
1
+ import type { DaikinDevice, DaikinDeviceInfo } from "../models/device.js";
2
+ import { decodeClassicText } from "./classic/keyValue.js";
2
3
 
3
4
  const DSIOT_TYPE = "GPF";
4
5
  const MARVELL_ADP_KIND = "3";
@@ -15,7 +16,10 @@ export function parseDevice(
15
16
  return {
16
17
  host: basicInfo.api_port ? `${host}:${basicInfo.api_port}` : host,
17
18
  protocol,
18
- name: decodeName(basicInfo.name),
19
+ name:
20
+ basicInfo.name === undefined
21
+ ? undefined
22
+ : decodeClassicText(basicInfo.name),
19
23
  // Only classic adapters register terminals.
20
24
  requiresRegistration: secure && protocol === "classic",
21
25
  // Only a Marvell adapter (adp_kind=3) in secure mode speaks HTTPS; every
@@ -25,17 +29,38 @@ export function parseDevice(
25
29
  protocol === "classic" &&
26
30
  basicInfo.adp_kind === MARVELL_ADP_KIND,
27
31
  requiresLocalPassword: basicInfo.lpw_flag === "1",
32
+ ...parseDeviceInfo(basicInfo),
28
33
  };
29
34
  }
30
35
 
31
- // The name is form-encoded: `+` is a space.
32
- function decodeName(value: string | undefined): string | undefined {
33
- if (value === undefined) {
36
+ /** Reads what a `basic_info` reply says to identify the adapter. */
37
+ export function parseDeviceInfo(
38
+ basicInfo: Record<string, string>,
39
+ ): DaikinDeviceInfo {
40
+ return {
41
+ macAddress: decodeMacAddress(basicInfo.mac),
42
+ firmwareVersion: decodeFirmwareVersion(basicInfo.ver),
43
+ };
44
+ }
45
+
46
+ /** The adapter sends a MAC address without separators. It keeps the adapter's
47
+ * letter case. */
48
+ export function decodeMacAddress(
49
+ value: string | undefined,
50
+ ): string | undefined {
51
+ return value ? value.replace(/..(?=..)/g, "$&:") : undefined;
52
+ }
53
+
54
+ /** The adapter sends a version as three numbers joined by underscores, with a
55
+ * fourth part, `pre`, on a pre-release. This drops any other fourth part, and
56
+ * decodes a value that is not three numbers to `undefined`. */
57
+ export function decodeFirmwareVersion(
58
+ value: string | undefined,
59
+ ): string | undefined {
60
+ const parts = /^(\d+)_(\d+)_(\d+)(?:_([^_]*))?$/.exec(value ?? "");
61
+ if (parts === null) {
34
62
  return undefined;
35
63
  }
36
- try {
37
- return decodeURIComponent(value.replaceAll("+", " "));
38
- } catch {
39
- return value;
40
- }
64
+ const version = parts.slice(1, 4).map(Number).join(".");
65
+ return parts[4] === "pre" ? `${version}.pre` : version;
41
66
  }
@@ -11,6 +11,7 @@ import {
11
11
  HUMIDITY_SETTINGS,
12
12
  OPERATING_MODES,
13
13
  type OperatingMode,
14
+ SPECIAL_MODES,
14
15
  SWING_MODES,
15
16
  type SwingMode,
16
17
  VERTICAL_POSITIONS,
@@ -20,6 +21,7 @@ import {
20
21
  DSIOT_HORIZONTAL_POSITION_VALUES,
21
22
  DSIOT_MODE_PATHS,
22
23
  DSIOT_PATHS,
24
+ DSIOT_SPECIAL_MODE_PATHS,
23
25
  DSIOT_VERTICAL_POSITION_VALUES,
24
26
  type DsiotSetpoint,
25
27
  decodeDsiotLittleEndian,
@@ -35,6 +37,9 @@ const ADAPTER_PATHS = {
35
37
  scheduleTimer: "adp_i.func.en_scdltmr",
36
38
  } as const;
37
39
 
40
+ // The `pt` of a special mode's leaf on a unit that takes the mode.
41
+ const SPECIAL_MODE_OFFERED_TYPE = "2";
42
+
38
43
  const COOLING_ONLY_MODEL_TYPE = "01";
39
44
  const COOLING_ONLY_MODES: readonly OperatingMode[] = [
40
45
  "dehumidify",
@@ -48,8 +53,10 @@ const FIXED_POSITIONS: readonly string[] = FIXED_LOUVER_POSITIONS;
48
53
 
49
54
  type LouverAxis = "vertical" | "horizontal";
50
55
 
51
- /** Turns the flattened `adp_i` and indoor `dgc_status` trees (with `md`) into
52
- * `DaikinCapabilities`. Setpoints count half degrees. */
56
+ /** Turns the flattened `adp_i` tree and the indoor and outdoor `dgc_status`
57
+ * trees (with `pt` and `md`) into `DaikinCapabilities`. Setpoints count half
58
+ * degrees. Without the outdoor tree, `specialModes` lists no `powerful`. It
59
+ * lists econo in every mode but fan: see UNSUPPORTED.md. */
53
60
  export function parseDsiotCapabilities(
54
61
  values: Record<string, string>,
55
62
  ): DaikinCapabilities {
@@ -66,6 +73,17 @@ export function parseDsiotCapabilities(
66
73
  ? [...HUMIDITY_SETTINGS]
67
74
  : [];
68
75
  };
76
+ const takenSpecialModes = SPECIAL_MODES.filter(
77
+ (specialMode) =>
78
+ values[`${DSIOT_SPECIAL_MODE_PATHS[specialMode]}.pt`] ===
79
+ SPECIAL_MODE_OFFERED_TYPE,
80
+ );
81
+ const specialModes = (mode: OperatingMode) =>
82
+ modes.includes(mode)
83
+ ? takenSpecialModes.filter(
84
+ (specialMode) => mode !== "fan" || specialMode !== "econo",
85
+ )
86
+ : [];
69
87
  const vertical = hasLouver(values, "vertical");
70
88
  const horizontal = hasLouver(values, "horizontal");
71
89
  const energyMonitoring =
@@ -105,6 +123,13 @@ export function parseDsiotCapabilities(
105
123
  heat: humiditySettings("heat"),
106
124
  fan: [],
107
125
  },
126
+ specialModes: {
127
+ auto: specialModes("auto"),
128
+ dehumidify: specialModes("dehumidify"),
129
+ cool: specialModes("cool"),
130
+ heat: specialModes("heat"),
131
+ fan: specialModes("fan"),
132
+ },
108
133
  // Which leaf reports the special air outlet is not known: see
109
134
  // UNSUPPORTED.md.
110
135
  specialOutlet: false,
@@ -0,0 +1,20 @@
1
+ import type { DaikinDeviceInfo } from "../../models/device.js";
2
+ import { decodeFirmwareVersion, decodeMacAddress } from "../device.js";
3
+
4
+ const ADAPTER_PATHS = {
5
+ macAddress: "adp_i.mac",
6
+ firmwareVersion: "adp_i.ver",
7
+ } as const;
8
+
9
+ /** Reads what the flattened `adp_i` tree says to identify the adapter. Both
10
+ * leaves hold the value a classic adapter sends in `basic_info`. */
11
+ export function parseDsiotDeviceInfo(
12
+ values: Record<string, string>,
13
+ ): DaikinDeviceInfo {
14
+ return {
15
+ macAddress: decodeMacAddress(values[ADAPTER_PATHS.macAddress]),
16
+ firmwareVersion: decodeFirmwareVersion(
17
+ values[ADAPTER_PATHS.firmwareVersion],
18
+ ),
19
+ };
20
+ }
@@ -20,11 +20,12 @@ export const DSIOT_EDGES = {
20
20
  const EDGE_ROOT = "/dsiot/edge";
21
21
 
22
22
  /** What a read fetches, as the `to` of a request: whole trees, never single
23
- * leaves. */
23
+ * leaves. The status trees come with `pt`, which `parseDsiotCapabilities`
24
+ * reads. */
24
25
  export const DSIOT_TARGETS = {
25
26
  adapterInfo: `${EDGE_ROOT}.adp_i?filter=pv`,
26
- indoorStatus: `${EDGE_ROOT}/${DSIOT_EDGES.indoor}.dgc_status?filter=pv,md`,
27
- outdoorStatus: `${EDGE_ROOT}/${DSIOT_EDGES.outdoor}.dgc_status?filter=pv,md`,
27
+ indoorStatus: `${EDGE_ROOT}/${DSIOT_EDGES.indoor}.dgc_status?filter=pv,pt,md`,
28
+ outdoorStatus: `${EDGE_ROOT}/${DSIOT_EDGES.outdoor}.dgc_status?filter=pv,pt,md`,
28
29
  } as const;
29
30
 
30
31
  interface PropertyNode {
@@ -92,7 +93,8 @@ function childNamed(siblings: PropertyNode[], name: string): PropertyNode {
92
93
  }
93
94
 
94
95
  /** Flattens a `/dsiot/multireq` reply into dotted paths -> `pv`, plus
95
- * `<path>.md.mi` / `.md.mx` / `.md.st` where metadata came back.
96
+ * `<path>.pt` where a leaf's type came back and `<path>.md.mi` / `.md.mx` /
97
+ * `.md.st` where metadata did.
96
98
  *
97
99
  * Each of its `responses` carries its own `rsc`; any not starting with 200 is
98
100
  * the adapter refusing, and throws a `DaikinError`: status 401 for `rsc` 401
@@ -149,6 +151,9 @@ function flatten(
149
151
  if (node.pv !== undefined) {
150
152
  values[path] = stringify(node.pv);
151
153
  }
154
+ if (node.pt !== undefined) {
155
+ values[`${path}.pt`] = stringify(node.pt);
156
+ }
152
157
  for (const key of ["mi", "mx", "st"] as const) {
153
158
  const metadata = node.md?.[key];
154
159
  if (metadata !== undefined && metadata !== null) {
@@ -8,6 +8,8 @@ import {
8
8
  type HorizontalPosition,
9
9
  type HumiditySetting,
10
10
  type OperatingMode,
11
+ SPECIAL_MODES,
12
+ type SpecialMode,
11
13
  type SwingMode,
12
14
  type TargetHumidity,
13
15
  VERTICAL_POSITIONS,
@@ -30,6 +32,15 @@ export const DSIOT_PATHS = {
30
32
  energyMonitoring: "dgc_status.e_1002.e_A011.p_01",
31
33
  } as const;
32
34
 
35
+ /** Each leaf holds `01` while its mode is on. Powerful is on the outdoor
36
+ * edge. */
37
+ export const DSIOT_SPECIAL_MODE_PATHS: Record<SpecialMode, string> = {
38
+ powerful: "dgc_status.e_1003.e_3002.p_44",
39
+ econo: "dgc_status.e_1002.e_3003.p_24",
40
+ streamer: "dgc_status.e_1002.e_3003.p_20",
41
+ };
42
+ const SPECIAL_MODE_ON = "01";
43
+
33
44
  export interface DsiotSetpoint {
34
45
  path: string;
35
46
  /** Auto and dehumidify are a signed byte; cool and heat are little-endian. */
@@ -476,7 +487,8 @@ const POWER_TRANSITIONS = {
476
487
  /** Turns the flattened indoor `dgc_status` tree, read with `md`, into a
477
488
  * `DaikinState`. Every value is per mode: the setpoint, humidity, fan speed and
478
489
  * louvers each have a leaf for each mode, and the state reads the current
479
- * mode's. */
490
+ * mode's. Powerful is read from the outdoor tree, and reads as off without
491
+ * it. */
480
492
  export function parseDsiotState(values: Record<string, string>): DaikinState {
481
493
  const mode = decodeDsiotMode(values[DSIOT_PATHS.mode]) ?? "auto";
482
494
  const paths = DSIOT_MODE_PATHS[mode];
@@ -491,6 +503,9 @@ export function parseDsiotState(values: Record<string, string>): DaikinState {
491
503
  swingMode: decodeSwing(values[paths.vertical], values[paths.horizontal]),
492
504
  verticalPosition: parseVerticalPosition(values, paths.vertical),
493
505
  horizontalPosition: parseHorizontalPosition(values[paths.horizontal]),
506
+ specialModes: SPECIAL_MODES.filter(
507
+ (mode) => values[DSIOT_SPECIAL_MODE_PATHS[mode]] === SPECIAL_MODE_ON,
508
+ ),
494
509
  };
495
510
  }
496
511
 
package/src/factory.ts CHANGED
@@ -26,6 +26,13 @@ export interface CreateDaikinClientOptions {
26
26
  * waits of 0, 50, 100, 200 and 400 milliseconds: see
27
27
  * `DaikinRetryOptions.delaysInMs`. */
28
28
  retry?: DaikinRetryOptions;
29
+ /** How old, in milliseconds, a reply to `getState` or `getStateAndSensors`
30
+ * may be for `setState` to build its write from it instead of reading the
31
+ * unit first. Defaults to 0: `setState` always reads first. It changes no
32
+ * read: `getState` asks the unit every time. The write undoes a change made
33
+ * on the remote control since that reply: see
34
+ * DESIGN.md#the-read-before-a-write. */
35
+ maxStateAgeForSetStateInMs?: number;
29
36
  /** HTTPS adapters only. Without a CA the adapter's certificate isn't verified. */
30
37
  tls?: DaikinTlsOptions;
31
38
  }
@@ -49,8 +56,16 @@ export function createDaikinClient(
49
56
  });
50
57
 
51
58
  return device.protocol === "dsiot"
52
- ? new DsiotDaikinClient(transport, options.retry)
53
- : new ClassicDaikinClient(transport, options.retry);
59
+ ? new DsiotDaikinClient(
60
+ transport,
61
+ options.retry,
62
+ options.maxStateAgeForSetStateInMs,
63
+ )
64
+ : new ClassicDaikinClient(
65
+ transport,
66
+ options.retry,
67
+ options.maxStateAgeForSetStateInMs,
68
+ );
54
69
  }
55
70
 
56
71
  function localPasswordAuthentication(
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./codecs/classic/state.js";
9
9
  export * from "./codecs/credentials.js";
10
10
  export * from "./codecs/device.js";
11
11
  export * from "./codecs/dsiot/capabilities.js";
12
+ export * from "./codecs/dsiot/device.js";
12
13
  export * from "./codecs/dsiot/multireq.js";
13
14
  export * from "./codecs/dsiot/state.js";
14
15
  export * from "./discovery.js";
@@ -2,6 +2,7 @@ import type {
2
2
  FanSpeed,
3
3
  HorizontalPosition,
4
4
  OperatingMode,
5
+ SpecialMode,
5
6
  SwingMode,
6
7
  TargetHumidity,
7
8
  VerticalPosition,
@@ -41,6 +42,10 @@ export interface DaikinCapabilities {
41
42
  /** Humidity settings per mode; empty where the mode has none or the unit does
42
43
  * not offer it. A percentage is listed for no unit: see UNSUPPORTED.md. */
43
44
  targetHumidities: Readonly<Record<OperatingMode, readonly TargetHumidity[]>>;
45
+ /** Special modes per mode; empty where the unit takes none or does not offer
46
+ * the mode. `getCapabilities` lists econo in every mode but fan: see
47
+ * UNSUPPORTED.md. */
48
+ specialModes: Readonly<Record<OperatingMode, readonly SpecialMode[]>>;
44
49
  /** Whether the unit has the special air outlet, which heat can swing
45
50
  * through. That swing is not modelled: see UNSUPPORTED.md. */
46
51
  specialOutlet: boolean;
@@ -5,11 +5,23 @@ export const DAIKIN_PROTOCOLS = ["classic", "dsiot"] as const;
5
5
  * a CGI adapter such as the BRP069 or BRP072, `dsiot` for a BRP084. */
6
6
  export type DaikinProtocol = (typeof DAIKIN_PROTOCOLS)[number];
7
7
 
8
+ /** What an adapter reports to identify itself. `discoverDevices` fills it into
9
+ * each `DaikinDevice`, and `DaikinClient.getDeviceInfo` reads it for a device
10
+ * written by hand. */
11
+ export interface DaikinDeviceInfo {
12
+ /** In pairs joined by colons, in the letter case the adapter sends, such as
13
+ * `A4:08:EA:CC:91:D4`. Left out when the adapter reports none. */
14
+ macAddress?: string;
15
+ /** The adapter's firmware, such as `1.2.51`, or `1.2.51.pre` for a
16
+ * pre-release. Left out when the adapter reports none. */
17
+ firmwareVersion?: string;
18
+ }
19
+
8
20
  /** An adapter on the LAN, as `discoverDevices` finds it or as a caller writes
9
21
  * it by hand, carrying what `createDaikinClient` needs to reach it. A flag left
10
22
  * out means `false`, so a device written by hand needs only `host` and
11
23
  * `protocol`. */
12
- export interface DaikinDevice {
24
+ export interface DaikinDevice extends DaikinDeviceInfo {
13
25
  /** Address, with `:port` when the adapter announces one. */
14
26
  host: string;
15
27
  protocol: DaikinProtocol;
@@ -22,17 +22,14 @@ export interface DemandControl {
22
22
  mode: DemandMode;
23
23
  }
24
24
 
25
- export const SPECIAL_MODES = ["powerful", "econo", "streamer"] as const;
26
-
27
- export type SpecialMode = (typeof SPECIAL_MODES)[number];
28
-
29
25
  export interface DeviceGroup {
30
26
  name: string;
31
27
  /** Whether the unit is in the group. */
32
28
  enabled: boolean;
33
29
  }
34
30
 
35
- /** A field left out is not written. */
31
+ /** `setMetadata` writes no field that is left out, and `getMetadata` leaves
32
+ * out a field the adapter does not report. */
36
33
  export interface DeviceMetadata {
37
34
  name?: string;
38
35
  /** The adapter's own code for the icon: see UNSUPPORTED.md. */
@@ -78,6 +78,10 @@ export type HumiditySetting = (typeof HUMIDITY_SETTINGS)[number];
78
78
  * `DaikinCapabilities.targetHumidities` lists which of them a unit takes. */
79
79
  export type TargetHumidity = HumiditySetting | { percent: number };
80
80
 
81
+ export const SPECIAL_MODES = ["powerful", "econo", "streamer"] as const;
82
+
83
+ export type SpecialMode = (typeof SPECIAL_MODES)[number];
84
+
81
85
  export interface DaikinState {
82
86
  power: boolean;
83
87
  mode: OperatingMode;
@@ -93,14 +97,19 @@ export interface DaikinState {
93
97
  /** On a 3D unit. `null` where the unit reports none, or one outside
94
98
  * `HorizontalPosition`. */
95
99
  horizontalPosition: HorizontalPosition | null;
100
+ /** The special modes that are on, in the order of `SPECIAL_MODES`; empty
101
+ * when none is. `setState` does not take it: see `DaikinControl`. */
102
+ specialModes: SpecialMode[];
96
103
  }
97
104
 
98
105
  /** A partial `DaikinState`, as `DaikinClient.setState` takes it: set only the
99
- * fields to change. The client reads the current state, overlays the control,
100
- * and writes the whole state back. It drops a field the unit does not take in
106
+ * fields to change. The client overlays the control on the state the unit
107
+ * holds, and writes the whole state back. It drops a field the unit does not take in
101
108
  * the target mode, such as a louver position outside
102
- * `DaikinCapabilities.verticalPositions`. */
103
- export type DaikinControl = Partial<DaikinState>;
109
+ * `DaikinCapabilities.verticalPositions`. It takes no `specialModes`: a
110
+ * classic adapter is set one with `ClassicDaikinClient.setSpecialMode`, and
111
+ * UNSUPPORTED.md says what is not known about the rest. */
112
+ export type DaikinControl = Partial<Omit<DaikinState, "specialModes">>;
104
113
 
105
114
  /** A reading is `null` when the unit does not report it. */
106
115
  export interface DaikinSensors {
@@ -108,3 +117,8 @@ export interface DaikinSensors {
108
117
  outdoorTemperatureInC: number | null;
109
118
  indoorHumidityInPercent: number | null;
110
119
  }
120
+
121
+ export interface DaikinStateAndSensors {
122
+ state: DaikinState;
123
+ sensors: DaikinSensors;
124
+ }