matterbridge-daikin-local 0.3.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.
- package/CHANGELOG.md +24 -0
- package/README.md +112 -22
- package/apps/frontend/build/app.js +2 -2
- package/apps/frontend/build/app.js.map +3 -3
- package/dist/accessory/daikinAccessory.js +32 -7
- package/dist/accessory/endpoints.js +28 -6
- package/dist/accessory/layout.js +5 -1
- package/dist/accessory/matterState.js +9 -2
- package/dist/config.js +10 -2
- package/dist/controls.js +4 -2
- package/dist/mapping.js +12 -1
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.5.0] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- [devices]: Add the option of a mode select for the mode instead of the dry and fan mode switches, which picks Off or any mode of the unit as the thermostat mode does, on a unit with a dry or a fan mode. The swing can be a mode select instead of the switches too. No preset adds them: few Matter apps show a mode select. Home Assistant does, Apple Home does not.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- [sensors]: Keep the humidity on the air conditioner when it is also on a humidity sensor of its own, as the room temperature is, so that the sensor is an addition for the Matter apps that show no humidity on an air conditioner.
|
|
16
|
+
- [sensors]: Make the outdoor temperature sensor a device of its own even when the air conditioner is a composed device, so that it can be put in another room of a Matter app instead of reporting the outdoor temperature for the room of the air conditioner. On an air conditioner that had the sensor as a part, the sensor comes back as a new device.
|
|
17
|
+
|
|
18
|
+
## [0.4.0] - 2026-10-14
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- [devices]: Expose each air conditioner as one composed device in Matter: its switches, a fan of its own and its sensors are parts of the air conditioner, and move between the rooms of a Matter app with it. Each switch is tagged with a number and with what it is. The setup page can make each of them a device of its own instead, as before.
|
|
23
|
+
- [sensors]: Add the option to show the humidity on a humidity sensor of its own instead of on the air conditioner, for Matter apps that show no humidity there. The Apple Home preset turns it on.
|
|
24
|
+
- [sensors]: Add the option to show the outdoor temperature on a sensor. It is off unless you or the Full preset turn it on, because a Matter app may take it for the temperature of the room.
|
|
25
|
+
- [readme]: Add diagrams of how an air conditioner is built in Matter, and the device types it uses.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- [devices]: A device stored by an earlier version is a composed device without an outdoor temperature sensor, and is built again in Matter. A Matter app may take its switches, its fan and its sensors for new devices.
|
|
30
|
+
|
|
7
31
|
## [0.3.0] - 2026-10-13
|
|
8
32
|
|
|
9
33
|
### Changed
|
package/README.md
CHANGED
|
@@ -102,21 +102,65 @@ The plugin presents itself to every adapter with a **Terminal ID**, which it gen
|
|
|
102
102
|
| Thermostat mode | Off, Cool, Heat, Auto, Dry (dehumidify) and Fan only, for the modes the unit has. Not every Matter app offers Dry and Fan only |
|
|
103
103
|
| Target temperature | The setpoint of the current mode, within the limits and the step the unit reports. In Auto the unit has one setpoint, which both the heating and the cooling setpoint show |
|
|
104
104
|
| Current temperature | The indoor temperature the unit measures |
|
|
105
|
-
| Humidity | The indoor humidity, on the air conditioner
|
|
106
|
-
| Outdoor temperature | A temperature sensor named `<name> Outdoor Temperature`, for a unit that gives it: see below
|
|
105
|
+
| Humidity | The indoor humidity, on the air conditioner and, when you add one, on a sensor of its own, for a unit that gives it: see below |
|
|
106
|
+
| Outdoor temperature | A temperature sensor named `<name> Outdoor Temperature`, when you turn it on, for a unit that gives it: see below |
|
|
107
107
|
| Fan speed | Your choice: see below |
|
|
108
108
|
| Swing | Your choice: see below |
|
|
109
109
|
|
|
110
110
|
The adapter does not report whether the compressor is running, so a unit that is on is shown as cooling or heating according to its mode.
|
|
111
111
|
|
|
112
|
+
### How it is built in Matter
|
|
113
|
+
|
|
114
|
+
Matter describes a device as endpoints. Each endpoint has one or more device types, which say what it is, and clusters, which hold its attributes and commands. Each air conditioner is one device: an endpoint for the air conditioner, with every control or sensor that has no place on it as a part of it, on an endpoint of its own. The outdoor temperature sensor is the exception, and is always a device of its own:
|
|
115
|
+
|
|
116
|
+
```mermaid
|
|
117
|
+
flowchart TD
|
|
118
|
+
bridge["Matterbridge<br/>Aggregator"]
|
|
119
|
+
subgraph device["One device in a Matter app"]
|
|
120
|
+
ac["<b>Living Room</b><br/>Room Air Conditioner, Bridged Node, Power Source<br/>On/Off, Thermostat<br/>Fan Control when it is here, and Relative Humidity once the unit gives it"]
|
|
121
|
+
fan["Fan<br/>Fan Control"]
|
|
122
|
+
switches["On/Off Plug-in Unit, one for each switch<br/>tagged 1 Vertical Swing, 2 Horizontal Swing and so on<br/>On/Off"]
|
|
123
|
+
selects["Mode Select, one for the mode and one for the swing<br/>tagged 1 Mode, 2 Swing<br/>Mode Select"]
|
|
124
|
+
humidity["Humidity Sensor<br/>Relative Humidity"]
|
|
125
|
+
end
|
|
126
|
+
outdoor["<b>Living Room Outdoor Temperature</b><br/>Temperature Sensor, Bridged Node, Power Source<br/>Temperature Measurement"]
|
|
127
|
+
bridge --> ac
|
|
128
|
+
ac -.-> fan
|
|
129
|
+
ac -.-> switches
|
|
130
|
+
ac -.-> selects
|
|
131
|
+
ac -.-> humidity
|
|
132
|
+
bridge -.-> outdoor
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A solid line is always there. A dashed one is there when you choose it in the setup, and for a sensor once the unit gives its reading. The air conditioner also has the Bridged Node and Power Source device types, with the Identify, Bridged Device Basic Information and Power Source clusters: the name and the serial number of the device, whether it is reachable, and that it runs on mains power. Its parts have none of these, because in Matter they belong to the device. The outdoor temperature sensor has them all, with a name and a serial number of its own, so that you can put it in another room of your Matter app, such as outside, and it does not report the outdoor temperature for the room of the air conditioner.
|
|
136
|
+
|
|
137
|
+
| Device type | ID | Stands for |
|
|
138
|
+
| -------------------- | ------ | ----------------------------------------------------- |
|
|
139
|
+
| Aggregator | 0x000E | Matterbridge, which holds the devices it bridges |
|
|
140
|
+
| Bridged Node | 0x0013 | Each device: its name, serial number and reachability |
|
|
141
|
+
| Power Source | 0x0011 | Each device: mains powered |
|
|
142
|
+
| Room Air Conditioner | 0x0072 | The air conditioner |
|
|
143
|
+
| Fan | 0x002B | A fan of its own |
|
|
144
|
+
| On/Off Plug-in Unit | 0x010A | Each switch |
|
|
145
|
+
| Mode Select | 0x0027 | The mode select of the mode, and that of the swing |
|
|
146
|
+
| Humidity Sensor | 0x0307 | A humidity sensor of its own |
|
|
147
|
+
| Temperature Sensor | 0x0302 | The outdoor temperature |
|
|
148
|
+
|
|
149
|
+
The setup can also make each part a device of its own: see [One device or several](#one-device-or-several). The names below, such as `<name> Fan`, are those of the parts as devices of their own. As parts they have no name in Matter: a switch or a mode select is tagged with what it is, such as `Vertical Swing`, and a Matter app shows each part in its own way.
|
|
150
|
+
|
|
112
151
|
### Room temperature, humidity and outdoor temperature
|
|
113
152
|
|
|
114
|
-
The
|
|
153
|
+
The room temperature and the humidity are always on the air conditioner. You choose whether the humidity also gets a sensor of its own, and whether the outdoor temperature gets a sensor, and each appears once the unit gives it.
|
|
154
|
+
|
|
155
|
+
| Reading | In Matter |
|
|
156
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
+
| Humidity | On the air conditioner, beside the room temperature. You can add a humidity sensor of its own, named `<name> Humidity`, for Matter apps that show no humidity on an air conditioner. It shows the same humidity, which stays on the air conditioner for the apps that do |
|
|
158
|
+
| Outdoor temperature | None, unless you turn on a temperature sensor named `<name> Outdoor Temperature`. It is a device of its own, never on the air conditioner or a part of it, so that you can put it in another room, such as outside. It is off by default because a Matter app puts a new device in a room of its choosing, which may show the outdoor temperature as its own until you move the sensor |
|
|
115
159
|
|
|
116
|
-
- **No adapter says which readings its unit has**, so the plugin goes by what the unit reads. The humidity and the outdoor temperature get their place in Matter the first time the unit gives them: at once for a unit that gave them during setup, and otherwise the air conditioner is built again in Matter when the reading first comes.
|
|
160
|
+
- **No adapter says which readings its unit has**, so the plugin goes by what the unit reads. The humidity and the outdoor temperature get their place in Matter the first time the unit gives them: at once for a unit that gave them during setup, and otherwise the air conditioner is built again in Matter when the reading first comes. An outdoor temperature with its sensor turned off builds nothing.
|
|
117
161
|
- **A reading keeps its place from then on.** Some units give no outdoor temperature while they are idle, and a sensor that came and went would be a device that comes and goes in your Matter apps.
|
|
118
162
|
- **A reading the unit does not give right now shows no value**, instead of the last one it gave. This holds for all three, the room temperature included.
|
|
119
|
-
- **
|
|
163
|
+
- **Apple Home is reported to show a humidity only on a sensor of its own**, and not on an air conditioner: see the [Home Assistant Matter Hub notes](https://riddix.github.io/home-assistant-matter-hub/devices/temperature-humidity-sensor). The Apple Home preset adds that sensor for that reason. It has not been tested with this plugin.
|
|
120
164
|
|
|
121
165
|
### Fan speed
|
|
122
166
|
|
|
@@ -128,16 +172,17 @@ The three readings are not settings: each appears when the unit gives it.
|
|
|
128
172
|
|
|
129
173
|
An air conditioner has no fan-off of its own, so a Matter app turning the fan to off or 0 is ignored and the fan control goes back to the speed the unit holds. To let the unit pick the speed, choose the Auto fan mode.
|
|
130
174
|
|
|
131
|
-
The fan speed is a control of the air conditioner itself. For Matter apps that show no fan speed on an air conditioner, the setup can
|
|
175
|
+
The fan speed is a control of the air conditioner itself. For Matter apps that show no fan speed on an air conditioner, or do not show it well, the setup can move it to a fan of its own instead, named `<name> Fan`, as steps or as a percentage. The air conditioner then has no fan speed, so an app does not show two. No preset does this. The fan behaves the same in either place: it shows the speed the unit holds, also while the unit is off, and it cannot be turned off.
|
|
132
176
|
|
|
133
177
|
For Matter apps that show no Auto fan mode, the setup can add a switch named `<name> Auto Fan` on a unit that has an auto fan speed. On sets the fan to auto. Off puts it back at the speed it last ran at, or at a middle speed when the plugin never saw it at one. The switch is not offered while the fan is hidden with a fixed speed, because that speed is sent with every command.
|
|
134
178
|
|
|
135
179
|
### Swing
|
|
136
180
|
|
|
137
|
-
| Option
|
|
138
|
-
|
|
|
139
|
-
| Switches
|
|
140
|
-
|
|
|
181
|
+
| Option | In Matter |
|
|
182
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| Switches | One on/off switch for each axis the unit swings on, named `<name> Vertical Swing` and `<name> Horizontal Swing`. A unit that swings on one axis gets one switch. They appear as outlets, the on/off device type every Matter app supports |
|
|
184
|
+
| Mode select | One mode select named `<name> Swing`, with Off, Vertical, Horizontal and Both for the swing modes the unit has. Few Matter apps show it: see [Mode selects](#mode-selects) |
|
|
185
|
+
| Not shown | No swing control. The swing either stays as it is, or is set to the mode you pick each time the plugin sends a command |
|
|
141
186
|
|
|
142
187
|
Fixed louver positions are not controlled from Matter.
|
|
143
188
|
|
|
@@ -150,7 +195,20 @@ Dry and Fan only are always among the thermostat modes of the air conditioner, b
|
|
|
150
195
|
| Turned on | Turns the unit on in that mode |
|
|
151
196
|
| Turned off | Puts the unit back in Cool, or in Heat on a unit that only heats. The unit stays on: turn it off from the air conditioner |
|
|
152
197
|
|
|
153
|
-
A switch is on while the unit is on and in its mode, whichever way it got there.
|
|
198
|
+
A switch is on while the unit is on and in its mode, whichever way it got there. A mode select can take the place of the two switches: see [Mode selects](#mode-selects).
|
|
199
|
+
|
|
200
|
+
### Mode selects
|
|
201
|
+
|
|
202
|
+
A mode select is the Matter device type that picks one value from a list, as a drop-down does. The setup can add one for the mode, named `<name> Mode`, on a unit with a dry or a fan mode, and one for the swing, named `<name> Swing`, as a choice under [Swing](#swing). Each lists only what the unit has, and takes the place of the switches: the mode select those of Dry and Fan only, the swing select those of the swing. No preset adds them.
|
|
203
|
+
|
|
204
|
+
| Mode select | Lists | Picking a value |
|
|
205
|
+
| ----------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
206
|
+
| Mode | Off, Auto, Cool, Heat, Dry and Fan Only, as the thermostat mode has them | Off turns the unit off, and any other mode turns it on in that mode. The select shows Off while the unit is off, as the thermostat mode does |
|
|
207
|
+
| Swing | Off, Vertical, Horizontal and Both | Sets the swing |
|
|
208
|
+
|
|
209
|
+
**Few Matter apps show a mode select.** Home Assistant shows each one as a select among the settings of the device. Apple Home is reported not to show them, and Google Home, Amazon Alexa and SmartThings do not list them among the devices they support. This has not been tested with this plugin. In the other apps, the switches are the way to reach these controls.
|
|
210
|
+
|
|
211
|
+
The thermostat mode of the air conditioner follows the unit, whichever control changed it: picking Dry or Fan Only on the mode select shows Dry or Fan Only there, and Auto on a unit that only cools or only heats shows Cool or Heat, because Matter has Auto only with both. How an app without Dry or Fan Only, such as Apple Home, shows the air conditioner in those modes is up to the app, as it is with the switches.
|
|
154
212
|
|
|
155
213
|
### Special modes
|
|
156
214
|
|
|
@@ -163,14 +221,46 @@ Matter has no control for Powerful, Econo and Streamer, so the setup can add a s
|
|
|
163
221
|
|
|
164
222
|
In the Options step, a preset fills in every choice above for what the unit can do. You can change any of them afterwards.
|
|
165
223
|
|
|
166
|
-
| Preset | Fan speed | Swing | Dry mode switch | Fan mode switch | Auto fan switch | Special mode switches |
|
|
167
|
-
| ----------- | ---------- | --------- | --------------- | --------------- | --------------- | --------------------- |
|
|
168
|
-
| Apple Home | Percentage | Switches | Yes | Yes | Yes | None |
|
|
169
|
-
| Google Home | Steps | Switches | Yes | No | No | None |
|
|
170
|
-
| Full | Steps | Switches | Yes | Yes | Yes | All |
|
|
171
|
-
| Minimal | Not shown | Not shown | No | No | No | None |
|
|
224
|
+
| Preset | Fan speed | Swing | Dry mode switch | Fan mode switch | Auto fan switch | Special mode switches | Humidity | Outdoor sensor |
|
|
225
|
+
| ----------- | ---------- | --------- | --------------- | --------------- | --------------- | --------------------- | --------------------------- | -------------- |
|
|
226
|
+
| Apple Home | Percentage | Switches | Yes | Yes | Yes | None | Also on a sensor of its own | No |
|
|
227
|
+
| Google Home | Steps | Switches | Yes | No | No | None | On the air conditioner | No |
|
|
228
|
+
| Full | Steps | Switches | Yes | Yes | Yes | All | On the air conditioner | Yes |
|
|
229
|
+
| Minimal | Not shown | Not shown | No | No | No | None | On the air conditioner | No |
|
|
230
|
+
|
|
231
|
+
The Apple Home and Google Home presets are starting points, not tested recommendations. They add switches and sensors for what each app is not known to show: Home's climate controls have Off, Heat, Cool and Auto only and are reported to show no humidity, and Google Home is reported to show Fan only but not Dry.
|
|
232
|
+
|
|
233
|
+
### One device or several
|
|
234
|
+
|
|
235
|
+
The switches, the mode selects, a fan of its own and a humidity sensor of its own are parts of the air conditioner, which Matter calls a composed device, so they move between rooms with it. The outdoor temperature sensor is a device of its own either way, so that you can put it in another room than the air conditioner. The setup can make each of them a device of its own instead, beside the air conditioner. A Matter app then does not know that they belong together, so each is put in a room by itself: moving the air conditioner to another room leaves them where they were. A new device starts as one composed device, and no preset changes the choice.
|
|
236
|
+
|
|
237
|
+
```mermaid
|
|
238
|
+
flowchart TD
|
|
239
|
+
bridge["Matterbridge<br/>Aggregator"]
|
|
240
|
+
ac["<b>Living Room</b><br/>Room Air Conditioner<br/>On/Off, Thermostat<br/>Fan Control when it is here, and Relative Humidity once the unit gives it"]
|
|
241
|
+
fan["<b>Living Room Fan</b><br/>Fan<br/>Fan Control"]
|
|
242
|
+
switches["<b>Living Room Vertical Swing</b>, and the other switches<br/>On/Off Plug-in Unit, one for each switch<br/>On/Off"]
|
|
243
|
+
selects["<b>Living Room Mode</b> and <b>Living Room Swing</b><br/>Mode Select, one for each<br/>Mode Select"]
|
|
244
|
+
humidity["<b>Living Room Humidity</b><br/>Humidity Sensor<br/>Relative Humidity"]
|
|
245
|
+
outdoor["<b>Living Room Outdoor Temperature</b><br/>Temperature Sensor<br/>Temperature Measurement"]
|
|
246
|
+
bridge --> ac
|
|
247
|
+
bridge -.-> fan
|
|
248
|
+
bridge -.-> switches
|
|
249
|
+
bridge -.-> selects
|
|
250
|
+
bridge -.-> humidity
|
|
251
|
+
bridge -.-> outdoor
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
| | Parts of the air conditioner | Devices of their own |
|
|
255
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
256
|
+
| In a Matter app | One device | One device for the air conditioner, and one for each of the others |
|
|
257
|
+
| Rooms | The parts move with the air conditioner | Each is put in a room by itself |
|
|
258
|
+
| Names | The air conditioner alone has one. Each switch and each mode select is tagged with a number and with what it is, such as `Vertical Swing` | Each has its own, such as `<name> Vertical Swing` |
|
|
259
|
+
| Unreachable | The air conditioner says so for all of them | Each says so itself |
|
|
260
|
+
|
|
261
|
+
Matter names a device, not its parts, so what a part is called is up to the Matter app. One that reads the tags can show `Vertical Swing`, one that does not shows numbered outlets to rename yourself, and one that shows no parts at all shows the air conditioner alone. This has not been tested in each Matter app: try it in yours, and turn it off if the parts do not show as you want.
|
|
172
262
|
|
|
173
|
-
|
|
263
|
+
Changing the choice builds the air conditioner again in Matter. The switches, the mode selects, the fan and the humidity sensor are removed and added in their new place, so a Matter app takes them for new devices: they lose the rooms, the names and the automations they had there.
|
|
174
264
|
|
|
175
265
|
## Compared with other Daikin plugins
|
|
176
266
|
|
|
@@ -185,12 +275,12 @@ This plugin is the only one of the four that finds the adapters itself, that spe
|
|
|
185
275
|
| Built on | [daikin-local-control](https://github.com/cheziyi/daikin-local-control), a library by the same author that works on its own | [daikin-controller](https://github.com/Apollon77/daikin-controller) | Its own DSIOT code | [daikin-controller-cloud](https://github.com/Apollon77/daikin-controller-cloud) |
|
|
186
276
|
| Adding a unit | A search of the network and a guided setup page, or by address. No restart | IP addresses in the plugin settings | IP addresses in the plugin settings, then a restart | Every unit of the account, with a white list and a black list |
|
|
187
277
|
| KEY and child lock code | Asked for in the setup page, by the adapters that need them | Not supported: it cannot connect to an adapter that wants its KEY or a child lock code | Not supported: it cannot connect to an adapter with a child lock code. DSIOT adapters have no KEY | Neither applies: it signs in to the Daikin cloud |
|
|
188
|
-
| Thermostat modes | Off, Cool, Heat, Auto, Dry and Fan only, and an optional switch for each of Dry and Fan only
|
|
278
|
+
| Thermostat modes | Off, Cool, Heat, Auto, Dry and Fan only, and an optional switch for each of Dry and Fan only or a mode select | Off, Cool, Heat, Auto, Dry and Fan only | Off, Cool, Heat, Auto and Fan only. Dry and humidify through a mode select | Off, Cool, Heat, Auto, Dry and Fan only |
|
|
189
279
|
| Fan speed | Steps, a percentage or not shown, on the air conditioner or on a fan of its own, and an optional switch for the auto speed | No | Low, Medium, High and Auto, a percentage, or a mode select | A fan device |
|
|
190
|
-
| Swing | A switch for each axis, or not shown with the mode you fix
|
|
280
|
+
| Swing | A switch for each axis, a mode select, or not shown with the mode you fix | No | The airflow direction of each axis, through a mode select | No |
|
|
191
281
|
| Powerful, Econo and Streamer | An optional switch for each | No | No | A switch for Powerful |
|
|
192
282
|
| Humidity | Shown on the air conditioner, when the unit gives it | No | Target humidity and ventilation as fans, and a humidity sensor | A humidity sensor, on units that report it |
|
|
193
|
-
| Outdoor temperature | A sensor, when the unit gives it
|
|
283
|
+
| Outdoor temperature | A sensor you turn on, when the unit gives it | No | Yes | Yes |
|
|
194
284
|
| Other devices | None | None | The ACK70Z air purifier, with its humidifier, its water tank and its air quality sensors | None |
|
|
195
285
|
| How often a unit is read | Every 5 seconds, or what you set from 2 to 3600 | Every 3 seconds, or what you set from 2 | No setting | Every 10 minutes by day and 30 by night, or what you set. Daikin allows 200 calls a day |
|
|
196
286
|
| Node.js | 22.19 or later, 24 and 26 | 24 and 26 | 20, 22 and 24 | 20.19 or later, 22.13 or later, 24 and 26 |
|
|
@@ -229,7 +319,7 @@ A stored device that this version of the plugin cannot read, for example one sto
|
|
|
229
319
|
- **Requests to one adapter go one at a time.** The reads and the commands of a device wait for each other, and different devices are read at the same time. A check from the setup page uses a connection of its own, so it can overlap with a read.
|
|
230
320
|
- **A command is built from the last poll** instead of reading the unit again first, when that poll is no older than the poll interval, or than 30 seconds when the interval is longer. In that window, a change made on the unit's own remote control since the last poll is undone by the command. Set a shorter poll interval to narrow it.
|
|
231
321
|
- **A device is reported as unreachable** after two reads in a row fail, and as reachable again on the first read that works.
|
|
232
|
-
- **Changing how the fan, the swing or any
|
|
322
|
+
- **Changing how the fan, the swing, any switch or any sensor appears** rebuilds the air conditioner in Matter. Some Matter apps take a moment to show the change, or need the accessory page reopened.
|
|
233
323
|
- **The settings and the setup page do not overlap.** The Terminal ID and the poll interval are set in the plugin's settings in Matterbridge, and the air conditioners in the setup page. A save of the settings never changes the air conditioners.
|
|
234
324
|
- **The setup page is as open as Matterbridge itself.** Anyone who can reach the Matterbridge frontend can use it. It never returns a stored child lock code.
|
|
235
325
|
- **A ducted unit on an Airbase adapter** gets the controls of any other unit: power, mode, target temperature, fan speed and swing. Its zones, its fan auto and its airside are not in Matter. A unit with zones reports no auto fan speed and no swing, so it gets neither, and an Airbase adapter asks for no KEY and no child lock code.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
async function g(e,n,o){try{let[r,a]=e.split(" "),i={method:r};n!==void 0&&(i.headers={"Content-Type":"application/json"},i.body=JSON.stringify(n));let d=await fetch(new URL(`api/${a}${o===void 0?"":`?${o}`}`,document.baseURI),i);if(d.status===204)return{ok:!0};if(d.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let p=await d.json();return"ok"in p?p:{ok:!1,error:p.error??`The plugin answered with status ${d.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}function S(e){let{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:y}=e;return{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:[...y]}}var ee={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!1,hasFanModeSwitch:!1,hasFanAutoSwitch:!1,hasSeparateFan:!1,specialModeSwitches:[]};function q(e){return{adapter:e,name:e.name??"",isNew:!0,...S(ee)}}function I(e){let{capabilities:n,controls:o,name:r,macAddress:a,host:i,protocol:d,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y}=e;return{adapter:{host:i,protocol:d,name:r,macAddress:a,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y},name:r,isNew:!1,capabilities:n,controls:o,...S(e)}}var O={fanSteps:[],hasFanAuto:!1,hasSwing:!1,hasDryMode:!1,hasFanMode:!1,specialModes:[]};var s={screen:"loading",setup:void 0,found:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",confirmingRemoval:""};function f(){return s.queue[0]}function v(e){return e.name===""?e.adapter.host:e.name}function $(e){return(s.setup?.devices??[]).some(n=>n.macAddress===e.macAddress)}var P;function W(e){P=e}function l(){let e=document.getElementById("app");if(e===null||P===void 0)return;let n=`${s.screen}:${f()?.adapter.host??""}`,o=e.dataset.form===n,r=document.activeElement instanceof HTMLElement?document.activeElement.id:"",a=new Map;for(let i of e.querySelectorAll("input[type=text], input[type=password]"))i.id!==""&&a.set(i.id,i.value);if(e.replaceChildren(...P[s.screen]().filter(i=>i!==!1&&i!==null&&i!==void 0)),e.dataset.form=n,!!o){for(let[i,d]of a){let p=document.getElementById(i);p instanceof HTMLInputElement&&(p.value=d)}r!==""&&document.getElementById(r)?.focus()}}async function x(e,n){s.isBusy=!0,s.busyText=e,s.error="",l();try{return await n()}finally{s.isBusy=!1,l()}}async function E(){let e=await g("GET state");if(!e.ok){s.error=e.error,s.screen="home",l();return}if(s.setup=e.state,e.state.devices.length===0){await k();return}s.screen="home",l()}async function T(){let e=await g("GET state");e.ok&&(s.setup=e.state)}function w(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",T().then(l),l()}async function k(){s.screen="find";let e=await x("Searching your network. This takes about three seconds\u2026",async()=>g("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,l();return}await T();let n=s.found.filter(o=>!e.devices.some(r=>r.host===o.host)&&s.selected.has(o.host)).map(o=>({...o,isConfigured:$(o)}));s.found=[...e.devices,...n];for(let o of e.devices)!o.isConfigured&&!o.hasDuplicateName&&s.selected.add(o.host);l()}async function B(e){let n=await x(`Looking for an adapter at ${e}\u2026`,async()=>g("POST identify",{host:e}));if(!n.ok){s.error=n.error,l();return}let o=$(n.device),r=n.device.requiresRegistration===!0;s.found=[...s.found.filter(a=>a.host!==n.device.host),{...n.device,isConfigured:o,hasDuplicateName:!1,needsRegistration:r}],o||s.selected.add(n.device.host),l()}function R(e){s.queue=e,s.saved=[],s.handled=[],N()}async function N(){let e=f();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await T(),l();return}s.screen="connect",s.error="",e.probe=void 0,l(),await M(e)}async function M(e,n){let r={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:n},a=n===void 0?`Checking ${v(e)}\u2026`:`Registering with ${v(e)}\u2026`,i=await x(a,async()=>g(n===void 0?"POST probe":"POST register",r));if(!i.ok){s.error=i.error,l();return}if(e.probe=i.result,e.adapter=i.result.device,i.result.status==="ready"){e.capabilities=i.result.capabilities,e.controls=i.result.controls;let d=i.result.device.name??"";d!==""&&(e.name=d),e.name!==""&&(s.screen="options")}l()}function K(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:v(e),isSaved:!1}),N()}async function U(e){let{adapter:n,name:o,localPassword:r}=e,a={...n,name:o,localPassword:r,...S(e)},i=await x(`Saving ${o}\u2026`,async()=>g("POST device",{device:a}));if(!i.ok){s.error=i.error,l();return}s.setup=i.state,s.saved.push(o),s.handled.push({label:o,isSaved:!0}),s.queue.shift(),await N()}async function Y(e){let n=await x(`Removing ${e.name}\u2026`,async()=>g("DELETE device",void 0,`macAddress=${encodeURIComponent(e.macAddress)}`));s.confirmingRemoval="",n.ok||(s.error=n.error),await T(),l()}function t(e,n={},...o){let r=document.createElement(e);for(let[a,i]of Object.entries(n))i===void 0||i===!1||(a==="class"?r.className=String(i):a==="html"?r.innerHTML=String(i):a.startsWith("on")&&typeof i=="function"?r.addEventListener(a.slice(2),i):a in r?r[a]=i:r.setAttribute(a,String(i)));for(let a of o)a!==!1&&a!==null&&a!==void 0&&r.append(a);return r}function c(e,n,o="quiet",r=!1){return t("button",{type:"button",class:`button ${o}`,onclick:n,disabled:r||s.isBusy},e)}function m(){return s.isBusy?t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),s.busyText):s.error!==""?t("p",{class:"notice problem",role:"alert"},s.error):null}function L(){let e=f();if(e===void 0||!e.isNew||s.handled.length+s.queue.length<2)return null;let n=s.screen==="connect"?"Connecting":"Choosing options";return t("ol",{class:"units","aria-label":"Air conditioners in this setup"},...s.handled.map(o=>t("li",{class:o.isSaved?"done":"skipped"},t("strong",{},o.label),t("span",{class:"meta"},o.isSaved?"Added":"Skipped"))),...s.queue.map((o,r)=>t("li",{class:r===0?"current":"ahead","aria-current":r===0?"true":void 0},t("strong",{},v(o)),t("span",{class:"meta"},r===0?n:"Waiting"))))}function H(e,n){return t("header",{class:"unit-header"},t("h1",{},v(e)),t("p",{class:"meta"},n))}function b(){if(s.queue.length>0&&!s.queue.some(r=>r.isNew))return null;let e=["Find","Connect","Options"],o=["find","connect","options"].indexOf(s.screen);return t("ol",{class:"steps"},...e.map((r,a)=>{let i="ahead";return a<o?i="done":a===o&&(i="current"),t("li",{class:i,"aria-current":a===o?"step":void 0},r)}))}function te(e){let n=t("input",{type:"text",id:"device-name",maxLength:32,autocomplete:"off"}),o=()=>{let r=n.value.trim();if(r!==""){if(s.setup?.devices.some(a=>a.name===r)){s.error=`"${r}" is already the name of another air conditioner. Pick a different one.`,l();return}e.name=r,s.error="",s.screen="options",l()}};return t("div",{class:"field"},t("label",{for:"device-name"},"Name this air conditioner"),t("p",{class:"help"},"Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead."),t("div",{class:"inline-form"},n,c("Continue",o,"primary")))}function ne(e,n){let o=t("input",{type:"password",id:"lock-code",inputMode:"numeric",autocomplete:"off",maxLength:64});return t("div",{class:"field"},t("label",{for:"lock-code"},"Child lock code"),t("p",{class:"help"},"The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter."),n?t("p",{class:"problem-text"},"The adapter did not accept that code."):null,t("div",{class:"inline-form"},o,c("Check the code",()=>{o.value!==""&&(e.localPassword=o.value,M(e))},"primary")))}function oe(e,n){let o=t("input",{type:"text",id:"adapter-key",autocomplete:"off",spellcheck:!1,maxLength:64});return t("div",{class:"field"},t("label",{for:"adapter-key"},"KEY on the adapter"),t("p",{class:"help"},"This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored."),n?t("p",{class:"problem-text"},"The adapter did not accept that KEY."):null,t("div",{class:"inline-form"},o,c("Register",()=>{o.value.trim()!==""&&M(e,o.value.trim())},"primary")))}function se(e){let n=e.probe?.status;return n==="ready"&&e.name===""?te(e):n==="localPasswordRequired"||n==="localPasswordRejected"?ne(e,n==="localPasswordRejected"):n==="registrationRequired"||n==="registrationRejected"?oe(e,n==="registrationRejected"):n==="unreachable"?t("p",{class:"notice problem",role:"alert"},`${e.name} did not answer at ${e.adapter.host}. Check that it has power and is on the network, then try again.`):n==="failed"?t("p",{class:"notice problem",role:"alert"},`${e.name} answered with an error: ${e.probe?.message??"unknown"}`):null}function G(){let e=f();if(e===void 0)return[m()];let n=e.probe?.status==="unreachable"||e.probe?.status==="failed"||s.error!==""&&e.name!=="";return[b(),L(),H(e,`Connecting to its adapter at ${e.adapter.host}.`),m(),s.isBusy?null:se(e),t("div",{class:"footer"},n?c("Try again",()=>{M(e)}):null,c(e.isNew&&s.queue.length>1?"Skip this one":"Cancel",e.isNew&&s.queue.length>1?K:w))]}function V(){let e=s.saved;return[t("header",{},t("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} air conditioners are set up`)),t("p",{class:"lead"},"They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page."),t("div",{class:"footer"},c("Done",w,"primary"))]}function ie(e){let n=!e.isConfigured&&!e.hasDuplicateName,o=[];e.isConfigured&&o.push("Already set up."),e.hasDuplicateName&&o.push("Another adapter has the same name. Give each its own name in its Daikin app, then search again."),e.name===void 0&&o.push("It has not told its name yet. If it has none, you will name it after connecting."),n&&e.needsRegistration?o.push("Asks for the KEY printed on its label."):n&&e.requiresRegistration&&o.push("Already registered with this plugin, so it will not ask for its KEY."),n&&e.requiresLocalPassword&&o.push("Asks for its child lock code.");let r=t("input",{type:"checkbox",id:`found-${e.host}`,checked:n&&s.selected.has(e.host),disabled:!n,onchange:()=>{r.checked?s.selected.add(e.host):s.selected.delete(e.host),l()}});return t("li",{class:n?"found":"found unavailable"},t("label",{for:`found-${e.host}`},r,t("span",{},t("strong",{},e.name??"Unnamed adapter"),t("span",{class:"meta"},` ${e.host}`),...o.map(a=>t("span",{class:"meta note"},a)))))}function z(){let e=s.found.filter(a=>s.selected.has(a.host)&&!a.isConfigured&&!a.hasDuplicateName),n=t("input",{type:"text",id:"host",placeholder:"192.168.1.50",spellcheck:!1,autocomplete:"off"}),o=()=>{n.value.trim()!==""&&B(n.value.trim())};n.addEventListener("keydown",a=>{a.key==="Enter"&&o()});let r=e.length===1?"Set up 1 air conditioner":`Set up ${e.length} air conditioners`;return[b(),t("header",{},t("h1",{},"Find your air conditioners")),m(),s.found.length>0?t("ul",{class:"found-list"},...s.found.map(ie)):null,!s.isBusy&&s.found.length===0?t("p",{class:"empty"},"No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below."):null,t("details",{class:"manual",open:!s.isBusy&&s.found.length===0},t("summary",{},"Can't see your air conditioner?"),t("p",{class:"help"},"Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address."),t("div",{class:"inline-form"},t("label",{for:"host",class:"visually-hidden"},"Adapter address"),n,c("Look it up",o))),t("div",{class:"footer"},c("Search again",()=>{k()}),c("Cancel",w),c(r,()=>R(e.map(q)),"primary",e.length===0))]}function re(e){let n=e.controls===void 0||e.controls.fanSteps.length>0,o=e.controls===void 0||e.controls.hasSwing,r=e.hasSeparateFan?" on a fan of its own":"",a=e.fanControl==="hidden"?"fan speed not shown":`fan speed as ${e.fanControl==="steps"?"steps":"a percentage"}${r}`,i=e.swingControl==="hidden"?"swing not shown":"swing as switches",d=[n?a:"",o?i:""].filter(y=>y!=="").join(", "),p=[e.hasDryModeSwitch?"dry mode":"",e.hasFanModeSwitch?"fan mode":"",e.hasFanAutoSwitch?"auto fan":"",...e.specialModeSwitches].filter(y=>y!==""),h=p.length===0?"":`a switch for ${p.join(", ")}`;return d===""?h===""?"power, mode and temperature":h:h===""?d:`${d}, and ${h}`}function ae(e){let n={online:"Online",offline:"Not answering",stopped:"Not started"}[e.status],o=s.confirmingRemoval===e.macAddress;return t("li",{class:"device"},t("div",{class:"device-main"},t("h3",{},e.name),t("p",{class:"meta"},t("span",{class:`status ${e.status}`},n),` at ${e.host}. In Matter: ${re(e)}.`),e.status!=="online"&&e.error?t("p",{class:"meta problem-text"},e.error):null),o?t("div",{class:"actions"},t("span",{class:"meta"},"Remove it from Matter too?"),c("Remove",()=>{Y(e)},"danger"),c("Keep",()=>{s.confirmingRemoval="",l()})):t("div",{class:"actions"},c(e.status==="online"?"Options":"Reconnect",()=>R([I(e)])),c("Remove",()=>{s.confirmingRemoval=e.macAddress,l()})))}function J(){let e=s.setup?.devices??[];return[t("header",{},t("h1",{},"Daikin air conditioners"),s.setup?c("Add air conditioners",()=>{k()},"primary"):null),m(),e.length===0&&s.setup?t("p",{class:"empty"},"No air conditioner is set up yet. Add one to control it from Matter."):t("ul",{class:"devices"},...e.map(ae)),s.setup?null:c("Try again",()=>{E()},"primary")]}var F={apple:{title:"Apple Home",text:"Fan speed as a percentage, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan."},google:{title:"Google Home",text:"Fan speed as steps, swing switches and a dry mode switch."},full:{title:"Full",text:"Every control and every switch this unit can have, special modes included."},minimal:{title:"Minimal",text:"No fan control, no swing control and no switches. The fan and the swing stay as they are."}};function j(e,n){let o=e==="minimal";return{fanControl:o||n.fanSteps.length===0?"hidden":e==="apple"?"percent":"steps",fanSpeedWhenHidden:"keep",swingControl:o||!n.hasSwing?"hidden":"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!o&&n.hasDryMode,hasFanModeSwitch:(e==="apple"||e==="full")&&n.hasFanMode,hasFanAutoSwitch:(e==="apple"||e==="full")&&n.hasFanAuto,hasSeparateFan:!1,specialModeSwitches:e==="full"?[...n.specialModes]:[]}}function Q(e,n,o){let r=S(e);return o.fanSteps.length===0&&Object.assign(r,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),r.fanControl==="hidden"&&(r.hasSeparateFan=!1),o.hasSwing||Object.assign(r,{swingControl:"hidden",swingModeWhenHidden:"keep"}),r.specialModeSwitches=o.specialModes.filter(a=>r.specialModeSwitches.includes(a)),JSON.stringify(r)===JSON.stringify(j(n,o))}var le={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},ce={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},Z={powerful:{title:"Powerful",text:"Runs the unit at full power for a while."},econo:{title:"Econo",text:"Limits how much power the unit draws."},streamer:{title:"Streamer",text:"Runs the air purifying of the unit."}},C={steps:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect x="2" y="20" width="8" height="6"/><rect x="14" y="14" width="8" height="12"/><rect x="26" y="8" width="8" height="18"/><rect x="38" y="2" width="8" height="24"/></svg>',percent:'<svg viewBox="0 0 48 28" aria-hidden="true"><path d="M2 26 L46 2 L46 26 Z"/></svg>',hidden:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M6 14 H42" stroke-dasharray="3 5"/></svg>',switches:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19"/><path class="stroke" d="M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19"/></svg>'};function u(e,n){Object.assign(e,n),l()}function D(e,n,o,r,a,i,d){let p=`${e}-${n}`,h=t("input",{type:"radio",name:e,id:p,checked:n===o,onchange:()=>d(n)});return t("label",{class:"choice pictured",for:p},h,t("span",{class:"glyph",html:i}),t("span",{},t("strong",{},r),t("span",{class:"meta"},a)))}function A(e,n,o,r,a){let i=t("input",{type:"checkbox",id:e,checked:n,onchange:()=>a(i.checked)});return t("label",{class:"choice",for:e},i,t("span",{},t("strong",{},o),t("span",{class:"meta"},r)))}function de(e,n){return t("section",{class:"presets"},t("h2",{},"Start from a preset"),t("div",{class:"preset-list"},...Object.keys(F).map(o=>t("button",{type:"button",class:"preset","aria-pressed":String(Q(e,o,n)),onclick:()=>u(e,j(o,n))},t("strong",{},F[o].title),t("span",{class:"meta"},F[o].text)))),t("p",{class:"help"},"A preset fills in the choices below for what this unit can do. Change any of them afterwards."))}function pe(e,n){let o=e.capabilities?.fanSpeeds??[],r=n.fanSteps.length;if(r===0)return t("section",{},t("h2",{},"Fan speed"),t("p",{class:"help"},"This unit does not let apps set its fan speed, so there is nothing to choose."));let a=n.hasFanAuto&&!(e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep");return t("section",{},t("h2",{},"Fan speed"),t("div",{class:"choices"},D("fan","steps",e.fanControl,"Steps",`${r} speeds, one for each speed of the unit${o.includes("auto")?", and auto":""}.`,C.steps,i=>u(e,{fanControl:i})),D("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.",C.percent,i=>u(e,{fanControl:i})),D("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",C.hidden,i=>u(e,{fanControl:i}))),e.fanControl==="hidden"?t("div",{class:"field"},t("label",{for:"fan-fixed"},"When the plugin sends a command, the fan speed"),t("select",{id:"fan-fixed",onchange:i=>u(e,{fanSpeedWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.fanSpeedWhenHidden==="keep"},"stays as it is"),...o.map(i=>t("option",{value:i,selected:e.fanSpeedWhenHidden===i},`is set to ${le[i]}`)))):null,e.fanControl==="hidden"?null:A("separate-fan",e.hasSeparateFan,"Show the fan as a device of its own",`It appears as "${e.name} Fan", and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner. It always shows the speed the unit holds, also while the unit is off.`,i=>u(e,{hasSeparateFan:i})),a?A("fan-auto-switch",e.hasFanAutoSwitch,"Add a switch for auto fan speed",`It appears as "${e.name} Auto Fan". On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,i=>u(e,{hasFanAutoSwitch:i})):null)}function ue(e){let n=e.capabilities?.swingModes??[];if(n.length===0)return t("section",{},t("h2",{},"Swing"),t("p",{class:"help"},"This unit does not let apps move its louvers, so there is nothing to choose."));let o=n.includes("vertical"),r=n.includes("horizontal"),a="One switch, for the up and down swing.";return o&&r?a="Two switches: one for the up and down swing, one for left and right.":r&&(a="One switch, for the left and right swing."),t("section",{},t("h2",{},"Swing"),t("div",{class:"choices"},D("swing","switches",e.swingControl,"Switches",a,C.switches,i=>u(e,{swingControl:i})),D("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",C.hidden,i=>u(e,{swingControl:i}))),e.swingControl==="hidden"?t("div",{class:"field"},t("label",{for:"swing-fixed"},"When the plugin sends a command, the swing"),t("select",{id:"swing-fixed",onchange:i=>u(e,{swingModeWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.swingModeWhenHidden==="keep"},"stays as it is"),...n.map(i=>t("option",{value:i,selected:e.swingModeWhenHidden===i},`is set to: ${ce[i]}`)))):null)}function he(e,n){if(!n.hasDryMode&&!n.hasFanMode)return null;let o=e.capabilities?.modes??[];return t("section",{},t("h2",{},"Dry and fan modes"),t("p",{class:"help"},`These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${o.includes("cool")?"cool":"heat"}.`),t("div",{class:"choices"},n.hasDryMode?A("dry-mode-switch",e.hasDryModeSwitch,"Add a switch for dry mode",`It appears as "${e.name} Dry Mode".`,r=>u(e,{hasDryModeSwitch:r})):null,n.hasFanMode?A("fan-mode-switch",e.hasFanModeSwitch,"Add a switch for fan mode",`It appears as "${e.name} Fan Mode".`,r=>u(e,{hasFanModeSwitch:r})):null))}function fe(e,n){return n.specialModes.length===0?null:t("section",{},t("h2",{},"Special modes"),t("p",{class:"help"},"Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in."),t("div",{class:"choices"},...n.specialModes.map(o=>A(`special-${o}`,e.specialModeSwitches.includes(o),`Add a switch for ${Z[o].title}`,Z[o].text,r=>u(e,{specialModeSwitches:n.specialModes.filter(a=>a===o?r:e.specialModeSwitches.includes(a))})))))}function X(){let e=f();if(e===void 0)return[m()];let n=e.controls??O;return[b(),L(),H(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter, and so do the humidity and the outdoor temperature once the unit gives them: choose how the rest appears.`),de(e,n),pe(e,n),ue(e),he(e,n),fe(e,n),e.isNew?null:t("p",{class:"help"},"Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change."),m(),t("div",{class:"footer"},c("Cancel",w),c(e.isNew?`Add ${e.name}`:"Save options",()=>{U(e)},"primary"))]}function _(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let n of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let o=e.getPropertyValue(n).trim();o!==""&&document.documentElement.style.setProperty(`--host${n.slice(1)}`,o)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}W({loading:()=>[t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),"Loading\u2026")],home:J,find:z,connect:G,options:X,done:V});_();l();E();
|
|
1
|
+
async function y(e,o,n){try{let[a,r]=e.split(" "),i={method:a};o!==void 0&&(i.headers={"Content-Type":"application/json"},i.body=JSON.stringify(o));let d=await fetch(new URL(`api/${r}${n===void 0?"":`?${n}`}`,document.baseURI),i);if(d.status===204)return{ok:!0};if(d.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let h=await d.json();return"ok"in h?h:{ok:!1,error:h.error??`The plugin answered with status ${d.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}function M(e){let{fanControl:o,fanSpeedWhenHidden:n,swingControl:a,swingModeWhenHidden:r,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:h,hasModeSelect:u,hasSeparateFan:x,specialModeSwitches:g,hasOutdoorTemperatureSensor:se,hasSeparateHumiditySensor:ie,isComposed:ae}=e;return{fanControl:o,fanSpeedWhenHidden:n,swingControl:a,swingModeWhenHidden:r,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:h,hasModeSelect:u,hasSeparateFan:x,specialModeSwitches:[...g],hasOutdoorTemperatureSensor:se,hasSeparateHumiditySensor:ie,isComposed:ae}}var re={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!1,hasFanModeSwitch:!1,hasFanAutoSwitch:!1,hasModeSelect:!1,hasSeparateFan:!1,specialModeSwitches:[],hasOutdoorTemperatureSensor:!1,hasSeparateHumiditySensor:!1,isComposed:!0};function q(e){return{adapter:e,name:e.name??"",isNew:!0,...M(re)}}function W(e){let{capabilities:o,controls:n,name:a,macAddress:r,host:i,protocol:d,requiresRegistration:h,usesHttps:u,requiresLocalPassword:x}=e;return{adapter:{host:i,protocol:d,name:a,macAddress:r,requiresRegistration:h,usesHttps:u,requiresLocalPassword:x},name:a,isNew:!1,capabilities:o,controls:n,...M(e)}}var B={fanSteps:[],hasFanAuto:!1,hasSwing:!1,hasDryMode:!1,hasFanMode:!1,specialModes:[]};var s={screen:"loading",setup:void 0,found:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",confirmingRemoval:""};function f(){return s.queue[0]}function S(e){return e.name===""?e.adapter.host:e.name}function N(e){return(s.setup?.devices??[]).some(o=>o.macAddress===e.macAddress)}var O;function U(e){O=e}function l(){let e=document.getElementById("app");if(e===null||O===void 0)return;let o=`${s.screen}:${f()?.adapter.host??""}`,n=e.dataset.form===o,a=document.activeElement instanceof HTMLElement?document.activeElement.id:"",r=new Map;for(let i of e.querySelectorAll("input[type=text], input[type=password]"))i.id!==""&&r.set(i.id,i.value);if(e.replaceChildren(...O[s.screen]().filter(i=>i!==!1&&i!==null&&i!==void 0)),e.dataset.form=o,!!n){for(let[i,d]of r){let h=document.getElementById(i);h instanceof HTMLInputElement&&(h.value=d)}a!==""&&document.getElementById(a)?.focus()}}async function A(e,o){s.isBusy=!0,s.busyText=e,s.error="",l();try{return await o()}finally{s.isBusy=!1,l()}}async function H(){let e=await y("GET state");if(!e.ok){s.error=e.error,s.screen="home",l();return}if(s.setup=e.state,e.state.devices.length===0){await T();return}s.screen="home",l()}async function $(){let e=await y("GET state");e.ok&&(s.setup=e.state)}function v(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",$().then(l),l()}async function T(){s.screen="find";let e=await A("Searching your network. This takes about three seconds\u2026",async()=>y("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,l();return}await $();let o=s.found.filter(n=>!e.devices.some(a=>a.host===n.host)&&s.selected.has(n.host)).map(n=>({...n,isConfigured:N(n)}));s.found=[...e.devices,...o];for(let n of e.devices)!n.isConfigured&&!n.hasDuplicateName&&s.selected.add(n.host);l()}async function K(e){let o=await A(`Looking for an adapter at ${e}\u2026`,async()=>y("POST identify",{host:e}));if(!o.ok){s.error=o.error,l();return}let n=N(o.device),a=o.device.requiresRegistration===!0;s.found=[...s.found.filter(r=>r.host!==o.device.host),{...o.device,isConfigured:n,hasDuplicateName:!1,needsRegistration:a}],n||s.selected.add(o.device.host),l()}function L(e){s.queue=e,s.saved=[],s.handled=[],j()}async function j(){let e=f();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await $(),l();return}s.screen="connect",s.error="",e.probe=void 0,l(),await E(e)}async function E(e,o){let a={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:o},r=o===void 0?`Checking ${S(e)}\u2026`:`Registering with ${S(e)}\u2026`,i=await A(r,async()=>y(o===void 0?"POST probe":"POST register",a));if(!i.ok){s.error=i.error,l();return}if(e.probe=i.result,e.adapter=i.result.device,i.result.status==="ready"){e.capabilities=i.result.capabilities,e.controls=i.result.controls;let d=i.result.device.name??"";d!==""&&(e.name=d),e.name!==""&&(s.screen="options")}l()}function Y(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:S(e),isSaved:!1}),j()}async function G(e){let{adapter:o,name:n,localPassword:a}=e,r={...o,name:n,localPassword:a,...M(e)},i=await A(`Saving ${n}\u2026`,async()=>y("POST device",{device:r}));if(!i.ok){s.error=i.error,l();return}s.setup=i.state,s.saved.push(n),s.handled.push({label:n,isSaved:!0}),s.queue.shift(),await j()}async function V(e){let o=await A(`Removing ${e.name}\u2026`,async()=>y("DELETE device",void 0,`macAddress=${encodeURIComponent(e.macAddress)}`));s.confirmingRemoval="",o.ok||(s.error=o.error),await $(),l()}function t(e,o={},...n){let a=document.createElement(e);for(let[r,i]of Object.entries(o))i===void 0||i===!1||(r==="class"?a.className=String(i):r==="html"?a.innerHTML=String(i):r.startsWith("on")&&typeof i=="function"?a.addEventListener(r.slice(2),i):r in a?a[r]=i:a.setAttribute(r,String(i)));for(let r of n)r!==!1&&r!==null&&r!==void 0&&a.append(r);return a}function c(e,o,n="quiet",a=!1){return t("button",{type:"button",class:`button ${n}`,onclick:o,disabled:a||s.isBusy},e)}function m(){return s.isBusy?t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),s.busyText):s.error!==""?t("p",{class:"notice problem",role:"alert"},s.error):null}function R(){let e=f();if(e===void 0||!e.isNew||s.handled.length+s.queue.length<2)return null;let o=s.screen==="connect"?"Connecting":"Choosing options";return t("ol",{class:"units","aria-label":"Air conditioners in this setup"},...s.handled.map(n=>t("li",{class:n.isSaved?"done":"skipped"},t("strong",{},n.label),t("span",{class:"meta"},n.isSaved?"Added":"Skipped"))),...s.queue.map((n,a)=>t("li",{class:a===0?"current":"ahead","aria-current":a===0?"true":void 0},t("strong",{},S(n)),t("span",{class:"meta"},a===0?o:"Waiting"))))}function F(e,o){return t("header",{class:"unit-header"},t("h1",{},S(e)),t("p",{class:"meta"},o))}function k(){if(s.queue.length>0&&!s.queue.some(a=>a.isNew))return null;let e=["Find","Connect","Options"],n=["find","connect","options"].indexOf(s.screen);return t("ol",{class:"steps"},...e.map((a,r)=>{let i="ahead";return r<n?i="done":r===n&&(i="current"),t("li",{class:i,"aria-current":r===n?"step":void 0},a)}))}function le(e){let o=t("input",{type:"text",id:"device-name",maxLength:32,autocomplete:"off"}),n=()=>{let a=o.value.trim();if(a!==""){if(s.setup?.devices.some(r=>r.name===a)){s.error=`"${a}" is already the name of another air conditioner. Pick a different one.`,l();return}e.name=a,s.error="",s.screen="options",l()}};return t("div",{class:"field"},t("label",{for:"device-name"},"Name this air conditioner"),t("p",{class:"help"},"Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead."),t("div",{class:"inline-form"},o,c("Continue",n,"primary")))}function ce(e,o){let n=t("input",{type:"password",id:"lock-code",inputMode:"numeric",autocomplete:"off",maxLength:64});return t("div",{class:"field"},t("label",{for:"lock-code"},"Child lock code"),t("p",{class:"help"},"The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter."),o?t("p",{class:"problem-text"},"The adapter did not accept that code."):null,t("div",{class:"inline-form"},n,c("Check the code",()=>{n.value!==""&&(e.localPassword=n.value,E(e))},"primary")))}function de(e,o){let n=t("input",{type:"text",id:"adapter-key",autocomplete:"off",spellcheck:!1,maxLength:64});return t("div",{class:"field"},t("label",{for:"adapter-key"},"KEY on the adapter"),t("p",{class:"help"},"This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored."),o?t("p",{class:"problem-text"},"The adapter did not accept that KEY."):null,t("div",{class:"inline-form"},n,c("Register",()=>{n.value.trim()!==""&&E(e,n.value.trim())},"primary")))}function pe(e){let o=e.probe?.status;return o==="ready"&&e.name===""?le(e):o==="localPasswordRequired"||o==="localPasswordRejected"?ce(e,o==="localPasswordRejected"):o==="registrationRequired"||o==="registrationRejected"?de(e,o==="registrationRejected"):o==="unreachable"?t("p",{class:"notice problem",role:"alert"},`${e.name} did not answer at ${e.adapter.host}. Check that it has power and is on the network, then try again.`):o==="failed"?t("p",{class:"notice problem",role:"alert"},`${e.name} answered with an error: ${e.probe?.message??"unknown"}`):null}function z(){let e=f();if(e===void 0)return[m()];let o=e.probe?.status==="unreachable"||e.probe?.status==="failed"||s.error!==""&&e.name!=="";return[k(),R(),F(e,`Connecting to its adapter at ${e.adapter.host}.`),m(),s.isBusy?null:pe(e),t("div",{class:"footer"},o?c("Try again",()=>{E(e)}):null,c(e.isNew&&s.queue.length>1?"Skip this one":"Cancel",e.isNew&&s.queue.length>1?Y:v))]}function J(){let e=s.saved;return[t("header",{},t("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} air conditioners are set up`)),t("p",{class:"lead"},"They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page."),t("div",{class:"footer"},c("Done",v,"primary"))]}function he(e){let o=!e.isConfigured&&!e.hasDuplicateName,n=[];e.isConfigured&&n.push("Already set up."),e.hasDuplicateName&&n.push("Another adapter has the same name. Give each its own name in its Daikin app, then search again."),e.name===void 0&&n.push("It has not told its name yet. If it has none, you will name it after connecting."),o&&e.needsRegistration?n.push("Asks for the KEY printed on its label."):o&&e.requiresRegistration&&n.push("Already registered with this plugin, so it will not ask for its KEY."),o&&e.requiresLocalPassword&&n.push("Asks for its child lock code.");let a=t("input",{type:"checkbox",id:`found-${e.host}`,checked:o&&s.selected.has(e.host),disabled:!o,onchange:()=>{a.checked?s.selected.add(e.host):s.selected.delete(e.host),l()}});return t("li",{class:o?"found":"found unavailable"},t("label",{for:`found-${e.host}`},a,t("span",{},t("strong",{},e.name??"Unnamed adapter"),t("span",{class:"meta"},` ${e.host}`),...n.map(r=>t("span",{class:"meta note"},r)))))}function Q(){let e=s.found.filter(r=>s.selected.has(r.host)&&!r.isConfigured&&!r.hasDuplicateName),o=t("input",{type:"text",id:"host",placeholder:"192.168.1.50",spellcheck:!1,autocomplete:"off"}),n=()=>{o.value.trim()!==""&&K(o.value.trim())};o.addEventListener("keydown",r=>{r.key==="Enter"&&n()});let a=e.length===1?"Set up 1 air conditioner":`Set up ${e.length} air conditioners`;return[k(),t("header",{},t("h1",{},"Find your air conditioners")),m(),s.found.length>0?t("ul",{class:"found-list"},...s.found.map(he)):null,!s.isBusy&&s.found.length===0?t("p",{class:"empty"},"No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below."):null,t("details",{class:"manual",open:!s.isBusy&&s.found.length===0},t("summary",{},"Can't see your air conditioner?"),t("p",{class:"help"},"Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address."),t("div",{class:"inline-form"},t("label",{for:"host",class:"visually-hidden"},"Adapter address"),o,c("Look it up",n))),t("div",{class:"footer"},c("Search again",()=>{T()}),c("Cancel",v),c(a,()=>L(e.map(q)),"primary",e.length===0))]}function ue(e){let o=e.controls===void 0||e.controls.fanSteps.length>0,n=e.controls===void 0||e.controls.hasSwing,a=e.hasSeparateFan?" on a fan of its own":"",r=e.fanControl==="hidden"?"fan speed not shown":`fan speed as ${e.fanControl==="steps"?"steps":"a percentage"}${a}`,i={switches:"swing as switches",select:"swing as a mode select",hidden:"swing not shown"}[e.swingControl],d=[o?r:"",n?i:""].filter(g=>g!=="").join(", "),h=[e.hasDryModeSwitch?"dry mode":"",e.hasFanModeSwitch?"fan mode":"",e.hasFanAutoSwitch?"auto fan":"",...e.specialModeSwitches].filter(g=>g!==""),u=[h.length===0?"":`a switch for ${h.join(", ")}`,e.hasModeSelect?"a mode select":""].filter(g=>g!=="").join(" and "),x=e.isComposed?", all as one device":"";return d===""?`${u===""?"power, mode and temperature":u}${x}`:`${u===""?d:`${d}, and ${u}`}${x}`}function fe(e){let o={online:"Online",offline:"Not answering",stopped:"Not started"}[e.status],n=s.confirmingRemoval===e.macAddress;return t("li",{class:"device"},t("div",{class:"device-main"},t("h3",{},e.name),t("p",{class:"meta"},t("span",{class:`status ${e.status}`},o),` at ${e.host}. In Matter: ${ue(e)}.`),e.status!=="online"&&e.error?t("p",{class:"meta problem-text"},e.error):null),n?t("div",{class:"actions"},t("span",{class:"meta"},"Remove it from Matter too?"),c("Remove",()=>{V(e)},"danger"),c("Keep",()=>{s.confirmingRemoval="",l()})):t("div",{class:"actions"},c(e.status==="online"?"Options":"Reconnect",()=>L([W(e)])),c("Remove",()=>{s.confirmingRemoval=e.macAddress,l()})))}function Z(){let e=s.setup?.devices??[];return[t("header",{},t("h1",{},"Daikin air conditioners"),s.setup?c("Add air conditioners",()=>{T()},"primary"):null),m(),e.length===0&&s.setup?t("p",{class:"empty"},"No air conditioner is set up yet. Add one to control it from Matter."):t("ul",{class:"devices"},...e.map(fe)),s.setup?null:c("Try again",()=>{H()},"primary")]}var P={apple:{title:"Apple Home",text:"Fan speed as a percentage, the humidity on a sensor of its own too, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan."},google:{title:"Google Home",text:"Fan speed as steps, swing switches and a dry mode switch."},full:{title:"Full",text:"Every control, switch and sensor this unit can have, special modes and the outdoor temperature included."},minimal:{title:"Minimal",text:"No fan control, no swing control and no switches. The fan and the swing stay as they are."}};function I(e,o){let n=e==="minimal";return{fanControl:n||o.fanSteps.length===0?"hidden":e==="apple"?"percent":"steps",fanSpeedWhenHidden:"keep",swingControl:n||!o.hasSwing?"hidden":"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!n&&o.hasDryMode,hasFanModeSwitch:(e==="apple"||e==="full")&&o.hasFanMode,hasFanAutoSwitch:(e==="apple"||e==="full")&&o.hasFanAuto,hasModeSelect:!1,hasSeparateFan:!1,specialModeSwitches:e==="full"?[...o.specialModes]:[],hasOutdoorTemperatureSensor:e==="full",hasSeparateHumiditySensor:e==="apple"}}function X(e,o,n){let a=M(e);return n.fanSteps.length===0&&Object.assign(a,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),a.fanControl==="hidden"&&(a.hasSeparateFan=!1),n.hasSwing||Object.assign(a,{swingControl:"hidden",swingModeWhenHidden:"keep"}),!n.hasDryMode&&!n.hasFanMode&&(a.hasModeSelect=!1),a.specialModeSwitches=n.specialModes.filter(r=>a.specialModeSwitches.includes(r)),JSON.stringify(a)===JSON.stringify({...I(o,n),isComposed:a.isComposed})}var me={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},_={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},ee={powerful:{title:"Powerful",text:"Runs the unit at full power for a while."},econo:{title:"Econo",text:"Limits how much power the unit draws."},streamer:{title:"Streamer",text:"Runs the air purifying of the unit."}},C={steps:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect x="2" y="20" width="8" height="6"/><rect x="14" y="14" width="8" height="12"/><rect x="26" y="8" width="8" height="18"/><rect x="38" y="2" width="8" height="24"/></svg>',percent:'<svg viewBox="0 0 48 28" aria-hidden="true"><path d="M2 26 L46 2 L46 26 Z"/></svg>',hidden:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M6 14 H42" stroke-dasharray="3 5"/></svg>',switches:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19"/><path class="stroke" d="M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19"/></svg>',select:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect class="stroke" x="3" y="5" width="42" height="18" rx="4"/><path class="stroke" d="M31 12 L35 16 L39 12"/></svg>'},te="Few apps show a mode select: Home Assistant does, Apple Home does not.";function b(e,o,n){return e.isComposed?n?`It appears as a part of "${e.name}", tagged "${o}"`:`It appears as a part of "${e.name}"`:`It appears as "${e.name} ${o}"`}function p(e,o){Object.assign(e,o),l()}function D(e,o,n,a,r,i,d){let h=`${e}-${o}`,u=t("input",{type:"radio",name:e,id:h,checked:o===n,onchange:()=>d(o)});return t("label",{class:"choice pictured",for:h},u,t("span",{class:"glyph",html:i}),t("span",{},t("strong",{},a),t("span",{class:"meta"},r)))}function w(e,o,n,a,r){let i=t("input",{type:"checkbox",id:e,checked:o,onchange:()=>r(i.checked)});return t("label",{class:"choice",for:e},i,t("span",{},t("strong",{},n),t("span",{class:"meta"},a)))}function we(e,o){return t("section",{class:"presets"},t("h2",{},"Start from a preset"),t("div",{class:"preset-list"},...Object.keys(P).map(n=>t("button",{type:"button",class:"preset","aria-pressed":String(X(e,n,o)),onclick:()=>p(e,I(n,o))},t("strong",{},P[n].title),t("span",{class:"meta"},P[n].text)))),t("p",{class:"help"},"A preset fills in the choices below for what this unit can do. Change any of them afterwards."))}function ge(e,o){let n=e.capabilities?.fanSpeeds??[],a=o.fanSteps.length;if(a===0)return t("section",{},t("h2",{},"Fan speed"),t("p",{class:"help"},"This unit does not let apps set its fan speed, so there is nothing to choose."));let r=o.hasFanAuto&&!(e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep");return t("section",{},t("h2",{},"Fan speed"),t("div",{class:"choices"},D("fan","steps",e.fanControl,"Steps",`${a} speeds, one for each speed of the unit${n.includes("auto")?", and auto":""}.`,C.steps,i=>p(e,{fanControl:i})),D("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.",C.percent,i=>p(e,{fanControl:i})),D("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",C.hidden,i=>p(e,{fanControl:i}))),e.fanControl==="hidden"?t("div",{class:"field"},t("label",{for:"fan-fixed"},"When the plugin sends a command, the fan speed"),t("select",{id:"fan-fixed",onchange:i=>p(e,{fanSpeedWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.fanSpeedWhenHidden==="keep"},"stays as it is"),...n.map(i=>t("option",{value:i,selected:e.fanSpeedWhenHidden===i},`is set to ${me[i]}`)))):null,e.fanControl==="hidden"?null:w("separate-fan",e.hasSeparateFan,"Show the fan as a device of its own",`${b(e,"Fan",!1)}, and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner, or do not show it well. It always shows the speed the unit holds, also while the unit is off.`,i=>p(e,{hasSeparateFan:i})),r?w("fan-auto-switch",e.hasFanAutoSwitch,"Add a switch for auto fan speed",`${b(e,"Auto Fan",!0)}. On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,i=>p(e,{hasFanAutoSwitch:i})):null)}function ye(e){let o=e.capabilities?.swingModes??[];if(o.length===0)return t("section",{},t("h2",{},"Swing"),t("p",{class:"help"},"This unit does not let apps move its louvers, so there is nothing to choose."));let n=o.includes("vertical"),a=o.includes("horizontal"),r="One switch, for the up and down swing.";return n&&a?r="Two switches: one for the up and down swing, one for left and right.":a&&(r="One switch, for the left and right swing."),t("section",{},t("h2",{},"Swing"),t("div",{class:"choices"},D("swing","switches",e.swingControl,"Switches",r,C.switches,i=>p(e,{swingControl:i})),D("swing","select",e.swingControl,"A mode select",`${b(e,"Swing",!0)}, and picks one of: ${o.map(i=>_[i].toLowerCase()).join(", ")}. ${te}`,C.select,i=>p(e,{swingControl:i})),D("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",C.hidden,i=>p(e,{swingControl:i}))),e.swingControl==="hidden"?t("div",{class:"field"},t("label",{for:"swing-fixed"},"When the plugin sends a command, the swing"),t("select",{id:"swing-fixed",onchange:i=>p(e,{swingModeWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.swingModeWhenHidden==="keep"},"stays as it is"),...o.map(i=>t("option",{value:i,selected:e.swingModeWhenHidden===i},`is set to: ${_[i]}`)))):null)}function ve(e,o){if(!o.hasDryMode&&!o.hasFanMode)return null;let n=e.capabilities?.modes??[];return t("section",{},t("h2",{},"Dry and fan modes"),t("p",{class:"help"},`These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${n.includes("cool")?"cool":"heat"}. A mode select instead offers every mode in one control.`),t("div",{class:"choices"},w("mode-select",e.hasModeSelect,"Use a mode select instead of switches",`${b(e,"Mode",!0)}, with Off and every mode of the unit, as the thermostat mode has them. Picking a mode turns the unit on in it. ${te}`,a=>p(e,a?{hasModeSelect:a,hasDryModeSwitch:!1,hasFanModeSwitch:!1}:{hasModeSelect:a})),o.hasDryMode&&!e.hasModeSelect?w("dry-mode-switch",e.hasDryModeSwitch,"Add a switch for dry mode",`${b(e,"Dry Mode",!0)}.`,a=>p(e,{hasDryModeSwitch:a})):null,o.hasFanMode&&!e.hasModeSelect?w("fan-mode-switch",e.hasFanModeSwitch,"Add a switch for fan mode",`${b(e,"Fan Mode",!0)}.`,a=>p(e,{hasFanModeSwitch:a})):null))}function Se(e,o){return o.specialModes.length===0?null:t("section",{},t("h2",{},"Special modes"),t("p",{class:"help"},"Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in."),t("div",{class:"choices"},...o.specialModes.map(n=>w(`special-${n}`,e.specialModeSwitches.includes(n),`Add a switch for ${ee[n].title}`,ee[n].text,a=>p(e,{specialModeSwitches:o.specialModes.filter(r=>r===n?a:e.specialModeSwitches.includes(r))})))))}function be(e){return t("section",{},t("h2",{},"Humidity and outdoor temperature"),t("p",{class:"help"},"No adapter says which readings its unit has, so each of these takes effect once the unit gives that reading. The room temperature and the humidity are always on the air conditioner."),t("div",{class:"choices"},w("separate-humidity",e.hasSeparateHumiditySensor,"Also show the humidity on a sensor of its own",`${b(e,"Humidity",!1)}, and the air conditioner keeps its humidity. For apps that show no humidity on an air conditioner, which Apple Home is reported to be.`,o=>p(e,{hasSeparateHumiditySensor:o})),w("outdoor-sensor",e.hasOutdoorTemperatureSensor,"Add a sensor for the outdoor temperature",`It appears as "${e.name} Outdoor Temperature". It is a device of its own, which you can put in another room, such as outside. Until you do, a Matter app may show the outdoor temperature as that of the room it put the sensor in. Some units give no outdoor temperature while they are idle: the sensor then shows no value.`,o=>p(e,{hasOutdoorTemperatureSensor:o}))))}function xe(e){return t("section",{},t("h2",{},"One device or several"),t("p",{class:"help"},"The switches, a fan of its own and a humidity sensor of its own can be parts of the air conditioner in Matter, or devices of their own beside it. The outdoor temperature sensor is always a device of its own, so that you can put it in another room than the air conditioner."),t("div",{class:"choices"},w("composed",e.isComposed,"Make them parts of the air conditioner",`Your Matter app has one device, "${e.name}", and they move between rooms with it. A part has no name of its own in Matter, so an app may show the switches as numbered outlets, and an app that shows no parts shows the air conditioner alone. Turned off, each is a device of its own with its own name, which your app puts in a room by itself.`,o=>p(e,{isComposed:o}))))}function oe(){let e=f();if(e===void 0)return[m()];let o=e.controls??B;return[k(),R(),F(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter: choose how the rest appears.`),we(e,o),ge(e,o),ye(e),ve(e,o),Se(e,o),be(e),xe(e),e.isNew?null:t("p",{class:"help"},"Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change."),m(),t("div",{class:"footer"},c("Cancel",v),c(e.isNew?`Add ${e.name}`:"Save options",()=>{G(e)},"primary"))]}function ne(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let o of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let n=e.getPropertyValue(o).trim();n!==""&&document.documentElement.style.setProperty(`--host${o.slice(1)}`,n)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}U({loading:()=>[t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),"Loading\u2026")],home:Z,find:Q,connect:z,options:oe,done:J});ne();l();H();
|
|
2
2
|
/**
|
|
3
3
|
* @file apps/frontend/src/api.ts
|
|
4
4
|
* @description What the plugin's API sends and takes, and the call to it: see src/setup/setupApi.ts in the plugin.
|
|
@@ -74,7 +74,7 @@ async function g(e,n,o){try{let[r,a]=e.split(" "),i={method:r};n!==void 0&&(i.he
|
|
|
74
74
|
*/
|
|
75
75
|
/**
|
|
76
76
|
* @file apps/frontend/src/screens/options.ts
|
|
77
|
-
* @description The Options step: how the fan, the swing
|
|
77
|
+
* @description The Options step: how the fan, the swing, the extra modes and the readings of a unit appear in Matter, one section each, and whether as one device.
|
|
78
78
|
* @author Calvin Che
|
|
79
79
|
* @license Apache-2.0
|
|
80
80
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/api.ts", "../src/exposure.ts", "../src/state.ts", "../src/render.ts", "../src/actions.ts", "../src/dom.ts", "../src/components.ts", "../src/screens/connect.ts", "../src/screens/done.ts", "../src/screens/find.ts", "../src/screens/home.ts", "../src/presets.ts", "../src/screens/options.ts", "../src/theme.ts", "../src/app.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @file apps/frontend/src/api.ts\n * @description What the plugin's API sends and takes, and the call to it: see src/setup/setupApi.ts in the plugin.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { DaikinCapabilities, DaikinDevice } from 'daikin-local-control';\n\nimport type { DaikinDeviceExposure } from '../../../src/config.js';\nimport type { DiscoveredDevice, SetupDevice, SetupReplies, SetupReply } from '../../../src/setup/setupApi.js';\n\n// The types are the plugin's own, so the page and the plugin cannot drift apart. They are only types: nothing of the\n// plugin is loaded in the browser. The page has its own words for some of them.\nexport type { FanSpeed, SpecialMode, SwingMode } from 'daikin-local-control';\nexport type { AvailableControls } from '../../../src/controls.js';\nexport type { ProbeResult } from '../../../src/setup/probe.js';\nexport type { SetupState } from '../../../src/setup/setupApi.js';\n\nexport type Capabilities = DaikinCapabilities;\nexport type Adapter = DaikinDevice;\nexport type Exposure = DaikinDeviceExposure;\nexport type StoredDevice = SetupDevice;\nexport type FoundAdapter = DiscoveredDevice;\n\n/** A request the plugin serves, by its method and its path after `api/`. */\nexport type Route = keyof SetupReplies;\n\n/** The reply to a request, with what the plugin sends back when the request worked. */\nexport type Reply<R extends Route> = ({ ok: true } & SetupReplies[R]) | Extract<SetupReply, { ok: false }>;\n\n/**\n * Call the plugin.\n *\n * @param route The method and the path, such as `POST probe`.\n * @param body The JSON body.\n * @param query The query string, without its `?`.\n * @returns The reply, with `ok: false` and the reason when the call did not work.\n */\nexport async function api<R extends Route>(route: R, body?: unknown, query?: string): Promise<Reply<R>> {\n try {\n const [method, path] = route.split(' ');\n const init: RequestInit = { method };\n if (body !== undefined) {\n init.headers = { 'Content-Type': 'application/json' };\n init.body = JSON.stringify(body);\n }\n const response = await fetch(new URL(`api/${path}${query === undefined ? '' : `?${query}`}`, document.baseURI), init);\n if (response.status === 204) return { ok: true } as Reply<R>;\n if (response.status === 503) return { ok: false, error: 'The plugin is not running. Start it in Matterbridge, then reload this page.' };\n const reply = (await response.json()) as Reply<R> | { error?: string };\n if ('ok' in reply) return reply;\n return { ok: false, error: reply.error ?? `The plugin answered with status ${response.status}` };\n } catch {\n return { ok: false, error: 'Matterbridge did not answer. Check that it is running, then try again.' };\n }\n}\n", "/**\n * @file apps/frontend/src/exposure.ts\n * @description How a device appears in Matter: the choices a draft starts with.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Exposure, StoredDevice } from './api.js';\nimport type { Draft } from './state.js';\n\n/** Copy the choices of how a device appears in Matter, and nothing else. */\nexport function pickExposure(source: Exposure): Exposure {\n const { fanControl, fanSpeedWhenHidden, swingControl, swingModeWhenHidden, hasDryModeSwitch, hasFanModeSwitch, hasFanAutoSwitch, hasSeparateFan, specialModeSwitches } = source;\n return {\n fanControl,\n fanSpeedWhenHidden,\n swingControl,\n swingModeWhenHidden,\n hasDryModeSwitch,\n hasFanModeSwitch,\n hasFanAutoSwitch,\n hasSeparateFan,\n specialModeSwitches: [...specialModeSwitches],\n };\n}\n\n// What a new device starts with. What a stored device without a setting gets is the default of `parseDeviceConfig`\n// in the plugin, which stands for how the plugin behaved before that setting existed: the two may differ.\nconst defaultExposure: Exposure = {\n fanControl: 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: false,\n hasFanModeSwitch: false,\n hasFanAutoSwitch: false,\n hasSeparateFan: false,\n specialModeSwitches: [],\n};\n\nexport function draftFromAdapter(adapter: Adapter): Draft {\n return { adapter, name: adapter.name ?? '', isNew: true, ...pickExposure(defaultExposure) };\n}\n\nexport function draftFromStored(device: StoredDevice): Draft {\n const { capabilities, controls, name, macAddress, host, protocol, requiresRegistration, usesHttps, requiresLocalPassword } = device;\n const adapter: Adapter = { host, protocol, name, macAddress, requiresRegistration, usesHttps, requiresLocalPassword };\n return { adapter, name, isNew: false, capabilities, controls, ...pickExposure(device) };\n}\n\n// What a unit the plugin told nothing about can have: no control and no switch.\nexport const noControls: AvailableControls = { fanSteps: [], hasFanAuto: false, hasSwing: false, hasDryMode: false, hasFanMode: false, specialModes: [] };\n", "/**\n * @file apps/frontend/src/state.ts\n * @description What the setup page knows at any moment, and what it reads from that.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Capabilities, Exposure, FoundAdapter, ProbeResult, SetupState } from './api.js';\n\n/** One air conditioner on its way through Connect and Options. */\nexport interface Draft extends Exposure {\n adapter: Adapter;\n name: string;\n isNew: boolean;\n localPassword?: string;\n capabilities?: Capabilities;\n /** What the plugin says the unit leaves to choose in Options. */\n controls?: AvailableControls;\n probe?: ProbeResult;\n}\n\nexport type ScreenName = 'loading' | 'home' | 'find' | 'connect' | 'options' | 'done';\n\nexport const view = {\n screen: 'loading' as ScreenName,\n setup: undefined as SetupState | undefined,\n found: [] as FoundAdapter[],\n selected: new Set<string>(),\n queue: [] as Draft[],\n saved: [] as string[],\n /** The units this run is done with, in the order it went through them. */\n handled: [] as { label: string; isSaved: boolean }[],\n isBusy: false,\n busyText: '',\n error: '',\n confirmingRemoval: '',\n};\n\nexport function currentDraft(): Draft | undefined {\n return view.queue[0];\n}\n\n/** What a unit is called on the page: its name, or its address until it has one. */\nexport function draftLabel(draft: Draft): string {\n return draft.name === '' ? draft.adapter.host : draft.name;\n}\n\n/** Whether an adapter is that of a stored device, which is known by its MAC address. */\nexport function isStored(adapter: Adapter): boolean {\n return (view.setup?.devices ?? []).some((device) => device.macAddress === adapter.macAddress);\n}\n", "/**\n * @file apps/frontend/src/render.ts\n * @description Draws the screen the setup page is on. The screens are handed in by app.ts, so that they and the\n * actions can call for a redraw without this file depending on them.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Child } from './dom.js';\nimport { currentDraft, type ScreenName, view } from './state.js';\n\nexport type Screens = Record<ScreenName, () => Child[]>;\n\nlet screens: Screens | undefined;\n\n/** Tell the page what each screen draws. */\nexport function setScreens(all: Screens): void {\n screens = all;\n}\n\nexport function render(): void {\n const root = document.getElementById('app');\n if (root === null || screens === undefined) return;\n // Keep what the user is typing when the same form redraws around it, and never carry it over to another form.\n const form = `${view.screen}:${currentDraft()?.adapter.host ?? ''}`;\n const isSameForm = root.dataset.form === form;\n const activeId = document.activeElement instanceof HTMLElement ? document.activeElement.id : '';\n const typed = new Map<string, string>();\n for (const input of root.querySelectorAll<HTMLInputElement>('input[type=text], input[type=password]')) {\n if (input.id !== '') typed.set(input.id, input.value);\n }\n root.replaceChildren(...screens[view.screen]().filter((child): child is Node | string => child !== false && child !== null && child !== undefined));\n root.dataset.form = form;\n if (!isSameForm) return;\n for (const [id, value] of typed) {\n const input = document.getElementById(id);\n if (input instanceof HTMLInputElement) input.value = value;\n }\n if (activeId !== '') document.getElementById(activeId)?.focus();\n}\n", "/**\n * @file apps/frontend/src/actions.ts\n * @description What the setup page does: each action calls the plugin, changes the state of the page and redraws it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { api, type StoredDevice } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport { render } from './render.js';\nimport { currentDraft, type Draft, draftLabel, isStored, view } from './state.js';\n\n/**\n * Run a call while the page shows that it is working.\n *\n * @param text What the page says meanwhile.\n * @param task The call.\n * @returns What the call returned.\n */\nasync function busy<T>(text: string, task: () => Promise<T>): Promise<T> {\n view.isBusy = true;\n view.busyText = text;\n view.error = '';\n render();\n try {\n return await task();\n } finally {\n view.isBusy = false;\n render();\n }\n}\n\nexport async function loadState(): Promise<void> {\n const reply = await api('GET state');\n if (!reply.ok) {\n view.error = reply.error;\n view.screen = 'home';\n render();\n return;\n }\n view.setup = reply.state;\n // With nothing set up yet, the page goes straight to finding the air conditioners.\n if (reply.state.devices.length === 0) {\n await search();\n return;\n }\n view.screen = 'home';\n render();\n}\n\nasync function refreshState(): Promise<void> {\n const reply = await api('GET state');\n if (reply.ok) view.setup = reply.state;\n}\n\nexport function goHome(): void {\n view.queue = [];\n view.saved = [];\n view.handled = [];\n view.error = '';\n view.screen = 'home';\n void refreshState().then(render);\n render();\n}\n\nexport async function search(): Promise<void> {\n view.screen = 'find';\n const reply = await busy('Searching your network. This takes about three seconds\u2026', async () => api('POST discover'));\n if (!reply.ok) {\n view.error = `The search did not run: ${reply.error}`;\n render();\n return;\n }\n // Keep the adapters added by address: a search does not find them. One of them may have been set up since.\n await refreshState();\n const byAddress = view.found\n .filter((adapter) => !reply.devices.some((device) => device.host === adapter.host) && view.selected.has(adapter.host))\n .map((adapter) => ({ ...adapter, isConfigured: isStored(adapter) }));\n view.found = [...reply.devices, ...byAddress];\n for (const adapter of reply.devices) {\n if (!adapter.isConfigured && !adapter.hasDuplicateName) view.selected.add(adapter.host);\n }\n render();\n}\n\nexport async function lookUp(host: string): Promise<void> {\n const reply = await busy(`Looking for an adapter at ${host}\u2026`, async () => api('POST identify', { host }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n const isConfigured = isStored(reply.device);\n // Whether the adapter knows this plugin already shows in the Connect step: an address alone does not tell.\n const needsRegistration = reply.device.requiresRegistration === true;\n view.found = [...view.found.filter((adapter) => adapter.host !== reply.device.host), { ...reply.device, isConfigured, hasDuplicateName: false, needsRegistration }];\n if (!isConfigured) view.selected.add(reply.device.host);\n render();\n}\n\nexport function startQueue(drafts: Draft[]): void {\n view.queue = drafts;\n view.saved = [];\n view.handled = [];\n void enterConnect();\n}\n\nasync function enterConnect(): Promise<void> {\n const draft = currentDraft();\n if (draft === undefined) {\n view.screen = view.saved.length > 0 ? 'done' : 'home';\n await refreshState();\n render();\n return;\n }\n view.screen = 'connect';\n view.error = '';\n draft.probe = undefined;\n render();\n await check(draft);\n}\n\nexport async function check(draft: Draft, key?: string): Promise<void> {\n const device = { ...draft.adapter, name: draft.name === '' ? undefined : draft.name };\n const body = { device, localPassword: draft.localPassword, key };\n const text = key === undefined ? `Checking ${draftLabel(draft)}\u2026` : `Registering with ${draftLabel(draft)}\u2026`;\n const reply = await busy(text, async () => api(key === undefined ? 'POST probe' : 'POST register', body));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n draft.probe = reply.result;\n draft.adapter = reply.result.device;\n if (reply.result.status === 'ready') {\n draft.capabilities = reply.result.capabilities;\n draft.controls = reply.result.controls;\n // The name the adapter tells is the name of the device. Only an adapter that has none is named here.\n const told = reply.result.device.name ?? '';\n if (told !== '') draft.name = told;\n if (draft.name !== '') view.screen = 'options';\n }\n render();\n}\n\nexport function skipDraft(): void {\n const skipped = view.queue.shift();\n if (skipped !== undefined) view.handled.push({ label: draftLabel(skipped), isSaved: false });\n void enterConnect();\n}\n\nexport async function saveDraft(draft: Draft): Promise<void> {\n const { adapter, name, localPassword } = draft;\n const device = { ...adapter, name, localPassword, ...pickExposure(draft) };\n const reply = await busy(`Saving ${name}\u2026`, async () => api('POST device', { device }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n view.setup = reply.state;\n view.saved.push(name);\n view.handled.push({ label: name, isSaved: true });\n view.queue.shift();\n await enterConnect();\n}\n\nexport async function removeDevice(device: StoredDevice): Promise<void> {\n const reply = await busy(`Removing ${device.name}\u2026`, async () => api('DELETE device', undefined, `macAddress=${encodeURIComponent(device.macAddress)}`));\n view.confirmingRemoval = '';\n if (!reply.ok) view.error = reply.error;\n await refreshState();\n render();\n}\n", "/**\n * @file apps/frontend/src/dom.ts\n * @description The one helper the setup page builds its elements with.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nexport type Child = Node | string | false | null | undefined;\n\n/**\n * Create an element.\n *\n * @param tag The tag name.\n * @param props `class`, `on<event>` listeners, and anything else as a property when the element has it, else an attribute.\n * @param children Nodes or text; false, null and undefined are skipped.\n * @returns The element.\n */\nexport function h<K extends keyof HTMLElementTagNameMap>(tag: K, props: Record<string, unknown> = {}, ...children: Child[]): HTMLElementTagNameMap[K] {\n const element = document.createElement(tag);\n for (const [key, value] of Object.entries(props)) {\n if (value === undefined || value === false) continue;\n if (key === 'class') element.className = String(value);\n else if (key === 'html') element.innerHTML = String(value);\n else if (key.startsWith('on') && typeof value === 'function') element.addEventListener(key.slice(2), value as EventListener);\n else if (key in element) (element as unknown as Record<string, unknown>)[key] = value;\n else element.setAttribute(key, String(value));\n }\n for (const child of children) {\n if (child !== false && child !== null && child !== undefined) element.append(child);\n }\n return element;\n}\n", "/**\n * @file apps/frontend/src/components.ts\n * @description The pieces more than one screen of the setup page is made of.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { type Child, h } from './dom.js';\nimport { currentDraft, type Draft, draftLabel, type ScreenName, view } from './state.js';\n\nexport function button(label: string, onclick: () => void, kind: 'primary' | 'quiet' | 'danger' = 'quiet', disabled = false): HTMLButtonElement {\n return h('button', { type: 'button', class: `button ${kind}`, onclick, disabled: disabled || view.isBusy }, label);\n}\n\nexport function notice(): Child {\n if (view.isBusy) return h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), view.busyText);\n if (view.error !== '') return h('p', { class: 'notice problem', role: 'alert' }, view.error);\n return null;\n}\n\n/**\n * The units of this run, with the one the page is on marked: shown when a run sets up more than one, so that it is\n * plain which unit the Connect and Options steps are about.\n */\nexport function unitStrip(): Child {\n const current = currentDraft();\n if (current === undefined || !current.isNew || view.handled.length + view.queue.length < 2) return null;\n const doing = view.screen === 'connect' ? 'Connecting' : 'Choosing options';\n return h(\n 'ol',\n { 'class': 'units', 'aria-label': 'Air conditioners in this setup' },\n ...view.handled.map((unit) => h('li', { class: unit.isSaved ? 'done' : 'skipped' }, h('strong', {}, unit.label), h('span', { class: 'meta' }, unit.isSaved ? 'Added' : 'Skipped'))),\n ...view.queue.map((draft, index) =>\n h('li', { 'class': index === 0 ? 'current' : 'ahead', 'aria-current': index === 0 ? 'true' : undefined }, h('strong', {}, draftLabel(draft)), h('span', { class: 'meta' }, index === 0 ? doing : 'Waiting')),\n ),\n );\n}\n\n/** The heading of a step that is about one unit: the unit first, then what the step does with it. */\nexport function unitHeader(draft: Draft, text: string): HTMLElement {\n return h('header', { class: 'unit-header' }, h('h1', {}, draftLabel(draft)), h('p', { class: 'meta' }, text));\n}\n\nexport function steps(): Child {\n if (view.queue.length > 0 && !view.queue.some((draft) => draft.isNew)) return null;\n const names = ['Find', 'Connect', 'Options'];\n const screens: ScreenName[] = ['find', 'connect', 'options'];\n const current = screens.indexOf(view.screen);\n return h(\n 'ol',\n { class: 'steps' },\n ...names.map((name, index) => {\n let state = 'ahead';\n if (index < current) state = 'done';\n else if (index === current) state = 'current';\n return h('li', { 'class': state, 'aria-current': index === current ? 'step' : undefined }, name);\n }),\n );\n}\n", "/**\n * @file apps/frontend/src/screens/connect.ts\n * @description The Connect step: the page checks an adapter and asks for what the adapter wants before it answers.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { check, goHome, skipDraft } from '../actions.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft, view } from '../state.js';\n\n/** Asks for the name of a unit whose adapter has none. */\nfunction nameForm(draft: Draft): HTMLElement {\n const name = h('input', { type: 'text', id: 'device-name', maxLength: 32, autocomplete: 'off' });\n const submit = (): void => {\n const value = name.value.trim();\n if (value === '') return;\n if (view.setup?.devices.some((device) => device.name === value)) {\n view.error = `\"${value}\" is already the name of another air conditioner. Pick a different one.`;\n render();\n return;\n }\n draft.name = value;\n view.error = '';\n view.screen = 'options';\n render();\n };\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'device-name' }, 'Name this air conditioner'),\n h(\n 'p',\n { class: 'help' },\n 'Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead.',\n ),\n h('div', { class: 'inline-form' }, name, button('Continue', submit, 'primary')),\n );\n}\n\nfunction lockCodeForm(draft: Draft, isRejected: boolean): HTMLElement {\n const code = h('input', { type: 'password', id: 'lock-code', inputMode: 'numeric', autocomplete: 'off', maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'lock-code' }, 'Child lock code'),\n h('p', { class: 'help' }, 'The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter.'),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that code.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n code,\n button(\n 'Check the code',\n () => {\n if (code.value === '') return;\n draft.localPassword = code.value;\n void check(draft);\n },\n 'primary',\n ),\n ),\n );\n}\n\nfunction keyForm(draft: Draft, isRejected: boolean): HTMLElement {\n const key = h('input', { type: 'text', id: 'adapter-key', autocomplete: 'off', spellcheck: false, maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'adapter-key' }, 'KEY on the adapter'),\n h(\n 'p',\n { class: 'help' },\n 'This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored.',\n ),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that KEY.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n key,\n button(\n 'Register',\n () => {\n if (key.value.trim() !== '') void check(draft, key.value.trim());\n },\n 'primary',\n ),\n ),\n );\n}\n\n/** What the adapter wants before the page can go on, or why the page cannot: nothing while the check has no answer. */\nfunction credentialForm(draft: Draft): Child {\n const status = draft.probe?.status;\n if (status === 'ready' && draft.name === '') return nameForm(draft);\n if (status === 'localPasswordRequired' || status === 'localPasswordRejected') return lockCodeForm(draft, status === 'localPasswordRejected');\n if (status === 'registrationRequired' || status === 'registrationRejected') return keyForm(draft, status === 'registrationRejected');\n if (status === 'unreachable') {\n return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} did not answer at ${draft.adapter.host}. Check that it has power and is on the network, then try again.`);\n }\n if (status === 'failed') return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} answered with an error: ${draft.probe?.message ?? 'unknown'}`);\n return null;\n}\n\nexport function connectScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const canRetry = draft.probe?.status === 'unreachable' || draft.probe?.status === 'failed' || (view.error !== '' && draft.name !== '');\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Connecting to its adapter at ${draft.adapter.host}.`),\n notice(),\n view.isBusy ? null : credentialForm(draft),\n h(\n 'div',\n { class: 'footer' },\n canRetry ? button('Try again', () => void check(draft)) : null,\n button(draft.isNew && view.queue.length > 1 ? 'Skip this one' : 'Cancel', draft.isNew && view.queue.length > 1 ? skipDraft : goHome),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/done.ts\n * @description The screen a run ends on: what it set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome } from '../actions.js';\nimport { button } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { view } from '../state.js';\n\nexport function doneScreen(): Child[] {\n const names = view.saved;\n return [\n h('header', {}, h('h1', {}, names.length === 1 ? `${names[0]} is set up` : `${names.length} air conditioners are set up`)),\n h('p', { class: 'lead' }, 'They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page.'),\n h('div', { class: 'footer' }, button('Done', goHome, 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/find.ts\n * @description The Find step: the adapters a search found or an address led to, and which of them to set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, lookUp, search, startQueue } from '../actions.js';\nimport type { FoundAdapter } from '../api.js';\nimport { button, notice, steps } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromAdapter } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction foundRow(adapter: FoundAdapter): HTMLElement {\n const isSelectable = !adapter.isConfigured && !adapter.hasDuplicateName;\n const notes: string[] = [];\n if (adapter.isConfigured) notes.push('Already set up.');\n if (adapter.hasDuplicateName) notes.push('Another adapter has the same name. Give each its own name in its Daikin app, then search again.');\n if (adapter.name === undefined) notes.push('It has not told its name yet. If it has none, you will name it after connecting.');\n if (isSelectable && adapter.needsRegistration) notes.push('Asks for the KEY printed on its label.');\n else if (isSelectable && adapter.requiresRegistration) notes.push('Already registered with this plugin, so it will not ask for its KEY.');\n if (isSelectable && adapter.requiresLocalPassword) notes.push('Asks for its child lock code.');\n const checkbox = h('input', {\n type: 'checkbox',\n id: `found-${adapter.host}`,\n checked: isSelectable && view.selected.has(adapter.host),\n disabled: !isSelectable,\n onchange: () => {\n if (checkbox.checked) view.selected.add(adapter.host);\n else view.selected.delete(adapter.host);\n render();\n },\n });\n return h(\n 'li',\n { class: isSelectable ? 'found' : 'found unavailable' },\n h(\n 'label',\n { for: `found-${adapter.host}` },\n checkbox,\n h('span', {}, h('strong', {}, adapter.name ?? 'Unnamed adapter'), h('span', { class: 'meta' }, ` ${adapter.host}`), ...notes.map((note) => h('span', { class: 'meta note' }, note))),\n ),\n );\n}\n\nexport function findScreen(): Child[] {\n const chosen = view.found.filter((adapter) => view.selected.has(adapter.host) && !adapter.isConfigured && !adapter.hasDuplicateName);\n const host = h('input', { type: 'text', id: 'host', placeholder: '192.168.1.50', spellcheck: false, autocomplete: 'off' });\n const lookUpHost = (): void => {\n if (host.value.trim() !== '') void lookUp(host.value.trim());\n };\n host.addEventListener('keydown', (event) => {\n if (event.key === 'Enter') lookUpHost();\n });\n const continueLabel = chosen.length === 1 ? 'Set up 1 air conditioner' : `Set up ${chosen.length} air conditioners`;\n return [\n steps(),\n h('header', {}, h('h1', {}, 'Find your air conditioners')),\n notice(),\n view.found.length > 0 ? h('ul', { class: 'found-list' }, ...view.found.map(foundRow)) : null,\n !view.isBusy && view.found.length === 0\n ? h('p', { class: 'empty' }, 'No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below.')\n : null,\n h(\n 'details',\n { class: 'manual', open: !view.isBusy && view.found.length === 0 },\n h('summary', {}, \"Can't see your air conditioner?\"),\n h('p', { class: 'help' }, 'Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address.'),\n h('div', { class: 'inline-form' }, h('label', { for: 'host', class: 'visually-hidden' }, 'Adapter address'), host, button('Look it up', lookUpHost)),\n ),\n h(\n 'div',\n { class: 'footer' },\n button('Search again', () => void search()),\n button('Cancel', goHome),\n button(continueLabel, () => startQueue(chosen.map(draftFromAdapter)), 'primary', chosen.length === 0),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/home.ts\n * @description The home screen: the air conditioners that are set up, and the way to add more.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState, removeDevice, search, startQueue } from '../actions.js';\nimport type { StoredDevice } from '../api.js';\nimport { button, notice } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromStored } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction describeExposure(device: StoredDevice): string {\n // What the unit cannot do is left out, whatever is stored for it. A unit the plugin has not asked yet is described as stored.\n const hasFan = device.controls === undefined || device.controls.fanSteps.length > 0;\n const hasSwing = device.controls === undefined || device.controls.hasSwing;\n const fanPlace = device.hasSeparateFan ? ' on a fan of its own' : '';\n const fan = device.fanControl === 'hidden' ? 'fan speed not shown' : `fan speed as ${device.fanControl === 'steps' ? 'steps' : 'a percentage'}${fanPlace}`;\n const swing = device.swingControl === 'hidden' ? 'swing not shown' : 'swing as switches';\n const controls = [hasFan ? fan : '', hasSwing ? swing : ''].filter((text) => text !== '').join(', ');\n const switches = [device.hasDryModeSwitch ? 'dry mode' : '', device.hasFanModeSwitch ? 'fan mode' : '', device.hasFanAutoSwitch ? 'auto fan' : '', ...device.specialModeSwitches].filter(\n (name) => name !== '',\n );\n const extra = switches.length === 0 ? '' : `a switch for ${switches.join(', ')}`;\n if (controls === '') return extra === '' ? 'power, mode and temperature' : extra;\n return extra === '' ? controls : `${controls}, and ${extra}`;\n}\n\nfunction deviceRow(device: StoredDevice): HTMLElement {\n const statusText = { online: 'Online', offline: 'Not answering', stopped: 'Not started' }[device.status];\n const isConfirming = view.confirmingRemoval === device.macAddress;\n return h(\n 'li',\n { class: 'device' },\n h(\n 'div',\n { class: 'device-main' },\n h('h3', {}, device.name),\n h('p', { class: 'meta' }, h('span', { class: `status ${device.status}` }, statusText), ` at ${device.host}. In Matter: ${describeExposure(device)}.`),\n device.status !== 'online' && device.error ? h('p', { class: 'meta problem-text' }, device.error) : null,\n ),\n isConfirming\n ? h(\n 'div',\n { class: 'actions' },\n h('span', { class: 'meta' }, 'Remove it from Matter too?'),\n button('Remove', () => void removeDevice(device), 'danger'),\n button('Keep', () => {\n view.confirmingRemoval = '';\n render();\n }),\n )\n : h(\n 'div',\n { class: 'actions' },\n // Either way the unit is asked first what it accepts now, so the options offered are the ones it has today.\n button(device.status === 'online' ? 'Options' : 'Reconnect', () => startQueue([draftFromStored(device)])),\n button('Remove', () => {\n view.confirmingRemoval = device.macAddress;\n render();\n }),\n ),\n );\n}\n\nexport function homeScreen(): Child[] {\n const devices = view.setup?.devices ?? [];\n return [\n h('header', {}, h('h1', {}, 'Daikin air conditioners'), view.setup ? button('Add air conditioners', () => void search(), 'primary') : null),\n notice(),\n devices.length === 0 && view.setup\n ? h('p', { class: 'empty' }, 'No air conditioner is set up yet. Add one to control it from Matter.')\n : h('ul', { class: 'devices' }, ...devices.map(deviceRow)),\n view.setup ? null : button('Try again', () => void loadState(), 'primary'),\n ];\n}\n", "/**\n * @file apps/frontend/src/presets.ts\n * @description The presets of the Options step: each fills in the choices of how a device appears in Matter.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { AvailableControls, Exposure } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport type { Draft } from './state.js';\n\nexport type PresetName = 'apple' | 'google' | 'full' | 'minimal';\n\nexport const presets: Record<PresetName, { title: string; text: string }> = {\n apple: { title: 'Apple Home', text: 'Fan speed as a percentage, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan.' },\n google: { title: 'Google Home', text: 'Fan speed as steps, swing switches and a dry mode switch.' },\n full: { title: 'Full', text: 'Every control and every switch this unit can have, special modes included.' },\n minimal: { title: 'Minimal', text: 'No fan control, no swing control and no switches. The fan and the swing stay as they are.' },\n};\n\n/** The choices a preset makes for a unit: never a switch or a control the unit has no use for. */\nexport function presetExposure(name: PresetName, controls: AvailableControls): Exposure {\n const isMinimal = name === 'minimal';\n return {\n fanControl: isMinimal || controls.fanSteps.length === 0 ? 'hidden' : name === 'apple' ? 'percent' : 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: isMinimal || !controls.hasSwing ? 'hidden' : 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: !isMinimal && controls.hasDryMode,\n hasFanModeSwitch: (name === 'apple' || name === 'full') && controls.hasFanMode,\n hasFanAutoSwitch: (name === 'apple' || name === 'full') && controls.hasFanAuto,\n // No preset takes the fan off the air conditioner: that is for an app that shows no fan speed there.\n hasSeparateFan: false,\n specialModeSwitches: name === 'full' ? [...controls.specialModes] : [],\n };\n}\n\n/** Whether the choices in a draft are exactly those of a preset, leaving aside what the unit has no use for. */\nexport function matchesPreset(draft: Draft, name: PresetName, controls: AvailableControls): boolean {\n const chosen = pickExposure(draft);\n if (controls.fanSteps.length === 0) Object.assign(chosen, { fanControl: 'hidden', fanSpeedWhenHidden: 'keep' });\n // The choice shows nowhere while the fan is hidden.\n if (chosen.fanControl === 'hidden') chosen.hasSeparateFan = false;\n if (!controls.hasSwing) Object.assign(chosen, { swingControl: 'hidden', swingModeWhenHidden: 'keep' });\n chosen.specialModeSwitches = controls.specialModes.filter((mode) => chosen.specialModeSwitches.includes(mode));\n return JSON.stringify(chosen) === JSON.stringify(presetExposure(name, controls));\n}\n", "/**\n * @file apps/frontend/src/screens/options.ts\n * @description The Options step: how the fan, the swing and the extra modes of a unit appear in Matter, one section each.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, saveDraft } from '../actions.js';\nimport type { AvailableControls, Exposure, FanSpeed, SpecialMode, SwingMode } from '../api.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { noControls } from '../exposure.js';\nimport { matchesPreset, presetExposure, type PresetName, presets } from '../presets.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft } from '../state.js';\n\nconst fanSpeedLabels: Record<FanSpeed, string> = {\n auto: 'Auto',\n silent: 'Silent',\n level1: 'Level 1',\n level2: 'Level 2',\n level3: 'Level 3',\n level4: 'Level 4',\n level5: 'Level 5',\n};\n\nconst swingModeLabels: Record<SwingMode, string> = {\n off: 'No swing',\n vertical: 'Up and down',\n horizontal: 'Left and right',\n both: 'Up, down, left and right',\n};\n\nconst specialModeTexts: Record<SpecialMode, { title: string; text: string }> = {\n powerful: { title: 'Powerful', text: 'Runs the unit at full power for a while.' },\n econo: { title: 'Econo', text: 'Limits how much power the unit draws.' },\n streamer: { title: 'Streamer', text: 'Runs the air purifying of the unit.' },\n};\n\n// Static drawings, so innerHTML is safe for them. Everything that comes from the network goes through text nodes.\nconst glyphs = {\n steps:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect x=\"2\" y=\"20\" width=\"8\" height=\"6\"/><rect x=\"14\" y=\"14\" width=\"8\" height=\"12\"/><rect x=\"26\" y=\"8\" width=\"8\" height=\"18\"/><rect x=\"38\" y=\"2\" width=\"8\" height=\"24\"/></svg>',\n percent: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path d=\"M2 26 L46 2 L46 26 Z\"/></svg>',\n hidden: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M6 14 H42\" stroke-dasharray=\"3 5\"/></svg>',\n switches:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19\"/><path class=\"stroke\" d=\"M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19\"/></svg>',\n};\n\nfunction update(draft: Draft, change: Partial<Exposure>): void {\n Object.assign(draft, change);\n render();\n}\n\nfunction choice<T extends string>(group: string, value: T, current: T, title: string, text: string, glyph: string, onpick: (value: T) => void): HTMLElement {\n const id = `${group}-${value}`;\n const input = h('input', { type: 'radio', name: group, id, checked: value === current, onchange: () => onpick(value) });\n return h('label', { class: 'choice pictured', for: id }, input, h('span', { class: 'glyph', html: glyph }), h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction toggle(id: string, isOn: boolean, title: string, text: string, onpick: (isOn: boolean) => void): HTMLElement {\n const input = h('input', { type: 'checkbox', id, checked: isOn, onchange: () => onpick(input.checked) });\n return h('label', { class: 'choice', for: id }, input, h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction presetSection(draft: Draft, controls: AvailableControls): HTMLElement {\n return h(\n 'section',\n { class: 'presets' },\n h('h2', {}, 'Start from a preset'),\n h(\n 'div',\n { class: 'preset-list' },\n ...(Object.keys(presets) as PresetName[]).map((name) =>\n h(\n 'button',\n { 'type': 'button', 'class': 'preset', 'aria-pressed': String(matchesPreset(draft, name, controls)), 'onclick': () => update(draft, presetExposure(name, controls)) },\n h('strong', {}, presets[name].title),\n h('span', { class: 'meta' }, presets[name].text),\n ),\n ),\n ),\n h('p', { class: 'help' }, 'A preset fills in the choices below for what this unit can do. Change any of them afterwards.'),\n );\n}\n\nfunction fanSection(draft: Draft, controls: AvailableControls): HTMLElement {\n const fanSpeeds = draft.capabilities?.fanSpeeds ?? [];\n const stepCount = controls.fanSteps.length;\n if (stepCount === 0) return h('section', {}, h('h2', {}, 'Fan speed'), h('p', { class: 'help' }, 'This unit does not let apps set its fan speed, so there is nothing to choose.'));\n // A fixed fan speed is sent with every command, so a switch for auto would not hold.\n const canSwitchFanAuto = controls.hasFanAuto && !(draft.fanControl === 'hidden' && draft.fanSpeedWhenHidden !== 'keep');\n return h(\n 'section',\n {},\n h('h2', {}, 'Fan speed'),\n h(\n 'div',\n { class: 'choices' },\n choice('fan', 'steps', draft.fanControl, 'Steps', `${stepCount} speeds, one for each speed of the unit${fanSpeeds.includes('auto') ? ', and auto' : ''}.`, glyphs.steps, (fanControl) =>\n update(draft, { fanControl }),\n ),\n choice('fan', 'percent', draft.fanControl, 'Percentage', 'A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.', glyphs.percent, (fanControl) => update(draft, { fanControl })),\n choice('fan', 'hidden', draft.fanControl, 'Not shown in Matter', 'No fan control. Pick below what the plugin does with the fan speed instead.', glyphs.hidden, (fanControl) =>\n update(draft, { fanControl }),\n ),\n ),\n draft.fanControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'fan-fixed' }, 'When the plugin sends a command, the fan speed'),\n h(\n 'select',\n { id: 'fan-fixed', onchange: (event: Event) => update(draft, { fanSpeedWhenHidden: (event.target as HTMLSelectElement).value as Exposure['fanSpeedWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.fanSpeedWhenHidden === 'keep' }, 'stays as it is'),\n ...fanSpeeds.map((speed) => h('option', { value: speed, selected: draft.fanSpeedWhenHidden === speed }, `is set to ${fanSpeedLabels[speed]}`)),\n ),\n )\n : null,\n draft.fanControl === 'hidden'\n ? null\n : toggle(\n 'separate-fan',\n draft.hasSeparateFan,\n 'Show the fan as a device of its own',\n `It appears as \"${draft.name} Fan\", and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner. It always shows the speed the unit holds, also while the unit is off.`,\n (hasSeparateFan) => update(draft, { hasSeparateFan }),\n ),\n canSwitchFanAuto\n ? toggle(\n 'fan-auto-switch',\n draft.hasFanAutoSwitch,\n 'Add a switch for auto fan speed',\n `It appears as \"${draft.name} Auto Fan\". On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,\n (hasFanAutoSwitch) => update(draft, { hasFanAutoSwitch }),\n )\n : null,\n );\n}\n\nfunction swingSection(draft: Draft): HTMLElement {\n const swingModes = draft.capabilities?.swingModes ?? [];\n if (swingModes.length === 0) return h('section', {}, h('h2', {}, 'Swing'), h('p', { class: 'help' }, 'This unit does not let apps move its louvers, so there is nothing to choose.'));\n const hasVertical = swingModes.includes('vertical');\n const hasHorizontal = swingModes.includes('horizontal');\n let switchText = 'One switch, for the up and down swing.';\n if (hasVertical && hasHorizontal) switchText = 'Two switches: one for the up and down swing, one for left and right.';\n else if (hasHorizontal) switchText = 'One switch, for the left and right swing.';\n return h(\n 'section',\n {},\n h('h2', {}, 'Swing'),\n h(\n 'div',\n { class: 'choices' },\n choice('swing', 'switches', draft.swingControl, 'Switches', switchText, glyphs.switches, (swingControl) => update(draft, { swingControl })),\n choice('swing', 'hidden', draft.swingControl, 'Not shown in Matter', 'No swing control. Pick below what the plugin does with the swing instead.', glyphs.hidden, (swingControl) =>\n update(draft, { swingControl }),\n ),\n ),\n draft.swingControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'swing-fixed' }, 'When the plugin sends a command, the swing'),\n h(\n 'select',\n { id: 'swing-fixed', onchange: (event: Event) => update(draft, { swingModeWhenHidden: (event.target as HTMLSelectElement).value as Exposure['swingModeWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.swingModeWhenHidden === 'keep' }, 'stays as it is'),\n ...swingModes.map((mode) => h('option', { value: mode, selected: draft.swingModeWhenHidden === mode }, `is set to: ${swingModeLabels[mode]}`)),\n ),\n )\n : null,\n );\n}\n\nfunction modeSection(draft: Draft, controls: AvailableControls): Child {\n if (!controls.hasDryMode && !controls.hasFanMode) return null;\n const modes = draft.capabilities?.modes ?? [];\n return h(\n 'section',\n {},\n h('h2', {}, 'Dry and fan modes'),\n h(\n 'p',\n { class: 'help' },\n `These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${modes.includes('cool') ? 'cool' : 'heat'}.`,\n ),\n h(\n 'div',\n { class: 'choices' },\n controls.hasDryMode\n ? toggle('dry-mode-switch', draft.hasDryModeSwitch, 'Add a switch for dry mode', `It appears as \"${draft.name} Dry Mode\".`, (hasDryModeSwitch) => update(draft, { hasDryModeSwitch }))\n : null,\n controls.hasFanMode\n ? toggle('fan-mode-switch', draft.hasFanModeSwitch, 'Add a switch for fan mode', `It appears as \"${draft.name} Fan Mode\".`, (hasFanModeSwitch) => update(draft, { hasFanModeSwitch }))\n : null,\n ),\n );\n}\n\nfunction specialSection(draft: Draft, controls: AvailableControls): Child {\n if (controls.specialModes.length === 0) return null;\n return h(\n 'section',\n {},\n h('h2', {}, 'Special modes'),\n h('p', { class: 'help' }, 'Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in.'),\n h(\n 'div',\n { class: 'choices' },\n ...controls.specialModes.map((mode) =>\n toggle(`special-${mode}`, draft.specialModeSwitches.includes(mode), `Add a switch for ${specialModeTexts[mode].title}`, specialModeTexts[mode].text, (isOn) =>\n update(draft, { specialModeSwitches: controls.specialModes.filter((other) => (other === mode ? isOn : draft.specialModeSwitches.includes(other))) }),\n ),\n ),\n ),\n );\n}\n\nexport function optionsScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const controls = draft.controls ?? noControls;\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Options for the air conditioner at ${draft.adapter.host}. Power, mode and temperature always appear in Matter, and so do the humidity and the outdoor temperature once the unit gives them: choose how the rest appears.`),\n presetSection(draft, controls),\n fanSection(draft, controls),\n swingSection(draft),\n modeSection(draft, controls),\n specialSection(draft, controls),\n draft.isNew ? null : h('p', { class: 'help' }, 'Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change.'),\n notice(),\n h('div', { class: 'footer' }, button('Cancel', goHome), button(draft.isNew ? `Add ${draft.name}` : 'Save options', () => void saveDraft(draft), 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/theme.ts\n * @description Makes the setup page look like the Matterbridge page it is shown in.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\n/** Take the colours and the font of the Matterbridge page this one is shown in. */\nexport function adoptHostTheme(): void {\n try {\n if (window.parent === window) return;\n const host = getComputedStyle(window.parent.document.body);\n for (const name of ['--primary-color', '--div-bg-color', '--div-text-color', '--main-bg-color', '--main-label-color', '--div-border-color', '--main-light-color']) {\n const value = host.getPropertyValue(name).trim();\n if (value !== '') document.documentElement.style.setProperty(`--host${name.slice(1)}`, value);\n }\n document.documentElement.style.setProperty('--host-font', host.fontFamily);\n } catch {\n // Opened on its own or from another origin: the page keeps its own colours.\n }\n}\n", "/**\n * @file apps/frontend/src/app.ts\n * @description The guided setup page of the Matterbridge Daikin Local plugin. Matterbridge serves it at\n * /plugins/matterbridge-daikin-local/ and it talks to the plugin through api/<path> below that address.\n * This file starts the page: the rest is in the files next to it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState } from './actions.js';\nimport { h } from './dom.js';\nimport { render, setScreens } from './render.js';\nimport { connectScreen } from './screens/connect.js';\nimport { doneScreen } from './screens/done.js';\nimport { findScreen } from './screens/find.js';\nimport { homeScreen } from './screens/home.js';\nimport { optionsScreen } from './screens/options.js';\nimport { adoptHostTheme } from './theme.js';\n\nsetScreens({\n loading: () => [h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), 'Loading\u2026')],\n home: homeScreen,\n find: findScreen,\n connect: connectScreen,\n options: optionsScreen,\n done: doneScreen,\n});\n\nadoptHostTheme();\nrender();\nvoid loadState();\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["api", "route", "body", "query", "method", "path", "init", "response", "reply", "pickExposure", "source", "fanControl", "fanSpeedWhenHidden", "swingControl", "swingModeWhenHidden", "hasDryModeSwitch", "hasFanModeSwitch", "hasFanAutoSwitch", "hasSeparateFan", "specialModeSwitches", "defaultExposure", "draftFromAdapter", "adapter", "draftFromStored", "device", "capabilities", "controls", "name", "macAddress", "host", "protocol", "requiresRegistration", "usesHttps", "requiresLocalPassword", "noControls", "view", "currentDraft", "draftLabel", "draft", "isStored", "adapter", "device", "screens", "setScreens", "all", "render", "root", "form", "view", "currentDraft", "isSameForm", "activeId", "typed", "input", "child", "id", "value", "busy", "text", "task", "view", "render", "loadState", "reply", "api", "search", "refreshState", "goHome", "byAddress", "adapter", "device", "isStored", "lookUp", "host", "isConfigured", "needsRegistration", "startQueue", "drafts", "enterConnect", "draft", "currentDraft", "check", "key", "body", "draftLabel", "told", "skipDraft", "skipped", "saveDraft", "name", "localPassword", "pickExposure", "removeDevice", "h", "tag", "props", "children", "element", "key", "value", "child", "button", "label", "onclick", "kind", "disabled", "h", "view", "notice", "unitStrip", "current", "currentDraft", "doing", "unit", "draft", "index", "draftLabel", "unitHeader", "text", "steps", "names", "name", "state", "nameForm", "draft", "name", "h", "submit", "value", "view", "device", "render", "button", "lockCodeForm", "isRejected", "code", "check", "keyForm", "key", "credentialForm", "status", "connectScreen", "currentDraft", "notice", "canRetry", "steps", "unitStrip", "unitHeader", "skipDraft", "goHome", "doneScreen", "names", "view", "h", "button", "goHome", "foundRow", "adapter", "isSelectable", "notes", "checkbox", "h", "view", "render", "note", "findScreen", "chosen", "host", "lookUpHost", "lookUp", "event", "continueLabel", "steps", "notice", "button", "search", "goHome", "startQueue", "draftFromAdapter", "describeExposure", "device", "hasFan", "hasSwing", "fanPlace", "fan", "swing", "controls", "text", "switches", "name", "extra", "deviceRow", "statusText", "isConfirming", "view", "h", "button", "removeDevice", "render", "startQueue", "draftFromStored", "homeScreen", "devices", "search", "notice", "loadState", "presets", "presetExposure", "name", "controls", "isMinimal", "matchesPreset", "draft", "chosen", "pickExposure", "mode", "fanSpeedLabels", "swingModeLabels", "specialModeTexts", "glyphs", "
|
|
4
|
+
"sourcesContent": ["/**\n * @file apps/frontend/src/api.ts\n * @description What the plugin's API sends and takes, and the call to it: see src/setup/setupApi.ts in the plugin.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { DaikinCapabilities, DaikinDevice } from 'daikin-local-control';\n\nimport type { DaikinDeviceExposure } from '../../../src/config.js';\nimport type { DiscoveredDevice, SetupDevice, SetupReplies, SetupReply } from '../../../src/setup/setupApi.js';\n\n// The types are the plugin's own, so the page and the plugin cannot drift apart. They are only types: nothing of the\n// plugin is loaded in the browser. The page has its own words for some of them.\nexport type { FanSpeed, SpecialMode, SwingMode } from 'daikin-local-control';\nexport type { AvailableControls } from '../../../src/controls.js';\nexport type { ProbeResult } from '../../../src/setup/probe.js';\nexport type { SetupState } from '../../../src/setup/setupApi.js';\n\nexport type Capabilities = DaikinCapabilities;\nexport type Adapter = DaikinDevice;\nexport type Exposure = DaikinDeviceExposure;\nexport type StoredDevice = SetupDevice;\nexport type FoundAdapter = DiscoveredDevice;\n\n/** A request the plugin serves, by its method and its path after `api/`. */\nexport type Route = keyof SetupReplies;\n\n/** The reply to a request, with what the plugin sends back when the request worked. */\nexport type Reply<R extends Route> = ({ ok: true } & SetupReplies[R]) | Extract<SetupReply, { ok: false }>;\n\n/**\n * Call the plugin.\n *\n * @param route The method and the path, such as `POST probe`.\n * @param body The JSON body.\n * @param query The query string, without its `?`.\n * @returns The reply, with `ok: false` and the reason when the call did not work.\n */\nexport async function api<R extends Route>(route: R, body?: unknown, query?: string): Promise<Reply<R>> {\n try {\n const [method, path] = route.split(' ');\n const init: RequestInit = { method };\n if (body !== undefined) {\n init.headers = { 'Content-Type': 'application/json' };\n init.body = JSON.stringify(body);\n }\n const response = await fetch(new URL(`api/${path}${query === undefined ? '' : `?${query}`}`, document.baseURI), init);\n if (response.status === 204) return { ok: true } as Reply<R>;\n if (response.status === 503) return { ok: false, error: 'The plugin is not running. Start it in Matterbridge, then reload this page.' };\n const reply = (await response.json()) as Reply<R> | { error?: string };\n if ('ok' in reply) return reply;\n return { ok: false, error: reply.error ?? `The plugin answered with status ${response.status}` };\n } catch {\n return { ok: false, error: 'Matterbridge did not answer. Check that it is running, then try again.' };\n }\n}\n", "/**\n * @file apps/frontend/src/exposure.ts\n * @description How a device appears in Matter: the choices a draft starts with.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Exposure, StoredDevice } from './api.js';\nimport type { Draft } from './state.js';\n\n/** Copy the choices of how a device appears in Matter, and nothing else. */\nexport function pickExposure(source: Exposure): Exposure {\n const { fanControl, fanSpeedWhenHidden, swingControl, swingModeWhenHidden, hasDryModeSwitch, hasFanModeSwitch, hasFanAutoSwitch, hasModeSelect, hasSeparateFan, specialModeSwitches, hasOutdoorTemperatureSensor, hasSeparateHumiditySensor, isComposed } =\n source;\n return {\n fanControl,\n fanSpeedWhenHidden,\n swingControl,\n swingModeWhenHidden,\n hasDryModeSwitch,\n hasFanModeSwitch,\n hasFanAutoSwitch,\n hasModeSelect,\n hasSeparateFan,\n specialModeSwitches: [...specialModeSwitches],\n hasOutdoorTemperatureSensor,\n hasSeparateHumiditySensor,\n isComposed,\n };\n}\n\n// What a new device starts with. What a stored device without a setting gets is the default of `parseDeviceConfig`\n// in the plugin, which stands for how the plugin behaved before that setting existed: the two may differ.\nconst defaultExposure: Exposure = {\n fanControl: 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: false,\n hasFanModeSwitch: false,\n hasFanAutoSwitch: false,\n hasModeSelect: false,\n hasSeparateFan: false,\n specialModeSwitches: [],\n hasOutdoorTemperatureSensor: false,\n hasSeparateHumiditySensor: false,\n isComposed: true,\n};\n\nexport function draftFromAdapter(adapter: Adapter): Draft {\n return { adapter, name: adapter.name ?? '', isNew: true, ...pickExposure(defaultExposure) };\n}\n\nexport function draftFromStored(device: StoredDevice): Draft {\n const { capabilities, controls, name, macAddress, host, protocol, requiresRegistration, usesHttps, requiresLocalPassword } = device;\n const adapter: Adapter = { host, protocol, name, macAddress, requiresRegistration, usesHttps, requiresLocalPassword };\n return { adapter, name, isNew: false, capabilities, controls, ...pickExposure(device) };\n}\n\n// What a unit the plugin told nothing about can have: no control and no switch.\nexport const noControls: AvailableControls = { fanSteps: [], hasFanAuto: false, hasSwing: false, hasDryMode: false, hasFanMode: false, specialModes: [] };\n", "/**\n * @file apps/frontend/src/state.ts\n * @description What the setup page knows at any moment, and what it reads from that.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Capabilities, Exposure, FoundAdapter, ProbeResult, SetupState } from './api.js';\n\n/** One air conditioner on its way through Connect and Options. */\nexport interface Draft extends Exposure {\n adapter: Adapter;\n name: string;\n isNew: boolean;\n localPassword?: string;\n capabilities?: Capabilities;\n /** What the plugin says the unit leaves to choose in Options. */\n controls?: AvailableControls;\n probe?: ProbeResult;\n}\n\nexport type ScreenName = 'loading' | 'home' | 'find' | 'connect' | 'options' | 'done';\n\nexport const view = {\n screen: 'loading' as ScreenName,\n setup: undefined as SetupState | undefined,\n found: [] as FoundAdapter[],\n selected: new Set<string>(),\n queue: [] as Draft[],\n saved: [] as string[],\n /** The units this run is done with, in the order it went through them. */\n handled: [] as { label: string; isSaved: boolean }[],\n isBusy: false,\n busyText: '',\n error: '',\n confirmingRemoval: '',\n};\n\nexport function currentDraft(): Draft | undefined {\n return view.queue[0];\n}\n\n/** What a unit is called on the page: its name, or its address until it has one. */\nexport function draftLabel(draft: Draft): string {\n return draft.name === '' ? draft.adapter.host : draft.name;\n}\n\n/** Whether an adapter is that of a stored device, which is known by its MAC address. */\nexport function isStored(adapter: Adapter): boolean {\n return (view.setup?.devices ?? []).some((device) => device.macAddress === adapter.macAddress);\n}\n", "/**\n * @file apps/frontend/src/render.ts\n * @description Draws the screen the setup page is on. The screens are handed in by app.ts, so that they and the\n * actions can call for a redraw without this file depending on them.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Child } from './dom.js';\nimport { currentDraft, type ScreenName, view } from './state.js';\n\nexport type Screens = Record<ScreenName, () => Child[]>;\n\nlet screens: Screens | undefined;\n\n/** Tell the page what each screen draws. */\nexport function setScreens(all: Screens): void {\n screens = all;\n}\n\nexport function render(): void {\n const root = document.getElementById('app');\n if (root === null || screens === undefined) return;\n // Keep what the user is typing when the same form redraws around it, and never carry it over to another form.\n const form = `${view.screen}:${currentDraft()?.adapter.host ?? ''}`;\n const isSameForm = root.dataset.form === form;\n const activeId = document.activeElement instanceof HTMLElement ? document.activeElement.id : '';\n const typed = new Map<string, string>();\n for (const input of root.querySelectorAll<HTMLInputElement>('input[type=text], input[type=password]')) {\n if (input.id !== '') typed.set(input.id, input.value);\n }\n root.replaceChildren(...screens[view.screen]().filter((child): child is Node | string => child !== false && child !== null && child !== undefined));\n root.dataset.form = form;\n if (!isSameForm) return;\n for (const [id, value] of typed) {\n const input = document.getElementById(id);\n if (input instanceof HTMLInputElement) input.value = value;\n }\n if (activeId !== '') document.getElementById(activeId)?.focus();\n}\n", "/**\n * @file apps/frontend/src/actions.ts\n * @description What the setup page does: each action calls the plugin, changes the state of the page and redraws it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { api, type StoredDevice } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport { render } from './render.js';\nimport { currentDraft, type Draft, draftLabel, isStored, view } from './state.js';\n\n/**\n * Run a call while the page shows that it is working.\n *\n * @param text What the page says meanwhile.\n * @param task The call.\n * @returns What the call returned.\n */\nasync function busy<T>(text: string, task: () => Promise<T>): Promise<T> {\n view.isBusy = true;\n view.busyText = text;\n view.error = '';\n render();\n try {\n return await task();\n } finally {\n view.isBusy = false;\n render();\n }\n}\n\nexport async function loadState(): Promise<void> {\n const reply = await api('GET state');\n if (!reply.ok) {\n view.error = reply.error;\n view.screen = 'home';\n render();\n return;\n }\n view.setup = reply.state;\n // With nothing set up yet, the page goes straight to finding the air conditioners.\n if (reply.state.devices.length === 0) {\n await search();\n return;\n }\n view.screen = 'home';\n render();\n}\n\nasync function refreshState(): Promise<void> {\n const reply = await api('GET state');\n if (reply.ok) view.setup = reply.state;\n}\n\nexport function goHome(): void {\n view.queue = [];\n view.saved = [];\n view.handled = [];\n view.error = '';\n view.screen = 'home';\n void refreshState().then(render);\n render();\n}\n\nexport async function search(): Promise<void> {\n view.screen = 'find';\n const reply = await busy('Searching your network. This takes about three seconds\u2026', async () => api('POST discover'));\n if (!reply.ok) {\n view.error = `The search did not run: ${reply.error}`;\n render();\n return;\n }\n // Keep the adapters added by address: a search does not find them. One of them may have been set up since.\n await refreshState();\n const byAddress = view.found\n .filter((adapter) => !reply.devices.some((device) => device.host === adapter.host) && view.selected.has(adapter.host))\n .map((adapter) => ({ ...adapter, isConfigured: isStored(adapter) }));\n view.found = [...reply.devices, ...byAddress];\n for (const adapter of reply.devices) {\n if (!adapter.isConfigured && !adapter.hasDuplicateName) view.selected.add(adapter.host);\n }\n render();\n}\n\nexport async function lookUp(host: string): Promise<void> {\n const reply = await busy(`Looking for an adapter at ${host}\u2026`, async () => api('POST identify', { host }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n const isConfigured = isStored(reply.device);\n // Whether the adapter knows this plugin already shows in the Connect step: an address alone does not tell.\n const needsRegistration = reply.device.requiresRegistration === true;\n view.found = [...view.found.filter((adapter) => adapter.host !== reply.device.host), { ...reply.device, isConfigured, hasDuplicateName: false, needsRegistration }];\n if (!isConfigured) view.selected.add(reply.device.host);\n render();\n}\n\nexport function startQueue(drafts: Draft[]): void {\n view.queue = drafts;\n view.saved = [];\n view.handled = [];\n void enterConnect();\n}\n\nasync function enterConnect(): Promise<void> {\n const draft = currentDraft();\n if (draft === undefined) {\n view.screen = view.saved.length > 0 ? 'done' : 'home';\n await refreshState();\n render();\n return;\n }\n view.screen = 'connect';\n view.error = '';\n draft.probe = undefined;\n render();\n await check(draft);\n}\n\nexport async function check(draft: Draft, key?: string): Promise<void> {\n const device = { ...draft.adapter, name: draft.name === '' ? undefined : draft.name };\n const body = { device, localPassword: draft.localPassword, key };\n const text = key === undefined ? `Checking ${draftLabel(draft)}\u2026` : `Registering with ${draftLabel(draft)}\u2026`;\n const reply = await busy(text, async () => api(key === undefined ? 'POST probe' : 'POST register', body));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n draft.probe = reply.result;\n draft.adapter = reply.result.device;\n if (reply.result.status === 'ready') {\n draft.capabilities = reply.result.capabilities;\n draft.controls = reply.result.controls;\n // The name the adapter tells is the name of the device. Only an adapter that has none is named here.\n const told = reply.result.device.name ?? '';\n if (told !== '') draft.name = told;\n if (draft.name !== '') view.screen = 'options';\n }\n render();\n}\n\nexport function skipDraft(): void {\n const skipped = view.queue.shift();\n if (skipped !== undefined) view.handled.push({ label: draftLabel(skipped), isSaved: false });\n void enterConnect();\n}\n\nexport async function saveDraft(draft: Draft): Promise<void> {\n const { adapter, name, localPassword } = draft;\n const device = { ...adapter, name, localPassword, ...pickExposure(draft) };\n const reply = await busy(`Saving ${name}\u2026`, async () => api('POST device', { device }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n view.setup = reply.state;\n view.saved.push(name);\n view.handled.push({ label: name, isSaved: true });\n view.queue.shift();\n await enterConnect();\n}\n\nexport async function removeDevice(device: StoredDevice): Promise<void> {\n const reply = await busy(`Removing ${device.name}\u2026`, async () => api('DELETE device', undefined, `macAddress=${encodeURIComponent(device.macAddress)}`));\n view.confirmingRemoval = '';\n if (!reply.ok) view.error = reply.error;\n await refreshState();\n render();\n}\n", "/**\n * @file apps/frontend/src/dom.ts\n * @description The one helper the setup page builds its elements with.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nexport type Child = Node | string | false | null | undefined;\n\n/**\n * Create an element.\n *\n * @param tag The tag name.\n * @param props `class`, `on<event>` listeners, and anything else as a property when the element has it, else an attribute.\n * @param children Nodes or text; false, null and undefined are skipped.\n * @returns The element.\n */\nexport function h<K extends keyof HTMLElementTagNameMap>(tag: K, props: Record<string, unknown> = {}, ...children: Child[]): HTMLElementTagNameMap[K] {\n const element = document.createElement(tag);\n for (const [key, value] of Object.entries(props)) {\n if (value === undefined || value === false) continue;\n if (key === 'class') element.className = String(value);\n else if (key === 'html') element.innerHTML = String(value);\n else if (key.startsWith('on') && typeof value === 'function') element.addEventListener(key.slice(2), value as EventListener);\n else if (key in element) (element as unknown as Record<string, unknown>)[key] = value;\n else element.setAttribute(key, String(value));\n }\n for (const child of children) {\n if (child !== false && child !== null && child !== undefined) element.append(child);\n }\n return element;\n}\n", "/**\n * @file apps/frontend/src/components.ts\n * @description The pieces more than one screen of the setup page is made of.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { type Child, h } from './dom.js';\nimport { currentDraft, type Draft, draftLabel, type ScreenName, view } from './state.js';\n\nexport function button(label: string, onclick: () => void, kind: 'primary' | 'quiet' | 'danger' = 'quiet', disabled = false): HTMLButtonElement {\n return h('button', { type: 'button', class: `button ${kind}`, onclick, disabled: disabled || view.isBusy }, label);\n}\n\nexport function notice(): Child {\n if (view.isBusy) return h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), view.busyText);\n if (view.error !== '') return h('p', { class: 'notice problem', role: 'alert' }, view.error);\n return null;\n}\n\n/**\n * The units of this run, with the one the page is on marked: shown when a run sets up more than one, so that it is\n * plain which unit the Connect and Options steps are about.\n */\nexport function unitStrip(): Child {\n const current = currentDraft();\n if (current === undefined || !current.isNew || view.handled.length + view.queue.length < 2) return null;\n const doing = view.screen === 'connect' ? 'Connecting' : 'Choosing options';\n return h(\n 'ol',\n { 'class': 'units', 'aria-label': 'Air conditioners in this setup' },\n ...view.handled.map((unit) => h('li', { class: unit.isSaved ? 'done' : 'skipped' }, h('strong', {}, unit.label), h('span', { class: 'meta' }, unit.isSaved ? 'Added' : 'Skipped'))),\n ...view.queue.map((draft, index) =>\n h('li', { 'class': index === 0 ? 'current' : 'ahead', 'aria-current': index === 0 ? 'true' : undefined }, h('strong', {}, draftLabel(draft)), h('span', { class: 'meta' }, index === 0 ? doing : 'Waiting')),\n ),\n );\n}\n\n/** The heading of a step that is about one unit: the unit first, then what the step does with it. */\nexport function unitHeader(draft: Draft, text: string): HTMLElement {\n return h('header', { class: 'unit-header' }, h('h1', {}, draftLabel(draft)), h('p', { class: 'meta' }, text));\n}\n\nexport function steps(): Child {\n if (view.queue.length > 0 && !view.queue.some((draft) => draft.isNew)) return null;\n const names = ['Find', 'Connect', 'Options'];\n const screens: ScreenName[] = ['find', 'connect', 'options'];\n const current = screens.indexOf(view.screen);\n return h(\n 'ol',\n { class: 'steps' },\n ...names.map((name, index) => {\n let state = 'ahead';\n if (index < current) state = 'done';\n else if (index === current) state = 'current';\n return h('li', { 'class': state, 'aria-current': index === current ? 'step' : undefined }, name);\n }),\n );\n}\n", "/**\n * @file apps/frontend/src/screens/connect.ts\n * @description The Connect step: the page checks an adapter and asks for what the adapter wants before it answers.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { check, goHome, skipDraft } from '../actions.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft, view } from '../state.js';\n\n/** Asks for the name of a unit whose adapter has none. */\nfunction nameForm(draft: Draft): HTMLElement {\n const name = h('input', { type: 'text', id: 'device-name', maxLength: 32, autocomplete: 'off' });\n const submit = (): void => {\n const value = name.value.trim();\n if (value === '') return;\n if (view.setup?.devices.some((device) => device.name === value)) {\n view.error = `\"${value}\" is already the name of another air conditioner. Pick a different one.`;\n render();\n return;\n }\n draft.name = value;\n view.error = '';\n view.screen = 'options';\n render();\n };\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'device-name' }, 'Name this air conditioner'),\n h(\n 'p',\n { class: 'help' },\n 'Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead.',\n ),\n h('div', { class: 'inline-form' }, name, button('Continue', submit, 'primary')),\n );\n}\n\nfunction lockCodeForm(draft: Draft, isRejected: boolean): HTMLElement {\n const code = h('input', { type: 'password', id: 'lock-code', inputMode: 'numeric', autocomplete: 'off', maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'lock-code' }, 'Child lock code'),\n h('p', { class: 'help' }, 'The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter.'),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that code.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n code,\n button(\n 'Check the code',\n () => {\n if (code.value === '') return;\n draft.localPassword = code.value;\n void check(draft);\n },\n 'primary',\n ),\n ),\n );\n}\n\nfunction keyForm(draft: Draft, isRejected: boolean): HTMLElement {\n const key = h('input', { type: 'text', id: 'adapter-key', autocomplete: 'off', spellcheck: false, maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'adapter-key' }, 'KEY on the adapter'),\n h(\n 'p',\n { class: 'help' },\n 'This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored.',\n ),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that KEY.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n key,\n button(\n 'Register',\n () => {\n if (key.value.trim() !== '') void check(draft, key.value.trim());\n },\n 'primary',\n ),\n ),\n );\n}\n\n/** What the adapter wants before the page can go on, or why the page cannot: nothing while the check has no answer. */\nfunction credentialForm(draft: Draft): Child {\n const status = draft.probe?.status;\n if (status === 'ready' && draft.name === '') return nameForm(draft);\n if (status === 'localPasswordRequired' || status === 'localPasswordRejected') return lockCodeForm(draft, status === 'localPasswordRejected');\n if (status === 'registrationRequired' || status === 'registrationRejected') return keyForm(draft, status === 'registrationRejected');\n if (status === 'unreachable') {\n return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} did not answer at ${draft.adapter.host}. Check that it has power and is on the network, then try again.`);\n }\n if (status === 'failed') return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} answered with an error: ${draft.probe?.message ?? 'unknown'}`);\n return null;\n}\n\nexport function connectScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const canRetry = draft.probe?.status === 'unreachable' || draft.probe?.status === 'failed' || (view.error !== '' && draft.name !== '');\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Connecting to its adapter at ${draft.adapter.host}.`),\n notice(),\n view.isBusy ? null : credentialForm(draft),\n h(\n 'div',\n { class: 'footer' },\n canRetry ? button('Try again', () => void check(draft)) : null,\n button(draft.isNew && view.queue.length > 1 ? 'Skip this one' : 'Cancel', draft.isNew && view.queue.length > 1 ? skipDraft : goHome),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/done.ts\n * @description The screen a run ends on: what it set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome } from '../actions.js';\nimport { button } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { view } from '../state.js';\n\nexport function doneScreen(): Child[] {\n const names = view.saved;\n return [\n h('header', {}, h('h1', {}, names.length === 1 ? `${names[0]} is set up` : `${names.length} air conditioners are set up`)),\n h('p', { class: 'lead' }, 'They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page.'),\n h('div', { class: 'footer' }, button('Done', goHome, 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/find.ts\n * @description The Find step: the adapters a search found or an address led to, and which of them to set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, lookUp, search, startQueue } from '../actions.js';\nimport type { FoundAdapter } from '../api.js';\nimport { button, notice, steps } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromAdapter } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction foundRow(adapter: FoundAdapter): HTMLElement {\n const isSelectable = !adapter.isConfigured && !adapter.hasDuplicateName;\n const notes: string[] = [];\n if (adapter.isConfigured) notes.push('Already set up.');\n if (adapter.hasDuplicateName) notes.push('Another adapter has the same name. Give each its own name in its Daikin app, then search again.');\n if (adapter.name === undefined) notes.push('It has not told its name yet. If it has none, you will name it after connecting.');\n if (isSelectable && adapter.needsRegistration) notes.push('Asks for the KEY printed on its label.');\n else if (isSelectable && adapter.requiresRegistration) notes.push('Already registered with this plugin, so it will not ask for its KEY.');\n if (isSelectable && adapter.requiresLocalPassword) notes.push('Asks for its child lock code.');\n const checkbox = h('input', {\n type: 'checkbox',\n id: `found-${adapter.host}`,\n checked: isSelectable && view.selected.has(adapter.host),\n disabled: !isSelectable,\n onchange: () => {\n if (checkbox.checked) view.selected.add(adapter.host);\n else view.selected.delete(adapter.host);\n render();\n },\n });\n return h(\n 'li',\n { class: isSelectable ? 'found' : 'found unavailable' },\n h(\n 'label',\n { for: `found-${adapter.host}` },\n checkbox,\n h('span', {}, h('strong', {}, adapter.name ?? 'Unnamed adapter'), h('span', { class: 'meta' }, ` ${adapter.host}`), ...notes.map((note) => h('span', { class: 'meta note' }, note))),\n ),\n );\n}\n\nexport function findScreen(): Child[] {\n const chosen = view.found.filter((adapter) => view.selected.has(adapter.host) && !adapter.isConfigured && !adapter.hasDuplicateName);\n const host = h('input', { type: 'text', id: 'host', placeholder: '192.168.1.50', spellcheck: false, autocomplete: 'off' });\n const lookUpHost = (): void => {\n if (host.value.trim() !== '') void lookUp(host.value.trim());\n };\n host.addEventListener('keydown', (event) => {\n if (event.key === 'Enter') lookUpHost();\n });\n const continueLabel = chosen.length === 1 ? 'Set up 1 air conditioner' : `Set up ${chosen.length} air conditioners`;\n return [\n steps(),\n h('header', {}, h('h1', {}, 'Find your air conditioners')),\n notice(),\n view.found.length > 0 ? h('ul', { class: 'found-list' }, ...view.found.map(foundRow)) : null,\n !view.isBusy && view.found.length === 0\n ? h('p', { class: 'empty' }, 'No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below.')\n : null,\n h(\n 'details',\n { class: 'manual', open: !view.isBusy && view.found.length === 0 },\n h('summary', {}, \"Can't see your air conditioner?\"),\n h('p', { class: 'help' }, 'Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address.'),\n h('div', { class: 'inline-form' }, h('label', { for: 'host', class: 'visually-hidden' }, 'Adapter address'), host, button('Look it up', lookUpHost)),\n ),\n h(\n 'div',\n { class: 'footer' },\n button('Search again', () => void search()),\n button('Cancel', goHome),\n button(continueLabel, () => startQueue(chosen.map(draftFromAdapter)), 'primary', chosen.length === 0),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/home.ts\n * @description The home screen: the air conditioners that are set up, and the way to add more.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState, removeDevice, search, startQueue } from '../actions.js';\nimport type { StoredDevice } from '../api.js';\nimport { button, notice } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromStored } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction describeExposure(device: StoredDevice): string {\n // What the unit cannot do is left out, whatever is stored for it. A unit the plugin has not asked yet is described as stored.\n const hasFan = device.controls === undefined || device.controls.fanSteps.length > 0;\n const hasSwing = device.controls === undefined || device.controls.hasSwing;\n const fanPlace = device.hasSeparateFan ? ' on a fan of its own' : '';\n const fan = device.fanControl === 'hidden' ? 'fan speed not shown' : `fan speed as ${device.fanControl === 'steps' ? 'steps' : 'a percentage'}${fanPlace}`;\n const swing = { switches: 'swing as switches', select: 'swing as a mode select', hidden: 'swing not shown' }[device.swingControl];\n const controls = [hasFan ? fan : '', hasSwing ? swing : ''].filter((text) => text !== '').join(', ');\n const switches = [device.hasDryModeSwitch ? 'dry mode' : '', device.hasFanModeSwitch ? 'fan mode' : '', device.hasFanAutoSwitch ? 'auto fan' : '', ...device.specialModeSwitches].filter(\n (name) => name !== '',\n );\n const extra = [switches.length === 0 ? '' : `a switch for ${switches.join(', ')}`, device.hasModeSelect ? 'a mode select' : ''].filter((text) => text !== '').join(' and ');\n const place = device.isComposed ? ', all as one device' : '';\n if (controls === '') return `${extra === '' ? 'power, mode and temperature' : extra}${place}`;\n return `${extra === '' ? controls : `${controls}, and ${extra}`}${place}`;\n}\n\nfunction deviceRow(device: StoredDevice): HTMLElement {\n const statusText = { online: 'Online', offline: 'Not answering', stopped: 'Not started' }[device.status];\n const isConfirming = view.confirmingRemoval === device.macAddress;\n return h(\n 'li',\n { class: 'device' },\n h(\n 'div',\n { class: 'device-main' },\n h('h3', {}, device.name),\n h('p', { class: 'meta' }, h('span', { class: `status ${device.status}` }, statusText), ` at ${device.host}. In Matter: ${describeExposure(device)}.`),\n device.status !== 'online' && device.error ? h('p', { class: 'meta problem-text' }, device.error) : null,\n ),\n isConfirming\n ? h(\n 'div',\n { class: 'actions' },\n h('span', { class: 'meta' }, 'Remove it from Matter too?'),\n button('Remove', () => void removeDevice(device), 'danger'),\n button('Keep', () => {\n view.confirmingRemoval = '';\n render();\n }),\n )\n : h(\n 'div',\n { class: 'actions' },\n // Either way the unit is asked first what it accepts now, so the options offered are the ones it has today.\n button(device.status === 'online' ? 'Options' : 'Reconnect', () => startQueue([draftFromStored(device)])),\n button('Remove', () => {\n view.confirmingRemoval = device.macAddress;\n render();\n }),\n ),\n );\n}\n\nexport function homeScreen(): Child[] {\n const devices = view.setup?.devices ?? [];\n return [\n h('header', {}, h('h1', {}, 'Daikin air conditioners'), view.setup ? button('Add air conditioners', () => void search(), 'primary') : null),\n notice(),\n devices.length === 0 && view.setup\n ? h('p', { class: 'empty' }, 'No air conditioner is set up yet. Add one to control it from Matter.')\n : h('ul', { class: 'devices' }, ...devices.map(deviceRow)),\n view.setup ? null : button('Try again', () => void loadState(), 'primary'),\n ];\n}\n", "/**\n * @file apps/frontend/src/presets.ts\n * @description The presets of the Options step: each fills in the choices of how a device appears in Matter.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { AvailableControls, Exposure } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport type { Draft } from './state.js';\n\nexport type PresetName = 'apple' | 'google' | 'full' | 'minimal';\n\nexport const presets: Record<PresetName, { title: string; text: string }> = {\n apple: {\n title: 'Apple Home',\n text: 'Fan speed as a percentage, the humidity on a sensor of its own too, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan.',\n },\n google: { title: 'Google Home', text: 'Fan speed as steps, swing switches and a dry mode switch.' },\n full: { title: 'Full', text: 'Every control, switch and sensor this unit can have, special modes and the outdoor temperature included.' },\n minimal: { title: 'Minimal', text: 'No fan control, no swing control and no switches. The fan and the swing stay as they are.' },\n};\n\n/**\n * The choices a preset makes for a unit: never a switch or a control the unit has no use for. A preset says which\n * controls and switches there are, not whether they are parts of the air conditioner: that choice stays as it is.\n */\nexport function presetExposure(name: PresetName, controls: AvailableControls): Omit<Exposure, 'isComposed'> {\n const isMinimal = name === 'minimal';\n return {\n fanControl: isMinimal || controls.fanSteps.length === 0 ? 'hidden' : name === 'apple' ? 'percent' : 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: isMinimal || !controls.hasSwing ? 'hidden' : 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: !isMinimal && controls.hasDryMode,\n hasFanModeSwitch: (name === 'apple' || name === 'full') && controls.hasFanMode,\n hasFanAutoSwitch: (name === 'apple' || name === 'full') && controls.hasFanAuto,\n // Few apps show a mode select, so no preset has one: the switches take its place.\n hasModeSelect: false,\n // No preset takes the fan off the air conditioner: that is for an app that shows no fan speed there.\n hasSeparateFan: false,\n specialModeSwitches: name === 'full' ? [...controls.specialModes] : [],\n // An app may show the outdoor temperature as that of the room it puts the sensor in, until the sensor is moved.\n hasOutdoorTemperatureSensor: name === 'full',\n // Home is reported to show a humidity on a sensor of its own only.\n hasSeparateHumiditySensor: name === 'apple',\n };\n}\n\n/** Whether the choices in a draft are exactly those of a preset, leaving aside what the unit has no use for. */\nexport function matchesPreset(draft: Draft, name: PresetName, controls: AvailableControls): boolean {\n const chosen = pickExposure(draft);\n if (controls.fanSteps.length === 0) Object.assign(chosen, { fanControl: 'hidden', fanSpeedWhenHidden: 'keep' });\n // The choice shows nowhere while the fan is hidden.\n if (chosen.fanControl === 'hidden') chosen.hasSeparateFan = false;\n if (!controls.hasSwing) Object.assign(chosen, { swingControl: 'hidden', swingModeWhenHidden: 'keep' });\n // The choice shows nowhere on a unit without a dry or a fan mode.\n if (!controls.hasDryMode && !controls.hasFanMode) chosen.hasModeSelect = false;\n chosen.specialModeSwitches = controls.specialModes.filter((mode) => chosen.specialModeSwitches.includes(mode));\n return JSON.stringify(chosen) === JSON.stringify({ ...presetExposure(name, controls), isComposed: chosen.isComposed });\n}\n", "/**\n * @file apps/frontend/src/screens/options.ts\n * @description The Options step: how the fan, the swing, the extra modes and the readings of a unit appear in Matter, one section each, and whether as one device.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, saveDraft } from '../actions.js';\nimport type { AvailableControls, Exposure, FanSpeed, SpecialMode, SwingMode } from '../api.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { noControls } from '../exposure.js';\nimport { matchesPreset, presetExposure, type PresetName, presets } from '../presets.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft } from '../state.js';\n\nconst fanSpeedLabels: Record<FanSpeed, string> = {\n auto: 'Auto',\n silent: 'Silent',\n level1: 'Level 1',\n level2: 'Level 2',\n level3: 'Level 3',\n level4: 'Level 4',\n level5: 'Level 5',\n};\n\nconst swingModeLabels: Record<SwingMode, string> = {\n off: 'No swing',\n vertical: 'Up and down',\n horizontal: 'Left and right',\n both: 'Up, down, left and right',\n};\n\nconst specialModeTexts: Record<SpecialMode, { title: string; text: string }> = {\n powerful: { title: 'Powerful', text: 'Runs the unit at full power for a while.' },\n econo: { title: 'Econo', text: 'Limits how much power the unit draws.' },\n streamer: { title: 'Streamer', text: 'Runs the air purifying of the unit.' },\n};\n\n// Static drawings, so innerHTML is safe for them. Everything that comes from the network goes through text nodes.\nconst glyphs = {\n steps:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect x=\"2\" y=\"20\" width=\"8\" height=\"6\"/><rect x=\"14\" y=\"14\" width=\"8\" height=\"12\"/><rect x=\"26\" y=\"8\" width=\"8\" height=\"18\"/><rect x=\"38\" y=\"2\" width=\"8\" height=\"24\"/></svg>',\n percent: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path d=\"M2 26 L46 2 L46 26 Z\"/></svg>',\n hidden: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M6 14 H42\" stroke-dasharray=\"3 5\"/></svg>',\n switches:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19\"/><path class=\"stroke\" d=\"M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19\"/></svg>',\n select: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect class=\"stroke\" x=\"3\" y=\"5\" width=\"42\" height=\"18\" rx=\"4\"/><path class=\"stroke\" d=\"M31 12 L35 16 L39 12\"/></svg>',\n};\n\n// Home Assistant is the one app known to show a mode select.\nconst selectSupport = 'Few apps show a mode select: Home Assistant does, Apple Home does not.';\n\n// A part of the air conditioner has no name of its own in Matter: a switch or a mode select is tagged with what it is.\nfunction placement(draft: Draft, label: string, isTagged: boolean): string {\n if (!draft.isComposed) return `It appears as \"${draft.name} ${label}\"`;\n return isTagged ? `It appears as a part of \"${draft.name}\", tagged \"${label}\"` : `It appears as a part of \"${draft.name}\"`;\n}\n\nfunction update(draft: Draft, change: Partial<Exposure>): void {\n Object.assign(draft, change);\n render();\n}\n\nfunction choice<T extends string>(group: string, value: T, current: T, title: string, text: string, glyph: string, onpick: (value: T) => void): HTMLElement {\n const id = `${group}-${value}`;\n const input = h('input', { type: 'radio', name: group, id, checked: value === current, onchange: () => onpick(value) });\n return h('label', { class: 'choice pictured', for: id }, input, h('span', { class: 'glyph', html: glyph }), h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction toggle(id: string, isOn: boolean, title: string, text: string, onpick: (isOn: boolean) => void): HTMLElement {\n const input = h('input', { type: 'checkbox', id, checked: isOn, onchange: () => onpick(input.checked) });\n return h('label', { class: 'choice', for: id }, input, h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction presetSection(draft: Draft, controls: AvailableControls): HTMLElement {\n return h(\n 'section',\n { class: 'presets' },\n h('h2', {}, 'Start from a preset'),\n h(\n 'div',\n { class: 'preset-list' },\n ...(Object.keys(presets) as PresetName[]).map((name) =>\n h(\n 'button',\n { 'type': 'button', 'class': 'preset', 'aria-pressed': String(matchesPreset(draft, name, controls)), 'onclick': () => update(draft, presetExposure(name, controls)) },\n h('strong', {}, presets[name].title),\n h('span', { class: 'meta' }, presets[name].text),\n ),\n ),\n ),\n h('p', { class: 'help' }, 'A preset fills in the choices below for what this unit can do. Change any of them afterwards.'),\n );\n}\n\nfunction fanSection(draft: Draft, controls: AvailableControls): HTMLElement {\n const fanSpeeds = draft.capabilities?.fanSpeeds ?? [];\n const stepCount = controls.fanSteps.length;\n if (stepCount === 0) return h('section', {}, h('h2', {}, 'Fan speed'), h('p', { class: 'help' }, 'This unit does not let apps set its fan speed, so there is nothing to choose.'));\n // A fixed fan speed is sent with every command, so a switch for auto would not hold.\n const canSwitchFanAuto = controls.hasFanAuto && !(draft.fanControl === 'hidden' && draft.fanSpeedWhenHidden !== 'keep');\n return h(\n 'section',\n {},\n h('h2', {}, 'Fan speed'),\n h(\n 'div',\n { class: 'choices' },\n choice('fan', 'steps', draft.fanControl, 'Steps', `${stepCount} speeds, one for each speed of the unit${fanSpeeds.includes('auto') ? ', and auto' : ''}.`, glyphs.steps, (fanControl) =>\n update(draft, { fanControl }),\n ),\n choice('fan', 'percent', draft.fanControl, 'Percentage', 'A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.', glyphs.percent, (fanControl) => update(draft, { fanControl })),\n choice('fan', 'hidden', draft.fanControl, 'Not shown in Matter', 'No fan control. Pick below what the plugin does with the fan speed instead.', glyphs.hidden, (fanControl) =>\n update(draft, { fanControl }),\n ),\n ),\n draft.fanControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'fan-fixed' }, 'When the plugin sends a command, the fan speed'),\n h(\n 'select',\n { id: 'fan-fixed', onchange: (event: Event) => update(draft, { fanSpeedWhenHidden: (event.target as HTMLSelectElement).value as Exposure['fanSpeedWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.fanSpeedWhenHidden === 'keep' }, 'stays as it is'),\n ...fanSpeeds.map((speed) => h('option', { value: speed, selected: draft.fanSpeedWhenHidden === speed }, `is set to ${fanSpeedLabels[speed]}`)),\n ),\n )\n : null,\n draft.fanControl === 'hidden'\n ? null\n : toggle(\n 'separate-fan',\n draft.hasSeparateFan,\n 'Show the fan as a device of its own',\n `${placement(draft, 'Fan', false)}, and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner, or do not show it well. It always shows the speed the unit holds, also while the unit is off.`,\n (hasSeparateFan) => update(draft, { hasSeparateFan }),\n ),\n canSwitchFanAuto\n ? toggle(\n 'fan-auto-switch',\n draft.hasFanAutoSwitch,\n 'Add a switch for auto fan speed',\n `${placement(draft, 'Auto Fan', true)}. On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,\n (hasFanAutoSwitch) => update(draft, { hasFanAutoSwitch }),\n )\n : null,\n );\n}\n\nfunction swingSection(draft: Draft): HTMLElement {\n const swingModes = draft.capabilities?.swingModes ?? [];\n if (swingModes.length === 0) return h('section', {}, h('h2', {}, 'Swing'), h('p', { class: 'help' }, 'This unit does not let apps move its louvers, so there is nothing to choose.'));\n const hasVertical = swingModes.includes('vertical');\n const hasHorizontal = swingModes.includes('horizontal');\n let switchText = 'One switch, for the up and down swing.';\n if (hasVertical && hasHorizontal) switchText = 'Two switches: one for the up and down swing, one for left and right.';\n else if (hasHorizontal) switchText = 'One switch, for the left and right swing.';\n return h(\n 'section',\n {},\n h('h2', {}, 'Swing'),\n h(\n 'div',\n { class: 'choices' },\n choice('swing', 'switches', draft.swingControl, 'Switches', switchText, glyphs.switches, (swingControl) => update(draft, { swingControl })),\n choice(\n 'swing',\n 'select',\n draft.swingControl,\n 'A mode select',\n `${placement(draft, 'Swing', true)}, and picks one of: ${swingModes.map((mode) => swingModeLabels[mode].toLowerCase()).join(', ')}. ${selectSupport}`,\n glyphs.select,\n (swingControl) => update(draft, { swingControl }),\n ),\n choice('swing', 'hidden', draft.swingControl, 'Not shown in Matter', 'No swing control. Pick below what the plugin does with the swing instead.', glyphs.hidden, (swingControl) =>\n update(draft, { swingControl }),\n ),\n ),\n draft.swingControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'swing-fixed' }, 'When the plugin sends a command, the swing'),\n h(\n 'select',\n { id: 'swing-fixed', onchange: (event: Event) => update(draft, { swingModeWhenHidden: (event.target as HTMLSelectElement).value as Exposure['swingModeWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.swingModeWhenHidden === 'keep' }, 'stays as it is'),\n ...swingModes.map((mode) => h('option', { value: mode, selected: draft.swingModeWhenHidden === mode }, `is set to: ${swingModeLabels[mode]}`)),\n ),\n )\n : null,\n );\n}\n\nfunction modeSection(draft: Draft, controls: AvailableControls): Child {\n if (!controls.hasDryMode && !controls.hasFanMode) return null;\n const modes = draft.capabilities?.modes ?? [];\n return h(\n 'section',\n {},\n h('h2', {}, 'Dry and fan modes'),\n h(\n 'p',\n { class: 'help' },\n `These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${modes.includes('cool') ? 'cool' : 'heat'}. A mode select instead offers every mode in one control.`,\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'mode-select',\n draft.hasModeSelect,\n 'Use a mode select instead of switches',\n `${placement(draft, 'Mode', true)}, with Off and every mode of the unit, as the thermostat mode has them. Picking a mode turns the unit on in it. ${selectSupport}`,\n // The select takes the place of the switches.\n (hasModeSelect) => update(draft, hasModeSelect ? { hasModeSelect, hasDryModeSwitch: false, hasFanModeSwitch: false } : { hasModeSelect }),\n ),\n controls.hasDryMode && !draft.hasModeSelect\n ? toggle('dry-mode-switch', draft.hasDryModeSwitch, 'Add a switch for dry mode', `${placement(draft, 'Dry Mode', true)}.`, (hasDryModeSwitch) => update(draft, { hasDryModeSwitch }))\n : null,\n controls.hasFanMode && !draft.hasModeSelect\n ? toggle('fan-mode-switch', draft.hasFanModeSwitch, 'Add a switch for fan mode', `${placement(draft, 'Fan Mode', true)}.`, (hasFanModeSwitch) => update(draft, { hasFanModeSwitch }))\n : null,\n ),\n );\n}\n\nfunction specialSection(draft: Draft, controls: AvailableControls): Child {\n if (controls.specialModes.length === 0) return null;\n return h(\n 'section',\n {},\n h('h2', {}, 'Special modes'),\n h('p', { class: 'help' }, 'Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in.'),\n h(\n 'div',\n { class: 'choices' },\n ...controls.specialModes.map((mode) =>\n toggle(`special-${mode}`, draft.specialModeSwitches.includes(mode), `Add a switch for ${specialModeTexts[mode].title}`, specialModeTexts[mode].text, (isOn) =>\n update(draft, { specialModeSwitches: controls.specialModes.filter((other) => (other === mode ? isOn : draft.specialModeSwitches.includes(other))) }),\n ),\n ),\n ),\n );\n}\n\nfunction sensorSection(draft: Draft): HTMLElement {\n return h(\n 'section',\n {},\n h('h2', {}, 'Humidity and outdoor temperature'),\n h(\n 'p',\n { class: 'help' },\n 'No adapter says which readings its unit has, so each of these takes effect once the unit gives that reading. The room temperature and the humidity are always on the air conditioner.',\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'separate-humidity',\n draft.hasSeparateHumiditySensor,\n 'Also show the humidity on a sensor of its own',\n `${placement(draft, 'Humidity', false)}, and the air conditioner keeps its humidity. For apps that show no humidity on an air conditioner, which Apple Home is reported to be.`,\n (hasSeparateHumiditySensor) => update(draft, { hasSeparateHumiditySensor }),\n ),\n toggle(\n 'outdoor-sensor',\n draft.hasOutdoorTemperatureSensor,\n 'Add a sensor for the outdoor temperature',\n `It appears as \"${draft.name} Outdoor Temperature\". It is a device of its own, which you can put in another room, such as outside. Until you do, a Matter app may show the outdoor temperature as that of the room it put the sensor in. Some units give no outdoor temperature while they are idle: the sensor then shows no value.`,\n (hasOutdoorTemperatureSensor) => update(draft, { hasOutdoorTemperatureSensor }),\n ),\n ),\n );\n}\n\nfunction composedSection(draft: Draft): HTMLElement {\n return h(\n 'section',\n {},\n h('h2', {}, 'One device or several'),\n h(\n 'p',\n { class: 'help' },\n 'The switches, a fan of its own and a humidity sensor of its own can be parts of the air conditioner in Matter, or devices of their own beside it. The outdoor temperature sensor is always a device of its own, so that you can put it in another room than the air conditioner.',\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'composed',\n draft.isComposed,\n 'Make them parts of the air conditioner',\n `Your Matter app has one device, \"${draft.name}\", and they move between rooms with it. A part has no name of its own in Matter, so an app may show the switches as numbered outlets, and an app that shows no parts shows the air conditioner alone. Turned off, each is a device of its own with its own name, which your app puts in a room by itself.`,\n (isComposed) => update(draft, { isComposed }),\n ),\n ),\n );\n}\n\nexport function optionsScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const controls = draft.controls ?? noControls;\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Options for the air conditioner at ${draft.adapter.host}. Power, mode and temperature always appear in Matter: choose how the rest appears.`),\n presetSection(draft, controls),\n fanSection(draft, controls),\n swingSection(draft),\n modeSection(draft, controls),\n specialSection(draft, controls),\n sensorSection(draft),\n composedSection(draft),\n draft.isNew ? null : h('p', { class: 'help' }, 'Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change.'),\n notice(),\n h('div', { class: 'footer' }, button('Cancel', goHome), button(draft.isNew ? `Add ${draft.name}` : 'Save options', () => void saveDraft(draft), 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/theme.ts\n * @description Makes the setup page look like the Matterbridge page it is shown in.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\n/** Take the colours and the font of the Matterbridge page this one is shown in. */\nexport function adoptHostTheme(): void {\n try {\n if (window.parent === window) return;\n const host = getComputedStyle(window.parent.document.body);\n for (const name of ['--primary-color', '--div-bg-color', '--div-text-color', '--main-bg-color', '--main-label-color', '--div-border-color', '--main-light-color']) {\n const value = host.getPropertyValue(name).trim();\n if (value !== '') document.documentElement.style.setProperty(`--host${name.slice(1)}`, value);\n }\n document.documentElement.style.setProperty('--host-font', host.fontFamily);\n } catch {\n // Opened on its own or from another origin: the page keeps its own colours.\n }\n}\n", "/**\n * @file apps/frontend/src/app.ts\n * @description The guided setup page of the Matterbridge Daikin Local plugin. Matterbridge serves it at\n * /plugins/matterbridge-daikin-local/ and it talks to the plugin through api/<path> below that address.\n * This file starts the page: the rest is in the files next to it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState } from './actions.js';\nimport { h } from './dom.js';\nimport { render, setScreens } from './render.js';\nimport { connectScreen } from './screens/connect.js';\nimport { doneScreen } from './screens/done.js';\nimport { findScreen } from './screens/find.js';\nimport { homeScreen } from './screens/home.js';\nimport { optionsScreen } from './screens/options.js';\nimport { adoptHostTheme } from './theme.js';\n\nsetScreens({\n loading: () => [h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), 'Loading\u2026')],\n home: homeScreen,\n find: findScreen,\n connect: connectScreen,\n options: optionsScreen,\n done: doneScreen,\n});\n\nadoptHostTheme();\nrender();\nvoid loadState();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["api", "route", "body", "query", "method", "path", "init", "response", "reply", "pickExposure", "source", "fanControl", "fanSpeedWhenHidden", "swingControl", "swingModeWhenHidden", "hasDryModeSwitch", "hasFanModeSwitch", "hasFanAutoSwitch", "hasModeSelect", "hasSeparateFan", "specialModeSwitches", "hasOutdoorTemperatureSensor", "hasSeparateHumiditySensor", "isComposed", "defaultExposure", "draftFromAdapter", "adapter", "draftFromStored", "device", "capabilities", "controls", "name", "macAddress", "host", "protocol", "requiresRegistration", "usesHttps", "requiresLocalPassword", "noControls", "view", "currentDraft", "draftLabel", "draft", "isStored", "adapter", "device", "screens", "setScreens", "all", "render", "root", "form", "view", "currentDraft", "isSameForm", "activeId", "typed", "input", "child", "id", "value", "busy", "text", "task", "view", "render", "loadState", "reply", "api", "search", "refreshState", "goHome", "byAddress", "adapter", "device", "isStored", "lookUp", "host", "isConfigured", "needsRegistration", "startQueue", "drafts", "enterConnect", "draft", "currentDraft", "check", "key", "body", "draftLabel", "told", "skipDraft", "skipped", "saveDraft", "name", "localPassword", "pickExposure", "removeDevice", "h", "tag", "props", "children", "element", "key", "value", "child", "button", "label", "onclick", "kind", "disabled", "h", "view", "notice", "unitStrip", "current", "currentDraft", "doing", "unit", "draft", "index", "draftLabel", "unitHeader", "text", "steps", "names", "name", "state", "nameForm", "draft", "name", "h", "submit", "value", "view", "device", "render", "button", "lockCodeForm", "isRejected", "code", "check", "keyForm", "key", "credentialForm", "status", "connectScreen", "currentDraft", "notice", "canRetry", "steps", "unitStrip", "unitHeader", "skipDraft", "goHome", "doneScreen", "names", "view", "h", "button", "goHome", "foundRow", "adapter", "isSelectable", "notes", "checkbox", "h", "view", "render", "note", "findScreen", "chosen", "host", "lookUpHost", "lookUp", "event", "continueLabel", "steps", "notice", "button", "search", "goHome", "startQueue", "draftFromAdapter", "describeExposure", "device", "hasFan", "hasSwing", "fanPlace", "fan", "swing", "controls", "text", "switches", "name", "extra", "place", "deviceRow", "statusText", "isConfirming", "view", "h", "button", "removeDevice", "render", "startQueue", "draftFromStored", "homeScreen", "devices", "search", "notice", "loadState", "presets", "presetExposure", "name", "controls", "isMinimal", "matchesPreset", "draft", "chosen", "pickExposure", "mode", "fanSpeedLabels", "swingModeLabels", "specialModeTexts", "glyphs", "selectSupport", "placement", "draft", "label", "isTagged", "update", "change", "render", "choice", "group", "value", "current", "title", "text", "glyph", "onpick", "id", "input", "h", "toggle", "isOn", "presetSection", "controls", "presets", "name", "matchesPreset", "presetExposure", "fanSection", "fanSpeeds", "stepCount", "canSwitchFanAuto", "fanControl", "event", "speed", "hasSeparateFan", "hasFanAutoSwitch", "swingSection", "swingModes", "hasVertical", "hasHorizontal", "switchText", "swingControl", "mode", "modeSection", "modes", "hasModeSelect", "hasDryModeSwitch", "hasFanModeSwitch", "specialSection", "other", "sensorSection", "hasSeparateHumiditySensor", "hasOutdoorTemperatureSensor", "composedSection", "isComposed", "optionsScreen", "currentDraft", "notice", "noControls", "steps", "unitStrip", "unitHeader", "button", "goHome", "saveDraft", "adoptHostTheme", "host", "name", "value", "setScreens", "h", "homeScreen", "findScreen", "connectScreen", "optionsScreen", "doneScreen", "adoptHostTheme", "render", "loadState"]
|
|
7
7
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { SPECIAL_MODES, } from 'daikin-local-control';
|
|
2
|
-
import { BridgedDeviceBasicInformation, FanControl, OnOff, Thermostat } from 'matterbridge/matter/clusters';
|
|
2
|
+
import { BridgedDeviceBasicInformation, FanControl, ModeSelect, OnOff, Thermostat } from 'matterbridge/matter/clusters';
|
|
3
3
|
import { addReportedSensors, getSwitchableSpecialModes, hasFanAuto, noReportedSensors } from '../controls.js';
|
|
4
4
|
import { getErrorMessage } from '../errors.js';
|
|
5
|
-
import { toOperatingMode } from '../mapping.js';
|
|
6
|
-
import { createAirConditionerEndpoint, createFanEndpoint, createOutdoorTemperatureEndpoint, createSwitchEndpoint } from './endpoints.js';
|
|
5
|
+
import { modeSelectChoices, swingSelectChoices, toModeOptions, toOperatingMode } from '../mapping.js';
|
|
6
|
+
import { createAirConditionerEndpoint, createFanEndpoint, createHumidityEndpoint, createModeSelectEndpoint, createOutdoorTemperatureEndpoint, createSwitchEndpoint, } from './endpoints.js';
|
|
7
7
|
import { getAccessoryLayout } from './layout.js';
|
|
8
8
|
import { applySensors, applyState, readFanSpeed } from './matterState.js';
|
|
9
9
|
import { addHiddenSettings, PendingCommand } from './pendingCommand.js';
|
|
@@ -25,6 +25,9 @@ export class DaikinAccessory {
|
|
|
25
25
|
fanAutoSwitch;
|
|
26
26
|
fan;
|
|
27
27
|
specialModeSwitches;
|
|
28
|
+
modeSelect;
|
|
29
|
+
swingSelect;
|
|
30
|
+
humiditySensor;
|
|
28
31
|
outdoorTemperatureSensor;
|
|
29
32
|
capabilities;
|
|
30
33
|
reportedSensors;
|
|
@@ -34,6 +37,7 @@ export class DaikinAccessory {
|
|
|
34
37
|
commandDelayInMs;
|
|
35
38
|
identity;
|
|
36
39
|
layout;
|
|
40
|
+
parent;
|
|
37
41
|
state;
|
|
38
42
|
sensors;
|
|
39
43
|
lastManualFanSpeed;
|
|
@@ -56,8 +60,10 @@ export class DaikinAccessory {
|
|
|
56
60
|
this.identity = { name: this.name, serial: options.config.macAddress, vendorId: options.vendorId };
|
|
57
61
|
this.layout = getAccessoryLayout(this.config, this.capabilities, this.reportedSensors);
|
|
58
62
|
this.endpoint = this.createAirConditioner();
|
|
63
|
+
if (this.layout.isComposed)
|
|
64
|
+
this.parent = this.endpoint;
|
|
59
65
|
if (this.layout.hasSeparateFan)
|
|
60
|
-
this.fan = createFanEndpoint(this.identity, this.layout);
|
|
66
|
+
this.fan = createFanEndpoint(this.identity, this.layout, this.parent);
|
|
61
67
|
if (this.layout.hasFan)
|
|
62
68
|
this.subscribeFan(this.fan ?? this.endpoint);
|
|
63
69
|
if (this.config.swingControl === 'switches') {
|
|
@@ -82,17 +88,28 @@ export class DaikinAccessory {
|
|
|
82
88
|
specialModeSwitches.set(specialMode, this.createSpecialModeSwitch(specialMode));
|
|
83
89
|
}
|
|
84
90
|
this.specialModeSwitches = specialModeSwitches;
|
|
91
|
+
if (this.layout.modeSelectModes.length > 0) {
|
|
92
|
+
this.modeSelect = this.createSelect('mode-select', 'M', 'Mode', modeSelectChoices, this.layout.modeSelectModes, (mode) => this.queueControl(mode === 'off' ? { power: false } : { power: true, mode }, `mode select ${mode}`));
|
|
93
|
+
}
|
|
94
|
+
if (this.layout.swingSelectModes.length > 0) {
|
|
95
|
+
this.swingSelect = this.createSelect('swing-select', 'W', 'Swing', swingSelectChoices, this.layout.swingSelectModes, (swingMode) => this.queueControl({ swingMode }, `swing select ${swingMode}`));
|
|
96
|
+
}
|
|
97
|
+
if (this.layout.hasSeparateHumiditySensor)
|
|
98
|
+
this.humiditySensor = createHumidityEndpoint(this.identity, this.parent);
|
|
85
99
|
if (this.layout.hasOutdoorTemperature)
|
|
86
100
|
this.outdoorTemperatureSensor = createOutdoorTemperatureEndpoint(this.identity);
|
|
87
101
|
}
|
|
88
102
|
get endpoints() {
|
|
103
|
+
if (this.parent !== undefined)
|
|
104
|
+
return [this.parent, this.outdoorTemperatureSensor].filter((endpoint) => endpoint !== undefined);
|
|
89
105
|
const switches = [this.verticalSwitch, this.horizontalSwitch, this.dryModeSwitch, this.fanModeSwitch, this.fanAutoSwitch, ...this.specialModeSwitches.values()];
|
|
90
|
-
return [this.endpoint, this.fan, ...switches, this.outdoorTemperatureSensor].filter((endpoint) => endpoint !== undefined);
|
|
106
|
+
return [this.endpoint, this.fan, ...switches, this.modeSelect, this.swingSelect, this.humiditySensor, this.outdoorTemperatureSensor].filter((endpoint) => endpoint !== undefined);
|
|
91
107
|
}
|
|
92
108
|
getNewlyReportedSensors() {
|
|
93
109
|
if (this.sensors === undefined)
|
|
94
110
|
return undefined;
|
|
95
|
-
const
|
|
111
|
+
const sensors = this.config.hasOutdoorTemperatureSensor ? this.sensors : { ...this.sensors, outdoorTemperatureInC: null };
|
|
112
|
+
const reported = addReportedSensors(this.reportedSensors, sensors);
|
|
96
113
|
return reported === this.reportedSensors ? undefined : reported;
|
|
97
114
|
}
|
|
98
115
|
async clearReadings() {
|
|
@@ -163,13 +180,21 @@ export class DaikinAccessory {
|
|
|
163
180
|
endpoint.subscribeAttribute(FanControl, 'percentSetting', onFanWrite);
|
|
164
181
|
}
|
|
165
182
|
createSwitch(key, code, label, onWrite) {
|
|
166
|
-
const endpoint = createSwitchEndpoint(this.identity, key, code, label);
|
|
183
|
+
const endpoint = createSwitchEndpoint(this.identity, key, code, label, this.parent);
|
|
167
184
|
endpoint.subscribeAttribute(OnOff, 'onOff', (isOn, _wasOn, context) => {
|
|
168
185
|
if (isControllerWrite(context))
|
|
169
186
|
onWrite(isOn);
|
|
170
187
|
});
|
|
171
188
|
return endpoint;
|
|
172
189
|
}
|
|
190
|
+
createSelect(key, code, label, choices, supported, onPick) {
|
|
191
|
+
const endpoint = createModeSelectEndpoint(this.identity, key, code, label, toModeOptions(choices, supported), this.parent);
|
|
192
|
+
endpoint.subscribeAttribute(ModeSelect, 'currentMode', (mode, _previous, context) => {
|
|
193
|
+
if (isControllerWrite(context))
|
|
194
|
+
onPick(choices.values[mode]);
|
|
195
|
+
});
|
|
196
|
+
return endpoint;
|
|
197
|
+
}
|
|
173
198
|
createSwingSwitch(axis) {
|
|
174
199
|
const isVertical = axis === 'vertical';
|
|
175
200
|
return this.createSwitch(`${axis}-swing`, isVertical ? 'V' : 'H', isVertical ? 'Vertical Swing' : 'Horizontal Swing', (isOn) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { bridgedNode, fan, MatterbridgeEndpoint, onOffPlugInUnit, powerSource, roomAirConditioner, temperatureSensor } from 'matterbridge';
|
|
1
|
+
import { bridgedNode, fan, humiditySensor, MatterbridgeEndpoint, modeSelect, onOffPlugInUnit, powerSource, roomAirConditioner, temperatureSensor, } from 'matterbridge';
|
|
2
2
|
import { MatterbridgeFanControlServer } from 'matterbridge/behaviors';
|
|
3
|
-
import {
|
|
3
|
+
import { CommonNumberTag } from 'matterbridge/matter';
|
|
4
|
+
import { FanControl, ModeSelect, OnOff } from 'matterbridge/matter/clusters';
|
|
4
5
|
import { clampTemperature, composeDeviceName, getFanModeSequence } from '../mapping.js';
|
|
5
6
|
const vendorName = 'Daikin';
|
|
6
7
|
function addFanControl(endpoint, layout) {
|
|
@@ -55,14 +56,35 @@ function createExtraEndpoint(deviceType, identity, key, code, label) {
|
|
|
55
56
|
.createDefaultBridgedDeviceBasicInformationClusterServer(composeDeviceName(identity.name, label), `${identity.serial}${code}`, identity.vendorId, vendorName, `Daikin ${label}`)
|
|
56
57
|
.createDefaultPowerSourceWiredClusterServer();
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
return
|
|
59
|
+
function createPartEndpoint(airConditioner, deviceType, key, tag) {
|
|
60
|
+
return airConditioner.addChildDeviceType(key, deviceType, tag === undefined ? {} : { tagList: [tag] }).createDefaultIdentifyClusterServer();
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
62
|
+
function getNumberTag(airConditioner, cluster, label) {
|
|
63
|
+
const count = airConditioner.getChildEndpoints().filter((part) => part.hasClusterServer(cluster)).length;
|
|
64
|
+
return { ...CommonNumberTag.One, tag: count + 1, label };
|
|
65
|
+
}
|
|
66
|
+
export function createSwitchEndpoint(identity, key, code, label, airConditioner) {
|
|
67
|
+
const endpoint = airConditioner === undefined
|
|
68
|
+
? createExtraEndpoint(onOffPlugInUnit, identity, key, code, label)
|
|
69
|
+
: createPartEndpoint(airConditioner, onOffPlugInUnit, key, getNumberTag(airConditioner, OnOff, label));
|
|
70
|
+
return endpoint.addRequiredClusters();
|
|
71
|
+
}
|
|
72
|
+
export function createModeSelectEndpoint(identity, key, code, label, supportedModes, airConditioner) {
|
|
73
|
+
const endpoint = airConditioner === undefined
|
|
74
|
+
? createExtraEndpoint(modeSelect, identity, key, code, label)
|
|
75
|
+
: createPartEndpoint(airConditioner, modeSelect, key, getNumberTag(airConditioner, ModeSelect, label));
|
|
76
|
+
const { mode } = supportedModes[0];
|
|
77
|
+
return endpoint.createDefaultModeSelectClusterServer(label, supportedModes, mode, mode).addRequiredClusters();
|
|
78
|
+
}
|
|
79
|
+
export function createFanEndpoint(identity, layout, airConditioner) {
|
|
80
|
+
const endpoint = airConditioner === undefined ? createExtraEndpoint(fan, identity, 'fan', 'N', 'Fan') : createPartEndpoint(airConditioner, fan, 'fan');
|
|
63
81
|
addFanControl(endpoint, layout);
|
|
64
82
|
return endpoint.addRequiredClusters();
|
|
65
83
|
}
|
|
84
|
+
export function createHumidityEndpoint(identity, airConditioner) {
|
|
85
|
+
const endpoint = airConditioner === undefined ? createExtraEndpoint(humiditySensor, identity, 'humidity', 'R', 'Humidity') : createPartEndpoint(airConditioner, humiditySensor, 'humidity');
|
|
86
|
+
return endpoint.createDefaultRelativeHumidityMeasurementClusterServer(null).addRequiredClusters();
|
|
87
|
+
}
|
|
66
88
|
export function createOutdoorTemperatureEndpoint(identity) {
|
|
67
89
|
return createExtraEndpoint(temperatureSensor, identity, 'outdoor-temperature', 'O', 'Outdoor Temperature')
|
|
68
90
|
.createDefaultTemperatureMeasurementClusterServer(null)
|
package/dist/accessory/layout.js
CHANGED
|
@@ -17,6 +17,10 @@ export function getAccessoryLayout(config, capabilities, reportedSensors) {
|
|
|
17
17
|
heatLimits: profile === 'heatCool' ? { ...heatRange, min: Math.min(heatRange.min, coolRange.min) } : heatRange,
|
|
18
18
|
coolLimits: profile === 'heatCool' ? { ...coolRange, max: Math.max(coolRange.max, heatRange.max) } : coolRange,
|
|
19
19
|
hasHumidity: reportedSensors.hasHumidity,
|
|
20
|
-
|
|
20
|
+
hasSeparateHumiditySensor: reportedSensors.hasHumidity && config.hasSeparateHumiditySensor,
|
|
21
|
+
modeSelectModes: config.hasModeSelect ? ['off', ...capabilities.modes] : [],
|
|
22
|
+
swingSelectModes: config.swingControl === 'select' ? capabilities.swingModes : [],
|
|
23
|
+
hasOutdoorTemperature: reportedSensors.hasOutdoorTemperature && config.hasOutdoorTemperatureSensor,
|
|
24
|
+
isComposed: config.isComposed,
|
|
21
25
|
};
|
|
22
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FanControl, OnOff, RelativeHumidityMeasurement, TemperatureMeasurement, Thermostat } from 'matterbridge/matter/clusters';
|
|
1
|
+
import { FanControl, ModeSelect, OnOff, RelativeHumidityMeasurement, TemperatureMeasurement, Thermostat } from 'matterbridge/matter/clusters';
|
|
2
2
|
import { isValidNumber } from 'matterbridge/utils';
|
|
3
|
-
import { fanStepToPercent, getSwingAxes, percentToFanStep, toMatterTemperature, toSystemMode } from '../mapping.js';
|
|
3
|
+
import { fanStepToPercent, getSwingAxes, modeSelectChoices, percentToFanStep, swingSelectChoices, toMatterTemperature, toSystemMode } from '../mapping.js';
|
|
4
4
|
export function readFanSpeed(endpoint, fanSteps) {
|
|
5
5
|
const fanMode = endpoint.getAttribute(FanControl, 'fanMode');
|
|
6
6
|
const percent = endpoint.getAttribute(FanControl, 'percentSetting');
|
|
@@ -50,6 +50,10 @@ async function applyFanSpeed(endpoint, layout, fanSpeed) {
|
|
|
50
50
|
if (layout.hasFanSteps)
|
|
51
51
|
await endpoint.updateAttribute(FanControl, 'speedCurrent', step);
|
|
52
52
|
}
|
|
53
|
+
async function applySelect(endpoint, choices, supported, value) {
|
|
54
|
+
if (endpoint !== undefined && supported.includes(value))
|
|
55
|
+
await endpoint.updateAttribute(ModeSelect, 'currentMode', choices.values.indexOf(value));
|
|
56
|
+
}
|
|
53
57
|
export async function applySensors(endpoints, layout, sensors) {
|
|
54
58
|
const { endpoint } = endpoints;
|
|
55
59
|
const indoor = sensors.indoorTemperatureInC === null ? null : toMatterTemperature(sensors.indoorTemperatureInC);
|
|
@@ -58,6 +62,7 @@ export async function applySensors(endpoints, layout, sensors) {
|
|
|
58
62
|
const humidity = sensors.indoorHumidityInPercent === null ? null : Math.round(sensors.indoorHumidityInPercent * 100);
|
|
59
63
|
if (layout.hasHumidity)
|
|
60
64
|
await endpoint.updateAttribute(RelativeHumidityMeasurement, 'measuredValue', humidity);
|
|
65
|
+
await endpoints.humiditySensor?.updateAttribute(RelativeHumidityMeasurement, 'measuredValue', humidity);
|
|
61
66
|
const outdoor = sensors.outdoorTemperatureInC === null ? null : toMatterTemperature(sensors.outdoorTemperatureInC);
|
|
62
67
|
await endpoints.outdoorTemperatureSensor?.updateAttribute(TemperatureMeasurement, 'measuredValue', outdoor);
|
|
63
68
|
}
|
|
@@ -79,4 +84,6 @@ export async function applyState(endpoints, layout, state, sensors) {
|
|
|
79
84
|
await endpoints.fanAutoSwitch?.updateAttribute(OnOff, 'onOff', state.fanSpeed === 'auto');
|
|
80
85
|
for (const [specialMode, specialModeSwitch] of endpoints.specialModeSwitches)
|
|
81
86
|
await specialModeSwitch.updateAttribute(OnOff, 'onOff', state.specialModes.includes(specialMode));
|
|
87
|
+
await applySelect(endpoints.modeSelect, modeSelectChoices, layout.modeSelectModes, state.power ? state.mode : 'off');
|
|
88
|
+
await applySelect(endpoints.swingSelect, swingSelectChoices, layout.swingSelectModes, state.swingMode);
|
|
82
89
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DAIKIN_PROTOCOLS, FAN_SPEEDS, SPECIAL_MODES, SWING_MODES, } from 'daikin-local-control';
|
|
2
2
|
import { isValidBoolean, isValidNumber, isValidObject, isValidString } from 'matterbridge/utils';
|
|
3
3
|
export const FAN_CONTROLS = ['steps', 'percent', 'hidden'];
|
|
4
|
-
export const SWING_CONTROLS = ['switches', 'hidden'];
|
|
4
|
+
export const SWING_CONTROLS = ['switches', 'select', 'hidden'];
|
|
5
5
|
export const KEEP_SETTING = 'keep';
|
|
6
6
|
const hiddenFanSpeeds = [KEEP_SETTING, ...FAN_SPEEDS];
|
|
7
7
|
const hiddenSwingModes = [KEEP_SETTING, ...SWING_MODES];
|
|
@@ -79,8 +79,12 @@ export function parseDeviceConfig(value, report) {
|
|
|
79
79
|
hasDryModeSwitch: isValidBoolean(raw.hasDryModeSwitch) ? raw.hasDryModeSwitch : useDefault('hasDryModeSwitch', false),
|
|
80
80
|
hasFanModeSwitch: isValidBoolean(raw.hasFanModeSwitch) ? raw.hasFanModeSwitch : useDefault('hasFanModeSwitch', false),
|
|
81
81
|
hasFanAutoSwitch: isValidBoolean(raw.hasFanAutoSwitch) ? raw.hasFanAutoSwitch : useDefault('hasFanAutoSwitch', false),
|
|
82
|
+
hasModeSelect: isValidBoolean(raw.hasModeSelect) ? raw.hasModeSelect : useDefault('hasModeSelect', false),
|
|
82
83
|
hasSeparateFan: isValidBoolean(raw.hasSeparateFan) ? raw.hasSeparateFan : useDefault('hasSeparateFan', false),
|
|
83
84
|
specialModeSwitches: SPECIAL_MODES.filter((mode) => specialModeSwitches.includes(mode)),
|
|
85
|
+
hasOutdoorTemperatureSensor: isValidBoolean(raw.hasOutdoorTemperatureSensor) ? raw.hasOutdoorTemperatureSensor : useDefault('hasOutdoorTemperatureSensor', false),
|
|
86
|
+
hasSeparateHumiditySensor: isValidBoolean(raw.hasSeparateHumiditySensor) ? raw.hasSeparateHumiditySensor : useDefault('hasSeparateHumiditySensor', false),
|
|
87
|
+
isComposed: isValidBoolean(raw.isComposed) ? raw.isComposed : useDefault('isComposed', true),
|
|
84
88
|
};
|
|
85
89
|
if (isValidString(raw.localPassword, 1))
|
|
86
90
|
device.localPassword = raw.localPassword;
|
|
@@ -141,9 +145,13 @@ export function hasSameExposure(left, right) {
|
|
|
141
145
|
return (left.name === right.name &&
|
|
142
146
|
left.hasFanAutoSwitch === right.hasFanAutoSwitch &&
|
|
143
147
|
left.hasSeparateFan === right.hasSeparateFan &&
|
|
148
|
+
left.hasOutdoorTemperatureSensor === right.hasOutdoorTemperatureSensor &&
|
|
149
|
+
left.hasSeparateHumiditySensor === right.hasSeparateHumiditySensor &&
|
|
150
|
+
left.isComposed === right.isComposed &&
|
|
144
151
|
left.specialModeSwitches.join() === right.specialModeSwitches.join() &&
|
|
145
152
|
left.fanControl === right.fanControl &&
|
|
146
153
|
left.swingControl === right.swingControl &&
|
|
147
154
|
left.hasDryModeSwitch === right.hasDryModeSwitch &&
|
|
148
|
-
left.hasFanModeSwitch === right.hasFanModeSwitch
|
|
155
|
+
left.hasFanModeSwitch === right.hasFanModeSwitch &&
|
|
156
|
+
left.hasModeSelect === right.hasModeSelect);
|
|
149
157
|
}
|
package/dist/controls.js
CHANGED
|
@@ -21,12 +21,14 @@ export function fitToCapabilities(device, capabilities) {
|
|
|
21
21
|
const fanSpeedWhenHidden = device.fanSpeedWhenHidden !== KEEP_SETTING && capabilities.fanSpeeds.includes(device.fanSpeedWhenHidden) ? device.fanSpeedWhenHidden : KEEP_SETTING;
|
|
22
22
|
const swingModeWhenHidden = device.swingModeWhenHidden !== KEEP_SETTING && capabilities.swingModes.includes(device.swingModeWhenHidden) ? device.swingModeWhenHidden : KEEP_SETTING;
|
|
23
23
|
const hasFixedFanSpeed = device.fanControl === 'hidden' && fanSpeedWhenHidden !== KEEP_SETTING;
|
|
24
|
+
const hasModeSelect = device.hasModeSelect && (capabilities.modes.includes('dehumidify') || capabilities.modes.includes('fan'));
|
|
24
25
|
return {
|
|
25
26
|
...device,
|
|
26
27
|
fanSpeedWhenHidden,
|
|
27
28
|
swingModeWhenHidden,
|
|
28
|
-
hasDryModeSwitch: device.hasDryModeSwitch && capabilities.modes.includes('dehumidify'),
|
|
29
|
-
hasFanModeSwitch: device.hasFanModeSwitch && capabilities.modes.includes('fan'),
|
|
29
|
+
hasDryModeSwitch: device.hasDryModeSwitch && !hasModeSelect && capabilities.modes.includes('dehumidify'),
|
|
30
|
+
hasFanModeSwitch: device.hasFanModeSwitch && !hasModeSelect && capabilities.modes.includes('fan'),
|
|
31
|
+
hasModeSelect,
|
|
30
32
|
hasFanAutoSwitch: device.hasFanAutoSwitch && hasFanAuto(capabilities) && !hasFixedFanSpeed,
|
|
31
33
|
hasSeparateFan: device.hasSeparateFan && device.fanControl !== 'hidden' && getFanSteps(capabilities.fanSpeeds).length > 0,
|
|
32
34
|
specialModeSwitches: getSwitchableSpecialModes(capabilities).filter((mode) => device.specialModeSwitches.includes(mode)),
|
package/dist/mapping.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { FAN_SPEEDS } from 'daikin-local-control';
|
|
1
|
+
import { FAN_SPEEDS, SWING_MODES } from 'daikin-local-control';
|
|
2
2
|
import { FanControl, Thermostat } from 'matterbridge/matter/clusters';
|
|
3
3
|
import { maxDeviceNameLength } from './config.js';
|
|
4
4
|
export const fallbackTemperatureRange = { min: 16, max: 32, step: 1 };
|
|
5
|
+
export const modeSelectChoices = {
|
|
6
|
+
values: ['off', 'auto', 'cool', 'heat', 'dehumidify', 'fan'],
|
|
7
|
+
labels: { off: 'Off', auto: 'Auto', cool: 'Cool', heat: 'Heat', dehumidify: 'Dry', fan: 'Fan Only' },
|
|
8
|
+
};
|
|
9
|
+
export const swingSelectChoices = {
|
|
10
|
+
values: SWING_MODES,
|
|
11
|
+
labels: { off: 'Off', vertical: 'Vertical', horizontal: 'Horizontal', both: 'Both' },
|
|
12
|
+
};
|
|
13
|
+
export function toModeOptions(choices, supported) {
|
|
14
|
+
return choices.values.flatMap((value, mode) => (supported.includes(value) ? [{ label: choices.labels[value], mode, semanticTags: [] }] : []));
|
|
15
|
+
}
|
|
5
16
|
export function getFanSteps(fanSpeeds) {
|
|
6
17
|
return FAN_SPEEDS.filter((speed) => speed !== 'auto' && fanSpeeds.includes(speed));
|
|
7
18
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge-daikin-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "matterbridge-daikin-local",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.5.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"daikin-local-control": "0.5.0",
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"node_modules/undici": {
|
|
103
|
-
"version": "8.
|
|
104
|
-
"resolved": "https://registry.npmjs.org/undici/-/undici-8.
|
|
105
|
-
"integrity": "sha512-
|
|
103
|
+
"version": "8.12.1",
|
|
104
|
+
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.1.tgz",
|
|
105
|
+
"integrity": "sha512-usz3OVQBeh1LxNn0DzU0voLXMgn5CU/WUdpBsY6Ti5afNao+vTkmn4H/NxoXolc10cOqe2+YIG0Hspq2rsl64w==",
|
|
106
106
|
"license": "MIT",
|
|
107
107
|
"engines": {
|
|
108
108
|
"node": ">=22.19.0"
|