matterbridge-daikin-local 0.5.0 → 0.6.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 +16 -0
- package/README.md +32 -16
- package/apps/frontend/build/app.js +1 -1
- package/apps/frontend/build/app.js.map +3 -3
- package/dist/accessory/daikinAccessory.js +6 -5
- package/dist/accessory/endpoints.js +2 -2
- package/dist/accessory/layout.js +11 -2
- package/dist/accessory/matterState.js +5 -5
- package/dist/config.js +13 -8
- package/dist/controls.js +9 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ 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.6.0] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- [devices]: Add the option to make the auto fan speed the first or the last step of the fan speed, for Matter apps that show no auto fan mode. First is step 1 or the bottom of the slider, last is the top step or 100 %, and the speeds of the unit share the rest.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- [devices]: A Matter app turning the fan to off or 0 sets the first step of the fan speed, the slowest speed or auto when auto is the first step, instead of being ignored. Every Matter fan control has an off position below its first step, and an air conditioner has no fan-off.
|
|
16
|
+
- [setup]: Choose where auto is in Matter in one place in the setup page: the native Auto fan mode, a switch beside it, or the first or the last step of the fan speed in place of it. The Apple Home and Google Home presets pick the switch.
|
|
17
|
+
- [setup]: Choose how the dry and fan modes appear in one place in the setup page: the native Matter modes of the air conditioner only, a mode select, or a switch for each picked mode.
|
|
18
|
+
- [config]: **Breaking:** the `hasDryModeSwitch`, `hasFanModeSwitch` and `hasModeSelect` settings of a stored device are replaced by `modeControl`, which is `native`, `select` or `switches`, and `modeSwitches`, which lists `dehumidify`, `fan` or both while `modeControl` is `switches`. A device stored with a mode switch or a mode select comes back with the native modes only: choose them again in the setup page.
|
|
19
|
+
- [config]: **Breaking:** the `hasFanAutoSwitch` setting of a stored device is replaced by `fanAutoControl`, which is `native`, `switch`, `firstStep` or `lastStep`. A device stored with the auto fan switch comes back with the native Auto fan mode instead: choose the switch again in the setup page.
|
|
20
|
+
- [setup]: Make the parts of the air conditioner a choice of the presets. The Full Matter support preset makes the switches, a fan of its own and a humidity sensor of its own parts of one composed device, and the Apple Home and Google Home presets make each of them a device of its own, since neither app is known to show the parts of a composed device well. A preset now shows as chosen only while that choice matches too.
|
|
21
|
+
- [setup]: Replace the Full and Minimal presets with a Full Matter support preset, for apps that show every Matter control: fan speed as steps with its auto mode, the swing as a mode select, a switch for each special mode and the outdoor temperature sensor, all as one composed device. The Apple Home and Google Home presets no longer add the dry and fan mode switches, and the Google Home preset adds the humidity sensor of its own, as the Apple Home preset does. Both leave the swing out of Matter and as it is, instead of adding a switch for each axis.
|
|
22
|
+
|
|
7
23
|
## [0.5.0] - 2026-10-14
|
|
8
24
|
|
|
9
25
|
### Added
|
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ Afterwards the same page lists your air conditioners with their status. From the
|
|
|
80
80
|
</tr>
|
|
81
81
|
<tr>
|
|
82
82
|
<td valign="top">
|
|
83
|
-
<img src="screenshots/options-presets-fan.png" alt="The Options step with the
|
|
83
|
+
<img src="screenshots/options-presets-fan.png" alt="The Options step with the presets and the three ways to show the fan speed">
|
|
84
84
|
<br><b>Options.</b> Start from a preset, then choose how the fan speed appears.
|
|
85
85
|
</td>
|
|
86
86
|
<td valign="top">
|
|
@@ -170,11 +170,22 @@ The room temperature and the humidity are always on the air conditioner. You cho
|
|
|
170
170
|
| Percentage | A slider from 0 to 100. The plugin rounds it to the nearest speed of the unit and shows the percentage that speed stands for |
|
|
171
171
|
| Not shown | No fan control. The fan speed either stays as it is, or is set to the speed you pick each time the plugin sends a command |
|
|
172
172
|
|
|
173
|
-
An air conditioner has no fan-off of its own, so a Matter app turning the fan to off or 0
|
|
173
|
+
Every Matter fan control has an off position below its first step: speed 0, 0 %, or the fan mode Off. An air conditioner has no fan-off of its own, so a Matter app turning the fan to off or 0 sets the first step instead: the slowest speed, or auto when auto is the first step. To let the unit pick the speed, choose auto, wherever the setup puts it: see [Auto fan speed](#auto-fan-speed).
|
|
174
174
|
|
|
175
|
-
The fan speed is a control of the air conditioner itself. For Matter apps that show no fan speed on an air conditioner, or do not show it well, the setup can move it to a fan of its own instead, named `<name> Fan`, as steps or as a percentage. The air conditioner then has no fan speed, so an app does not show two. No preset does this. The fan behaves the same in either place: it shows the speed the unit holds, also while the unit is off, and it
|
|
175
|
+
The fan speed is a control of the air conditioner itself. For Matter apps that show no fan speed on an air conditioner, or do not show it well, the setup can move it to a fan of its own instead, named `<name> Fan`, as steps or as a percentage. The air conditioner then has no fan speed, so an app does not show two. No preset does this. The fan behaves the same in either place: it shows the speed the unit holds, also while the unit is off, and turning it off sets the first step.
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
#### Auto fan speed
|
|
178
|
+
|
|
179
|
+
On a unit that has an auto fan speed, the setup puts auto in one of these places in Matter:
|
|
180
|
+
|
|
181
|
+
| Option | In Matter |
|
|
182
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| Native fan mode only | The Auto fan mode of the fan speed, as Matter has it. Not every app shows it |
|
|
184
|
+
| A switch | The Auto fan mode, and a switch named `<name> Auto Fan` beside it. On sets the fan to auto. Off puts it back at the speed it last ran at, or at a middle speed when the plugin never saw it at one |
|
|
185
|
+
| The first step | Below the slowest speed of the unit: step 1, or the bottom of the slider |
|
|
186
|
+
| The last step | Above the fastest speed of the unit: the top step, or 100 % |
|
|
187
|
+
|
|
188
|
+
A switch or a step is for Matter apps that show no Auto fan mode. A step takes the place of the Auto fan mode, because the two cannot show at once: in the Auto fan mode, Matter holds no percent. As a step, auto shares the scale with the speeds of the unit, so a unit with six speeds gets seven steps. Off and 0 set the first step, as above, so with auto first, turning the fan off sets it to auto. While the fan speed is hidden, only the switch is offered, and not even that with a fixed speed, because that speed is sent with every command.
|
|
178
189
|
|
|
179
190
|
### Swing
|
|
180
191
|
|
|
@@ -188,18 +199,26 @@ Fixed louver positions are not controlled from Matter.
|
|
|
188
199
|
|
|
189
200
|
### Dry and fan modes
|
|
190
201
|
|
|
191
|
-
Dry and Fan only are always among the
|
|
202
|
+
Dry and Fan only are always among the native Matter modes of the air conditioner, but some Matter apps, such as Apple Home, only offer Off, Cool, Heat and Auto. For those apps the setup can add one of these:
|
|
203
|
+
|
|
204
|
+
| Option | In Matter |
|
|
205
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
206
|
+
| Native modes only | Nothing more: Dry and Fan only are among the modes of the air conditioner. The default |
|
|
207
|
+
| A mode select | A mode select named `<name> Mode`, with every mode of the unit: see [Mode selects](#mode-selects) |
|
|
208
|
+
| Switches | A switch named `<name> Dry Mode`, `<name> Fan Mode` or both, for whichever of the two modes the unit has that you pick |
|
|
209
|
+
|
|
210
|
+
A switch does this:
|
|
192
211
|
|
|
193
212
|
| Switch | Does |
|
|
194
213
|
| ---------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
195
214
|
| Turned on | Turns the unit on in that mode |
|
|
196
215
|
| Turned off | Puts the unit back in Cool, or in Heat on a unit that only heats. The unit stays on: turn it off from the air conditioner |
|
|
197
216
|
|
|
198
|
-
A switch is on while the unit is on and in its mode, whichever way it got there.
|
|
217
|
+
A switch is on while the unit is on and in its mode, whichever way it got there.
|
|
199
218
|
|
|
200
219
|
### Mode selects
|
|
201
220
|
|
|
202
|
-
A mode select is the Matter device type that picks one value from a list, as a drop-down does. The setup can add one for the mode, named `<name> Mode`, on a unit with a dry or a fan mode, and one for the swing, named `<name> Swing`, as a choice under [Swing](#swing). Each lists only what the unit has, and
|
|
221
|
+
A mode select is the Matter device type that picks one value from a list, as a drop-down does. The setup can add one for the mode, named `<name> Mode`, on a unit with a dry or a fan mode, and one for the swing, named `<name> Swing`, as a choice under [Swing](#swing). Each lists only what the unit has, and is chosen instead of the switches: the mode select under [Dry and fan modes](#dry-and-fan-modes), the swing select under [Swing](#swing). The Full Matter support preset adds the swing select, and no preset adds the mode select.
|
|
203
222
|
|
|
204
223
|
| Mode select | Lists | Picking a value |
|
|
205
224
|
| ----------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -221,18 +240,15 @@ Matter has no control for Powerful, Econo and Streamer, so the setup can add a s
|
|
|
221
240
|
|
|
222
241
|
In the Options step, a preset fills in every choice above for what the unit can do. You can change any of them afterwards.
|
|
223
242
|
|
|
224
|
-
| Preset
|
|
225
|
-
|
|
|
226
|
-
| Apple Home
|
|
227
|
-
| Google Home
|
|
228
|
-
| Full
|
|
229
|
-
| Minimal | Not shown | Not shown | No | No | No | None | On the air conditioner | No |
|
|
230
|
-
|
|
231
|
-
The Apple Home and Google Home presets are starting points, not tested recommendations. They add switches and sensors for what each app is not known to show: Home's climate controls have Off, Heat, Cool and Auto only and are reported to show no humidity, and Google Home is reported to show Fan only but not Dry.
|
|
243
|
+
| Preset | Fan speed | Swing | Dry and fan modes | Auto fan | Special mode switches | Humidity | Outdoor sensor | Parts of the air conditioner |
|
|
244
|
+
| ------------------- | ---------- | ------------- | ----------------- | -------- | --------------------- | --------------------------- | -------------- | ---------------------------- |
|
|
245
|
+
| Apple Home | Percentage | Not shown | Native modes only | Switch | None | Also on a sensor of its own | No | No |
|
|
246
|
+
| Google Home | Steps | Not shown | Native modes only | Switch | None | Also on a sensor of its own | No | No |
|
|
247
|
+
| Full Matter support | Steps | A mode select | Native modes only | Native | All | On the air conditioner | Yes | Yes |
|
|
232
248
|
|
|
233
249
|
### One device or several
|
|
234
250
|
|
|
235
|
-
The switches, the mode selects, a fan of its own and a humidity sensor of its own are parts of the air conditioner, which Matter calls a composed device, so they move between rooms with it. The outdoor temperature sensor is a device of its own either way, so that you can put it in another room than the air conditioner. The setup can make each of them a device of its own instead, beside the air conditioner. A Matter app then does not know that they belong together, so each is put in a room by itself: moving the air conditioner to another room leaves them where they were. A new device starts as one composed device
|
|
251
|
+
The switches, the mode selects, a fan of its own and a humidity sensor of its own are parts of the air conditioner, which Matter calls a composed device, so they move between rooms with it. The outdoor temperature sensor is a device of its own either way, so that you can put it in another room than the air conditioner. The setup can make each of them a device of its own instead, beside the air conditioner. A Matter app then does not know that they belong together, so each is put in a room by itself: moving the air conditioner to another room leaves them where they were. A new device starts as one composed device. The Full Matter support preset keeps it one, and the Apple Home and Google Home presets make each part a device of its own: see [Presets](#presets).
|
|
236
252
|
|
|
237
253
|
```mermaid
|
|
238
254
|
flowchart TD
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
async function y(e,o,n){try{let[a,r]=e.split(" "),i={method:a};o!==void 0&&(i.headers={"Content-Type":"application/json"},i.body=JSON.stringify(o));let d=await fetch(new URL(`api/${r}${n===void 0?"":`?${n}`}`,document.baseURI),i);if(d.status===204)return{ok:!0};if(d.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let h=await d.json();return"ok"in h?h:{ok:!1,error:h.error??`The plugin answered with status ${d.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}function M(e){let{fanControl:o,fanSpeedWhenHidden:n,swingControl:a,swingModeWhenHidden:r,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:h,hasModeSelect:u,hasSeparateFan:x,specialModeSwitches:g,hasOutdoorTemperatureSensor:se,hasSeparateHumiditySensor:ie,isComposed:ae}=e;return{fanControl:o,fanSpeedWhenHidden:n,swingControl:a,swingModeWhenHidden:r,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:h,hasModeSelect:u,hasSeparateFan:x,specialModeSwitches:[...g],hasOutdoorTemperatureSensor:se,hasSeparateHumiditySensor:ie,isComposed:ae}}var re={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!1,hasFanModeSwitch:!1,hasFanAutoSwitch:!1,hasModeSelect:!1,hasSeparateFan:!1,specialModeSwitches:[],hasOutdoorTemperatureSensor:!1,hasSeparateHumiditySensor:!1,isComposed:!0};function q(e){return{adapter:e,name:e.name??"",isNew:!0,...M(re)}}function W(e){let{capabilities:o,controls:n,name:a,macAddress:r,host:i,protocol:d,requiresRegistration:h,usesHttps:u,requiresLocalPassword:x}=e;return{adapter:{host:i,protocol:d,name:a,macAddress:r,requiresRegistration:h,usesHttps:u,requiresLocalPassword:x},name:a,isNew:!1,capabilities:o,controls:n,...M(e)}}var B={fanSteps:[],hasFanAuto:!1,hasSwing:!1,hasDryMode:!1,hasFanMode:!1,specialModes:[]};var s={screen:"loading",setup:void 0,found:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",confirmingRemoval:""};function f(){return s.queue[0]}function S(e){return e.name===""?e.adapter.host:e.name}function N(e){return(s.setup?.devices??[]).some(o=>o.macAddress===e.macAddress)}var O;function U(e){O=e}function l(){let e=document.getElementById("app");if(e===null||O===void 0)return;let o=`${s.screen}:${f()?.adapter.host??""}`,n=e.dataset.form===o,a=document.activeElement instanceof HTMLElement?document.activeElement.id:"",r=new Map;for(let i of e.querySelectorAll("input[type=text], input[type=password]"))i.id!==""&&r.set(i.id,i.value);if(e.replaceChildren(...O[s.screen]().filter(i=>i!==!1&&i!==null&&i!==void 0)),e.dataset.form=o,!!n){for(let[i,d]of r){let h=document.getElementById(i);h instanceof HTMLInputElement&&(h.value=d)}a!==""&&document.getElementById(a)?.focus()}}async function A(e,o){s.isBusy=!0,s.busyText=e,s.error="",l();try{return await o()}finally{s.isBusy=!1,l()}}async function H(){let e=await y("GET state");if(!e.ok){s.error=e.error,s.screen="home",l();return}if(s.setup=e.state,e.state.devices.length===0){await T();return}s.screen="home",l()}async function $(){let e=await y("GET state");e.ok&&(s.setup=e.state)}function v(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",$().then(l),l()}async function T(){s.screen="find";let e=await A("Searching your network. This takes about three seconds\u2026",async()=>y("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,l();return}await $();let o=s.found.filter(n=>!e.devices.some(a=>a.host===n.host)&&s.selected.has(n.host)).map(n=>({...n,isConfigured:N(n)}));s.found=[...e.devices,...o];for(let n of e.devices)!n.isConfigured&&!n.hasDuplicateName&&s.selected.add(n.host);l()}async function K(e){let o=await A(`Looking for an adapter at ${e}\u2026`,async()=>y("POST identify",{host:e}));if(!o.ok){s.error=o.error,l();return}let n=N(o.device),a=o.device.requiresRegistration===!0;s.found=[...s.found.filter(r=>r.host!==o.device.host),{...o.device,isConfigured:n,hasDuplicateName:!1,needsRegistration:a}],n||s.selected.add(o.device.host),l()}function L(e){s.queue=e,s.saved=[],s.handled=[],j()}async function j(){let e=f();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await $(),l();return}s.screen="connect",s.error="",e.probe=void 0,l(),await E(e)}async function E(e,o){let a={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:o},r=o===void 0?`Checking ${S(e)}\u2026`:`Registering with ${S(e)}\u2026`,i=await A(r,async()=>y(o===void 0?"POST probe":"POST register",a));if(!i.ok){s.error=i.error,l();return}if(e.probe=i.result,e.adapter=i.result.device,i.result.status==="ready"){e.capabilities=i.result.capabilities,e.controls=i.result.controls;let d=i.result.device.name??"";d!==""&&(e.name=d),e.name!==""&&(s.screen="options")}l()}function Y(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:S(e),isSaved:!1}),j()}async function G(e){let{adapter:o,name:n,localPassword:a}=e,r={...o,name:n,localPassword:a,...M(e)},i=await A(`Saving ${n}\u2026`,async()=>y("POST device",{device:r}));if(!i.ok){s.error=i.error,l();return}s.setup=i.state,s.saved.push(n),s.handled.push({label:n,isSaved:!0}),s.queue.shift(),await j()}async function V(e){let o=await A(`Removing ${e.name}\u2026`,async()=>y("DELETE device",void 0,`macAddress=${encodeURIComponent(e.macAddress)}`));s.confirmingRemoval="",o.ok||(s.error=o.error),await $(),l()}function t(e,o={},...n){let a=document.createElement(e);for(let[r,i]of Object.entries(o))i===void 0||i===!1||(r==="class"?a.className=String(i):r==="html"?a.innerHTML=String(i):r.startsWith("on")&&typeof i=="function"?a.addEventListener(r.slice(2),i):r in a?a[r]=i:a.setAttribute(r,String(i)));for(let r of n)r!==!1&&r!==null&&r!==void 0&&a.append(r);return a}function c(e,o,n="quiet",a=!1){return t("button",{type:"button",class:`button ${n}`,onclick:o,disabled:a||s.isBusy},e)}function m(){return s.isBusy?t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),s.busyText):s.error!==""?t("p",{class:"notice problem",role:"alert"},s.error):null}function R(){let e=f();if(e===void 0||!e.isNew||s.handled.length+s.queue.length<2)return null;let o=s.screen==="connect"?"Connecting":"Choosing options";return t("ol",{class:"units","aria-label":"Air conditioners in this setup"},...s.handled.map(n=>t("li",{class:n.isSaved?"done":"skipped"},t("strong",{},n.label),t("span",{class:"meta"},n.isSaved?"Added":"Skipped"))),...s.queue.map((n,a)=>t("li",{class:a===0?"current":"ahead","aria-current":a===0?"true":void 0},t("strong",{},S(n)),t("span",{class:"meta"},a===0?o:"Waiting"))))}function F(e,o){return t("header",{class:"unit-header"},t("h1",{},S(e)),t("p",{class:"meta"},o))}function k(){if(s.queue.length>0&&!s.queue.some(a=>a.isNew))return null;let e=["Find","Connect","Options"],n=["find","connect","options"].indexOf(s.screen);return t("ol",{class:"steps"},...e.map((a,r)=>{let i="ahead";return r<n?i="done":r===n&&(i="current"),t("li",{class:i,"aria-current":r===n?"step":void 0},a)}))}function le(e){let o=t("input",{type:"text",id:"device-name",maxLength:32,autocomplete:"off"}),n=()=>{let a=o.value.trim();if(a!==""){if(s.setup?.devices.some(r=>r.name===a)){s.error=`"${a}" is already the name of another air conditioner. Pick a different one.`,l();return}e.name=a,s.error="",s.screen="options",l()}};return t("div",{class:"field"},t("label",{for:"device-name"},"Name this air conditioner"),t("p",{class:"help"},"Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead."),t("div",{class:"inline-form"},o,c("Continue",n,"primary")))}function ce(e,o){let n=t("input",{type:"password",id:"lock-code",inputMode:"numeric",autocomplete:"off",maxLength:64});return t("div",{class:"field"},t("label",{for:"lock-code"},"Child lock code"),t("p",{class:"help"},"The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter."),o?t("p",{class:"problem-text"},"The adapter did not accept that code."):null,t("div",{class:"inline-form"},n,c("Check the code",()=>{n.value!==""&&(e.localPassword=n.value,E(e))},"primary")))}function de(e,o){let n=t("input",{type:"text",id:"adapter-key",autocomplete:"off",spellcheck:!1,maxLength:64});return t("div",{class:"field"},t("label",{for:"adapter-key"},"KEY on the adapter"),t("p",{class:"help"},"This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored."),o?t("p",{class:"problem-text"},"The adapter did not accept that KEY."):null,t("div",{class:"inline-form"},n,c("Register",()=>{n.value.trim()!==""&&E(e,n.value.trim())},"primary")))}function pe(e){let o=e.probe?.status;return o==="ready"&&e.name===""?le(e):o==="localPasswordRequired"||o==="localPasswordRejected"?ce(e,o==="localPasswordRejected"):o==="registrationRequired"||o==="registrationRejected"?de(e,o==="registrationRejected"):o==="unreachable"?t("p",{class:"notice problem",role:"alert"},`${e.name} did not answer at ${e.adapter.host}. Check that it has power and is on the network, then try again.`):o==="failed"?t("p",{class:"notice problem",role:"alert"},`${e.name} answered with an error: ${e.probe?.message??"unknown"}`):null}function z(){let e=f();if(e===void 0)return[m()];let o=e.probe?.status==="unreachable"||e.probe?.status==="failed"||s.error!==""&&e.name!=="";return[k(),R(),F(e,`Connecting to its adapter at ${e.adapter.host}.`),m(),s.isBusy?null:pe(e),t("div",{class:"footer"},o?c("Try again",()=>{E(e)}):null,c(e.isNew&&s.queue.length>1?"Skip this one":"Cancel",e.isNew&&s.queue.length>1?Y:v))]}function J(){let e=s.saved;return[t("header",{},t("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} air conditioners are set up`)),t("p",{class:"lead"},"They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page."),t("div",{class:"footer"},c("Done",v,"primary"))]}function he(e){let o=!e.isConfigured&&!e.hasDuplicateName,n=[];e.isConfigured&&n.push("Already set up."),e.hasDuplicateName&&n.push("Another adapter has the same name. Give each its own name in its Daikin app, then search again."),e.name===void 0&&n.push("It has not told its name yet. If it has none, you will name it after connecting."),o&&e.needsRegistration?n.push("Asks for the KEY printed on its label."):o&&e.requiresRegistration&&n.push("Already registered with this plugin, so it will not ask for its KEY."),o&&e.requiresLocalPassword&&n.push("Asks for its child lock code.");let a=t("input",{type:"checkbox",id:`found-${e.host}`,checked:o&&s.selected.has(e.host),disabled:!o,onchange:()=>{a.checked?s.selected.add(e.host):s.selected.delete(e.host),l()}});return t("li",{class:o?"found":"found unavailable"},t("label",{for:`found-${e.host}`},a,t("span",{},t("strong",{},e.name??"Unnamed adapter"),t("span",{class:"meta"},` ${e.host}`),...n.map(r=>t("span",{class:"meta note"},r)))))}function Q(){let e=s.found.filter(r=>s.selected.has(r.host)&&!r.isConfigured&&!r.hasDuplicateName),o=t("input",{type:"text",id:"host",placeholder:"192.168.1.50",spellcheck:!1,autocomplete:"off"}),n=()=>{o.value.trim()!==""&&K(o.value.trim())};o.addEventListener("keydown",r=>{r.key==="Enter"&&n()});let a=e.length===1?"Set up 1 air conditioner":`Set up ${e.length} air conditioners`;return[k(),t("header",{},t("h1",{},"Find your air conditioners")),m(),s.found.length>0?t("ul",{class:"found-list"},...s.found.map(he)):null,!s.isBusy&&s.found.length===0?t("p",{class:"empty"},"No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below."):null,t("details",{class:"manual",open:!s.isBusy&&s.found.length===0},t("summary",{},"Can't see your air conditioner?"),t("p",{class:"help"},"Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address."),t("div",{class:"inline-form"},t("label",{for:"host",class:"visually-hidden"},"Adapter address"),o,c("Look it up",n))),t("div",{class:"footer"},c("Search again",()=>{T()}),c("Cancel",v),c(a,()=>L(e.map(q)),"primary",e.length===0))]}function ue(e){let o=e.controls===void 0||e.controls.fanSteps.length>0,n=e.controls===void 0||e.controls.hasSwing,a=e.hasSeparateFan?" on a fan of its own":"",r=e.fanControl==="hidden"?"fan speed not shown":`fan speed as ${e.fanControl==="steps"?"steps":"a percentage"}${a}`,i={switches:"swing as switches",select:"swing as a mode select",hidden:"swing not shown"}[e.swingControl],d=[o?r:"",n?i:""].filter(g=>g!=="").join(", "),h=[e.hasDryModeSwitch?"dry mode":"",e.hasFanModeSwitch?"fan mode":"",e.hasFanAutoSwitch?"auto fan":"",...e.specialModeSwitches].filter(g=>g!==""),u=[h.length===0?"":`a switch for ${h.join(", ")}`,e.hasModeSelect?"a mode select":""].filter(g=>g!=="").join(" and "),x=e.isComposed?", all as one device":"";return d===""?`${u===""?"power, mode and temperature":u}${x}`:`${u===""?d:`${d}, and ${u}`}${x}`}function fe(e){let o={online:"Online",offline:"Not answering",stopped:"Not started"}[e.status],n=s.confirmingRemoval===e.macAddress;return t("li",{class:"device"},t("div",{class:"device-main"},t("h3",{},e.name),t("p",{class:"meta"},t("span",{class:`status ${e.status}`},o),` at ${e.host}. In Matter: ${ue(e)}.`),e.status!=="online"&&e.error?t("p",{class:"meta problem-text"},e.error):null),n?t("div",{class:"actions"},t("span",{class:"meta"},"Remove it from Matter too?"),c("Remove",()=>{V(e)},"danger"),c("Keep",()=>{s.confirmingRemoval="",l()})):t("div",{class:"actions"},c(e.status==="online"?"Options":"Reconnect",()=>L([W(e)])),c("Remove",()=>{s.confirmingRemoval=e.macAddress,l()})))}function Z(){let e=s.setup?.devices??[];return[t("header",{},t("h1",{},"Daikin air conditioners"),s.setup?c("Add air conditioners",()=>{T()},"primary"):null),m(),e.length===0&&s.setup?t("p",{class:"empty"},"No air conditioner is set up yet. Add one to control it from Matter."):t("ul",{class:"devices"},...e.map(fe)),s.setup?null:c("Try again",()=>{H()},"primary")]}var P={apple:{title:"Apple Home",text:"Fan speed as a percentage, the humidity on a sensor of its own too, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan."},google:{title:"Google Home",text:"Fan speed as steps, swing switches and a dry mode switch."},full:{title:"Full",text:"Every control, switch and sensor this unit can have, special modes and the outdoor temperature included."},minimal:{title:"Minimal",text:"No fan control, no swing control and no switches. The fan and the swing stay as they are."}};function I(e,o){let n=e==="minimal";return{fanControl:n||o.fanSteps.length===0?"hidden":e==="apple"?"percent":"steps",fanSpeedWhenHidden:"keep",swingControl:n||!o.hasSwing?"hidden":"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!n&&o.hasDryMode,hasFanModeSwitch:(e==="apple"||e==="full")&&o.hasFanMode,hasFanAutoSwitch:(e==="apple"||e==="full")&&o.hasFanAuto,hasModeSelect:!1,hasSeparateFan:!1,specialModeSwitches:e==="full"?[...o.specialModes]:[],hasOutdoorTemperatureSensor:e==="full",hasSeparateHumiditySensor:e==="apple"}}function X(e,o,n){let a=M(e);return n.fanSteps.length===0&&Object.assign(a,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),a.fanControl==="hidden"&&(a.hasSeparateFan=!1),n.hasSwing||Object.assign(a,{swingControl:"hidden",swingModeWhenHidden:"keep"}),!n.hasDryMode&&!n.hasFanMode&&(a.hasModeSelect=!1),a.specialModeSwitches=n.specialModes.filter(r=>a.specialModeSwitches.includes(r)),JSON.stringify(a)===JSON.stringify({...I(o,n),isComposed:a.isComposed})}var me={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},_={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},ee={powerful:{title:"Powerful",text:"Runs the unit at full power for a while."},econo:{title:"Econo",text:"Limits how much power the unit draws."},streamer:{title:"Streamer",text:"Runs the air purifying of the unit."}},C={steps:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect x="2" y="20" width="8" height="6"/><rect x="14" y="14" width="8" height="12"/><rect x="26" y="8" width="8" height="18"/><rect x="38" y="2" width="8" height="24"/></svg>',percent:'<svg viewBox="0 0 48 28" aria-hidden="true"><path d="M2 26 L46 2 L46 26 Z"/></svg>',hidden:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M6 14 H42" stroke-dasharray="3 5"/></svg>',switches:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19"/><path class="stroke" d="M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19"/></svg>',select:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect class="stroke" x="3" y="5" width="42" height="18" rx="4"/><path class="stroke" d="M31 12 L35 16 L39 12"/></svg>'},te="Few apps show a mode select: Home Assistant does, Apple Home does not.";function b(e,o,n){return e.isComposed?n?`It appears as a part of "${e.name}", tagged "${o}"`:`It appears as a part of "${e.name}"`:`It appears as "${e.name} ${o}"`}function p(e,o){Object.assign(e,o),l()}function D(e,o,n,a,r,i,d){let h=`${e}-${o}`,u=t("input",{type:"radio",name:e,id:h,checked:o===n,onchange:()=>d(o)});return t("label",{class:"choice pictured",for:h},u,t("span",{class:"glyph",html:i}),t("span",{},t("strong",{},a),t("span",{class:"meta"},r)))}function w(e,o,n,a,r){let i=t("input",{type:"checkbox",id:e,checked:o,onchange:()=>r(i.checked)});return t("label",{class:"choice",for:e},i,t("span",{},t("strong",{},n),t("span",{class:"meta"},a)))}function we(e,o){return t("section",{class:"presets"},t("h2",{},"Start from a preset"),t("div",{class:"preset-list"},...Object.keys(P).map(n=>t("button",{type:"button",class:"preset","aria-pressed":String(X(e,n,o)),onclick:()=>p(e,I(n,o))},t("strong",{},P[n].title),t("span",{class:"meta"},P[n].text)))),t("p",{class:"help"},"A preset fills in the choices below for what this unit can do. Change any of them afterwards."))}function ge(e,o){let n=e.capabilities?.fanSpeeds??[],a=o.fanSteps.length;if(a===0)return t("section",{},t("h2",{},"Fan speed"),t("p",{class:"help"},"This unit does not let apps set its fan speed, so there is nothing to choose."));let r=o.hasFanAuto&&!(e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep");return t("section",{},t("h2",{},"Fan speed"),t("div",{class:"choices"},D("fan","steps",e.fanControl,"Steps",`${a} speeds, one for each speed of the unit${n.includes("auto")?", and auto":""}.`,C.steps,i=>p(e,{fanControl:i})),D("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.",C.percent,i=>p(e,{fanControl:i})),D("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",C.hidden,i=>p(e,{fanControl:i}))),e.fanControl==="hidden"?t("div",{class:"field"},t("label",{for:"fan-fixed"},"When the plugin sends a command, the fan speed"),t("select",{id:"fan-fixed",onchange:i=>p(e,{fanSpeedWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.fanSpeedWhenHidden==="keep"},"stays as it is"),...n.map(i=>t("option",{value:i,selected:e.fanSpeedWhenHidden===i},`is set to ${me[i]}`)))):null,e.fanControl==="hidden"?null:w("separate-fan",e.hasSeparateFan,"Show the fan as a device of its own",`${b(e,"Fan",!1)}, and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner, or do not show it well. It always shows the speed the unit holds, also while the unit is off.`,i=>p(e,{hasSeparateFan:i})),r?w("fan-auto-switch",e.hasFanAutoSwitch,"Add a switch for auto fan speed",`${b(e,"Auto Fan",!0)}. On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,i=>p(e,{hasFanAutoSwitch:i})):null)}function ye(e){let o=e.capabilities?.swingModes??[];if(o.length===0)return t("section",{},t("h2",{},"Swing"),t("p",{class:"help"},"This unit does not let apps move its louvers, so there is nothing to choose."));let n=o.includes("vertical"),a=o.includes("horizontal"),r="One switch, for the up and down swing.";return n&&a?r="Two switches: one for the up and down swing, one for left and right.":a&&(r="One switch, for the left and right swing."),t("section",{},t("h2",{},"Swing"),t("div",{class:"choices"},D("swing","switches",e.swingControl,"Switches",r,C.switches,i=>p(e,{swingControl:i})),D("swing","select",e.swingControl,"A mode select",`${b(e,"Swing",!0)}, and picks one of: ${o.map(i=>_[i].toLowerCase()).join(", ")}. ${te}`,C.select,i=>p(e,{swingControl:i})),D("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",C.hidden,i=>p(e,{swingControl:i}))),e.swingControl==="hidden"?t("div",{class:"field"},t("label",{for:"swing-fixed"},"When the plugin sends a command, the swing"),t("select",{id:"swing-fixed",onchange:i=>p(e,{swingModeWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.swingModeWhenHidden==="keep"},"stays as it is"),...o.map(i=>t("option",{value:i,selected:e.swingModeWhenHidden===i},`is set to: ${_[i]}`)))):null)}function ve(e,o){if(!o.hasDryMode&&!o.hasFanMode)return null;let n=e.capabilities?.modes??[];return t("section",{},t("h2",{},"Dry and fan modes"),t("p",{class:"help"},`These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${n.includes("cool")?"cool":"heat"}. A mode select instead offers every mode in one control.`),t("div",{class:"choices"},w("mode-select",e.hasModeSelect,"Use a mode select instead of switches",`${b(e,"Mode",!0)}, with Off and every mode of the unit, as the thermostat mode has them. Picking a mode turns the unit on in it. ${te}`,a=>p(e,a?{hasModeSelect:a,hasDryModeSwitch:!1,hasFanModeSwitch:!1}:{hasModeSelect:a})),o.hasDryMode&&!e.hasModeSelect?w("dry-mode-switch",e.hasDryModeSwitch,"Add a switch for dry mode",`${b(e,"Dry Mode",!0)}.`,a=>p(e,{hasDryModeSwitch:a})):null,o.hasFanMode&&!e.hasModeSelect?w("fan-mode-switch",e.hasFanModeSwitch,"Add a switch for fan mode",`${b(e,"Fan Mode",!0)}.`,a=>p(e,{hasFanModeSwitch:a})):null))}function Se(e,o){return o.specialModes.length===0?null:t("section",{},t("h2",{},"Special modes"),t("p",{class:"help"},"Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in."),t("div",{class:"choices"},...o.specialModes.map(n=>w(`special-${n}`,e.specialModeSwitches.includes(n),`Add a switch for ${ee[n].title}`,ee[n].text,a=>p(e,{specialModeSwitches:o.specialModes.filter(r=>r===n?a:e.specialModeSwitches.includes(r))})))))}function be(e){return t("section",{},t("h2",{},"Humidity and outdoor temperature"),t("p",{class:"help"},"No adapter says which readings its unit has, so each of these takes effect once the unit gives that reading. The room temperature and the humidity are always on the air conditioner."),t("div",{class:"choices"},w("separate-humidity",e.hasSeparateHumiditySensor,"Also show the humidity on a sensor of its own",`${b(e,"Humidity",!1)}, and the air conditioner keeps its humidity. For apps that show no humidity on an air conditioner, which Apple Home is reported to be.`,o=>p(e,{hasSeparateHumiditySensor:o})),w("outdoor-sensor",e.hasOutdoorTemperatureSensor,"Add a sensor for the outdoor temperature",`It appears as "${e.name} Outdoor Temperature". It is a device of its own, which you can put in another room, such as outside. Until you do, a Matter app may show the outdoor temperature as that of the room it put the sensor in. Some units give no outdoor temperature while they are idle: the sensor then shows no value.`,o=>p(e,{hasOutdoorTemperatureSensor:o}))))}function xe(e){return t("section",{},t("h2",{},"One device or several"),t("p",{class:"help"},"The switches, a fan of its own and a humidity sensor of its own can be parts of the air conditioner in Matter, or devices of their own beside it. The outdoor temperature sensor is always a device of its own, so that you can put it in another room than the air conditioner."),t("div",{class:"choices"},w("composed",e.isComposed,"Make them parts of the air conditioner",`Your Matter app has one device, "${e.name}", and they move between rooms with it. A part has no name of its own in Matter, so an app may show the switches as numbered outlets, and an app that shows no parts shows the air conditioner alone. Turned off, each is a device of its own with its own name, which your app puts in a room by itself.`,o=>p(e,{isComposed:o}))))}function oe(){let e=f();if(e===void 0)return[m()];let o=e.controls??B;return[k(),R(),F(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter: choose how the rest appears.`),we(e,o),ge(e,o),ye(e),ve(e,o),Se(e,o),be(e),xe(e),e.isNew?null:t("p",{class:"help"},"Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change."),m(),t("div",{class:"footer"},c("Cancel",v),c(e.isNew?`Add ${e.name}`:"Save options",()=>{G(e)},"primary"))]}function ne(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let o of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let n=e.getPropertyValue(o).trim();n!==""&&document.documentElement.style.setProperty(`--host${o.slice(1)}`,n)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}U({loading:()=>[t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),"Loading\u2026")],home:Z,find:Q,connect:z,options:oe,done:J});ne();l();H();
|
|
1
|
+
async function b(e,t,n){try{let[i,r]=e.split(" "),a={method:i};t!==void 0&&(a.headers={"Content-Type":"application/json"},a.body=JSON.stringify(t));let c=await fetch(new URL(`api/${r}${n===void 0?"":`?${n}`}`,document.baseURI),a);if(c.status===204)return{ok:!0};if(c.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let l=await c.json();return"ok"in l?l:{ok:!1,error:l.error??`The plugin answered with status ${c.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}function k(e){let{fanControl:t,fanSpeedWhenHidden:n,swingControl:i,swingModeWhenHidden:r,modeControl:a,modeSwitches:c,fanAutoControl:l,hasSeparateFan:m,specialModeSwitches:f,hasOutdoorTemperatureSensor:w,hasSeparateHumiditySensor:h,isComposed:v}=e;return{fanControl:t,fanSpeedWhenHidden:n,swingControl:i,swingModeWhenHidden:r,modeControl:a,modeSwitches:[...c],fanAutoControl:l,hasSeparateFan:m,specialModeSwitches:[...f],hasOutdoorTemperatureSensor:w,hasSeparateHumiditySensor:h,isComposed:v}}var le={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",modeControl:"native",modeSwitches:[],fanAutoControl:"native",hasSeparateFan:!1,specialModeSwitches:[],hasOutdoorTemperatureSensor:!1,hasSeparateHumiditySensor:!1,isComposed:!0};function K(e){return{adapter:e,name:e.name??"",isNew:!0,...k(le)}}function Y(e){let{capabilities:t,controls:n,name:i,macAddress:r,host:a,protocol:c,requiresRegistration:l,usesHttps:m,requiresLocalPassword:f}=e;return{adapter:{host:a,protocol:c,name:i,macAddress:r,requiresRegistration:l,usesHttps:m,requiresLocalPassword:f},name:i,isNew:!1,capabilities:t,controls:n,...k(e)}}var G={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 g(){return s.queue[0]}function x(e){return e.name===""?e.adapter.host:e.name}function W(e){return(s.setup?.devices??[]).some(t=>t.macAddress===e.macAddress)}var q;function V(e){q=e}function d(){let e=document.getElementById("app");if(e===null||q===void 0)return;let t=`${s.screen}:${g()?.adapter.host??""}`,n=e.dataset.form===t,i=document.activeElement instanceof HTMLElement?document.activeElement.id:"",r=new Map;for(let a of e.querySelectorAll("input[type=text], input[type=password]"))a.id!==""&&r.set(a.id,a.value);if(e.replaceChildren(...q[s.screen]().filter(a=>a!==!1&&a!==null&&a!==void 0)),e.dataset.form=t,!!n){for(let[a,c]of r){let l=document.getElementById(a);l instanceof HTMLInputElement&&(l.value=c)}i!==""&&document.getElementById(i)?.focus()}}async function E(e,t){s.isBusy=!0,s.busyText=e,s.error="",d();try{return await t()}finally{s.isBusy=!1,d()}}async function R(){let e=await b("GET state");if(!e.ok){s.error=e.error,s.screen="home",d();return}if(s.setup=e.state,e.state.devices.length===0){await H();return}s.screen="home",d()}async function F(){let e=await b("GET state");e.ok&&(s.setup=e.state)}function C(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",F().then(d),d()}async function H(){s.screen="find";let e=await E("Searching your network. This takes about three seconds\u2026",async()=>b("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,d();return}await F();let t=s.found.filter(n=>!e.devices.some(i=>i.host===n.host)&&s.selected.has(n.host)).map(n=>({...n,isConfigured:W(n)}));s.found=[...e.devices,...t];for(let n of e.devices)!n.isConfigured&&!n.hasDuplicateName&&s.selected.add(n.host);d()}async function z(e){let t=await E(`Looking for an adapter at ${e}\u2026`,async()=>b("POST identify",{host:e}));if(!t.ok){s.error=t.error,d();return}let n=W(t.device),i=t.device.requiresRegistration===!0;s.found=[...s.found.filter(r=>r.host!==t.device.host),{...t.device,isConfigured:n,hasDuplicateName:!1,needsRegistration:i}],n||s.selected.add(t.device.host),d()}function N(e){s.queue=e,s.saved=[],s.handled=[],B()}async function B(){let e=g();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await F(),d();return}s.screen="connect",s.error="",e.probe=void 0,d(),await L(e)}async function L(e,t){let i={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:t},r=t===void 0?`Checking ${x(e)}\u2026`:`Registering with ${x(e)}\u2026`,a=await E(r,async()=>b(t===void 0?"POST probe":"POST register",i));if(!a.ok){s.error=a.error,d();return}if(e.probe=a.result,e.adapter=a.result.device,a.result.status==="ready"){e.capabilities=a.result.capabilities,e.controls=a.result.controls;let c=a.result.device.name??"";c!==""&&(e.name=c),e.name!==""&&(s.screen="options")}d()}function J(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:x(e),isSaved:!1}),B()}async function Q(e){let{adapter:t,name:n,localPassword:i}=e,r={...t,name:n,localPassword:i,...k(e)},a=await E(`Saving ${n}\u2026`,async()=>b("POST device",{device:r}));if(!a.ok){s.error=a.error,d();return}s.setup=a.state,s.saved.push(n),s.handled.push({label:n,isSaved:!0}),s.queue.shift(),await B()}async function Z(e){let t=await E(`Removing ${e.name}\u2026`,async()=>b("DELETE device",void 0,`macAddress=${encodeURIComponent(e.macAddress)}`));s.confirmingRemoval="",t.ok||(s.error=t.error),await F(),d()}function o(e,t={},...n){let i=document.createElement(e);for(let[r,a]of Object.entries(t))a===void 0||a===!1||(r==="class"?i.className=String(a):r==="html"?i.innerHTML=String(a):r.startsWith("on")&&typeof a=="function"?i.addEventListener(r.slice(2),a):r in i?i[r]=a:i.setAttribute(r,String(a)));for(let r of n)r!==!1&&r!==null&&r!==void 0&&i.append(r);return i}function p(e,t,n="quiet",i=!1){return o("button",{type:"button",class:`button ${n}`,onclick:t,disabled:i||s.isBusy},e)}function y(){return s.isBusy?o("p",{class:"notice working",role:"status"},o("span",{class:"spinner"}),s.busyText):s.error!==""?o("p",{class:"notice problem",role:"alert"},s.error):null}function P(){let e=g();if(e===void 0||!e.isNew||s.handled.length+s.queue.length<2)return null;let t=s.screen==="connect"?"Connecting":"Choosing options";return o("ol",{class:"units","aria-label":"Air conditioners in this setup"},...s.handled.map(n=>o("li",{class:n.isSaved?"done":"skipped"},o("strong",{},n.label),o("span",{class:"meta"},n.isSaved?"Added":"Skipped"))),...s.queue.map((n,i)=>o("li",{class:i===0?"current":"ahead","aria-current":i===0?"true":void 0},o("strong",{},x(n)),o("span",{class:"meta"},i===0?t:"Waiting"))))}function j(e,t){return o("header",{class:"unit-header"},o("h1",{},x(e)),o("p",{class:"meta"},t))}function M(){if(s.queue.length>0&&!s.queue.some(i=>i.isNew))return null;let e=["Find","Connect","Options"],n=["find","connect","options"].indexOf(s.screen);return o("ol",{class:"steps"},...e.map((i,r)=>{let a="ahead";return r<n?a="done":r===n&&(a="current"),o("li",{class:a,"aria-current":r===n?"step":void 0},i)}))}function ce(e){let t=o("input",{type:"text",id:"device-name",maxLength:32,autocomplete:"off"}),n=()=>{let i=t.value.trim();if(i!==""){if(s.setup?.devices.some(r=>r.name===i)){s.error=`"${i}" is already the name of another air conditioner. Pick a different one.`,d();return}e.name=i,s.error="",s.screen="options",d()}};return o("div",{class:"field"},o("label",{for:"device-name"},"Name this air conditioner"),o("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."),o("div",{class:"inline-form"},t,p("Continue",n,"primary")))}function de(e,t){let n=o("input",{type:"password",id:"lock-code",inputMode:"numeric",autocomplete:"off",maxLength:64});return o("div",{class:"field"},o("label",{for:"lock-code"},"Child lock code"),o("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."),t?o("p",{class:"problem-text"},"The adapter did not accept that code."):null,o("div",{class:"inline-form"},n,p("Check the code",()=>{n.value!==""&&(e.localPassword=n.value,L(e))},"primary")))}function pe(e,t){let n=o("input",{type:"text",id:"adapter-key",autocomplete:"off",spellcheck:!1,maxLength:64});return o("div",{class:"field"},o("label",{for:"adapter-key"},"KEY on the adapter"),o("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."),t?o("p",{class:"problem-text"},"The adapter did not accept that KEY."):null,o("div",{class:"inline-form"},n,p("Register",()=>{n.value.trim()!==""&&L(e,n.value.trim())},"primary")))}function he(e){let t=e.probe?.status;return t==="ready"&&e.name===""?ce(e):t==="localPasswordRequired"||t==="localPasswordRejected"?de(e,t==="localPasswordRejected"):t==="registrationRequired"||t==="registrationRejected"?pe(e,t==="registrationRejected"):t==="unreachable"?o("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.`):t==="failed"?o("p",{class:"notice problem",role:"alert"},`${e.name} answered with an error: ${e.probe?.message??"unknown"}`):null}function X(){let e=g();if(e===void 0)return[y()];let t=e.probe?.status==="unreachable"||e.probe?.status==="failed"||s.error!==""&&e.name!=="";return[M(),P(),j(e,`Connecting to its adapter at ${e.adapter.host}.`),y(),s.isBusy?null:he(e),o("div",{class:"footer"},t?p("Try again",()=>{L(e)}):null,p(e.isNew&&s.queue.length>1?"Skip this one":"Cancel",e.isNew&&s.queue.length>1?J:C))]}function _(){let e=s.saved;return[o("header",{},o("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} air conditioners are set up`)),o("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."),o("div",{class:"footer"},p("Done",C,"primary"))]}function ue(e){let t=!e.isConfigured&&!e.hasDuplicateName,n=[];e.isConfigured&&n.push("Already set up."),e.hasDuplicateName&&n.push("Another adapter has the same name. Give each its own name in its Daikin app, then search again."),e.name===void 0&&n.push("It has not told its name yet. If it has none, you will name it after connecting."),t&&e.needsRegistration?n.push("Asks for the KEY printed on its label."):t&&e.requiresRegistration&&n.push("Already registered with this plugin, so it will not ask for its KEY."),t&&e.requiresLocalPassword&&n.push("Asks for its child lock code.");let i=o("input",{type:"checkbox",id:`found-${e.host}`,checked:t&&s.selected.has(e.host),disabled:!t,onchange:()=>{i.checked?s.selected.add(e.host):s.selected.delete(e.host),d()}});return o("li",{class:t?"found":"found unavailable"},o("label",{for:`found-${e.host}`},i,o("span",{},o("strong",{},e.name??"Unnamed adapter"),o("span",{class:"meta"},` ${e.host}`),...n.map(r=>o("span",{class:"meta note"},r)))))}function ee(){let e=s.found.filter(r=>s.selected.has(r.host)&&!r.isConfigured&&!r.hasDuplicateName),t=o("input",{type:"text",id:"host",placeholder:"192.168.1.50",spellcheck:!1,autocomplete:"off"}),n=()=>{t.value.trim()!==""&&z(t.value.trim())};t.addEventListener("keydown",r=>{r.key==="Enter"&&n()});let i=e.length===1?"Set up 1 air conditioner":`Set up ${e.length} air conditioners`;return[M(),o("header",{},o("h1",{},"Find your air conditioners")),y(),s.found.length>0?o("ul",{class:"found-list"},...s.found.map(ue)):null,!s.isBusy&&s.found.length===0?o("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,o("details",{class:"manual",open:!s.isBusy&&s.found.length===0},o("summary",{},"Can't see your air conditioner?"),o("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."),o("div",{class:"inline-form"},o("label",{for:"host",class:"visually-hidden"},"Adapter address"),t,p("Look it up",n))),o("div",{class:"footer"},p("Search again",()=>{H()}),p("Cancel",C),p(i,()=>N(e.map(K)),"primary",e.length===0))]}function fe(e){let t=e.controls===void 0||e.controls.fanSteps.length>0,n=e.controls===void 0||e.controls.hasSwing,i=e.hasSeparateFan?" on a fan of its own":"",r={native:"",switch:"",firstStep:" with auto first",lastStep:" with auto last"}[e.fanAutoControl],a=e.fanControl==="hidden"?"fan speed not shown":`fan speed as ${e.fanControl==="steps"?"steps":"a percentage"}${r}${i}`,c={switches:"swing as switches",select:"swing as a mode select",hidden:"swing not shown"}[e.swingControl],l=[t?a:"",n?c:""].filter(v=>v!=="").join(", "),f=[...e.modeControl==="switches"?e.modeSwitches.map(v=>v==="dehumidify"?"dry mode":"fan mode"):[],e.fanAutoControl==="switch"?"auto fan":"",...e.specialModeSwitches].filter(v=>v!==""),w=[f.length===0?"":`a switch for ${f.join(", ")}`,e.modeControl==="select"?"a mode select":""].filter(v=>v!=="").join(" and "),h=e.isComposed?", all as one device":"";return l===""?`${w===""?"power, mode and temperature":w}${h}`:`${w===""?l:`${l}, and ${w}`}${h}`}function me(e){let t={online:"Online",offline:"Not answering",stopped:"Not started"}[e.status],n=s.confirmingRemoval===e.macAddress;return o("li",{class:"device"},o("div",{class:"device-main"},o("h3",{},e.name),o("p",{class:"meta"},o("span",{class:`status ${e.status}`},t),` at ${e.host}. In Matter: ${fe(e)}.`),e.status!=="online"&&e.error?o("p",{class:"meta problem-text"},e.error):null),n?o("div",{class:"actions"},o("span",{class:"meta"},"Remove it from Matter too?"),p("Remove",()=>{Z(e)},"danger"),p("Keep",()=>{s.confirmingRemoval="",d()})):o("div",{class:"actions"},p(e.status==="online"?"Options":"Reconnect",()=>N([Y(e)])),p("Remove",()=>{s.confirmingRemoval=e.macAddress,d()})))}function te(){let e=s.setup?.devices??[];return[o("header",{},o("h1",{},"Daikin air conditioners"),s.setup?p("Add air conditioners",()=>{H()},"primary"):null),y(),e.length===0&&s.setup?o("p",{class:"empty"},"No air conditioner is set up yet. Add one to control it from Matter."):o("ul",{class:"devices"},...e.map(me)),s.setup?null:p("Try again",()=>{R()},"primary")]}var O={apple:{title:"Apple Home",text:"Fan speed as a percentage, the humidity on a sensor of its own too, and a switch for auto fan, each a device of its own. The swing is not shown and stays as it is."},google:{title:"Google Home",text:"Fan speed as steps, the humidity on a sensor of its own too, and a switch for auto fan, each a device of its own. The swing is not shown and stays as it is."},matter:{title:"Full Matter support",text:"For apps that show every Matter control: fan speed as steps with its auto mode, the swing as a mode select, a switch for each special mode and the outdoor temperature, all as one device."}};function U(e,t){let n=e==="matter",i=n&&t.hasSwing?"select":"hidden";return{fanControl:t.fanSteps.length===0?"hidden":e==="apple"?"percent":"steps",fanSpeedWhenHidden:"keep",swingControl:i,swingModeWhenHidden:"keep",modeControl:"native",modeSwitches:[],fanAutoControl:!n&&t.hasFanAuto?"switch":"native",hasSeparateFan:!1,specialModeSwitches:n?[...t.specialModes]:[],hasOutdoorTemperatureSensor:n,hasSeparateHumiditySensor:!n,isComposed:n}}function oe(e,t,n){let i=k(e);n.fanSteps.length===0&&Object.assign(i,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),i.fanControl==="hidden"&&(i.hasSeparateFan=!1);let r=i.fanAutoControl==="firstStep"||i.fanAutoControl==="lastStep",a=i.fanControl==="hidden"&&i.fanSpeedWhenHidden!=="keep";return(!n.hasFanAuto||r&&i.fanControl==="hidden"||a)&&(i.fanAutoControl="native"),n.hasSwing||Object.assign(i,{swingControl:"hidden",swingModeWhenHidden:"keep"}),i.modeControl!=="switches"&&(i.modeSwitches=[]),!n.hasDryMode&&!n.hasFanMode&&Object.assign(i,{modeControl:"native",modeSwitches:[]}),i.specialModeSwitches=n.specialModes.filter(c=>i.specialModeSwitches.includes(c)),JSON.stringify(i)===JSON.stringify(U(t,n))}var I={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},ne={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},se={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>',select:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect class="stroke" x="3" y="5" width="42" height="18" rx="4"/><path class="stroke" d="M31 12 L35 16 L39 12"/></svg>'},ie="Few apps show a mode select: Home Assistant does, Apple Home does not.";function T(e,t,n){return e.isComposed?n?`It appears as a part of "${e.name}", tagged "${t}"`:`It appears as a part of "${e.name}"`:`It appears as "${e.name} ${t}"`}function u(e,t){Object.assign(e,t),d()}function D(e,t,n,i,r,a,c){let l=`${e}-${t}`,m=o("input",{type:"radio",name:e,id:l,checked:t===n,onchange:()=>c(t)});return o("label",{class:"choice pictured",for:l},m,o("span",{class:"glyph",html:a}),o("span",{},o("strong",{},i),o("span",{class:"meta"},r)))}function S(e,t,n,i,r,a){let c=`${e}-${t}`,l=o("input",{type:"radio",name:e,id:c,checked:t===n,onchange:()=>a(t)});return o("label",{class:"choice",for:c},l,o("span",{},o("strong",{},i),o("span",{class:"meta"},r)))}function $(e,t,n,i,r){let a=o("input",{type:"checkbox",id:e,checked:t,onchange:()=>r(a.checked)});return o("label",{class:"choice",for:e},a,o("span",{},o("strong",{},n),o("span",{class:"meta"},i)))}function we(e,t){return o("section",{class:"presets"},o("h2",{},"Start from a preset"),o("div",{class:"preset-list"},...Object.keys(O).map(n=>o("button",{type:"button",class:"preset","aria-pressed":String(oe(e,n,t)),onclick:()=>u(e,U(n,t))},o("strong",{},O[n].title),o("span",{class:"meta"},O[n].text)))),o("p",{class:"help"},"A preset fills in the choices below for what this unit can do. Change any of them afterwards."))}function ge(e){let t=e.map(n=>I[n]);return t.length===1?t[0]:`${t.slice(0,-1).join(", ")} and ${t[t.length-1]}`}function ye(e,t,n){if(!t.hasFanAuto||e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep")return null;let i=f=>u(e,{fanAutoControl:f}),r=e.fanControl==="hidden",a=`${T(e,"Auto Fan",!0)}. On sets the fan to auto, off puts it back at the speed it last ran at.`,c=I[t.fanSteps[0]],l=I[t.fanSteps[t.fanSteps.length-1]],m=r?[S("fan-auto","native",n,"Not shown in Matter","Without a fan speed in Matter, auto has no place there either.",i),S("fan-auto","switch",n,"A switch",a,i)]:[S("fan-auto","native",n,"Native fan mode only","Auto is a mode of the fan speed, beside its speeds, as Matter has it.",i),S("fan-auto","switch",n,"A switch",`${a} Auto stays a fan mode as well.`,i),S("fan-auto","firstStep",n,"The first step",`Auto comes below ${c}: step 1, or the bottom of the slider.`,i),S("fan-auto","lastStep",n,"The last step",`Auto comes above ${l}: the top step, or 100 %.`,i)];return o("div",{class:"field"},o("h3",{id:"fan-auto-title"},"Auto fan speed"),r?null:o("p",{class:"help"},"Matter has auto as a fan mode, which not every app shows. A switch makes it reachable there too, and a step takes its place."),o("div",{class:"choices",role:"radiogroup","aria-labelledby":"fan-auto-title"},...m))}function ve(e,t){let n=e.capabilities?.fanSpeeds??[];if(t.fanSteps.length===0)return o("section",{},o("h2",{},"Fan speed"),o("p",{class:"help"},"This unit does not let apps set its fan speed, so there is nothing to choose."));let r=e.fanControl==="hidden",a=t.hasFanAuto&&!r&&(e.fanAutoControl==="firstStep"||e.fanAutoControl==="lastStep"),c=t.hasFanAuto&&e.fanAutoControl==="switch",l=t.fanSteps;a&&(l=e.fanAutoControl==="firstStep"?["auto",...t.fanSteps]:[...t.fanSteps,"auto"]);let f=t.hasFanAuto&&!a?" Auto is not among them: it is a fan mode of its own.":"",w=`${l.length} ${l.length===1?"speed":"speeds"}, ${a?"in order":"slowest first"}: ${ge(l)}.${f}`;return o("section",{},o("h2",{},"Fan speed"),o("div",{class:"choices"},D("fan","steps",e.fanControl,"Steps",w,A.steps,h=>u(e,{fanControl:h})),D("fan","percent",e.fanControl,"Percentage",`A slider from 0 to 100, which the plugin rounds to the nearest of ${w}`,A.percent,h=>u(e,{fanControl:h})),D("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",A.hidden,h=>u(e,{fanControl:h}))),e.fanControl==="hidden"?o("div",{class:"field"},o("label",{for:"fan-fixed"},"When the plugin sends a command, the fan speed"),o("select",{id:"fan-fixed",onchange:h=>u(e,{fanSpeedWhenHidden:h.target.value})},o("option",{value:"keep",selected:e.fanSpeedWhenHidden==="keep"},"stays as it is"),...n.map(h=>o("option",{value:h,selected:e.fanSpeedWhenHidden===h},`is set to ${I[h]}`)))):null,e.fanControl==="hidden"?null:$("separate-fan",e.hasSeparateFan,"Show the fan as a device of its own",`${T(e,"Fan",!1)}, and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner, or do not show it well. It always shows the speed the unit holds, also while the unit is off.`,h=>u(e,{hasSeparateFan:h})),ye(e,t,r&&!c?"native":e.fanAutoControl))}function Se(e){let t=e.capabilities?.swingModes??[];if(t.length===0)return o("section",{},o("h2",{},"Swing"),o("p",{class:"help"},"This unit does not let apps move its louvers, so there is nothing to choose."));let n=t.includes("vertical"),i=t.includes("horizontal"),r="One switch, for the up and down swing.";return n&&i?r="Two switches: one for the up and down swing, one for left and right.":i&&(r="One switch, for the left and right swing."),o("section",{},o("h2",{},"Swing"),o("div",{class:"choices"},D("swing","switches",e.swingControl,"Switches",r,A.switches,a=>u(e,{swingControl:a})),D("swing","select",e.swingControl,"A mode select",`${T(e,"Swing",!0)}, and picks one of: ${t.map(a=>ne[a].toLowerCase()).join(", ")}. ${ie}`,A.select,a=>u(e,{swingControl:a})),D("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",A.hidden,a=>u(e,{swingControl:a}))),e.swingControl==="hidden"?o("div",{class:"field"},o("label",{for:"swing-fixed"},"When the plugin sends a command, the swing"),o("select",{id:"swing-fixed",onchange:a=>u(e,{swingModeWhenHidden:a.target.value})},o("option",{value:"keep",selected:e.swingModeWhenHidden==="keep"},"stays as it is"),...t.map(a=>o("option",{value:a,selected:e.swingModeWhenHidden===a},`is set to: ${ne[a]}`)))):null)}function be(e,t){if(!t.hasDryMode&&!t.hasFanMode)return null;let n=e.capabilities?.modes??[],i=[...t.hasDryMode?["dehumidify"]:[],...t.hasFanMode?["fan"]:[]],r={dehumidify:"Dry Mode",fan:"Fan Mode"},a=i.filter(l=>e.modeSwitches.includes(l)),c=l=>u(e,l==="switches"&&a.length===0?{modeControl:l,modeSwitches:i}:{modeControl:l});return o("section",{},o("h2",{},"Dry and fan modes"),o("p",{class:"help"},"These are among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto."),o("div",{class:"choices",role:"radiogroup","aria-label":"Dry and fan modes"},S("mode-control","native",e.modeControl,"Native modes only","Dry and fan are modes of the air conditioner, as Matter has them, and nothing more is added.",c),S("mode-control","select",e.modeControl,"A mode select",`${T(e,"Mode",!0)}, with Off and every mode of the unit, as the air conditioner has them. Picking a mode turns the unit on in it. ${ie}`,c),S("mode-control","switches",e.modeControl,"Switches",`A switch for each mode picked below. On puts the unit in that mode, off puts it back in ${n.includes("cool")?"cool":"heat"}.`,c)),e.modeControl==="switches"?o("div",{class:"choices"},...i.map(l=>$(`mode-switch-${l}`,a.includes(l),`Add a switch for ${r[l].toLowerCase()}`,`${T(e,r[l],!0)}.`,m=>u(e,{modeSwitches:i.filter(f=>f===l?m:a.includes(f))})))):null)}function Ce(e,t){return t.specialModes.length===0?null:o("section",{},o("h2",{},"Special modes"),o("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."),o("div",{class:"choices"},...t.specialModes.map(n=>$(`special-${n}`,e.specialModeSwitches.includes(n),`Add a switch for ${se[n].title}`,se[n].text,i=>u(e,{specialModeSwitches:t.specialModes.filter(r=>r===n?i:e.specialModeSwitches.includes(r))})))))}function xe(e){return o("section",{},o("h2",{},"Humidity and outdoor temperature"),o("p",{class:"help"},"No adapter says which readings its unit has, so each of these takes effect once the unit gives that reading. The room temperature and the humidity are always on the air conditioner."),o("div",{class:"choices"},$("separate-humidity",e.hasSeparateHumiditySensor,"Also show the humidity on a sensor of its own",`${T(e,"Humidity",!1)}, and the air conditioner keeps its humidity. For apps that show no humidity on an air conditioner, which Apple Home is reported to be.`,t=>u(e,{hasSeparateHumiditySensor:t})),$("outdoor-sensor",e.hasOutdoorTemperatureSensor,"Add a sensor for the outdoor temperature",`It appears as "${e.name} Outdoor Temperature". It is a device of its own, which you can put in another room, such as outside. Until you do, a Matter app may show the outdoor temperature as that of the room it put the sensor in. Some units give no outdoor temperature while they are idle: the sensor then shows no value.`,t=>u(e,{hasOutdoorTemperatureSensor:t}))))}function ke(e){return o("section",{},o("h2",{},"One device or several"),o("p",{class:"help"},"The switches, a fan of its own and a humidity sensor of its own can be parts of the air conditioner in Matter, or devices of their own beside it. The outdoor temperature sensor is always a device of its own, so that you can put it in another room than the air conditioner."),o("div",{class:"choices"},$("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.`,t=>u(e,{isComposed:t}))))}function ae(){let e=g();if(e===void 0)return[y()];let t=e.controls??G;return[M(),P(),j(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter: choose how the rest appears.`),we(e,t),ve(e,t),Se(e),be(e,t),Ce(e,t),xe(e),ke(e),e.isNew?null:o("p",{class:"help"},"Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change."),y(),o("div",{class:"footer"},p("Cancel",C),p(e.isNew?`Add ${e.name}`:"Save options",()=>{Q(e)},"primary"))]}function re(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let t of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let n=e.getPropertyValue(t).trim();n!==""&&document.documentElement.style.setProperty(`--host${t.slice(1)}`,n)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}V({loading:()=>[o("p",{class:"notice working",role:"status"},o("span",{class:"spinner"}),"Loading\u2026")],home:te,find:ee,connect:X,options:ae,done:_});re();d();R();
|
|
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.
|
|
@@ -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, hasModeSelect, hasSeparateFan, specialModeSwitches, hasOutdoorTemperatureSensor, hasSeparateHumiditySensor, isComposed } =\n source;\n return {\n fanControl,\n fanSpeedWhenHidden,\n swingControl,\n swingModeWhenHidden,\n hasDryModeSwitch,\n hasFanModeSwitch,\n hasFanAutoSwitch,\n hasModeSelect,\n hasSeparateFan,\n specialModeSwitches: [...specialModeSwitches],\n hasOutdoorTemperatureSensor,\n hasSeparateHumiditySensor,\n isComposed,\n };\n}\n\n// What a new device starts with. What a stored device without a setting gets is the default of `parseDeviceConfig`\n// in the plugin, which stands for how the plugin behaved before that setting existed: the two may differ.\nconst defaultExposure: Exposure = {\n fanControl: 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: false,\n hasFanModeSwitch: false,\n hasFanAutoSwitch: false,\n hasModeSelect: false,\n hasSeparateFan: false,\n specialModeSwitches: [],\n hasOutdoorTemperatureSensor: false,\n hasSeparateHumiditySensor: false,\n isComposed: true,\n};\n\nexport function draftFromAdapter(adapter: Adapter): Draft {\n return { adapter, name: adapter.name ?? '', isNew: true, ...pickExposure(defaultExposure) };\n}\n\nexport function draftFromStored(device: StoredDevice): Draft {\n const { capabilities, controls, name, macAddress, host, protocol, requiresRegistration, usesHttps, requiresLocalPassword } = device;\n const adapter: Adapter = { host, protocol, name, macAddress, requiresRegistration, usesHttps, requiresLocalPassword };\n return { adapter, name, isNew: false, capabilities, controls, ...pickExposure(device) };\n}\n\n// What a unit the plugin told nothing about can have: no control and no switch.\nexport const noControls: AvailableControls = { fanSteps: [], hasFanAuto: false, hasSwing: false, hasDryMode: false, hasFanMode: false, specialModes: [] };\n", "/**\n * @file apps/frontend/src/state.ts\n * @description What the setup page knows at any moment, and what it reads from that.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Capabilities, Exposure, FoundAdapter, ProbeResult, SetupState } from './api.js';\n\n/** One air conditioner on its way through Connect and Options. */\nexport interface Draft extends Exposure {\n adapter: Adapter;\n name: string;\n isNew: boolean;\n localPassword?: string;\n capabilities?: Capabilities;\n /** What the plugin says the unit leaves to choose in Options. */\n controls?: AvailableControls;\n probe?: ProbeResult;\n}\n\nexport type ScreenName = 'loading' | 'home' | 'find' | 'connect' | 'options' | 'done';\n\nexport const view = {\n screen: 'loading' as ScreenName,\n setup: undefined as SetupState | undefined,\n found: [] as FoundAdapter[],\n selected: new Set<string>(),\n queue: [] as Draft[],\n saved: [] as string[],\n /** The units this run is done with, in the order it went through them. */\n handled: [] as { label: string; isSaved: boolean }[],\n isBusy: false,\n busyText: '',\n error: '',\n confirmingRemoval: '',\n};\n\nexport function currentDraft(): Draft | undefined {\n return view.queue[0];\n}\n\n/** What a unit is called on the page: its name, or its address until it has one. */\nexport function draftLabel(draft: Draft): string {\n return draft.name === '' ? draft.adapter.host : draft.name;\n}\n\n/** Whether an adapter is that of a stored device, which is known by its MAC address. */\nexport function isStored(adapter: Adapter): boolean {\n return (view.setup?.devices ?? []).some((device) => device.macAddress === adapter.macAddress);\n}\n", "/**\n * @file apps/frontend/src/render.ts\n * @description Draws the screen the setup page is on. The screens are handed in by app.ts, so that they and the\n * actions can call for a redraw without this file depending on them.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Child } from './dom.js';\nimport { currentDraft, type ScreenName, view } from './state.js';\n\nexport type Screens = Record<ScreenName, () => Child[]>;\n\nlet screens: Screens | undefined;\n\n/** Tell the page what each screen draws. */\nexport function setScreens(all: Screens): void {\n screens = all;\n}\n\nexport function render(): void {\n const root = document.getElementById('app');\n if (root === null || screens === undefined) return;\n // Keep what the user is typing when the same form redraws around it, and never carry it over to another form.\n const form = `${view.screen}:${currentDraft()?.adapter.host ?? ''}`;\n const isSameForm = root.dataset.form === form;\n const activeId = document.activeElement instanceof HTMLElement ? document.activeElement.id : '';\n const typed = new Map<string, string>();\n for (const input of root.querySelectorAll<HTMLInputElement>('input[type=text], input[type=password]')) {\n if (input.id !== '') typed.set(input.id, input.value);\n }\n root.replaceChildren(...screens[view.screen]().filter((child): child is Node | string => child !== false && child !== null && child !== undefined));\n root.dataset.form = form;\n if (!isSameForm) return;\n for (const [id, value] of typed) {\n const input = document.getElementById(id);\n if (input instanceof HTMLInputElement) input.value = value;\n }\n if (activeId !== '') document.getElementById(activeId)?.focus();\n}\n", "/**\n * @file apps/frontend/src/actions.ts\n * @description What the setup page does: each action calls the plugin, changes the state of the page and redraws it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { api, type StoredDevice } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport { render } from './render.js';\nimport { currentDraft, type Draft, draftLabel, isStored, view } from './state.js';\n\n/**\n * Run a call while the page shows that it is working.\n *\n * @param text What the page says meanwhile.\n * @param task The call.\n * @returns What the call returned.\n */\nasync function busy<T>(text: string, task: () => Promise<T>): Promise<T> {\n view.isBusy = true;\n view.busyText = text;\n view.error = '';\n render();\n try {\n return await task();\n } finally {\n view.isBusy = false;\n render();\n }\n}\n\nexport async function loadState(): Promise<void> {\n const reply = await api('GET state');\n if (!reply.ok) {\n view.error = reply.error;\n view.screen = 'home';\n render();\n return;\n }\n view.setup = reply.state;\n // With nothing set up yet, the page goes straight to finding the air conditioners.\n if (reply.state.devices.length === 0) {\n await search();\n return;\n }\n view.screen = 'home';\n render();\n}\n\nasync function refreshState(): Promise<void> {\n const reply = await api('GET state');\n if (reply.ok) view.setup = reply.state;\n}\n\nexport function goHome(): void {\n view.queue = [];\n view.saved = [];\n view.handled = [];\n view.error = '';\n view.screen = 'home';\n void refreshState().then(render);\n render();\n}\n\nexport async function search(): Promise<void> {\n view.screen = 'find';\n const reply = await busy('Searching your network. This takes about three seconds\u2026', async () => api('POST discover'));\n if (!reply.ok) {\n view.error = `The search did not run: ${reply.error}`;\n render();\n return;\n }\n // Keep the adapters added by address: a search does not find them. One of them may have been set up since.\n await refreshState();\n const byAddress = view.found\n .filter((adapter) => !reply.devices.some((device) => device.host === adapter.host) && view.selected.has(adapter.host))\n .map((adapter) => ({ ...adapter, isConfigured: isStored(adapter) }));\n view.found = [...reply.devices, ...byAddress];\n for (const adapter of reply.devices) {\n if (!adapter.isConfigured && !adapter.hasDuplicateName) view.selected.add(adapter.host);\n }\n render();\n}\n\nexport async function lookUp(host: string): Promise<void> {\n const reply = await busy(`Looking for an adapter at ${host}\u2026`, async () => api('POST identify', { host }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n const isConfigured = isStored(reply.device);\n // Whether the adapter knows this plugin already shows in the Connect step: an address alone does not tell.\n const needsRegistration = reply.device.requiresRegistration === true;\n view.found = [...view.found.filter((adapter) => adapter.host !== reply.device.host), { ...reply.device, isConfigured, hasDuplicateName: false, needsRegistration }];\n if (!isConfigured) view.selected.add(reply.device.host);\n render();\n}\n\nexport function startQueue(drafts: Draft[]): void {\n view.queue = drafts;\n view.saved = [];\n view.handled = [];\n void enterConnect();\n}\n\nasync function enterConnect(): Promise<void> {\n const draft = currentDraft();\n if (draft === undefined) {\n view.screen = view.saved.length > 0 ? 'done' : 'home';\n await refreshState();\n render();\n return;\n }\n view.screen = 'connect';\n view.error = '';\n draft.probe = undefined;\n render();\n await check(draft);\n}\n\nexport async function check(draft: Draft, key?: string): Promise<void> {\n const device = { ...draft.adapter, name: draft.name === '' ? undefined : draft.name };\n const body = { device, localPassword: draft.localPassword, key };\n const text = key === undefined ? `Checking ${draftLabel(draft)}\u2026` : `Registering with ${draftLabel(draft)}\u2026`;\n const reply = await busy(text, async () => api(key === undefined ? 'POST probe' : 'POST register', body));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n draft.probe = reply.result;\n draft.adapter = reply.result.device;\n if (reply.result.status === 'ready') {\n draft.capabilities = reply.result.capabilities;\n draft.controls = reply.result.controls;\n // The name the adapter tells is the name of the device. Only an adapter that has none is named here.\n const told = reply.result.device.name ?? '';\n if (told !== '') draft.name = told;\n if (draft.name !== '') view.screen = 'options';\n }\n render();\n}\n\nexport function skipDraft(): void {\n const skipped = view.queue.shift();\n if (skipped !== undefined) view.handled.push({ label: draftLabel(skipped), isSaved: false });\n void enterConnect();\n}\n\nexport async function saveDraft(draft: Draft): Promise<void> {\n const { adapter, name, localPassword } = draft;\n const device = { ...adapter, name, localPassword, ...pickExposure(draft) };\n const reply = await busy(`Saving ${name}\u2026`, async () => api('POST device', { device }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n view.setup = reply.state;\n view.saved.push(name);\n view.handled.push({ label: name, isSaved: true });\n view.queue.shift();\n await enterConnect();\n}\n\nexport async function removeDevice(device: StoredDevice): Promise<void> {\n const reply = await busy(`Removing ${device.name}\u2026`, async () => api('DELETE device', undefined, `macAddress=${encodeURIComponent(device.macAddress)}`));\n view.confirmingRemoval = '';\n if (!reply.ok) view.error = reply.error;\n await refreshState();\n render();\n}\n", "/**\n * @file apps/frontend/src/dom.ts\n * @description The one helper the setup page builds its elements with.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nexport type Child = Node | string | false | null | undefined;\n\n/**\n * Create an element.\n *\n * @param tag The tag name.\n * @param props `class`, `on<event>` listeners, and anything else as a property when the element has it, else an attribute.\n * @param children Nodes or text; false, null and undefined are skipped.\n * @returns The element.\n */\nexport function h<K extends keyof HTMLElementTagNameMap>(tag: K, props: Record<string, unknown> = {}, ...children: Child[]): HTMLElementTagNameMap[K] {\n const element = document.createElement(tag);\n for (const [key, value] of Object.entries(props)) {\n if (value === undefined || value === false) continue;\n if (key === 'class') element.className = String(value);\n else if (key === 'html') element.innerHTML = String(value);\n else if (key.startsWith('on') && typeof value === 'function') element.addEventListener(key.slice(2), value as EventListener);\n else if (key in element) (element as unknown as Record<string, unknown>)[key] = value;\n else element.setAttribute(key, String(value));\n }\n for (const child of children) {\n if (child !== false && child !== null && child !== undefined) element.append(child);\n }\n return element;\n}\n", "/**\n * @file apps/frontend/src/components.ts\n * @description The pieces more than one screen of the setup page is made of.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { type Child, h } from './dom.js';\nimport { currentDraft, type Draft, draftLabel, type ScreenName, view } from './state.js';\n\nexport function button(label: string, onclick: () => void, kind: 'primary' | 'quiet' | 'danger' = 'quiet', disabled = false): HTMLButtonElement {\n return h('button', { type: 'button', class: `button ${kind}`, onclick, disabled: disabled || view.isBusy }, label);\n}\n\nexport function notice(): Child {\n if (view.isBusy) return h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), view.busyText);\n if (view.error !== '') return h('p', { class: 'notice problem', role: 'alert' }, view.error);\n return null;\n}\n\n/**\n * The units of this run, with the one the page is on marked: shown when a run sets up more than one, so that it is\n * plain which unit the Connect and Options steps are about.\n */\nexport function unitStrip(): Child {\n const current = currentDraft();\n if (current === undefined || !current.isNew || view.handled.length + view.queue.length < 2) return null;\n const doing = view.screen === 'connect' ? 'Connecting' : 'Choosing options';\n return h(\n 'ol',\n { 'class': 'units', 'aria-label': 'Air conditioners in this setup' },\n ...view.handled.map((unit) => h('li', { class: unit.isSaved ? 'done' : 'skipped' }, h('strong', {}, unit.label), h('span', { class: 'meta' }, unit.isSaved ? 'Added' : 'Skipped'))),\n ...view.queue.map((draft, index) =>\n h('li', { 'class': index === 0 ? 'current' : 'ahead', 'aria-current': index === 0 ? 'true' : undefined }, h('strong', {}, draftLabel(draft)), h('span', { class: 'meta' }, index === 0 ? doing : 'Waiting')),\n ),\n );\n}\n\n/** The heading of a step that is about one unit: the unit first, then what the step does with it. */\nexport function unitHeader(draft: Draft, text: string): HTMLElement {\n return h('header', { class: 'unit-header' }, h('h1', {}, draftLabel(draft)), h('p', { class: 'meta' }, text));\n}\n\nexport function steps(): Child {\n if (view.queue.length > 0 && !view.queue.some((draft) => draft.isNew)) return null;\n const names = ['Find', 'Connect', 'Options'];\n const screens: ScreenName[] = ['find', 'connect', 'options'];\n const current = screens.indexOf(view.screen);\n return h(\n 'ol',\n { class: 'steps' },\n ...names.map((name, index) => {\n let state = 'ahead';\n if (index < current) state = 'done';\n else if (index === current) state = 'current';\n return h('li', { 'class': state, 'aria-current': index === current ? 'step' : undefined }, name);\n }),\n );\n}\n", "/**\n * @file apps/frontend/src/screens/connect.ts\n * @description The Connect step: the page checks an adapter and asks for what the adapter wants before it answers.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { check, goHome, skipDraft } from '../actions.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft, view } from '../state.js';\n\n/** Asks for the name of a unit whose adapter has none. */\nfunction nameForm(draft: Draft): HTMLElement {\n const name = h('input', { type: 'text', id: 'device-name', maxLength: 32, autocomplete: 'off' });\n const submit = (): void => {\n const value = name.value.trim();\n if (value === '') return;\n if (view.setup?.devices.some((device) => device.name === value)) {\n view.error = `\"${value}\" is already the name of another air conditioner. Pick a different one.`;\n render();\n return;\n }\n draft.name = value;\n view.error = '';\n view.screen = 'options';\n render();\n };\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'device-name' }, 'Name this air conditioner'),\n h(\n 'p',\n { class: 'help' },\n 'Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead.',\n ),\n h('div', { class: 'inline-form' }, name, button('Continue', submit, 'primary')),\n );\n}\n\nfunction lockCodeForm(draft: Draft, isRejected: boolean): HTMLElement {\n const code = h('input', { type: 'password', id: 'lock-code', inputMode: 'numeric', autocomplete: 'off', maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'lock-code' }, 'Child lock code'),\n h('p', { class: 'help' }, 'The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter.'),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that code.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n code,\n button(\n 'Check the code',\n () => {\n if (code.value === '') return;\n draft.localPassword = code.value;\n void check(draft);\n },\n 'primary',\n ),\n ),\n );\n}\n\nfunction keyForm(draft: Draft, isRejected: boolean): HTMLElement {\n const key = h('input', { type: 'text', id: 'adapter-key', autocomplete: 'off', spellcheck: false, maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'adapter-key' }, 'KEY on the adapter'),\n h(\n 'p',\n { class: 'help' },\n 'This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored.',\n ),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that KEY.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n key,\n button(\n 'Register',\n () => {\n if (key.value.trim() !== '') void check(draft, key.value.trim());\n },\n 'primary',\n ),\n ),\n );\n}\n\n/** What the adapter wants before the page can go on, or why the page cannot: nothing while the check has no answer. */\nfunction credentialForm(draft: Draft): Child {\n const status = draft.probe?.status;\n if (status === 'ready' && draft.name === '') return nameForm(draft);\n if (status === 'localPasswordRequired' || status === 'localPasswordRejected') return lockCodeForm(draft, status === 'localPasswordRejected');\n if (status === 'registrationRequired' || status === 'registrationRejected') return keyForm(draft, status === 'registrationRejected');\n if (status === 'unreachable') {\n return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} did not answer at ${draft.adapter.host}. Check that it has power and is on the network, then try again.`);\n }\n if (status === 'failed') return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} answered with an error: ${draft.probe?.message ?? 'unknown'}`);\n return null;\n}\n\nexport function connectScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const canRetry = draft.probe?.status === 'unreachable' || draft.probe?.status === 'failed' || (view.error !== '' && draft.name !== '');\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Connecting to its adapter at ${draft.adapter.host}.`),\n notice(),\n view.isBusy ? null : credentialForm(draft),\n h(\n 'div',\n { class: 'footer' },\n canRetry ? button('Try again', () => void check(draft)) : null,\n button(draft.isNew && view.queue.length > 1 ? 'Skip this one' : 'Cancel', draft.isNew && view.queue.length > 1 ? skipDraft : goHome),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/done.ts\n * @description The screen a run ends on: what it set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome } from '../actions.js';\nimport { button } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { view } from '../state.js';\n\nexport function doneScreen(): Child[] {\n const names = view.saved;\n return [\n h('header', {}, h('h1', {}, names.length === 1 ? `${names[0]} is set up` : `${names.length} air conditioners are set up`)),\n h('p', { class: 'lead' }, 'They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page.'),\n h('div', { class: 'footer' }, button('Done', goHome, 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/find.ts\n * @description The Find step: the adapters a search found or an address led to, and which of them to set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, lookUp, search, startQueue } from '../actions.js';\nimport type { FoundAdapter } from '../api.js';\nimport { button, notice, steps } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromAdapter } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction foundRow(adapter: FoundAdapter): HTMLElement {\n const isSelectable = !adapter.isConfigured && !adapter.hasDuplicateName;\n const notes: string[] = [];\n if (adapter.isConfigured) notes.push('Already set up.');\n if (adapter.hasDuplicateName) notes.push('Another adapter has the same name. Give each its own name in its Daikin app, then search again.');\n if (adapter.name === undefined) notes.push('It has not told its name yet. If it has none, you will name it after connecting.');\n if (isSelectable && adapter.needsRegistration) notes.push('Asks for the KEY printed on its label.');\n else if (isSelectable && adapter.requiresRegistration) notes.push('Already registered with this plugin, so it will not ask for its KEY.');\n if (isSelectable && adapter.requiresLocalPassword) notes.push('Asks for its child lock code.');\n const checkbox = h('input', {\n type: 'checkbox',\n id: `found-${adapter.host}`,\n checked: isSelectable && view.selected.has(adapter.host),\n disabled: !isSelectable,\n onchange: () => {\n if (checkbox.checked) view.selected.add(adapter.host);\n else view.selected.delete(adapter.host);\n render();\n },\n });\n return h(\n 'li',\n { class: isSelectable ? 'found' : 'found unavailable' },\n h(\n 'label',\n { for: `found-${adapter.host}` },\n checkbox,\n h('span', {}, h('strong', {}, adapter.name ?? 'Unnamed adapter'), h('span', { class: 'meta' }, ` ${adapter.host}`), ...notes.map((note) => h('span', { class: 'meta note' }, note))),\n ),\n );\n}\n\nexport function findScreen(): Child[] {\n const chosen = view.found.filter((adapter) => view.selected.has(adapter.host) && !adapter.isConfigured && !adapter.hasDuplicateName);\n const host = h('input', { type: 'text', id: 'host', placeholder: '192.168.1.50', spellcheck: false, autocomplete: 'off' });\n const lookUpHost = (): void => {\n if (host.value.trim() !== '') void lookUp(host.value.trim());\n };\n host.addEventListener('keydown', (event) => {\n if (event.key === 'Enter') lookUpHost();\n });\n const continueLabel = chosen.length === 1 ? 'Set up 1 air conditioner' : `Set up ${chosen.length} air conditioners`;\n return [\n steps(),\n h('header', {}, h('h1', {}, 'Find your air conditioners')),\n notice(),\n view.found.length > 0 ? h('ul', { class: 'found-list' }, ...view.found.map(foundRow)) : null,\n !view.isBusy && view.found.length === 0\n ? h('p', { class: 'empty' }, 'No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below.')\n : null,\n h(\n 'details',\n { class: 'manual', open: !view.isBusy && view.found.length === 0 },\n h('summary', {}, \"Can't see your air conditioner?\"),\n h('p', { class: 'help' }, 'Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address.'),\n h('div', { class: 'inline-form' }, h('label', { for: 'host', class: 'visually-hidden' }, 'Adapter address'), host, button('Look it up', lookUpHost)),\n ),\n h(\n 'div',\n { class: 'footer' },\n button('Search again', () => void search()),\n button('Cancel', goHome),\n button(continueLabel, () => startQueue(chosen.map(draftFromAdapter)), 'primary', chosen.length === 0),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/home.ts\n * @description The home screen: the air conditioners that are set up, and the way to add more.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState, removeDevice, search, startQueue } from '../actions.js';\nimport type { StoredDevice } from '../api.js';\nimport { button, notice } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromStored } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction describeExposure(device: StoredDevice): string {\n // What the unit cannot do is left out, whatever is stored for it. A unit the plugin has not asked yet is described as stored.\n const hasFan = device.controls === undefined || device.controls.fanSteps.length > 0;\n const hasSwing = device.controls === undefined || device.controls.hasSwing;\n const fanPlace = device.hasSeparateFan ? ' on a fan of its own' : '';\n const fan = device.fanControl === 'hidden' ? 'fan speed not shown' : `fan speed as ${device.fanControl === 'steps' ? 'steps' : 'a percentage'}${fanPlace}`;\n const swing = { switches: 'swing as switches', select: 'swing as a mode select', hidden: 'swing not shown' }[device.swingControl];\n const controls = [hasFan ? fan : '', hasSwing ? swing : ''].filter((text) => text !== '').join(', ');\n const switches = [device.hasDryModeSwitch ? 'dry mode' : '', device.hasFanModeSwitch ? 'fan mode' : '', device.hasFanAutoSwitch ? 'auto fan' : '', ...device.specialModeSwitches].filter(\n (name) => name !== '',\n );\n const extra = [switches.length === 0 ? '' : `a switch for ${switches.join(', ')}`, device.hasModeSelect ? 'a mode select' : ''].filter((text) => text !== '').join(' and ');\n const place = device.isComposed ? ', all as one device' : '';\n if (controls === '') return `${extra === '' ? 'power, mode and temperature' : extra}${place}`;\n return `${extra === '' ? controls : `${controls}, and ${extra}`}${place}`;\n}\n\nfunction deviceRow(device: StoredDevice): HTMLElement {\n const statusText = { online: 'Online', offline: 'Not answering', stopped: 'Not started' }[device.status];\n const isConfirming = view.confirmingRemoval === device.macAddress;\n return h(\n 'li',\n { class: 'device' },\n h(\n 'div',\n { class: 'device-main' },\n h('h3', {}, device.name),\n h('p', { class: 'meta' }, h('span', { class: `status ${device.status}` }, statusText), ` at ${device.host}. In Matter: ${describeExposure(device)}.`),\n device.status !== 'online' && device.error ? h('p', { class: 'meta problem-text' }, device.error) : null,\n ),\n isConfirming\n ? h(\n 'div',\n { class: 'actions' },\n h('span', { class: 'meta' }, 'Remove it from Matter too?'),\n button('Remove', () => void removeDevice(device), 'danger'),\n button('Keep', () => {\n view.confirmingRemoval = '';\n render();\n }),\n )\n : h(\n 'div',\n { class: 'actions' },\n // Either way the unit is asked first what it accepts now, so the options offered are the ones it has today.\n button(device.status === 'online' ? 'Options' : 'Reconnect', () => startQueue([draftFromStored(device)])),\n button('Remove', () => {\n view.confirmingRemoval = device.macAddress;\n render();\n }),\n ),\n );\n}\n\nexport function homeScreen(): Child[] {\n const devices = view.setup?.devices ?? [];\n return [\n h('header', {}, h('h1', {}, 'Daikin air conditioners'), view.setup ? button('Add air conditioners', () => void search(), 'primary') : null),\n notice(),\n devices.length === 0 && view.setup\n ? h('p', { class: 'empty' }, 'No air conditioner is set up yet. Add one to control it from Matter.')\n : h('ul', { class: 'devices' }, ...devices.map(deviceRow)),\n view.setup ? null : button('Try again', () => void loadState(), 'primary'),\n ];\n}\n", "/**\n * @file apps/frontend/src/presets.ts\n * @description The presets of the Options step: each fills in the choices of how a device appears in Matter.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { AvailableControls, Exposure } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport type { Draft } from './state.js';\n\nexport type PresetName = 'apple' | 'google' | 'full' | 'minimal';\n\nexport const presets: Record<PresetName, { title: string; text: string }> = {\n apple: {\n title: 'Apple Home',\n text: 'Fan speed as a percentage, the humidity on a sensor of its own too, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan.',\n },\n google: { title: 'Google Home', text: 'Fan speed as steps, swing switches and a dry mode switch.' },\n full: { title: 'Full', text: 'Every control, switch and sensor this unit can have, special modes and the outdoor temperature included.' },\n minimal: { title: 'Minimal', text: 'No fan control, no swing control and no switches. The fan and the swing stay as they are.' },\n};\n\n/**\n * The choices a preset makes for a unit: never a switch or a control the unit has no use for. A preset says which\n * controls and switches there are, not whether they are parts of the air conditioner: that choice stays as it is.\n */\nexport function presetExposure(name: PresetName, controls: AvailableControls): Omit<Exposure, 'isComposed'> {\n const isMinimal = name === 'minimal';\n return {\n fanControl: isMinimal || controls.fanSteps.length === 0 ? 'hidden' : name === 'apple' ? 'percent' : 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: isMinimal || !controls.hasSwing ? 'hidden' : 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: !isMinimal && controls.hasDryMode,\n hasFanModeSwitch: (name === 'apple' || name === 'full') && controls.hasFanMode,\n hasFanAutoSwitch: (name === 'apple' || name === 'full') && controls.hasFanAuto,\n // Few apps show a mode select, so no preset has one: the switches take its place.\n hasModeSelect: false,\n // No preset takes the fan off the air conditioner: that is for an app that shows no fan speed there.\n hasSeparateFan: false,\n specialModeSwitches: name === 'full' ? [...controls.specialModes] : [],\n // An app may show the outdoor temperature as that of the room it puts the sensor in, until the sensor is moved.\n hasOutdoorTemperatureSensor: name === 'full',\n // Home is reported to show a humidity on a sensor of its own only.\n hasSeparateHumiditySensor: name === 'apple',\n };\n}\n\n/** Whether the choices in a draft are exactly those of a preset, leaving aside what the unit has no use for. */\nexport function matchesPreset(draft: Draft, name: PresetName, controls: AvailableControls): boolean {\n const chosen = pickExposure(draft);\n if (controls.fanSteps.length === 0) Object.assign(chosen, { fanControl: 'hidden', fanSpeedWhenHidden: 'keep' });\n // The choice shows nowhere while the fan is hidden.\n if (chosen.fanControl === 'hidden') chosen.hasSeparateFan = false;\n if (!controls.hasSwing) Object.assign(chosen, { swingControl: 'hidden', swingModeWhenHidden: 'keep' });\n // The choice shows nowhere on a unit without a dry or a fan mode.\n if (!controls.hasDryMode && !controls.hasFanMode) chosen.hasModeSelect = false;\n chosen.specialModeSwitches = controls.specialModes.filter((mode) => chosen.specialModeSwitches.includes(mode));\n return JSON.stringify(chosen) === JSON.stringify({ ...presetExposure(name, controls), isComposed: chosen.isComposed });\n}\n", "/**\n * @file apps/frontend/src/screens/options.ts\n * @description The Options step: how the fan, the swing, the extra modes and the readings of a unit appear in Matter, one section each, and whether as one device.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, saveDraft } from '../actions.js';\nimport type { AvailableControls, Exposure, FanSpeed, SpecialMode, SwingMode } from '../api.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { noControls } from '../exposure.js';\nimport { matchesPreset, presetExposure, type PresetName, presets } from '../presets.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft } from '../state.js';\n\nconst fanSpeedLabels: Record<FanSpeed, string> = {\n auto: 'Auto',\n silent: 'Silent',\n level1: 'Level 1',\n level2: 'Level 2',\n level3: 'Level 3',\n level4: 'Level 4',\n level5: 'Level 5',\n};\n\nconst swingModeLabels: Record<SwingMode, string> = {\n off: 'No swing',\n vertical: 'Up and down',\n horizontal: 'Left and right',\n both: 'Up, down, left and right',\n};\n\nconst specialModeTexts: Record<SpecialMode, { title: string; text: string }> = {\n powerful: { title: 'Powerful', text: 'Runs the unit at full power for a while.' },\n econo: { title: 'Econo', text: 'Limits how much power the unit draws.' },\n streamer: { title: 'Streamer', text: 'Runs the air purifying of the unit.' },\n};\n\n// Static drawings, so innerHTML is safe for them. Everything that comes from the network goes through text nodes.\nconst glyphs = {\n steps:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect x=\"2\" y=\"20\" width=\"8\" height=\"6\"/><rect x=\"14\" y=\"14\" width=\"8\" height=\"12\"/><rect x=\"26\" y=\"8\" width=\"8\" height=\"18\"/><rect x=\"38\" y=\"2\" width=\"8\" height=\"24\"/></svg>',\n percent: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path d=\"M2 26 L46 2 L46 26 Z\"/></svg>',\n hidden: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M6 14 H42\" stroke-dasharray=\"3 5\"/></svg>',\n switches:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19\"/><path class=\"stroke\" d=\"M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19\"/></svg>',\n select: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect class=\"stroke\" x=\"3\" y=\"5\" width=\"42\" height=\"18\" rx=\"4\"/><path class=\"stroke\" d=\"M31 12 L35 16 L39 12\"/></svg>',\n};\n\n// Home Assistant is the one app known to show a mode select.\nconst selectSupport = 'Few apps show a mode select: Home Assistant does, Apple Home does not.';\n\n// A part of the air conditioner has no name of its own in Matter: a switch or a mode select is tagged with what it is.\nfunction placement(draft: Draft, label: string, isTagged: boolean): string {\n if (!draft.isComposed) return `It appears as \"${draft.name} ${label}\"`;\n return isTagged ? `It appears as a part of \"${draft.name}\", tagged \"${label}\"` : `It appears as a part of \"${draft.name}\"`;\n}\n\nfunction update(draft: Draft, change: Partial<Exposure>): void {\n Object.assign(draft, change);\n render();\n}\n\nfunction choice<T extends string>(group: string, value: T, current: T, title: string, text: string, glyph: string, onpick: (value: T) => void): HTMLElement {\n const id = `${group}-${value}`;\n const input = h('input', { type: 'radio', name: group, id, checked: value === current, onchange: () => onpick(value) });\n return h('label', { class: 'choice pictured', for: id }, input, h('span', { class: 'glyph', html: glyph }), h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction toggle(id: string, isOn: boolean, title: string, text: string, onpick: (isOn: boolean) => void): HTMLElement {\n const input = h('input', { type: 'checkbox', id, checked: isOn, onchange: () => onpick(input.checked) });\n return h('label', { class: 'choice', for: id }, input, h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction presetSection(draft: Draft, controls: AvailableControls): HTMLElement {\n return h(\n 'section',\n { class: 'presets' },\n h('h2', {}, 'Start from a preset'),\n h(\n 'div',\n { class: 'preset-list' },\n ...(Object.keys(presets) as PresetName[]).map((name) =>\n h(\n 'button',\n { 'type': 'button', 'class': 'preset', 'aria-pressed': String(matchesPreset(draft, name, controls)), 'onclick': () => update(draft, presetExposure(name, controls)) },\n h('strong', {}, presets[name].title),\n h('span', { class: 'meta' }, presets[name].text),\n ),\n ),\n ),\n h('p', { class: 'help' }, 'A preset fills in the choices below for what this unit can do. Change any of them afterwards.'),\n );\n}\n\nfunction fanSection(draft: Draft, controls: AvailableControls): HTMLElement {\n const fanSpeeds = draft.capabilities?.fanSpeeds ?? [];\n const stepCount = controls.fanSteps.length;\n if (stepCount === 0) return h('section', {}, h('h2', {}, 'Fan speed'), h('p', { class: 'help' }, 'This unit does not let apps set its fan speed, so there is nothing to choose.'));\n // A fixed fan speed is sent with every command, so a switch for auto would not hold.\n const canSwitchFanAuto = controls.hasFanAuto && !(draft.fanControl === 'hidden' && draft.fanSpeedWhenHidden !== 'keep');\n return h(\n 'section',\n {},\n h('h2', {}, 'Fan speed'),\n h(\n 'div',\n { class: 'choices' },\n choice('fan', 'steps', draft.fanControl, 'Steps', `${stepCount} speeds, one for each speed of the unit${fanSpeeds.includes('auto') ? ', and auto' : ''}.`, glyphs.steps, (fanControl) =>\n update(draft, { fanControl }),\n ),\n choice('fan', 'percent', draft.fanControl, 'Percentage', 'A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.', glyphs.percent, (fanControl) => update(draft, { fanControl })),\n choice('fan', 'hidden', draft.fanControl, 'Not shown in Matter', 'No fan control. Pick below what the plugin does with the fan speed instead.', glyphs.hidden, (fanControl) =>\n update(draft, { fanControl }),\n ),\n ),\n draft.fanControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'fan-fixed' }, 'When the plugin sends a command, the fan speed'),\n h(\n 'select',\n { id: 'fan-fixed', onchange: (event: Event) => update(draft, { fanSpeedWhenHidden: (event.target as HTMLSelectElement).value as Exposure['fanSpeedWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.fanSpeedWhenHidden === 'keep' }, 'stays as it is'),\n ...fanSpeeds.map((speed) => h('option', { value: speed, selected: draft.fanSpeedWhenHidden === speed }, `is set to ${fanSpeedLabels[speed]}`)),\n ),\n )\n : null,\n draft.fanControl === 'hidden'\n ? null\n : toggle(\n 'separate-fan',\n draft.hasSeparateFan,\n 'Show the fan as a device of its own',\n `${placement(draft, 'Fan', false)}, and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner, or do not show it well. It always shows the speed the unit holds, also while the unit is off.`,\n (hasSeparateFan) => update(draft, { hasSeparateFan }),\n ),\n canSwitchFanAuto\n ? toggle(\n 'fan-auto-switch',\n draft.hasFanAutoSwitch,\n 'Add a switch for auto fan speed',\n `${placement(draft, 'Auto Fan', true)}. On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,\n (hasFanAutoSwitch) => update(draft, { hasFanAutoSwitch }),\n )\n : null,\n );\n}\n\nfunction swingSection(draft: Draft): HTMLElement {\n const swingModes = draft.capabilities?.swingModes ?? [];\n if (swingModes.length === 0) return h('section', {}, h('h2', {}, 'Swing'), h('p', { class: 'help' }, 'This unit does not let apps move its louvers, so there is nothing to choose.'));\n const hasVertical = swingModes.includes('vertical');\n const hasHorizontal = swingModes.includes('horizontal');\n let switchText = 'One switch, for the up and down swing.';\n if (hasVertical && hasHorizontal) switchText = 'Two switches: one for the up and down swing, one for left and right.';\n else if (hasHorizontal) switchText = 'One switch, for the left and right swing.';\n return h(\n 'section',\n {},\n h('h2', {}, 'Swing'),\n h(\n 'div',\n { class: 'choices' },\n choice('swing', 'switches', draft.swingControl, 'Switches', switchText, glyphs.switches, (swingControl) => update(draft, { swingControl })),\n choice(\n 'swing',\n 'select',\n draft.swingControl,\n 'A mode select',\n `${placement(draft, 'Swing', true)}, and picks one of: ${swingModes.map((mode) => swingModeLabels[mode].toLowerCase()).join(', ')}. ${selectSupport}`,\n glyphs.select,\n (swingControl) => update(draft, { swingControl }),\n ),\n choice('swing', 'hidden', draft.swingControl, 'Not shown in Matter', 'No swing control. Pick below what the plugin does with the swing instead.', glyphs.hidden, (swingControl) =>\n update(draft, { swingControl }),\n ),\n ),\n draft.swingControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'swing-fixed' }, 'When the plugin sends a command, the swing'),\n h(\n 'select',\n { id: 'swing-fixed', onchange: (event: Event) => update(draft, { swingModeWhenHidden: (event.target as HTMLSelectElement).value as Exposure['swingModeWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.swingModeWhenHidden === 'keep' }, 'stays as it is'),\n ...swingModes.map((mode) => h('option', { value: mode, selected: draft.swingModeWhenHidden === mode }, `is set to: ${swingModeLabels[mode]}`)),\n ),\n )\n : null,\n );\n}\n\nfunction modeSection(draft: Draft, controls: AvailableControls): Child {\n if (!controls.hasDryMode && !controls.hasFanMode) return null;\n const modes = draft.capabilities?.modes ?? [];\n return h(\n 'section',\n {},\n h('h2', {}, 'Dry and fan modes'),\n h(\n 'p',\n { class: 'help' },\n `These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${modes.includes('cool') ? 'cool' : 'heat'}. A mode select instead offers every mode in one control.`,\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'mode-select',\n draft.hasModeSelect,\n 'Use a mode select instead of switches',\n `${placement(draft, 'Mode', true)}, with Off and every mode of the unit, as the thermostat mode has them. Picking a mode turns the unit on in it. ${selectSupport}`,\n // The select takes the place of the switches.\n (hasModeSelect) => update(draft, hasModeSelect ? { hasModeSelect, hasDryModeSwitch: false, hasFanModeSwitch: false } : { hasModeSelect }),\n ),\n controls.hasDryMode && !draft.hasModeSelect\n ? toggle('dry-mode-switch', draft.hasDryModeSwitch, 'Add a switch for dry mode', `${placement(draft, 'Dry Mode', true)}.`, (hasDryModeSwitch) => update(draft, { hasDryModeSwitch }))\n : null,\n controls.hasFanMode && !draft.hasModeSelect\n ? toggle('fan-mode-switch', draft.hasFanModeSwitch, 'Add a switch for fan mode', `${placement(draft, 'Fan Mode', true)}.`, (hasFanModeSwitch) => update(draft, { hasFanModeSwitch }))\n : null,\n ),\n );\n}\n\nfunction specialSection(draft: Draft, controls: AvailableControls): Child {\n if (controls.specialModes.length === 0) return null;\n return h(\n 'section',\n {},\n h('h2', {}, 'Special modes'),\n h('p', { class: 'help' }, 'Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in.'),\n h(\n 'div',\n { class: 'choices' },\n ...controls.specialModes.map((mode) =>\n toggle(`special-${mode}`, draft.specialModeSwitches.includes(mode), `Add a switch for ${specialModeTexts[mode].title}`, specialModeTexts[mode].text, (isOn) =>\n update(draft, { specialModeSwitches: controls.specialModes.filter((other) => (other === mode ? isOn : draft.specialModeSwitches.includes(other))) }),\n ),\n ),\n ),\n );\n}\n\nfunction sensorSection(draft: Draft): HTMLElement {\n return h(\n 'section',\n {},\n h('h2', {}, 'Humidity and outdoor temperature'),\n h(\n 'p',\n { class: 'help' },\n 'No adapter says which readings its unit has, so each of these takes effect once the unit gives that reading. The room temperature and the humidity are always on the air conditioner.',\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'separate-humidity',\n draft.hasSeparateHumiditySensor,\n 'Also show the humidity on a sensor of its own',\n `${placement(draft, 'Humidity', false)}, and the air conditioner keeps its humidity. For apps that show no humidity on an air conditioner, which Apple Home is reported to be.`,\n (hasSeparateHumiditySensor) => update(draft, { hasSeparateHumiditySensor }),\n ),\n toggle(\n 'outdoor-sensor',\n draft.hasOutdoorTemperatureSensor,\n 'Add a sensor for the outdoor temperature',\n `It appears as \"${draft.name} Outdoor Temperature\". It is a device of its own, which you can put in another room, such as outside. Until you do, a Matter app may show the outdoor temperature as that of the room it put the sensor in. Some units give no outdoor temperature while they are idle: the sensor then shows no value.`,\n (hasOutdoorTemperatureSensor) => update(draft, { hasOutdoorTemperatureSensor }),\n ),\n ),\n );\n}\n\nfunction composedSection(draft: Draft): HTMLElement {\n return h(\n 'section',\n {},\n h('h2', {}, 'One device or several'),\n h(\n 'p',\n { class: 'help' },\n 'The switches, a fan of its own and a humidity sensor of its own can be parts of the air conditioner in Matter, or devices of their own beside it. The outdoor temperature sensor is always a device of its own, so that you can put it in another room than the air conditioner.',\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'composed',\n draft.isComposed,\n 'Make them parts of the air conditioner',\n `Your Matter app has one device, \"${draft.name}\", and they move between rooms with it. A part has no name of its own in Matter, so an app may show the switches as numbered outlets, and an app that shows no parts shows the air conditioner alone. Turned off, each is a device of its own with its own name, which your app puts in a room by itself.`,\n (isComposed) => update(draft, { isComposed }),\n ),\n ),\n );\n}\n\nexport function optionsScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const controls = draft.controls ?? noControls;\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Options for the air conditioner at ${draft.adapter.host}. Power, mode and temperature always appear in Matter: choose how the rest appears.`),\n presetSection(draft, controls),\n fanSection(draft, controls),\n swingSection(draft),\n modeSection(draft, controls),\n specialSection(draft, controls),\n sensorSection(draft),\n composedSection(draft),\n draft.isNew ? null : h('p', { class: 'help' }, 'Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change.'),\n notice(),\n h('div', { class: 'footer' }, button('Cancel', goHome), button(draft.isNew ? `Add ${draft.name}` : 'Save options', () => void saveDraft(draft), 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/theme.ts\n * @description Makes the setup page look like the Matterbridge page it is shown in.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\n/** Take the colours and the font of the Matterbridge page this one is shown in. */\nexport function adoptHostTheme(): void {\n try {\n if (window.parent === window) return;\n const host = getComputedStyle(window.parent.document.body);\n for (const name of ['--primary-color', '--div-bg-color', '--div-text-color', '--main-bg-color', '--main-label-color', '--div-border-color', '--main-light-color']) {\n const value = host.getPropertyValue(name).trim();\n if (value !== '') document.documentElement.style.setProperty(`--host${name.slice(1)}`, value);\n }\n document.documentElement.style.setProperty('--host-font', host.fontFamily);\n } catch {\n // Opened on its own or from another origin: the page keeps its own colours.\n }\n}\n", "/**\n * @file apps/frontend/src/app.ts\n * @description The guided setup page of the Matterbridge Daikin Local plugin. Matterbridge serves it at\n * /plugins/matterbridge-daikin-local/ and it talks to the plugin through api/<path> below that address.\n * This file starts the page: the rest is in the files next to it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState } from './actions.js';\nimport { h } from './dom.js';\nimport { render, setScreens } from './render.js';\nimport { connectScreen } from './screens/connect.js';\nimport { doneScreen } from './screens/done.js';\nimport { findScreen } from './screens/find.js';\nimport { homeScreen } from './screens/home.js';\nimport { optionsScreen } from './screens/options.js';\nimport { adoptHostTheme } from './theme.js';\n\nsetScreens({\n loading: () => [h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), 'Loading\u2026')],\n home: homeScreen,\n find: findScreen,\n connect: connectScreen,\n options: optionsScreen,\n done: doneScreen,\n});\n\nadoptHostTheme();\nrender();\nvoid loadState();\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["api", "route", "body", "query", "method", "path", "init", "response", "reply", "pickExposure", "source", "fanControl", "fanSpeedWhenHidden", "swingControl", "swingModeWhenHidden", "
|
|
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 { SwitchedMode } from '../../../src/config.js';\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, modeControl, modeSwitches, fanAutoControl, hasSeparateFan, specialModeSwitches, hasOutdoorTemperatureSensor, hasSeparateHumiditySensor, isComposed } =\n source;\n return {\n fanControl,\n fanSpeedWhenHidden,\n swingControl,\n swingModeWhenHidden,\n modeControl,\n modeSwitches: [...modeSwitches],\n fanAutoControl,\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 modeControl: 'native',\n modeSwitches: [],\n fanAutoControl: 'native',\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 autoStep = { native: '', switch: '', firstStep: ' with auto first', lastStep: ' with auto last' }[device.fanAutoControl];\n const fan = device.fanControl === 'hidden' ? 'fan speed not shown' : `fan speed as ${device.fanControl === 'steps' ? 'steps' : 'a percentage'}${autoStep}${fanPlace}`;\n const swing = { switches: 'swing as switches', select: 'swing as a mode select', hidden: 'swing not shown' }[device.swingControl];\n const controls = [hasFan ? fan : '', hasSwing ? swing : ''].filter((text) => text !== '').join(', ');\n const modeSwitches = device.modeControl === 'switches' ? device.modeSwitches.map((mode) => (mode === 'dehumidify' ? 'dry mode' : 'fan mode')) : [];\n const switches = [...modeSwitches, device.fanAutoControl === 'switch' ? 'auto fan' : '', ...device.specialModeSwitches].filter(\n (name) => name !== '',\n );\n const extra = [switches.length === 0 ? '' : `a switch for ${switches.join(', ')}`, device.modeControl === 'select' ? 'a mode select' : ''].filter((text) => text !== '').join(' and ');\n const place = device.isComposed ? ', all as one device' : '';\n if (controls === '') return `${extra === '' ? 'power, mode and temperature' : extra}${place}`;\n return `${extra === '' ? controls : `${controls}, and ${extra}`}${place}`;\n}\n\nfunction deviceRow(device: StoredDevice): HTMLElement {\n const statusText = { online: 'Online', offline: 'Not answering', stopped: 'Not started' }[device.status];\n const isConfirming = view.confirmingRemoval === device.macAddress;\n return h(\n 'li',\n { class: 'device' },\n h(\n 'div',\n { class: 'device-main' },\n h('h3', {}, device.name),\n h('p', { class: 'meta' }, h('span', { class: `status ${device.status}` }, statusText), ` at ${device.host}. In Matter: ${describeExposure(device)}.`),\n device.status !== 'online' && device.error ? h('p', { class: 'meta problem-text' }, device.error) : null,\n ),\n isConfirming\n ? h(\n 'div',\n { class: 'actions' },\n h('span', { class: 'meta' }, 'Remove it from Matter too?'),\n button('Remove', () => void removeDevice(device), 'danger'),\n button('Keep', () => {\n view.confirmingRemoval = '';\n render();\n }),\n )\n : h(\n 'div',\n { class: 'actions' },\n // Either way the unit is asked first what it accepts now, so the options offered are the ones it has today.\n button(device.status === 'online' ? 'Options' : 'Reconnect', () => startQueue([draftFromStored(device)])),\n button('Remove', () => {\n view.confirmingRemoval = device.macAddress;\n render();\n }),\n ),\n );\n}\n\nexport function homeScreen(): Child[] {\n const devices = view.setup?.devices ?? [];\n return [\n h('header', {}, h('h1', {}, 'Daikin air conditioners'), view.setup ? button('Add air conditioners', () => void search(), 'primary') : null),\n notice(),\n devices.length === 0 && view.setup\n ? h('p', { class: 'empty' }, 'No air conditioner is set up yet. Add one to control it from Matter.')\n : h('ul', { class: 'devices' }, ...devices.map(deviceRow)),\n view.setup ? null : button('Try again', () => void loadState(), 'primary'),\n ];\n}\n", "/**\n * @file apps/frontend/src/presets.ts\n * @description The presets of the Options step: each fills in the choices of how a device appears in Matter.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { AvailableControls, Exposure } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport type { Draft } from './state.js';\n\nexport type PresetName = 'apple' | 'google' | 'matter';\n\nexport const presets: Record<PresetName, { title: string; text: string }> = {\n apple: {\n title: 'Apple Home',\n text: 'Fan speed as a percentage, the humidity on a sensor of its own too, and a switch for auto fan, each a device of its own. The swing is not shown and stays as it is.',\n },\n google: { title: 'Google Home', text: 'Fan speed as steps, the humidity on a sensor of its own too, and a switch for auto fan, each a device of its own. The swing is not shown and stays as it is.' },\n matter: {\n title: 'Full Matter support',\n text: 'For apps that show every Matter control: fan speed as steps with its auto mode, the swing as a mode select, a switch for each special mode and the outdoor temperature, all as one device.',\n },\n};\n\n/**\n * The choices a preset makes for a unit: never a switch or a control the unit has no use for. The Apple Home and the\n * Google Home presets make the switches, a fan of its own and a humidity sensor of its own devices of their own, and\n * the Full Matter support preset makes them parts of the air conditioner.\n */\nexport function presetExposure(name: PresetName, controls: AvailableControls): Exposure {\n const isMatter = name === 'matter';\n // An app that shows every Matter control shows a mode select, which Matter has for what it has no cluster for. The\n // other presets leave the swing as it is, rather than add a switch for each axis.\n const swingControl: Exposure['swingControl'] = isMatter && controls.hasSwing ? 'select' : 'hidden';\n return {\n fanControl: controls.fanSteps.length === 0 ? 'hidden' : name === 'apple' ? 'percent' : 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl,\n swingModeWhenHidden: 'keep',\n // The thermostat has dry and fan only among its modes, and no preset adds a switch or a mode select for them.\n modeControl: 'native',\n modeSwitches: [],\n // Neither Home nor Google Home is known to show the auto fan mode, which the switch is beside.\n fanAutoControl: !isMatter && controls.hasFanAuto ? 'switch' : 'native',\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 // Matter has nothing for the special modes, so a switch is the one way to them.\n specialModeSwitches: isMatter ? [...controls.specialModes] : [],\n // An app may show the outdoor temperature as that of the room it puts the sensor in, until the sensor is moved.\n hasOutdoorTemperatureSensor: isMatter,\n // Home is reported to show a humidity on a sensor of its own only, and Google Home lists the humidity sensor among\n // what it supports but not the Room Air Conditioner. The humidity stays on the air conditioner too.\n hasSeparateHumiditySensor: !isMatter,\n // Home shows the parts of a composed device without their names. Google lists neither composed devices nor the\n // Room Air Conditioner among what Google Home supports, but says every device behind a bridge appears as normal.\n isComposed: isMatter,\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 // Nor does the auto fan speed on a unit without one, a step of a hidden fan, or a switch beside a fixed fan speed.\n const isAutoStep = chosen.fanAutoControl === 'firstStep' || chosen.fanAutoControl === 'lastStep';\n const hasFixedFanSpeed = chosen.fanControl === 'hidden' && chosen.fanSpeedWhenHidden !== 'keep';\n if (!controls.hasFanAuto || (isAutoStep && chosen.fanControl === 'hidden') || hasFixedFanSpeed) chosen.fanAutoControl = 'native';\n if (!controls.hasSwing) Object.assign(chosen, { swingControl: 'hidden', swingModeWhenHidden: 'keep' });\n // The switches are a choice only while they are chosen, and nothing shows on a unit without a dry or a fan mode.\n if (chosen.modeControl !== 'switches') chosen.modeSwitches = [];\n if (!controls.hasDryMode && !controls.hasFanMode) Object.assign(chosen, { modeControl: 'native', modeSwitches: [] });\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, 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, SwitchedMode } from '../api.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { noControls } from '../exposure.js';\nimport { matchesPreset, presetExposure, type PresetName, presets } from '../presets.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft } from '../state.js';\n\nconst fanSpeedLabels: Record<FanSpeed, string> = {\n auto: 'Auto',\n silent: 'Silent',\n level1: 'Level 1',\n level2: 'Level 2',\n level3: 'Level 3',\n level4: 'Level 4',\n level5: 'Level 5',\n};\n\nconst swingModeLabels: Record<SwingMode, string> = {\n off: 'No swing',\n vertical: 'Up and down',\n horizontal: 'Left and right',\n both: 'Up, down, left and right',\n};\n\nconst specialModeTexts: Record<SpecialMode, { title: string; text: string }> = {\n powerful: { title: 'Powerful', text: 'Runs the unit at full power for a while.' },\n econo: { title: 'Econo', text: 'Limits how much power the unit draws.' },\n streamer: { title: 'Streamer', text: 'Runs the air purifying of the unit.' },\n};\n\n// Static drawings, so innerHTML is safe for them. Everything that comes from the network goes through text nodes.\nconst glyphs = {\n steps:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect x=\"2\" y=\"20\" width=\"8\" height=\"6\"/><rect x=\"14\" y=\"14\" width=\"8\" height=\"12\"/><rect x=\"26\" y=\"8\" width=\"8\" height=\"18\"/><rect x=\"38\" y=\"2\" width=\"8\" height=\"24\"/></svg>',\n percent: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path d=\"M2 26 L46 2 L46 26 Z\"/></svg>',\n hidden: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M6 14 H42\" stroke-dasharray=\"3 5\"/></svg>',\n switches:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19\"/><path class=\"stroke\" d=\"M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19\"/></svg>',\n select: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect class=\"stroke\" x=\"3\" y=\"5\" width=\"42\" height=\"18\" rx=\"4\"/><path class=\"stroke\" d=\"M31 12 L35 16 L39 12\"/></svg>',\n};\n\n// Home Assistant is the one app known to show a mode select.\nconst selectSupport = 'Few apps show a mode select: Home Assistant does, Apple Home does not.';\n\n// A part of the air conditioner has no name of its own in Matter: a switch or a mode select is tagged with what it is.\nfunction placement(draft: Draft, label: string, isTagged: boolean): string {\n if (!draft.isComposed) return `It appears as \"${draft.name} ${label}\"`;\n return isTagged ? `It appears as a part of \"${draft.name}\", tagged \"${label}\"` : `It appears as a part of \"${draft.name}\"`;\n}\n\nfunction update(draft: Draft, change: Partial<Exposure>): void {\n Object.assign(draft, change);\n render();\n}\n\nfunction choice<T extends string>(group: string, value: T, current: T, title: string, text: string, glyph: string, onpick: (value: T) => void): HTMLElement {\n const id = `${group}-${value}`;\n const input = h('input', { type: 'radio', name: group, id, checked: value === current, onchange: () => onpick(value) });\n return h('label', { class: 'choice pictured', for: id }, input, h('span', { class: 'glyph', html: glyph }), h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction radio<T extends string>(group: string, value: T, current: T, title: string, text: 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', for: id }, input, 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\n// Such as \"Silent, Level 1 and Level 2\".\nfunction listSpeeds(speeds: readonly FanSpeed[]): string {\n const labels = speeds.map((speed) => fanSpeedLabels[speed]);\n return labels.length === 1 ? labels[0] : `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`;\n}\n\ntype FanAutoControl = Exposure['fanAutoControl'];\n\nfunction fanAutoSection(draft: Draft, controls: AvailableControls, current: FanAutoControl): Child {\n // A fixed fan speed is sent with every command, so auto would not hold.\n if (!controls.hasFanAuto || (draft.fanControl === 'hidden' && draft.fanSpeedWhenHidden !== 'keep')) return null;\n const pick = (fanAutoControl: FanAutoControl): void => update(draft, { fanAutoControl });\n const isFanHidden = draft.fanControl === 'hidden';\n const switchText = `${placement(draft, 'Auto Fan', true)}. On sets the fan to auto, off puts it back at the speed it last ran at.`;\n const slowest = fanSpeedLabels[controls.fanSteps[0]];\n const fastest = fanSpeedLabels[controls.fanSteps[controls.fanSteps.length - 1]];\n const choices = isFanHidden\n ? [\n radio('fan-auto', 'native', current, 'Not shown in Matter', 'Without a fan speed in Matter, auto has no place there either.', pick),\n radio('fan-auto', 'switch', current, 'A switch', switchText, pick),\n ]\n : [\n radio('fan-auto', 'native', current, 'Native fan mode only', 'Auto is a mode of the fan speed, beside its speeds, as Matter has it.', pick),\n radio('fan-auto', 'switch', current, 'A switch', `${switchText} Auto stays a fan mode as well.`, pick),\n radio('fan-auto', 'firstStep', current, 'The first step', `Auto comes below ${slowest}: step 1, or the bottom of the slider.`, pick),\n radio('fan-auto', 'lastStep', current, 'The last step', `Auto comes above ${fastest}: the top step, or 100 %.`, pick),\n ];\n return h(\n 'div',\n { class: 'field' },\n h('h3', { id: 'fan-auto-title' }, 'Auto fan speed'),\n isFanHidden ? null : h('p', { class: 'help' }, 'Matter has auto as a fan mode, which not every app shows. A switch makes it reachable there too, and a step takes its place.'),\n h('div', { 'class': 'choices', 'role': 'radiogroup', 'aria-labelledby': 'fan-auto-title' }, ...choices),\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 const isFanHidden = draft.fanControl === 'hidden';\n // Auto is a step only of a fan control that is shown, and the step takes the place of the switch.\n const hasAutoStep = controls.hasFanAuto && !isFanHidden && (draft.fanAutoControl === 'firstStep' || draft.fanAutoControl === 'lastStep');\n const hasAutoSwitch = controls.hasFanAuto && draft.fanAutoControl === 'switch';\n let positions = controls.fanSteps;\n if (hasAutoStep) positions = draft.fanAutoControl === 'firstStep' ? ['auto', ...controls.fanSteps] : [...controls.fanSteps, 'auto'];\n // A step takes the place of the fan mode Auto.\n const hasAutoMode = controls.hasFanAuto && !hasAutoStep;\n const autoMode = hasAutoMode ? ' Auto is not among them: it is a fan mode of its own.' : '';\n // Such as \"6 speeds, slowest first: Silent, Level 1 \u2026 and Level 5. Auto is not among them: it is a fan mode of its own.\"\n const speedList = `${positions.length} ${positions.length === 1 ? 'speed' : 'speeds'}, ${hasAutoStep ? 'in order' : 'slowest first'}: ${listSpeeds(positions)}.${autoMode}`;\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', speedList, glyphs.steps, (fanControl) => update(draft, { fanControl })),\n choice('fan', 'percent', draft.fanControl, 'Percentage', `A slider from 0 to 100, which the plugin rounds to the nearest of ${speedList}`, glyphs.percent, (fanControl) => update(draft, { fanControl })),\n choice('fan', 'hidden', draft.fanControl, 'Not shown in Matter', 'No fan control. Pick below what the plugin does with the fan speed instead.', glyphs.hidden, (fanControl) =>\n update(draft, { fanControl }),\n ),\n ),\n draft.fanControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'fan-fixed' }, 'When the plugin sends a command, the fan speed'),\n h(\n 'select',\n { id: 'fan-fixed', onchange: (event: Event) => update(draft, { fanSpeedWhenHidden: (event.target as HTMLSelectElement).value as Exposure['fanSpeedWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.fanSpeedWhenHidden === 'keep' }, 'stays as it is'),\n ...fanSpeeds.map((speed) => h('option', { value: speed, selected: draft.fanSpeedWhenHidden === speed }, `is set to ${fanSpeedLabels[speed]}`)),\n ),\n )\n : null,\n draft.fanControl === 'hidden'\n ? null\n : toggle(\n 'separate-fan',\n draft.hasSeparateFan,\n 'Show the fan as a device of its own',\n `${placement(draft, 'Fan', false)}, and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner, or do not show it well. It always shows the speed the unit holds, also while the unit is off.`,\n (hasSeparateFan) => update(draft, { hasSeparateFan }),\n ),\n // A step of a fan that is hidden is no step.\n fanAutoSection(draft, controls, isFanHidden && !hasAutoSwitch ? 'native' : draft.fanAutoControl),\n );\n}\n\nfunction swingSection(draft: Draft): HTMLElement {\n const swingModes = draft.capabilities?.swingModes ?? [];\n if (swingModes.length === 0) return h('section', {}, h('h2', {}, 'Swing'), h('p', { class: 'help' }, 'This unit does not let apps move its louvers, so there is nothing to choose.'));\n const hasVertical = swingModes.includes('vertical');\n const hasHorizontal = swingModes.includes('horizontal');\n let switchText = 'One switch, for the up and down swing.';\n if (hasVertical && hasHorizontal) switchText = 'Two switches: one for the up and down swing, one for left and right.';\n else if (hasHorizontal) switchText = 'One switch, for the left and right swing.';\n return h(\n 'section',\n {},\n h('h2', {}, 'Swing'),\n h(\n 'div',\n { class: 'choices' },\n choice('swing', 'switches', draft.swingControl, 'Switches', switchText, glyphs.switches, (swingControl) => update(draft, { swingControl })),\n choice(\n 'swing',\n 'select',\n draft.swingControl,\n 'A mode select',\n `${placement(draft, 'Swing', true)}, and picks one of: ${swingModes.map((mode) => swingModeLabels[mode].toLowerCase()).join(', ')}. ${selectSupport}`,\n glyphs.select,\n (swingControl) => update(draft, { swingControl }),\n ),\n choice('swing', 'hidden', draft.swingControl, 'Not shown in Matter', 'No swing control. Pick below what the plugin does with the swing instead.', glyphs.hidden, (swingControl) =>\n update(draft, { swingControl }),\n ),\n ),\n draft.swingControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'swing-fixed' }, 'When the plugin sends a command, the swing'),\n h(\n 'select',\n { id: 'swing-fixed', onchange: (event: Event) => update(draft, { swingModeWhenHidden: (event.target as HTMLSelectElement).value as Exposure['swingModeWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.swingModeWhenHidden === 'keep' }, 'stays as it is'),\n ...swingModes.map((mode) => h('option', { value: mode, selected: draft.swingModeWhenHidden === mode }, `is set to: ${swingModeLabels[mode]}`)),\n ),\n )\n : null,\n );\n}\n\nfunction modeSection(draft: Draft, controls: AvailableControls): Child {\n if (!controls.hasDryMode && !controls.hasFanMode) return null;\n const modes = draft.capabilities?.modes ?? [];\n const switchable: SwitchedMode[] = [...(controls.hasDryMode ? ['dehumidify' as const] : []), ...(controls.hasFanMode ? ['fan' as const] : [])];\n const labels: Record<SwitchedMode, string> = { dehumidify: 'Dry Mode', fan: 'Fan Mode' };\n const modeSwitches = switchable.filter((mode) => draft.modeSwitches.includes(mode));\n // Switches with none picked would add nothing, so picking them starts with one for each mode.\n const pick = (modeControl: Exposure['modeControl']): void =>\n update(draft, modeControl === 'switches' && modeSwitches.length === 0 ? { modeControl, modeSwitches: switchable } : { modeControl });\n return h(\n 'section',\n {},\n h('h2', {}, 'Dry and fan modes'),\n h('p', { class: 'help' }, 'These are among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto.'),\n h(\n 'div',\n { 'class': 'choices', 'role': 'radiogroup', 'aria-label': 'Dry and fan modes' },\n radio('mode-control', 'native', draft.modeControl, 'Native modes only', 'Dry and fan are modes of the air conditioner, as Matter has them, and nothing more is added.', pick),\n radio('mode-control', 'select', draft.modeControl, 'A mode select', `${placement(draft, 'Mode', true)}, with Off and every mode of the unit, as the air conditioner has them. Picking a mode turns the unit on in it. ${selectSupport}`, pick),\n // Last, so that the switches picked below it stay with it.\n radio(\n 'mode-control',\n 'switches',\n draft.modeControl,\n 'Switches',\n `A switch for each mode picked below. On puts the unit in that mode, off puts it back in ${modes.includes('cool') ? 'cool' : 'heat'}.`,\n pick,\n ),\n ),\n draft.modeControl === 'switches'\n ? h(\n 'div',\n { class: 'choices' },\n ...switchable.map((mode) =>\n toggle(`mode-switch-${mode}`, modeSwitches.includes(mode), `Add a switch for ${labels[mode].toLowerCase()}`, `${placement(draft, labels[mode], true)}.`, (isOn) =>\n update(draft, { modeSwitches: switchable.filter((other) => (other === mode ? isOn : modeSwitches.includes(other))) }),\n ),\n ),\n )\n : null,\n );\n}\n\nfunction specialSection(draft: Draft, controls: AvailableControls): Child {\n if (controls.specialModes.length === 0) return null;\n return h(\n 'section',\n {},\n h('h2', {}, 'Special modes'),\n h('p', { class: 'help' }, 'Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in.'),\n h(\n 'div',\n { class: 'choices' },\n ...controls.specialModes.map((mode) =>\n toggle(`special-${mode}`, draft.specialModeSwitches.includes(mode), `Add a switch for ${specialModeTexts[mode].title}`, specialModeTexts[mode].text, (isOn) =>\n update(draft, { specialModeSwitches: controls.specialModes.filter((other) => (other === mode ? isOn : draft.specialModeSwitches.includes(other))) }),\n ),\n ),\n ),\n );\n}\n\nfunction sensorSection(draft: Draft): HTMLElement {\n return h(\n 'section',\n {},\n h('h2', {}, 'Humidity and outdoor temperature'),\n h(\n 'p',\n { class: 'help' },\n 'No adapter says which readings its unit has, so each of these takes effect once the unit gives that reading. The room temperature and the humidity are always on the air conditioner.',\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'separate-humidity',\n draft.hasSeparateHumiditySensor,\n 'Also show the humidity on a sensor of its own',\n `${placement(draft, 'Humidity', false)}, and the air conditioner keeps its humidity. For apps that show no humidity on an air conditioner, which Apple Home is reported to be.`,\n (hasSeparateHumiditySensor) => update(draft, { hasSeparateHumiditySensor }),\n ),\n toggle(\n 'outdoor-sensor',\n draft.hasOutdoorTemperatureSensor,\n 'Add a sensor for the outdoor temperature',\n `It appears as \"${draft.name} Outdoor Temperature\". It is a device of its own, which you can put in another room, such as outside. Until you do, a Matter app may show the outdoor temperature as that of the room it put the sensor in. Some units give no outdoor temperature while they are idle: the sensor then shows no value.`,\n (hasOutdoorTemperatureSensor) => update(draft, { hasOutdoorTemperatureSensor }),\n ),\n ),\n );\n}\n\nfunction composedSection(draft: Draft): HTMLElement {\n return h(\n 'section',\n {},\n h('h2', {}, 'One device or several'),\n h(\n 'p',\n { class: 'help' },\n 'The switches, a fan of its own and a humidity sensor of its own can be parts of the air conditioner in Matter, or devices of their own beside it. The outdoor temperature sensor is always a device of its own, so that you can put it in another room than the air conditioner.',\n ),\n h(\n 'div',\n { class: 'choices' },\n toggle(\n 'composed',\n draft.isComposed,\n 'Make them parts of the air conditioner',\n `Your Matter app has one device, \"${draft.name}\", and they move between rooms with it. A part has no name of its own in Matter, so an app may show the switches as numbered outlets, and an app that shows no parts shows the air conditioner alone. Turned off, each is a device of its own with its own name, which your app puts in a room by itself.`,\n (isComposed) => update(draft, { isComposed }),\n ),\n ),\n );\n}\n\nexport function optionsScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const controls = draft.controls ?? noControls;\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Options for the air conditioner at ${draft.adapter.host}. Power, mode and temperature always appear in Matter: choose how the rest appears.`),\n presetSection(draft, controls),\n fanSection(draft, controls),\n swingSection(draft),\n modeSection(draft, controls),\n specialSection(draft, controls),\n sensorSection(draft),\n composedSection(draft),\n draft.isNew ? null : h('p', { class: 'help' }, 'Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change.'),\n notice(),\n h('div', { class: 'footer' }, button('Cancel', goHome), button(draft.isNew ? `Add ${draft.name}` : 'Save options', () => void saveDraft(draft), 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/theme.ts\n * @description Makes the setup page look like the Matterbridge page it is shown in.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\n/** Take the colours and the font of the Matterbridge page this one is shown in. */\nexport function adoptHostTheme(): void {\n try {\n if (window.parent === window) return;\n const host = getComputedStyle(window.parent.document.body);\n for (const name of ['--primary-color', '--div-bg-color', '--div-text-color', '--main-bg-color', '--main-label-color', '--div-border-color', '--main-light-color']) {\n const value = host.getPropertyValue(name).trim();\n if (value !== '') document.documentElement.style.setProperty(`--host${name.slice(1)}`, value);\n }\n document.documentElement.style.setProperty('--host-font', host.fontFamily);\n } catch {\n // Opened on its own or from another origin: the page keeps its own colours.\n }\n}\n", "/**\n * @file apps/frontend/src/app.ts\n * @description The guided setup page of the Matterbridge Daikin Local plugin. Matterbridge serves it at\n * /plugins/matterbridge-daikin-local/ and it talks to the plugin through api/<path> below that address.\n * This file starts the page: the rest is in the files next to it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState } from './actions.js';\nimport { h } from './dom.js';\nimport { render, setScreens } from './render.js';\nimport { connectScreen } from './screens/connect.js';\nimport { doneScreen } from './screens/done.js';\nimport { findScreen } from './screens/find.js';\nimport { homeScreen } from './screens/home.js';\nimport { optionsScreen } from './screens/options.js';\nimport { adoptHostTheme } from './theme.js';\n\nsetScreens({\n loading: () => [h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), 'Loading\u2026')],\n home: homeScreen,\n find: findScreen,\n connect: connectScreen,\n options: optionsScreen,\n done: doneScreen,\n});\n\nadoptHostTheme();\nrender();\nvoid loadState();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["api", "route", "body", "query", "method", "path", "init", "response", "reply", "pickExposure", "source", "fanControl", "fanSpeedWhenHidden", "swingControl", "swingModeWhenHidden", "modeControl", "modeSwitches", "fanAutoControl", "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", "autoStep", "fan", "swing", "controls", "text", "switches", "mode", "name", "extra", "place", "deviceRow", "statusText", "isConfirming", "view", "h", "button", "removeDevice", "render", "startQueue", "draftFromStored", "homeScreen", "devices", "search", "notice", "loadState", "presets", "presetExposure", "name", "controls", "isMatter", "swingControl", "matchesPreset", "draft", "chosen", "pickExposure", "isAutoStep", "hasFixedFanSpeed", "mode", "fanSpeedLabels", "swingModeLabels", "specialModeTexts", "glyphs", "selectSupport", "placement", "draft", "label", "isTagged", "update", "change", "render", "choice", "group", "value", "current", "title", "text", "glyph", "onpick", "id", "input", "h", "radio", "toggle", "isOn", "presetSection", "controls", "presets", "name", "matchesPreset", "presetExposure", "listSpeeds", "speeds", "labels", "speed", "fanAutoSection", "pick", "fanAutoControl", "isFanHidden", "switchText", "slowest", "fastest", "choices", "fanSection", "fanSpeeds", "hasAutoStep", "hasAutoSwitch", "positions", "autoMode", "speedList", "fanControl", "event", "hasSeparateFan", "swingSection", "swingModes", "hasVertical", "hasHorizontal", "swingControl", "mode", "modeSection", "modes", "switchable", "modeSwitches", "modeControl", "other", "specialSection", "sensorSection", "hasSeparateHumiditySensor", "hasOutdoorTemperatureSensor", "composedSection", "isComposed", "optionsScreen", "currentDraft", "notice", "noControls", "steps", "unitStrip", "unitHeader", "button", "goHome", "saveDraft", "adoptHostTheme", "host", "name", "value", "setScreens", "h", "homeScreen", "findScreen", "connectScreen", "optionsScreen", "doneScreen", "adoptHostTheme", "render", "loadState"]
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SPECIAL_MODES, } from 'daikin-local-control';
|
|
2
2
|
import { BridgedDeviceBasicInformation, FanControl, ModeSelect, OnOff, Thermostat } from 'matterbridge/matter/clusters';
|
|
3
|
-
import { addReportedSensors, getSwitchableSpecialModes,
|
|
3
|
+
import { addReportedSensors, getSwitchableSpecialModes, noReportedSensors } from '../controls.js';
|
|
4
4
|
import { getErrorMessage } from '../errors.js';
|
|
5
5
|
import { modeSelectChoices, swingSelectChoices, toModeOptions, toOperatingMode } from '../mapping.js';
|
|
6
6
|
import { createAirConditionerEndpoint, createFanEndpoint, createHumidityEndpoint, createModeSelectEndpoint, createOutdoorTemperatureEndpoint, createSwitchEndpoint, } from './endpoints.js';
|
|
@@ -72,11 +72,12 @@ export class DaikinAccessory {
|
|
|
72
72
|
if (this.capabilities.swingModes.includes('horizontal'))
|
|
73
73
|
this.horizontalSwitch = this.createSwingSwitch('horizontal');
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
const hasModeSwitch = (mode) => this.config.modeControl === 'switches' && this.config.modeSwitches.includes(mode) && this.capabilities.modes.includes(mode);
|
|
76
|
+
if (hasModeSwitch('dehumidify'))
|
|
76
77
|
this.dryModeSwitch = this.createModeSwitch('dehumidify');
|
|
77
|
-
if (
|
|
78
|
+
if (hasModeSwitch('fan'))
|
|
78
79
|
this.fanModeSwitch = this.createModeSwitch('fan');
|
|
79
|
-
if (this.
|
|
80
|
+
if (this.layout.hasFanAutoSwitch) {
|
|
80
81
|
this.fanAutoSwitch = this.createSwitch('fan-auto', 'A', 'Auto Fan', (isOn) => {
|
|
81
82
|
this.pending.requestFanAuto(isOn);
|
|
82
83
|
this.scheduleCommand(`auto fan ${isOn ? 'on' : 'off'}`);
|
|
@@ -289,7 +290,7 @@ export class DaikinAccessory {
|
|
|
289
290
|
return;
|
|
290
291
|
this.hasPendingCommand = false;
|
|
291
292
|
const unit = { state: this.state, capabilities: this.capabilities, fanSteps: this.layout.fanSteps, lastManualFanSpeed: this.lastManualFanSpeed };
|
|
292
|
-
const { control, specialModes } = this.pending.take(unit, () => readFanSpeed(this.fan ?? this.endpoint, this.layout.
|
|
293
|
+
const { control, specialModes } = this.pending.take(unit, () => readFanSpeed(this.fan ?? this.endpoint, this.layout.fanPositions));
|
|
293
294
|
const mode = control.mode ?? this.state?.mode;
|
|
294
295
|
if (Object.keys(control).length > 0) {
|
|
295
296
|
addHiddenSettings(control, this.config, this.capabilities);
|
|
@@ -7,11 +7,11 @@ const vendorName = 'Daikin';
|
|
|
7
7
|
function addFanControl(endpoint, layout) {
|
|
8
8
|
const base = {
|
|
9
9
|
fanMode: FanControl.FanMode.Off,
|
|
10
|
-
fanModeSequence: getFanModeSequence(layout.
|
|
10
|
+
fanModeSequence: getFanModeSequence(layout.fanPositions.length, layout.hasFanAuto),
|
|
11
11
|
percentSetting: 0,
|
|
12
12
|
percentCurrent: 0,
|
|
13
13
|
};
|
|
14
|
-
const speeds = { speedMax: layout.
|
|
14
|
+
const speeds = { speedMax: layout.fanPositions.length, speedSetting: 0, speedCurrent: 0 };
|
|
15
15
|
if (layout.hasFanSteps && layout.hasFanAuto) {
|
|
16
16
|
endpoint.behaviors.require(MatterbridgeFanControlServer.with(FanControl.Feature.MultiSpeed, FanControl.Feature.Auto), { ...base, ...speeds });
|
|
17
17
|
}
|
package/dist/accessory/layout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasFanAuto } from '../controls.js';
|
|
1
2
|
import { getFanSteps, getTemperatureRange, getThermostatProfile } from '../mapping.js';
|
|
2
3
|
export function getAccessoryLayout(config, capabilities, reportedSensors) {
|
|
3
4
|
const profile = getThermostatProfile(capabilities.modes);
|
|
@@ -7,18 +8,26 @@ export function getAccessoryLayout(config, capabilities, reportedSensors) {
|
|
|
7
8
|
const heatRange = getTemperatureRange(capabilities, 'heat');
|
|
8
9
|
const coolRange = getTemperatureRange(capabilities, 'cool');
|
|
9
10
|
const hasFan = config.fanControl !== 'hidden' && fanSteps.length > 0;
|
|
11
|
+
const hasAutoStep = hasFan && capabilities.fanSpeeds.includes('auto') && (config.fanAutoControl === 'firstStep' || config.fanAutoControl === 'lastStep');
|
|
12
|
+
const hasAutoSwitch = config.fanAutoControl === 'switch' && hasFanAuto(capabilities);
|
|
13
|
+
let fanPositions = fanSteps;
|
|
14
|
+
if (hasAutoStep)
|
|
15
|
+
fanPositions = config.fanAutoControl === 'firstStep' ? ['auto', ...fanSteps] : [...fanSteps, 'auto'];
|
|
10
16
|
return {
|
|
11
17
|
profile,
|
|
12
18
|
fanSteps,
|
|
19
|
+
fanPositions,
|
|
13
20
|
hasFan,
|
|
14
21
|
hasSeparateFan: hasFan && config.hasSeparateFan,
|
|
15
|
-
hasFanAuto: capabilities.fanSpeeds.includes('auto'),
|
|
22
|
+
hasFanAuto: capabilities.fanSpeeds.includes('auto') && !hasAutoStep,
|
|
23
|
+
hasFanAutoStep: hasAutoStep,
|
|
24
|
+
hasFanAutoSwitch: hasAutoSwitch,
|
|
16
25
|
hasFanSteps: config.fanControl === 'steps',
|
|
17
26
|
heatLimits: profile === 'heatCool' ? { ...heatRange, min: Math.min(heatRange.min, coolRange.min) } : heatRange,
|
|
18
27
|
coolLimits: profile === 'heatCool' ? { ...coolRange, max: Math.max(coolRange.max, heatRange.max) } : coolRange,
|
|
19
28
|
hasHumidity: reportedSensors.hasHumidity,
|
|
20
29
|
hasSeparateHumiditySensor: reportedSensors.hasHumidity && config.hasSeparateHumiditySensor,
|
|
21
|
-
modeSelectModes: config.
|
|
30
|
+
modeSelectModes: config.modeControl === 'select' ? ['off', ...capabilities.modes] : [],
|
|
22
31
|
swingSelectModes: config.swingControl === 'select' ? capabilities.swingModes : [],
|
|
23
32
|
hasOutdoorTemperature: reportedSensors.hasOutdoorTemperature && config.hasOutdoorTemperatureSensor,
|
|
24
33
|
isComposed: config.isComposed,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { FanControl, ModeSelect, OnOff, RelativeHumidityMeasurement, TemperatureMeasurement, Thermostat } from 'matterbridge/matter/clusters';
|
|
2
2
|
import { isValidNumber } from 'matterbridge/utils';
|
|
3
3
|
import { fanStepToPercent, getSwingAxes, modeSelectChoices, percentToFanStep, swingSelectChoices, toMatterTemperature, toSystemMode } from '../mapping.js';
|
|
4
|
-
export function readFanSpeed(endpoint,
|
|
4
|
+
export function readFanSpeed(endpoint, fanPositions) {
|
|
5
5
|
const fanMode = endpoint.getAttribute(FanControl, 'fanMode');
|
|
6
6
|
const percent = endpoint.getAttribute(FanControl, 'percentSetting');
|
|
7
7
|
if (fanMode === FanControl.FanMode.Auto)
|
|
8
8
|
return 'auto';
|
|
9
|
-
if (!isValidNumber(percent,
|
|
9
|
+
if (!isValidNumber(percent, 0, 100))
|
|
10
10
|
return undefined;
|
|
11
|
-
return
|
|
11
|
+
return fanPositions[percentToFanStep(fanPositions.length, percent) - 1];
|
|
12
12
|
}
|
|
13
13
|
function getRunningState(state, sensors) {
|
|
14
14
|
const indoor = sensors.indoorTemperatureInC;
|
|
@@ -41,10 +41,10 @@ async function applyFanSpeed(endpoint, layout, fanSpeed) {
|
|
|
41
41
|
await endpoint.updateAttribute(FanControl, 'fanMode', FanControl.FanMode.Auto);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
-
const step = layout.
|
|
44
|
+
const step = layout.fanPositions.indexOf(fanSpeed) + 1;
|
|
45
45
|
if (step === 0)
|
|
46
46
|
return;
|
|
47
|
-
const percent = fanStepToPercent(layout.
|
|
47
|
+
const percent = fanStepToPercent(layout.fanPositions.length, step);
|
|
48
48
|
await endpoint.updateAttribute(FanControl, 'percentSetting', percent);
|
|
49
49
|
await endpoint.updateAttribute(FanControl, 'percentCurrent', percent);
|
|
50
50
|
if (layout.hasFanSteps)
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,9 @@ import { DAIKIN_PROTOCOLS, FAN_SPEEDS, SPECIAL_MODES, SWING_MODES, } from 'daiki
|
|
|
2
2
|
import { isValidBoolean, isValidNumber, isValidObject, isValidString } from 'matterbridge/utils';
|
|
3
3
|
export const FAN_CONTROLS = ['steps', 'percent', 'hidden'];
|
|
4
4
|
export const SWING_CONTROLS = ['switches', 'select', 'hidden'];
|
|
5
|
+
export const FAN_AUTO_CONTROLS = ['native', 'switch', 'firstStep', 'lastStep'];
|
|
6
|
+
export const MODE_CONTROLS = ['native', 'select', 'switches'];
|
|
7
|
+
export const SWITCHED_MODES = ['dehumidify', 'fan'];
|
|
5
8
|
export const KEEP_SETTING = 'keep';
|
|
6
9
|
const hiddenFanSpeeds = [KEEP_SETTING, ...FAN_SPEEDS];
|
|
7
10
|
const hiddenSwingModes = [KEEP_SETTING, ...SWING_MODES];
|
|
@@ -64,6 +67,10 @@ export function parseDeviceConfig(value, report) {
|
|
|
64
67
|
const unknownSwitches = specialModeSwitches.filter((mode) => !isMember(SPECIAL_MODES, mode));
|
|
65
68
|
if (unknownSwitches.length > 0)
|
|
66
69
|
report?.(`"${name}" has special mode switches this version does not know, which are dropped: ${JSON.stringify(unknownSwitches)}`);
|
|
70
|
+
const modeSwitches = Array.isArray(raw.modeSwitches) ? raw.modeSwitches : useDefault('modeSwitches', []);
|
|
71
|
+
const unknownModeSwitches = modeSwitches.filter((mode) => !isMember(SWITCHED_MODES, mode));
|
|
72
|
+
if (unknownModeSwitches.length > 0)
|
|
73
|
+
report?.(`"${name}" has mode switches this version does not know, which are dropped: ${JSON.stringify(unknownModeSwitches)}`);
|
|
67
74
|
const device = {
|
|
68
75
|
name,
|
|
69
76
|
macAddress,
|
|
@@ -76,10 +83,9 @@ export function parseDeviceConfig(value, report) {
|
|
|
76
83
|
fanSpeedWhenHidden: isMember(hiddenFanSpeeds, raw.fanSpeedWhenHidden) ? raw.fanSpeedWhenHidden : useDefault('fanSpeedWhenHidden', KEEP_SETTING),
|
|
77
84
|
swingControl: isMember(SWING_CONTROLS, raw.swingControl) ? raw.swingControl : useDefault('swingControl', 'switches'),
|
|
78
85
|
swingModeWhenHidden: isMember(hiddenSwingModes, raw.swingModeWhenHidden) ? raw.swingModeWhenHidden : useDefault('swingModeWhenHidden', KEEP_SETTING),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
hasModeSelect: isValidBoolean(raw.hasModeSelect) ? raw.hasModeSelect : useDefault('hasModeSelect', false),
|
|
86
|
+
modeControl: isMember(MODE_CONTROLS, raw.modeControl) ? raw.modeControl : useDefault('modeControl', 'native'),
|
|
87
|
+
modeSwitches: SWITCHED_MODES.filter((mode) => modeSwitches.includes(mode)),
|
|
88
|
+
fanAutoControl: isMember(FAN_AUTO_CONTROLS, raw.fanAutoControl) ? raw.fanAutoControl : useDefault('fanAutoControl', 'native'),
|
|
83
89
|
hasSeparateFan: isValidBoolean(raw.hasSeparateFan) ? raw.hasSeparateFan : useDefault('hasSeparateFan', false),
|
|
84
90
|
specialModeSwitches: SPECIAL_MODES.filter((mode) => specialModeSwitches.includes(mode)),
|
|
85
91
|
hasOutdoorTemperatureSensor: isValidBoolean(raw.hasOutdoorTemperatureSensor) ? raw.hasOutdoorTemperatureSensor : useDefault('hasOutdoorTemperatureSensor', false),
|
|
@@ -143,7 +149,7 @@ export function hasSameConnection(left, right) {
|
|
|
143
149
|
}
|
|
144
150
|
export function hasSameExposure(left, right) {
|
|
145
151
|
return (left.name === right.name &&
|
|
146
|
-
left.
|
|
152
|
+
left.fanAutoControl === right.fanAutoControl &&
|
|
147
153
|
left.hasSeparateFan === right.hasSeparateFan &&
|
|
148
154
|
left.hasOutdoorTemperatureSensor === right.hasOutdoorTemperatureSensor &&
|
|
149
155
|
left.hasSeparateHumiditySensor === right.hasSeparateHumiditySensor &&
|
|
@@ -151,7 +157,6 @@ export function hasSameExposure(left, right) {
|
|
|
151
157
|
left.specialModeSwitches.join() === right.specialModeSwitches.join() &&
|
|
152
158
|
left.fanControl === right.fanControl &&
|
|
153
159
|
left.swingControl === right.swingControl &&
|
|
154
|
-
left.
|
|
155
|
-
left.
|
|
156
|
-
left.hasModeSelect === right.hasModeSelect);
|
|
160
|
+
left.modeControl === right.modeControl &&
|
|
161
|
+
left.modeSwitches.join() === right.modeSwitches.join());
|
|
157
162
|
}
|
package/dist/controls.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OPERATING_MODES, SPECIAL_MODES } from 'daikin-local-control';
|
|
2
|
-
import { KEEP_SETTING } from './config.js';
|
|
2
|
+
import { KEEP_SETTING, SWITCHED_MODES } from './config.js';
|
|
3
3
|
import { getFanSteps } from './mapping.js';
|
|
4
4
|
export function hasFanAuto(capabilities) {
|
|
5
5
|
return capabilities.fanSpeeds.includes('auto') && capabilities.fanSpeeds.length > 1;
|
|
@@ -21,15 +21,18 @@ export function fitToCapabilities(device, capabilities) {
|
|
|
21
21
|
const fanSpeedWhenHidden = device.fanSpeedWhenHidden !== KEEP_SETTING && capabilities.fanSpeeds.includes(device.fanSpeedWhenHidden) ? device.fanSpeedWhenHidden : KEEP_SETTING;
|
|
22
22
|
const swingModeWhenHidden = device.swingModeWhenHidden !== KEEP_SETTING && capabilities.swingModes.includes(device.swingModeWhenHidden) ? device.swingModeWhenHidden : KEEP_SETTING;
|
|
23
23
|
const hasFixedFanSpeed = device.fanControl === 'hidden' && fanSpeedWhenHidden !== KEEP_SETTING;
|
|
24
|
-
const
|
|
24
|
+
const isAutoStep = device.fanAutoControl === 'firstStep' || device.fanAutoControl === 'lastStep';
|
|
25
|
+
const canHold = hasFanAuto(capabilities) && (isAutoStep ? device.fanControl !== 'hidden' : !hasFixedFanSpeed);
|
|
26
|
+
const switchedModes = SWITCHED_MODES.filter((mode) => capabilities.modes.includes(mode));
|
|
27
|
+
const modeSwitches = device.modeControl === 'switches' ? switchedModes.filter((mode) => device.modeSwitches.includes(mode)) : [];
|
|
28
|
+
const hasModeControl = device.modeControl === 'switches' ? modeSwitches.length > 0 : switchedModes.length > 0;
|
|
25
29
|
return {
|
|
26
30
|
...device,
|
|
27
31
|
fanSpeedWhenHidden,
|
|
28
32
|
swingModeWhenHidden,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
hasFanAutoSwitch: device.hasFanAutoSwitch && hasFanAuto(capabilities) && !hasFixedFanSpeed,
|
|
33
|
+
modeControl: hasModeControl ? device.modeControl : 'native',
|
|
34
|
+
modeSwitches,
|
|
35
|
+
fanAutoControl: canHold ? device.fanAutoControl : 'native',
|
|
33
36
|
hasSeparateFan: device.hasSeparateFan && device.fanControl !== 'hidden' && getFanSteps(capabilities.fanSpeeds).length > 0,
|
|
34
37
|
specialModeSwitches: getSwitchableSpecialModes(capabilities).filter((mode) => device.specialModeSwitches.includes(mode)),
|
|
35
38
|
};
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge-daikin-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "matterbridge-daikin-local",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.6.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"daikin-local-control": "0.5.0",
|