matterbridge-daikin-local 0.3.0 → 0.4.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 CHANGED
@@ -4,6 +4,19 @@ 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.4.0] - 2026-10-14
8
+
9
+ ### Added
10
+
11
+ - [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.
12
+ - [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.
13
+ - [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.
14
+ - [readme]: Add diagrams of how an air conditioner is built in Matter, and the device types it uses.
15
+
16
+ ### Changed
17
+
18
+ - [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.
19
+
7
20
  ## [0.3.0] - 2026-10-13
8
21
 
9
22
  ### Changed
package/README.md CHANGED
@@ -102,21 +102,62 @@ 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 itself, for a unit that gives it: see below |
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 or 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:
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/>and Fan Control, Relative Humidity when they are here"]
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
+ humidity["Humidity Sensor<br/>Relative Humidity"]
124
+ outdoor["Temperature Sensor, tagged Outdoor<br/>Temperature Measurement"]
125
+ end
126
+ bridge --> ac
127
+ ac -.-> fan
128
+ ac -.-> switches
129
+ ac -.-> humidity
130
+ ac -.-> outdoor
131
+ ```
132
+
133
+ 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.
134
+
135
+ | Device type | ID | Stands for |
136
+ | -------------------- | ------ | ----------------------------------------------------- |
137
+ | Aggregator | 0x000E | Matterbridge, which holds the devices it bridges |
138
+ | Bridged Node | 0x0013 | Each device: its name, serial number and reachability |
139
+ | Power Source | 0x0011 | Each device: mains powered |
140
+ | Room Air Conditioner | 0x0072 | The air conditioner |
141
+ | Fan | 0x002B | A fan of its own |
142
+ | On/Off Plug-in Unit | 0x010A | Each switch |
143
+ | Humidity Sensor | 0x0307 | A humidity sensor of its own |
144
+ | Temperature Sensor | 0x0302 | The outdoor temperature |
145
+
146
+ The setup can also make each part a device of its own: see [One device or several](#one-device-or-several).
147
+
112
148
  ### Room temperature, humidity and outdoor temperature
113
149
 
114
- The three readings are not settings: each appears when the unit gives it.
150
+ The room temperature is always on the air conditioner. You choose where the other two go, and each appears once the unit gives it.
115
151
 
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.
152
+ | Reading | In Matter |
153
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
154
+ | Humidity | On the air conditioner, beside the room temperature, where it adds no device. Or on a humidity sensor of its own, named `<name> Humidity`, for Matter apps that show no humidity on an air conditioner |
155
+ | Outdoor temperature | None, unless you turn on a temperature sensor named `<name> Outdoor Temperature`. It is off by default because a Matter app may take a temperature sensor beside or inside an air conditioner for the temperature of the room. It is never on the air conditioner itself, where it would be taken for the room temperature |
156
+
157
+ - **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
158
  - **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
159
  - **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
- - **The humidity is on the air conditioner**, like the room temperature, so it adds no device. A Matter app that does not read the humidity of an air conditioner does not show it.
160
+ - **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 puts the humidity there for that reason. It has not been tested with this plugin.
120
161
 
121
162
  ### Fan speed
122
163
 
@@ -163,14 +204,44 @@ Matter has no control for Powerful, Econo and Streamer, so the setup can add a s
163
204
 
164
205
  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
206
 
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 |
207
+ | Preset | Fan speed | Swing | Dry mode switch | Fan mode switch | Auto fan switch | Special mode switches | Humidity | Outdoor sensor |
208
+ | ----------- | ---------- | --------- | --------------- | --------------- | --------------- | --------------------- | ---------------------- | -------------- |
209
+ | Apple Home | Percentage | Switches | Yes | Yes | Yes | None | On a sensor of its own | No |
210
+ | Google Home | Steps | Switches | Yes | No | No | None | On the air conditioner | No |
211
+ | Full | Steps | Switches | Yes | Yes | Yes | All | On the air conditioner | Yes |
212
+ | Minimal | Not shown | Not shown | No | No | No | None | On the air conditioner | No |
213
+
214
+ 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.
215
+
216
+ ### One device or several
217
+
218
+ The switches, a fan of its own and the sensors are parts of the air conditioner, which Matter calls a composed device, so they move between rooms with it. 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.
219
+
220
+ ```mermaid
221
+ flowchart TD
222
+ bridge["Matterbridge<br/>Aggregator"]
223
+ ac["<b>Living Room</b><br/>Room Air Conditioner<br/>On/Off, Thermostat<br/>and Fan Control, Relative Humidity when they are here"]
224
+ fan["<b>Living Room Fan</b><br/>Fan<br/>Fan Control"]
225
+ switches["<b>Living Room Vertical Swing</b>, and the other switches<br/>On/Off Plug-in Unit, one for each switch<br/>On/Off"]
226
+ humidity["<b>Living Room Humidity</b><br/>Humidity Sensor<br/>Relative Humidity"]
227
+ outdoor["<b>Living Room Outdoor Temperature</b><br/>Temperature Sensor<br/>Temperature Measurement"]
228
+ bridge --> ac
229
+ bridge -.-> fan
230
+ bridge -.-> switches
231
+ bridge -.-> humidity
232
+ bridge -.-> outdoor
233
+ ```
234
+
235
+ | | Parts of the air conditioner | Devices of their own |
236
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
237
+ | In a Matter app | One device | One device for the air conditioner, and one for each of the others |
238
+ | Rooms | The parts move with the air conditioner | Each is put in a room by itself |
239
+ | Names | The air conditioner alone has one. Each switch is tagged with a number and with what it is, such as `Vertical Swing`, and the outdoor sensor is tagged outdoor | Each has its own, such as `<name> Vertical Swing` |
240
+ | Unreachable | The air conditioner says so for all of them | Each says so itself |
241
+
242
+ 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
243
 
173
- The Apple Home and Google Home presets are starting points, not tested recommendations. They add switches for what each app is not known to offer: Home's climate controls have Off, Heat, Cool and Auto only, and Google Home is reported to show Fan only but not Dry.
244
+ Changing the choice builds the air conditioner again in Matter. The switches, the fan and the sensors 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
245
 
175
246
  ## Compared with other Daikin plugins
176
247
 
@@ -190,7 +261,7 @@ This plugin is the only one of the four that finds the adapters itself, that spe
190
261
  | Swing | A switch for each axis, or not shown with the mode you fix | No | The airflow direction of each axis, through a mode select | No |
191
262
  | Powerful, Econo and Streamer | An optional switch for each | No | No | A switch for Powerful |
192
263
  | 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 | No | Yes | Yes |
264
+ | Outdoor temperature | A sensor you turn on, when the unit gives it | No | Yes | Yes |
194
265
  | Other devices | None | None | The ACK70Z air purifier, with its humidifier, its water tank and its air quality sensors | None |
195
266
  | 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
267
  | 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 +300,7 @@ A stored device that this version of the plugin cannot read, for example one sto
229
300
  - **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
301
  - **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
302
  - **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 switch appears** rebuilds the air conditioner in Matter. Some Matter apps take a moment to show the change, or need the accessory page reopened.
303
+ - **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
304
  - **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
305
  - **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
306
  - **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 w(e,n,o){try{let[a,r]=e.split(" "),i={method:a};n!==void 0&&(i.headers={"Content-Type":"application/json"},i.body=JSON.stringify(n));let d=await fetch(new URL(`api/${r}${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 x(e){let{fanControl:n,fanSpeedWhenHidden:o,swingControl:a,swingModeWhenHidden:r,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:S,hasOutdoorTemperatureSensor:b,hasSeparateHumiditySensor:te,isComposed:ne}=e;return{fanControl:n,fanSpeedWhenHidden:o,swingControl:a,swingModeWhenHidden:r,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:[...S],hasOutdoorTemperatureSensor:b,hasSeparateHumiditySensor:te,isComposed:ne}}var oe={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!1,hasFanModeSwitch:!1,hasFanAutoSwitch:!1,hasSeparateFan:!1,specialModeSwitches:[],hasOutdoorTemperatureSensor:!1,hasSeparateHumiditySensor:!1,isComposed:!0};function I(e){return{adapter:e,name:e.name??"",isNew:!0,...x(oe)}}function q(e){let{capabilities:n,controls:o,name:a,macAddress:r,host:i,protocol:d,requiresRegistration:p,usesHttps:h,requiresLocalPassword:S}=e;return{adapter:{host:i,protocol:d,name:a,macAddress:r,requiresRegistration:p,usesHttps:h,requiresLocalPassword:S},name:a,isNew:!1,capabilities:n,controls:o,...x(e)}}var W={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 N(e){return(s.setup?.devices??[]).some(n=>n.macAddress===e.macAddress)}var P;function B(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,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(...P[s.screen]().filter(i=>i!==!1&&i!==null&&i!==void 0)),e.dataset.form=n,!!o){for(let[i,d]of r){let p=document.getElementById(i);p instanceof HTMLInputElement&&(p.value=d)}a!==""&&document.getElementById(a)?.focus()}}async function M(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 w("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 C();return}s.screen="home",l()}async function H(){let e=await w("GET state");e.ok&&(s.setup=e.state)}function g(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",H().then(l),l()}async function C(){s.screen="find";let e=await M("Searching your network. This takes about three seconds\u2026",async()=>w("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,l();return}await H();let n=s.found.filter(o=>!e.devices.some(a=>a.host===o.host)&&s.selected.has(o.host)).map(o=>({...o,isConfigured:N(o)}));s.found=[...e.devices,...n];for(let o of e.devices)!o.isConfigured&&!o.hasDuplicateName&&s.selected.add(o.host);l()}async function K(e){let n=await M(`Looking for an adapter at ${e}\u2026`,async()=>w("POST identify",{host:e}));if(!n.ok){s.error=n.error,l();return}let o=N(n.device),a=n.device.requiresRegistration===!0;s.found=[...s.found.filter(r=>r.host!==n.device.host),{...n.device,isConfigured:o,hasDuplicateName:!1,needsRegistration:a}],o||s.selected.add(n.device.host),l()}function R(e){s.queue=e,s.saved=[],s.handled=[],O()}async function O(){let e=f();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await H(),l();return}s.screen="connect",s.error="",e.probe=void 0,l(),await D(e)}async function D(e,n){let a={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:n},r=n===void 0?`Checking ${v(e)}\u2026`:`Registering with ${v(e)}\u2026`,i=await M(r,async()=>w(n===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 U(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:v(e),isSaved:!1}),O()}async function Y(e){let{adapter:n,name:o,localPassword:a}=e,r={...n,name:o,localPassword:a,...x(e)},i=await M(`Saving ${o}\u2026`,async()=>w("POST device",{device:r}));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 O()}async function G(e){let n=await M(`Removing ${e.name}\u2026`,async()=>w("DELETE device",void 0,`macAddress=${encodeURIComponent(e.macAddress)}`));s.confirmingRemoval="",n.ok||(s.error=n.error),await H(),l()}function t(e,n={},...o){let a=document.createElement(e);for(let[r,i]of Object.entries(n))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 o)r!==!1&&r!==null&&r!==void 0&&a.append(r);return a}function c(e,n,o="quiet",a=!1){return t("button",{type:"button",class:`button ${o}`,onclick:n,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 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,a)=>t("li",{class:a===0?"current":"ahead","aria-current":a===0?"true":void 0},t("strong",{},v(o)),t("span",{class:"meta"},a===0?n:"Waiting"))))}function $(e,n){return t("header",{class:"unit-header"},t("h1",{},v(e)),t("p",{class:"meta"},n))}function k(){if(s.queue.length>0&&!s.queue.some(a=>a.isNew))return null;let e=["Find","Connect","Options"],o=["find","connect","options"].indexOf(s.screen);return t("ol",{class:"steps"},...e.map((a,r)=>{let i="ahead";return r<o?i="done":r===o&&(i="current"),t("li",{class:i,"aria-current":r===o?"step":void 0},a)}))}function se(e){let n=t("input",{type:"text",id:"device-name",maxLength:32,autocomplete:"off"}),o=()=>{let a=n.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"},n,c("Continue",o,"primary")))}function ie(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,D(e))},"primary")))}function ae(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()!==""&&D(e,o.value.trim())},"primary")))}function re(e){let n=e.probe?.status;return n==="ready"&&e.name===""?se(e):n==="localPasswordRequired"||n==="localPasswordRejected"?ie(e,n==="localPasswordRejected"):n==="registrationRequired"||n==="registrationRejected"?ae(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 V(){let e=f();if(e===void 0)return[m()];let n=e.probe?.status==="unreachable"||e.probe?.status==="failed"||s.error!==""&&e.name!=="";return[k(),L(),$(e,`Connecting to its adapter at ${e.adapter.host}.`),m(),s.isBusy?null:re(e),t("div",{class:"footer"},n?c("Try again",()=>{D(e)}):null,c(e.isNew&&s.queue.length>1?"Skip this one":"Cancel",e.isNew&&s.queue.length>1?U:g))]}function z(){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",g,"primary"))]}function le(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 a=t("input",{type:"checkbox",id:`found-${e.host}`,checked:n&&s.selected.has(e.host),disabled:!n,onchange:()=>{a.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}`},a,t("span",{},t("strong",{},e.name??"Unnamed adapter"),t("span",{class:"meta"},` ${e.host}`),...o.map(r=>t("span",{class:"meta note"},r)))))}function J(){let e=s.found.filter(r=>s.selected.has(r.host)&&!r.isConfigured&&!r.hasDuplicateName),n=t("input",{type:"text",id:"host",placeholder:"192.168.1.50",spellcheck:!1,autocomplete:"off"}),o=()=>{n.value.trim()!==""&&K(n.value.trim())};n.addEventListener("keydown",r=>{r.key==="Enter"&&o()});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(le)):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",()=>{C()}),c("Cancel",g),c(a,()=>R(e.map(I)),"primary",e.length===0))]}function ce(e){let n=e.controls===void 0||e.controls.fanSteps.length>0,o=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=e.swingControl==="hidden"?"swing not shown":"swing as switches",d=[n?r:"",o?i:""].filter(b=>b!=="").join(", "),p=[e.hasDryModeSwitch?"dry mode":"",e.hasFanModeSwitch?"fan mode":"",e.hasFanAutoSwitch?"auto fan":"",...e.specialModeSwitches].filter(b=>b!==""),h=p.length===0?"":`a switch for ${p.join(", ")}`,S=e.isComposed?", all as one device":"";return d===""?`${h===""?"power, mode and temperature":h}${S}`:`${h===""?d:`${d}, and ${h}`}${S}`}function de(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: ${ce(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",()=>{G(e)},"danger"),c("Keep",()=>{s.confirmingRemoval="",l()})):t("div",{class:"actions"},c(e.status==="online"?"Options":"Reconnect",()=>R([q(e)])),c("Remove",()=>{s.confirmingRemoval=e.macAddress,l()})))}function Q(){let e=s.setup?.devices??[];return[t("header",{},t("h1",{},"Daikin air conditioners"),s.setup?c("Add air conditioners",()=>{C()},"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(de)),s.setup?null:c("Try again",()=>{E()},"primary")]}var F={apple:{title:"Apple Home",text:"Fan speed as a percentage, the humidity on a sensor of its own, 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 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]:[],hasOutdoorTemperatureSensor:e==="full",hasSeparateHumiditySensor:e==="apple"}}function Z(e,n,o){let a=x(e);return o.fanSteps.length===0&&Object.assign(a,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),a.fanControl==="hidden"&&(a.hasSeparateFan=!1),o.hasSwing||Object.assign(a,{swingControl:"hidden",swingModeWhenHidden:"keep"}),a.specialModeSwitches=o.specialModes.filter(r=>a.specialModeSwitches.includes(r)),JSON.stringify(a)===JSON.stringify({...j(n,o),isComposed:a.isComposed})}var pe={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},ue={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},X={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."}},A={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 T(e,n,o,a,r,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",{},a),t("span",{class:"meta"},r)))}function y(e,n,o,a,r){let i=t("input",{type:"checkbox",id:e,checked:n,onchange:()=>r(i.checked)});return t("label",{class:"choice",for:e},i,t("span",{},t("strong",{},o),t("span",{class:"meta"},a)))}function he(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(Z(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 fe(e,n){let o=e.capabilities?.fanSpeeds??[],a=n.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=n.hasFanAuto&&!(e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep");return t("section",{},t("h2",{},"Fan speed"),t("div",{class:"choices"},T("fan","steps",e.fanControl,"Steps",`${a} speeds, one for each speed of the unit${o.includes("auto")?", and auto":""}.`,A.steps,i=>u(e,{fanControl:i})),T("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.",A.percent,i=>u(e,{fanControl:i})),T("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",A.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 ${pe[i]}`)))):null,e.fanControl==="hidden"?null:y("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})),r?y("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 me(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"),a=n.includes("horizontal"),r="One switch, for the up and down swing.";return o&&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"},T("swing","switches",e.swingControl,"Switches",r,A.switches,i=>u(e,{swingControl:i})),T("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",A.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: ${ue[i]}`)))):null)}function we(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?y("dry-mode-switch",e.hasDryModeSwitch,"Add a switch for dry mode",`It appears as "${e.name} Dry Mode".`,a=>u(e,{hasDryModeSwitch:a})):null,n.hasFanMode?y("fan-mode-switch",e.hasFanModeSwitch,"Add a switch for fan mode",`It appears as "${e.name} Fan Mode".`,a=>u(e,{hasFanModeSwitch:a})):null))}function ge(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=>y(`special-${o}`,e.specialModeSwitches.includes(o),`Add a switch for ${X[o].title}`,X[o].text,a=>u(e,{specialModeSwitches:n.specialModes.filter(r=>r===o?a:e.specialModeSwitches.includes(r))})))))}function ye(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 is always on the air conditioner."),t("div",{class:"choices"},y("separate-humidity",e.hasSeparateHumiditySensor,"Show the humidity on a sensor of its own",`It appears as "${e.name} Humidity", and the air conditioner has no humidity of its own. For apps that show no humidity on an air conditioner, which Apple Home is reported to be.`,n=>u(e,{hasSeparateHumiditySensor:n})),y("outdoor-sensor",e.hasOutdoorTemperatureSensor,"Add a sensor for the outdoor temperature",`It appears as "${e.name} Outdoor Temperature". A Matter app may take it for the temperature of the room, so it is off unless you turn it on. Some units give no outdoor temperature while they are idle: the sensor then shows no value.`,n=>u(e,{hasOutdoorTemperatureSensor:n}))))}function ve(e){return t("section",{},t("h2",{},"One device or several"),t("p",{class:"help"},"The switches, a fan of its own and the sensors can be parts of the air conditioner in Matter, or devices of their own beside it."),t("div",{class:"choices"},y("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.`,n=>u(e,{isComposed:n}))))}function _(){let e=f();if(e===void 0)return[m()];let n=e.controls??W;return[k(),L(),$(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter: choose how the rest appears.`),he(e,n),fe(e,n),me(e),we(e,n),ge(e,n),ye(e),ve(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",g),c(e.isNew?`Add ${e.name}`:"Save options",()=>{Y(e)},"primary"))]}function ee(){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{}}B({loading:()=>[t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),"Loading\u2026")],home:Q,find:J,connect:V,options:_,done:z});ee();l();E();
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 and the extra modes of a unit appear in Matter, one section each.
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", "update", "draft", "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", "hasDryModeSwitch", "hasFanModeSwitch", "specialSection", "other", "optionsScreen", "currentDraft", "notice", "noControls", "steps", "unitStrip", "unitHeader", "button", "goHome", "saveDraft", "adoptHostTheme", "host", "name", "value", "setScreens", "h", "homeScreen", "findScreen", "connectScreen", "optionsScreen", "doneScreen", "adoptHostTheme", "render", "loadState"]
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, hasOutdoorTemperatureSensor, hasSeparateHumiditySensor, isComposed } =\n source;\n return {\n fanControl,\n fanSpeedWhenHidden,\n swingControl,\n swingModeWhenHidden,\n hasDryModeSwitch,\n hasFanModeSwitch,\n hasFanAutoSwitch,\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 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 = 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 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, 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 // 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 take a temperature sensor beside an air conditioner for the room temperature.\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 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};\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\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 is always on the air conditioner.',\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'separate-humidity',\n draft.hasSeparateHumiditySensor,\n 'Show the humidity on a sensor of its own',\n `It appears as \"${draft.name} Humidity\", and the air conditioner has no humidity of its own. 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\". A Matter app may take it for the temperature of the room, so it is off unless you turn it on. 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 the sensors can be parts of the air conditioner in Matter, or devices of their own beside it.',\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", "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", "update", "draft", "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", "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
  }
@@ -3,7 +3,7 @@ import { BridgedDeviceBasicInformation, FanControl, OnOff, Thermostat } from 'ma
3
3
  import { addReportedSensors, getSwitchableSpecialModes, hasFanAuto, noReportedSensors } from '../controls.js';
4
4
  import { getErrorMessage } from '../errors.js';
5
5
  import { toOperatingMode } from '../mapping.js';
6
- import { createAirConditionerEndpoint, createFanEndpoint, createOutdoorTemperatureEndpoint, createSwitchEndpoint } from './endpoints.js';
6
+ import { createAirConditionerEndpoint, createFanEndpoint, createHumidityEndpoint, 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,7 @@ export class DaikinAccessory {
25
25
  fanAutoSwitch;
26
26
  fan;
27
27
  specialModeSwitches;
28
+ humiditySensor;
28
29
  outdoorTemperatureSensor;
29
30
  capabilities;
30
31
  reportedSensors;
@@ -34,6 +35,7 @@ export class DaikinAccessory {
34
35
  commandDelayInMs;
35
36
  identity;
36
37
  layout;
38
+ parent;
37
39
  state;
38
40
  sensors;
39
41
  lastManualFanSpeed;
@@ -56,8 +58,10 @@ export class DaikinAccessory {
56
58
  this.identity = { name: this.name, serial: options.config.macAddress, vendorId: options.vendorId };
57
59
  this.layout = getAccessoryLayout(this.config, this.capabilities, this.reportedSensors);
58
60
  this.endpoint = this.createAirConditioner();
61
+ if (this.layout.isComposed)
62
+ this.parent = this.endpoint;
59
63
  if (this.layout.hasSeparateFan)
60
- this.fan = createFanEndpoint(this.identity, this.layout);
64
+ this.fan = createFanEndpoint(this.identity, this.layout, this.parent);
61
65
  if (this.layout.hasFan)
62
66
  this.subscribeFan(this.fan ?? this.endpoint);
63
67
  if (this.config.swingControl === 'switches') {
@@ -82,17 +86,22 @@ export class DaikinAccessory {
82
86
  specialModeSwitches.set(specialMode, this.createSpecialModeSwitch(specialMode));
83
87
  }
84
88
  this.specialModeSwitches = specialModeSwitches;
89
+ if (this.layout.hasSeparateHumiditySensor)
90
+ this.humiditySensor = createHumidityEndpoint(this.identity, this.parent);
85
91
  if (this.layout.hasOutdoorTemperature)
86
- this.outdoorTemperatureSensor = createOutdoorTemperatureEndpoint(this.identity);
92
+ this.outdoorTemperatureSensor = createOutdoorTemperatureEndpoint(this.identity, this.parent);
87
93
  }
88
94
  get endpoints() {
95
+ if (this.parent !== undefined)
96
+ return [this.parent];
89
97
  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);
98
+ return [this.endpoint, this.fan, ...switches, this.humiditySensor, this.outdoorTemperatureSensor].filter((endpoint) => endpoint !== undefined);
91
99
  }
92
100
  getNewlyReportedSensors() {
93
101
  if (this.sensors === undefined)
94
102
  return undefined;
95
- const reported = addReportedSensors(this.reportedSensors, this.sensors);
103
+ const sensors = this.config.hasOutdoorTemperatureSensor ? this.sensors : { ...this.sensors, outdoorTemperatureInC: null };
104
+ const reported = addReportedSensors(this.reportedSensors, sensors);
96
105
  return reported === this.reportedSensors ? undefined : reported;
97
106
  }
98
107
  async clearReadings() {
@@ -163,7 +172,7 @@ export class DaikinAccessory {
163
172
  endpoint.subscribeAttribute(FanControl, 'percentSetting', onFanWrite);
164
173
  }
165
174
  createSwitch(key, code, label, onWrite) {
166
- const endpoint = createSwitchEndpoint(this.identity, key, code, label);
175
+ const endpoint = createSwitchEndpoint(this.identity, key, code, label, this.parent);
167
176
  endpoint.subscribeAttribute(OnOff, 'onOff', (isOn, _wasOn, context) => {
168
177
  if (isControllerWrite(context))
169
178
  onWrite(isOn);
@@ -1,6 +1,7 @@
1
- import { bridgedNode, fan, MatterbridgeEndpoint, onOffPlugInUnit, powerSource, roomAirConditioner, temperatureSensor } from 'matterbridge';
1
+ import { bridgedNode, fan, humiditySensor, MatterbridgeEndpoint, onOffPlugInUnit, powerSource, roomAirConditioner, temperatureSensor, } from 'matterbridge';
2
2
  import { MatterbridgeFanControlServer } from 'matterbridge/behaviors';
3
- import { FanControl } from 'matterbridge/matter/clusters';
3
+ import { CommonLocationTag, CommonNumberTag } from 'matterbridge/matter';
4
+ import { FanControl, 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) {
@@ -44,7 +45,7 @@ export function createAirConditionerEndpoint(identity, layout) {
44
45
  }
45
46
  if (layout.hasFan && !layout.hasSeparateFan)
46
47
  addFanControl(endpoint, layout);
47
- if (layout.hasHumidity)
48
+ if (layout.hasHumidity && !layout.hasSeparateHumiditySensor)
48
49
  endpoint.createDefaultRelativeHumidityMeasurementClusterServer(null);
49
50
  endpoint.addRequiredClusters();
50
51
  return endpoint;
@@ -55,16 +56,31 @@ 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
- export function createSwitchEndpoint(identity, key, code, label) {
59
- return createExtraEndpoint(onOffPlugInUnit, identity, key, code, label).addRequiredClusters();
59
+ function createPartEndpoint(airConditioner, deviceType, key, tag) {
60
+ return airConditioner.addChildDeviceType(key, deviceType, tag === undefined ? {} : { tagList: [tag] }).createDefaultIdentifyClusterServer();
60
61
  }
61
- export function createFanEndpoint(identity, layout) {
62
- const endpoint = createExtraEndpoint(fan, identity, 'fan', 'N', 'Fan');
62
+ function getSwitchTag(airConditioner, label) {
63
+ const switchCount = airConditioner.getChildEndpoints().filter((part) => part.hasClusterServer(OnOff)).length;
64
+ return { ...CommonNumberTag.One, tag: switchCount + 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, getSwitchTag(airConditioner, label));
70
+ return endpoint.addRequiredClusters();
71
+ }
72
+ export function createFanEndpoint(identity, layout, airConditioner) {
73
+ const endpoint = airConditioner === undefined ? createExtraEndpoint(fan, identity, 'fan', 'N', 'Fan') : createPartEndpoint(airConditioner, fan, 'fan');
63
74
  addFanControl(endpoint, layout);
64
75
  return endpoint.addRequiredClusters();
65
76
  }
66
- export function createOutdoorTemperatureEndpoint(identity) {
67
- return createExtraEndpoint(temperatureSensor, identity, 'outdoor-temperature', 'O', 'Outdoor Temperature')
68
- .createDefaultTemperatureMeasurementClusterServer(null)
69
- .addRequiredClusters();
77
+ export function createHumidityEndpoint(identity, airConditioner) {
78
+ const endpoint = airConditioner === undefined ? createExtraEndpoint(humiditySensor, identity, 'humidity', 'R', 'Humidity') : createPartEndpoint(airConditioner, humiditySensor, 'humidity');
79
+ return endpoint.createDefaultRelativeHumidityMeasurementClusterServer(null).addRequiredClusters();
80
+ }
81
+ export function createOutdoorTemperatureEndpoint(identity, airConditioner) {
82
+ const endpoint = airConditioner === undefined
83
+ ? createExtraEndpoint(temperatureSensor, identity, 'outdoor-temperature', 'O', 'Outdoor Temperature')
84
+ : createPartEndpoint(airConditioner, temperatureSensor, 'outdoor-temperature', CommonLocationTag.Outdoor);
85
+ return endpoint.createDefaultTemperatureMeasurementClusterServer(null).addRequiredClusters();
70
86
  }
@@ -17,6 +17,8 @@ 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
- hasOutdoorTemperature: reportedSensors.hasOutdoorTemperature,
20
+ hasSeparateHumiditySensor: reportedSensors.hasHumidity && config.hasSeparateHumiditySensor,
21
+ hasOutdoorTemperature: reportedSensors.hasOutdoorTemperature && config.hasOutdoorTemperatureSensor,
22
+ isComposed: config.isComposed,
21
23
  };
22
24
  }
@@ -57,7 +57,7 @@ export async function applySensors(endpoints, layout, sensors) {
57
57
  await endpoint.updateAttribute(Thermostat, 'localTemperature', indoor);
58
58
  const humidity = sensors.indoorHumidityInPercent === null ? null : Math.round(sensors.indoorHumidityInPercent * 100);
59
59
  if (layout.hasHumidity)
60
- await endpoint.updateAttribute(RelativeHumidityMeasurement, 'measuredValue', humidity);
60
+ await (endpoints.humiditySensor ?? endpoint).updateAttribute(RelativeHumidityMeasurement, 'measuredValue', humidity);
61
61
  const outdoor = sensors.outdoorTemperatureInC === null ? null : toMatterTemperature(sensors.outdoorTemperatureInC);
62
62
  await endpoints.outdoorTemperatureSensor?.updateAttribute(TemperatureMeasurement, 'measuredValue', outdoor);
63
63
  }
package/dist/config.js CHANGED
@@ -81,6 +81,9 @@ export function parseDeviceConfig(value, report) {
81
81
  hasFanAutoSwitch: isValidBoolean(raw.hasFanAutoSwitch) ? raw.hasFanAutoSwitch : useDefault('hasFanAutoSwitch', false),
82
82
  hasSeparateFan: isValidBoolean(raw.hasSeparateFan) ? raw.hasSeparateFan : useDefault('hasSeparateFan', false),
83
83
  specialModeSwitches: SPECIAL_MODES.filter((mode) => specialModeSwitches.includes(mode)),
84
+ hasOutdoorTemperatureSensor: isValidBoolean(raw.hasOutdoorTemperatureSensor) ? raw.hasOutdoorTemperatureSensor : useDefault('hasOutdoorTemperatureSensor', false),
85
+ hasSeparateHumiditySensor: isValidBoolean(raw.hasSeparateHumiditySensor) ? raw.hasSeparateHumiditySensor : useDefault('hasSeparateHumiditySensor', false),
86
+ isComposed: isValidBoolean(raw.isComposed) ? raw.isComposed : useDefault('isComposed', true),
84
87
  };
85
88
  if (isValidString(raw.localPassword, 1))
86
89
  device.localPassword = raw.localPassword;
@@ -141,6 +144,9 @@ export function hasSameExposure(left, right) {
141
144
  return (left.name === right.name &&
142
145
  left.hasFanAutoSwitch === right.hasFanAutoSwitch &&
143
146
  left.hasSeparateFan === right.hasSeparateFan &&
147
+ left.hasOutdoorTemperatureSensor === right.hasOutdoorTemperatureSensor &&
148
+ left.hasSeparateHumiditySensor === right.hasSeparateHumiditySensor &&
149
+ left.isComposed === right.isComposed &&
144
150
  left.specialModeSwitches.join() === right.specialModeSwitches.join() &&
145
151
  left.fanControl === right.fanControl &&
146
152
  left.swingControl === right.swingControl &&
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge-daikin-local",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge-daikin-local",
9
- "version": "0.3.0",
9
+ "version": "0.4.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "daikin-local-control": "0.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-daikin-local",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Matterbridge Daikin Local",
5
5
  "author": "https://github.com/cheziyi",
6
6
  "license": "Apache-2.0",