matterbridge-daikin-local 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +37 -26
- package/apps/frontend/build/app.js +1 -1
- package/apps/frontend/build/app.js.map +2 -2
- package/dist/accessory/daikinAccessory.js +2 -4
- package/dist/mapping.js +0 -4
- package/dist/module.js +30 -30
- package/dist/platform/discovery.js +5 -4
- package/dist/setup/probe.js +17 -0
- package/dist/setup/setupApi.js +4 -4
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- [devices]: Use the MAC address of the adapter as the serial number of a device in Matter, where a code made from it was used. A device set up with an earlier version is built again in Matter, and a Matter app may take it for a new device.
|
|
12
|
+
- [setup]: Remove a device by the MAC address of its adapter, where its name was used.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- [readme]: Say that a renamed adapter is built again in Matter and may be taken for a new device, where the README said it stays the same device.
|
|
17
|
+
|
|
18
|
+
## [0.2.0] - 2026-10-13
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- [airbase]: Support the Airbase adapter (BRP15B61) of a ducted unit, found by a search or added by address: power, mode, target temperature, fan speed and swing. Its zones, its fan auto and its airside are not exposed.
|
|
23
|
+
- [discovery]: Follow a stored Airbase device to a new address and a new name when a search reports its adapter as classic, which a search does for an Airbase adapter that was too slow to say which of the two it is.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- [package]: Use daikin-local-control 0.5.0: it adds the Airbase client, and a search tells an Airbase adapter from a classic one.
|
|
28
|
+
- [setup]: Speak of the Daikin app an adapter was set up with, in the setup page and in the README, where both named the DAIKIN Mobile Controller app alone: the adapters of the Daikin Airbase and ONECTA apps are supported as well.
|
|
29
|
+
- [readme]: Add the npm, build, CodeQL, coverage and license badges.
|
|
30
|
+
|
|
7
31
|
## [0.1.0] - 2026-10-13
|
|
8
32
|
|
|
9
33
|
### Added
|
package/README.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# Matterbridge Daikin Local
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/matterbridge-daikin-local)
|
|
4
|
+
[](https://www.npmjs.com/package/matterbridge-daikin-local)
|
|
5
|
+
[](https://github.com/cheziyi/matterbridge-daikin-local/actions/workflows/build.yml)
|
|
6
|
+
[](https://github.com/cheziyi/matterbridge-daikin-local/actions/workflows/codeql.yml)
|
|
7
|
+
[](https://codecov.io/gh/cheziyi/matterbridge-daikin-local)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
[](https://www.npmjs.com/package/matterbridge)
|
|
11
|
+
[](https://www.npmjs.com/package/daikin-local-control)
|
|
12
|
+
|
|
3
13
|
Control your Daikin air conditioners from Apple Home, Google Home, Alexa, Home Assistant and other Matter apps, **entirely over your local network**. No cloud, no Daikin account, and nothing that stops working when your internet does.
|
|
4
14
|
|
|
5
|
-
It is a [Matterbridge](https://matterbridge.io) plugin built on [daikin-local-control](https://www.npmjs.com/package/daikin-local-control), for
|
|
15
|
+
It is a [Matterbridge](https://matterbridge.io) plugin built on [daikin-local-control](https://www.npmjs.com/package/daikin-local-control), for units with a classic (BRP069, BRP072), DSIOT (BRP084) or Airbase (BRP15B61) Wi-Fi adapter: the adapters of the DAIKIN Mobile Controller, Daikin Airbase and ONECTA apps. Not every adapter of each app is covered: see its [coverage and limitations](https://github.com/cheziyi/daikin-local-control#coverage--limitations).
|
|
6
16
|
|
|
7
17
|
## Why this plugin
|
|
8
18
|
|
|
@@ -14,7 +24,7 @@ It is a [Matterbridge](https://matterbridge.io) plugin built on [daikin-local-co
|
|
|
14
24
|
- **It fits the Matter app you use.** Matter apps differ in what they show, so you choose how each control appears: the fan speed as steps, as a percentage or on a fan of its own, and a switch for each thing your app has no control for. Presets for Apple Home and Google Home give you a starting point: see [Presets](#presets).
|
|
15
25
|
- **It offers only what your unit can do.** The plugin asks each unit which modes, fan speeds, swing and temperature limits it has, builds its controls from the answer, and checks again after every start.
|
|
16
26
|
- **It is quick.** Each unit is read every 5 seconds unless you set otherwise, so what you change on the remote control shows up in your app within seconds. Several changes an app makes together go to the unit as one command.
|
|
17
|
-
- **One plugin for
|
|
27
|
+
- **One plugin for every kind of adapter.** The classic adapters, the newer BRP084 and the Airbase adapter (BRP15B61) of a ducted unit work side by side, set up the same way.
|
|
18
28
|
|
|
19
29
|
## Requirements
|
|
20
30
|
|
|
@@ -42,7 +52,7 @@ Open the plugin frontend from the plugin's row on the Matterbridge home page. It
|
|
|
42
52
|
1. **Find.** The plugin searches the network and lists the adapters that answer. If one is missing, add it by its address.
|
|
43
53
|
2. **Connect.** For each adapter you picked, the plugin checks what it needs and asks only for that:
|
|
44
54
|
- the **KEY** printed on the adapter's label, for an adapter that only answers registered apps. It is used once to register the Terminal ID and is not stored;
|
|
45
|
-
- the **child lock code**, the 4-digit code set under Child lock in the
|
|
55
|
+
- the **child lock code**, the 4-digit code set under Child lock in the Daikin app the adapter was set up with, for an adapter that has one.
|
|
46
56
|
3. **Options.** How the fan speed, the swing and the extra switches appear in Matter, from what the unit can do. Start from a [preset](#presets) or choose each one yourself.
|
|
47
57
|
|
|
48
58
|
Afterwards the same page lists your air conditioners with their status. From there you can add more, change the options of one, reconnect one that stopped answering, or remove one. None of this needs a Matterbridge restart.
|
|
@@ -166,35 +176,35 @@ The Apple Home and Google Home presets are starting points, not tested recommend
|
|
|
166
176
|
|
|
167
177
|
Three other Matterbridge plugins expose Daikin units. Which one fits depends first on how your unit is reached: this plugin and two of the others talk to the adapter over the local network, and the fourth goes through the Daikin Onecta cloud.
|
|
168
178
|
|
|
169
|
-
This plugin is the only one of the four that finds the adapters itself, that speaks to
|
|
170
|
-
|
|
171
|
-
| | matterbridge-daikin-local (this plugin)
|
|
172
|
-
| ---------------------------- |
|
|
173
|
-
| Connection | Local network, no cloud
|
|
174
|
-
| Units | Those
|
|
175
|
-
| Built on | [daikin-local-control](https://github.com/cheziyi/daikin-local-control), a library by the same author that works on its own
|
|
176
|
-
| Adding a unit | A search of the network and a guided setup page, or by address. No restart
|
|
177
|
-
| KEY and child lock code | Asked for in the setup page, by the adapters that need them
|
|
178
|
-
| Thermostat modes | Off, Cool, Heat, Auto, Dry and Fan only, and an optional switch for each of Dry and Fan only
|
|
179
|
-
| Fan speed | Steps, a percentage or not shown, on the air conditioner or on a fan of its own, and an optional switch for the auto speed
|
|
180
|
-
| Swing | A switch for each axis, or not shown with the mode you fix
|
|
181
|
-
| Powerful, Econo and Streamer | An optional switch for each
|
|
182
|
-
| Humidity | Shown on the air conditioner, when the unit gives it
|
|
183
|
-
| Outdoor temperature | A sensor, when the unit gives it
|
|
184
|
-
| Other devices | None
|
|
185
|
-
| How often a unit is read | Every 5 seconds, or what you set from 2 to 3600
|
|
186
|
-
| Node.js | 22.19 or later, 24 and 26
|
|
187
|
-
| Documentation | English
|
|
188
|
-
| License | Apache-2.0
|
|
179
|
+
This plugin is the only one of the four that finds the adapters itself, that speaks to classic, DSIOT (BRP084) and Airbase (BRP15B61) adapters, and that connects to an adapter which wants its KEY or a child lock code. The others have strengths of their own: matterbridge-daikin-device controls the humidity and an air purifier, and matterbridge-daikin-onecta is the one for a unit that only the Onecta cloud reaches.
|
|
180
|
+
|
|
181
|
+
| | matterbridge-daikin-local (this plugin) | [matterbridge-daikin-ac](https://github.com/andrasg/matterbridge-daikin-ac) | [matterbridge-daikin-device](https://github.com/lltcggie/matterbridge-daikin-device) | [matterbridge-daikin-onecta](https://github.com/mehmetc/matterbridge-daikin-onecta) |
|
|
182
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
183
|
+
| Connection | Local network, no cloud | Local network, no cloud | Local network, no cloud | Daikin Onecta cloud: needs internet, a Daikin Developer Portal app and a sign-in in a browser |
|
|
184
|
+
| Units | Those with a classic (BRP069, BRP072), DSIOT (BRP084) or Airbase (BRP15B61) adapter, from the DAIKIN Mobile Controller, Daikin Airbase and ONECTA apps | The Wi-Fi modules [daikin-controller](https://github.com/Apollon77/daikin-controller) talks to. Not Onecta units | Units that speak DSIOT. Tested on the AN22ZRS air conditioner and the ACK70Z air purifier | Units in the Onecta app: air conditioners and heat pumps |
|
|
185
|
+
| Built on | [daikin-local-control](https://github.com/cheziyi/daikin-local-control), a library by the same author that works on its own | [daikin-controller](https://github.com/Apollon77/daikin-controller) | Its own DSIOT code | [daikin-controller-cloud](https://github.com/Apollon77/daikin-controller-cloud) |
|
|
186
|
+
| Adding a unit | A search of the network and a guided setup page, or by address. No restart | IP addresses in the plugin settings | IP addresses in the plugin settings, then a restart | Every unit of the account, with a white list and a black list |
|
|
187
|
+
| KEY and child lock code | Asked for in the setup page, by the adapters that need them | Not supported: it cannot connect to an adapter that wants its KEY or a child lock code | Not supported: it cannot connect to an adapter with a child lock code. DSIOT adapters have no KEY | Neither applies: it signs in to the Daikin cloud |
|
|
188
|
+
| Thermostat modes | Off, Cool, Heat, Auto, Dry and Fan only, and an optional switch for each of Dry and Fan only | Off, Cool, Heat, Auto, Dry and Fan only | Off, Cool, Heat, Auto and Fan only. Dry and humidify through a mode select | Off, Cool, Heat, Auto, Dry and Fan only |
|
|
189
|
+
| Fan speed | Steps, a percentage or not shown, on the air conditioner or on a fan of its own, and an optional switch for the auto speed | No | Low, Medium, High and Auto, a percentage, or a mode select | A fan device |
|
|
190
|
+
| Swing | A switch for each axis, or not shown with the mode you fix | No | The airflow direction of each axis, through a mode select | No |
|
|
191
|
+
| Powerful, Econo and Streamer | An optional switch for each | No | No | A switch for Powerful |
|
|
192
|
+
| Humidity | Shown on the air conditioner, when the unit gives it | No | Target humidity and ventilation as fans, and a humidity sensor | A humidity sensor, on units that report it |
|
|
193
|
+
| Outdoor temperature | A sensor, when the unit gives it | No | Yes | Yes |
|
|
194
|
+
| Other devices | None | None | The ACK70Z air purifier, with its humidifier, its water tank and its air quality sensors | None |
|
|
195
|
+
| How often a unit is read | Every 5 seconds, or what you set from 2 to 3600 | Every 3 seconds, or what you set from 2 | No setting | Every 10 minutes by day and 30 by night, or what you set. Daikin allows 200 calls a day |
|
|
196
|
+
| Node.js | 22.19 or later, 24 and 26 | 24 and 26 | 20, 22 and 24 | 20.19 or later, 22.13 or later, 24 and 26 |
|
|
197
|
+
| Documentation | English | English | Japanese | English |
|
|
198
|
+
| License | Apache-2.0 | MIT | Apache-2.0 | Apache-2.0 |
|
|
189
199
|
|
|
190
200
|
The table is read from the README, the settings and the source of matterbridge-daikin-ac 2.0.0 and the daikin-controller library it uses, matterbridge-daikin-device 1.0.2 and matterbridge-daikin-onecta 0.1.1, in October 2026. None of the three was run for it.
|
|
191
201
|
|
|
192
202
|
## How devices are stored
|
|
193
203
|
|
|
194
|
-
The plugin knows each air conditioner by the **MAC address of its adapter**, so neither a new IP address nor a new name makes it lose track of one:
|
|
204
|
+
The plugin knows each air conditioner by the **MAC address of its adapter**, so neither a new IP address nor a new name makes it lose track of one. The MAC address is also the serial number the air conditioner has in Matter:
|
|
195
205
|
|
|
196
|
-
- **The address and the name are kept up to date.** The plugin searches the network when it starts, every 5 minutes, and at once when a device stops answering. An adapter that answers from a new address, or under a new name, has both updated. A renamed adapter
|
|
197
|
-
- **No two devices share a name**, because the name is what Matter shows. An adapter renamed to the name of another stored device keeps its old name in the plugin, and the setup page does not offer adapters that share a name: give each adapter its own name in the
|
|
206
|
+
- **The address and the name are kept up to date.** The plugin searches the network when it starts, every 5 minutes, and at once when a device stops answering. An adapter that answers from a new address, or under a new name, has both updated. A renamed adapter is built again in Matter under its new name, with the same serial number. A Matter app may take it for a new device, and ask you again for its room and its automations.
|
|
207
|
+
- **No two devices share a name**, because the name is what Matter shows. An adapter renamed to the name of another stored device keeps its old name in the plugin, and the setup page does not offer adapters that share a name: give each adapter its own name in the Daikin app it was set up with.
|
|
198
208
|
- **An adapter added by address is followed like any other** when a search can reach it: the plugin reads its MAC address at the address you enter. An adapter on another subnet or VLAN, which no search reaches, keeps the address you entered, so give it a fixed one on your router.
|
|
199
209
|
- **An adapter that has no name** is named by you in the setup page. The plugin takes the adapter's own name as soon as it has one.
|
|
200
210
|
- **An adapter that reports no MAC address cannot be added**, because the plugin would have nothing to follow it by.
|
|
@@ -222,6 +232,7 @@ A stored device that this version of the plugin cannot read, for example one sto
|
|
|
222
232
|
- **Changing how the fan, the swing or any switch appears** rebuilds the air conditioner in Matter. Some Matter apps take a moment to show the change, or need the accessory page reopened.
|
|
223
233
|
- **The settings and the setup page do not overlap.** The Terminal ID and the poll interval are set in the plugin's settings in Matterbridge, and the air conditioners in the setup page. A save of the settings never changes the air conditioners.
|
|
224
234
|
- **The setup page is as open as Matterbridge itself.** Anyone who can reach the Matterbridge frontend can use it. It never returns a stored child lock code.
|
|
235
|
+
- **A ducted unit on an Airbase adapter** gets the controls of any other unit: power, mode, target temperature, fan speed and swing. Its zones, its fan auto and its airside are not in Matter. A unit with zones reports no auto fan speed and no swing, so it gets neither, and an Airbase adapter asks for no KEY and no child lock code.
|
|
225
236
|
- **Local HTTPS.** Adapters that speak HTTPS present a certificate from Daikin's own CA, which is not verified: see [Local HTTPS](https://github.com/cheziyi/daikin-local-control#local-https).
|
|
226
237
|
|
|
227
238
|
## Development
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
async function g(e,n,o){try{let[r,a]=e.split(" "),i={method:r};n!==void 0&&(i.headers={"Content-Type":"application/json"},i.body=JSON.stringify(n));let d=await fetch(new URL(`api/${a}${o===void 0?"":`?${o}`}`,document.baseURI),i);if(d.status===204)return{ok:!0};if(d.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let p=await d.json();return"ok"in p?p:{ok:!1,error:p.error??`The plugin answered with status ${d.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}function S(e){let{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:y}=e;return{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:[...y]}}var ee={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!1,hasFanModeSwitch:!1,hasFanAutoSwitch:!1,hasSeparateFan:!1,specialModeSwitches:[]};function j(e){return{adapter:e,name:e.name??"",isNew:!0,...S(ee)}}function q(e){let{capabilities:n,controls:o,name:r,macAddress:a,host:i,protocol:d,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y}=e;return{adapter:{host:i,protocol:d,name:r,macAddress:a,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y},name:r,isNew:!1,capabilities:n,controls:o,...S(e)}}var O={fanSteps:[],hasFanAuto:!1,hasSwing:!1,hasDryMode:!1,hasFanMode:!1,specialModes:[]};var s={screen:"loading",setup:void 0,found:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",confirmingRemoval:""};function f(){return s.queue[0]}function v(e){return e.name===""?e.adapter.host:e.name}function N(e){return(s.setup?.devices??[]).some(n=>n.macAddress===e.macAddress)}var $;function W(e){$=e}function l(){let e=document.getElementById("app");if(e===null||$===void 0)return;let n=`${s.screen}:${f()?.adapter.host??""}`,o=e.dataset.form===n,r=document.activeElement instanceof HTMLElement?document.activeElement.id:"",a=new Map;for(let i of e.querySelectorAll("input[type=text], input[type=password]"))i.id!==""&&a.set(i.id,i.value);if(e.replaceChildren(...$[s.screen]().filter(i=>i!==!1&&i!==null&&i!==void 0)),e.dataset.form=n,!!o){for(let[i,d]of a){let p=document.getElementById(i);p instanceof HTMLInputElement&&(p.value=d)}r!==""&&document.getElementById(r)?.focus()}}async function x(e,n){s.isBusy=!0,s.busyText=e,s.error="",l();try{return await n()}finally{s.isBusy=!1,l()}}async function E(){let e=await g("GET state");if(!e.ok){s.error=e.error,s.screen="home",l();return}if(s.setup=e.state,e.state.devices.length===0){await k();return}s.screen="home",l()}async function T(){let e=await g("GET state");e.ok&&(s.setup=e.state)}function w(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",T().then(l),l()}async function k(){s.screen="find";let e=await x("Searching your network. This takes about three seconds\u2026",async()=>g("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,l();return}await T();let n=s.found.filter(o=>!e.devices.some(r=>r.host===o.host)&&s.selected.has(o.host)).map(o=>({...o,isConfigured:N(o)}));s.found=[...e.devices,...n];for(let o of e.devices)!o.isConfigured&&!o.hasDuplicateName&&s.selected.add(o.host);l()}async function B(e){let n=await x(`Looking for an adapter at ${e}\u2026`,async()=>g("POST identify",{host:e}));if(!n.ok){s.error=n.error,l();return}let o=N(n.device),r=n.device.requiresRegistration===!0;s.found=[...s.found.filter(a=>a.host!==n.device.host),{...n.device,isConfigured:o,hasDuplicateName:!1,needsRegistration:r}],o||s.selected.add(n.device.host),l()}function R(e){s.queue=e,s.saved=[],s.handled=[],P()}async function P(){let e=f();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await T(),l();return}s.screen="connect",s.error="",e.probe=void 0,l(),await M(e)}async function M(e,n){let r={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:n},a=n===void 0?`Checking ${v(e)}\u2026`:`Registering with ${v(e)}\u2026`,i=await x(a,async()=>g(n===void 0?"POST probe":"POST register",r));if(!i.ok){s.error=i.error,l();return}if(e.probe=i.result,e.adapter=i.result.device,i.result.status==="ready"){e.capabilities=i.result.capabilities,e.controls=i.result.controls;let d=i.result.device.name??"";d!==""&&(e.name=d),e.name!==""&&(s.screen="options")}l()}function K(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:v(e),isSaved:!1}),P()}async function U(e){let{adapter:n,name:o,localPassword:r}=e,a={...n,name:o,localPassword:r,...S(e)},i=await x(`Saving ${o}\u2026`,async()=>g("POST device",{device:a}));if(!i.ok){s.error=i.error,l();return}s.setup=i.state,s.saved.push(o),s.handled.push({label:o,isSaved:!0}),s.queue.shift(),await P()}async function Y(e){let n=await x(`Removing ${e}\u2026`,async()=>g("DELETE device",void 0,`name=${encodeURIComponent(e)}`));s.confirmingRemoval="",n.ok||(s.error=n.error),await T(),l()}function t(e,n={},...o){let r=document.createElement(e);for(let[a,i]of Object.entries(n))i===void 0||i===!1||(a==="class"?r.className=String(i):a==="html"?r.innerHTML=String(i):a.startsWith("on")&&typeof i=="function"?r.addEventListener(a.slice(2),i):a in r?r[a]=i:r.setAttribute(a,String(i)));for(let a of o)a!==!1&&a!==null&&a!==void 0&&r.append(a);return r}function c(e,n,o="quiet",r=!1){return t("button",{type:"button",class:`button ${o}`,onclick:n,disabled:r||s.isBusy},e)}function m(){return s.isBusy?t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),s.busyText):s.error!==""?t("p",{class:"notice problem",role:"alert"},s.error):null}function L(){let e=f();if(e===void 0||!e.isNew||s.handled.length+s.queue.length<2)return null;let n=s.screen==="connect"?"Connecting":"Choosing options";return t("ol",{class:"units","aria-label":"Air conditioners in this setup"},...s.handled.map(o=>t("li",{class:o.isSaved?"done":"skipped"},t("strong",{},o.label),t("span",{class:"meta"},o.isSaved?"Added":"Skipped"))),...s.queue.map((o,r)=>t("li",{class:r===0?"current":"ahead","aria-current":r===0?"true":void 0},t("strong",{},v(o)),t("span",{class:"meta"},r===0?n:"Waiting"))))}function H(e,n){return t("header",{class:"unit-header"},t("h1",{},v(e)),t("p",{class:"meta"},n))}function b(){if(s.queue.length>0&&!s.queue.some(r=>r.isNew))return null;let e=["Find","Connect","Options"],o=["find","connect","options"].indexOf(s.screen);return t("ol",{class:"steps"},...e.map((r,a)=>{let i="ahead";return a<o?i="done":a===o&&(i="current"),t("li",{class:i,"aria-current":a===o?"step":void 0},r)}))}function te(e){let n=t("input",{type:"text",id:"device-name",maxLength:32,autocomplete:"off"}),o=()=>{let r=n.value.trim();if(r!==""){if(s.setup?.devices.some(a=>a.name===r)){s.error=`"${r}" is already the name of another air conditioner. Pick a different one.`,l();return}e.name=r,s.error="",s.screen="options",l()}};return t("div",{class:"field"},t("label",{for:"device-name"},"Name this air conditioner"),t("p",{class:"help"},"Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in the DAIKIN Mobile Controller app later and the plugin takes that name instead."),t("div",{class:"inline-form"},n,c("Continue",o,"primary")))}function ne(e,n){let o=t("input",{type:"password",id:"lock-code",inputMode:"numeric",autocomplete:"off",maxLength:64});return t("div",{class:"field"},t("label",{for:"lock-code"},"Child lock code"),t("p",{class:"help"},"The 4-digit code set under Child lock in the DAIKIN Mobile Controller app. It is not printed on the adapter."),n?t("p",{class:"problem-text"},"The adapter did not accept that code."):null,t("div",{class:"inline-form"},o,c("Check the code",()=>{o.value!==""&&(e.localPassword=o.value,M(e))},"primary")))}function oe(e,n){let o=t("input",{type:"text",id:"adapter-key",autocomplete:"off",spellcheck:!1,maxLength:64});return t("div",{class:"field"},t("label",{for:"adapter-key"},"KEY on the adapter"),t("p",{class:"help"},"This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored."),n?t("p",{class:"problem-text"},"The adapter did not accept that KEY."):null,t("div",{class:"inline-form"},o,c("Register",()=>{o.value.trim()!==""&&M(e,o.value.trim())},"primary")))}function se(e){let n=e.probe?.status;return n==="ready"&&e.name===""?te(e):n==="localPasswordRequired"||n==="localPasswordRejected"?ne(e,n==="localPasswordRejected"):n==="registrationRequired"||n==="registrationRejected"?oe(e,n==="registrationRejected"):n==="unreachable"?t("p",{class:"notice problem",role:"alert"},`${e.name} did not answer at ${e.adapter.host}. Check that it has power and is on the network, then try again.`):n==="failed"?t("p",{class:"notice problem",role:"alert"},`${e.name} answered with an error: ${e.probe?.message??"unknown"}`):null}function G(){let e=f();if(e===void 0)return[m()];let n=e.probe?.status==="unreachable"||e.probe?.status==="failed"||s.error!==""&&e.name!=="";return[b(),L(),H(e,`Connecting to its adapter at ${e.adapter.host}.`),m(),s.isBusy?null:se(e),t("div",{class:"footer"},n?c("Try again",()=>{M(e)}):null,c(e.isNew&&s.queue.length>1?"Skip this one":"Cancel",e.isNew&&s.queue.length>1?K:w))]}function V(){let e=s.saved;return[t("header",{},t("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} air conditioners are set up`)),t("p",{class:"lead"},"They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page."),t("div",{class:"footer"},c("Done",w,"primary"))]}function ie(e){let n=!e.isConfigured&&!e.hasDuplicateName,o=[];e.isConfigured&&o.push("Already set up."),e.hasDuplicateName&&o.push("Another adapter has the same name. Give each its own name in the DAIKIN Mobile Controller app, then search again."),e.name===void 0&&o.push("It has not told its name yet. If it has none, you will name it after connecting."),n&&e.needsRegistration?o.push("Asks for the KEY printed on its label."):n&&e.requiresRegistration&&o.push("Already registered with this plugin, so it will not ask for its KEY."),n&&e.requiresLocalPassword&&o.push("Asks for its child lock code.");let r=t("input",{type:"checkbox",id:`found-${e.host}`,checked:n&&s.selected.has(e.host),disabled:!n,onchange:()=>{r.checked?s.selected.add(e.host):s.selected.delete(e.host),l()}});return t("li",{class:n?"found":"found unavailable"},t("label",{for:`found-${e.host}`},r,t("span",{},t("strong",{},e.name??"Unnamed adapter"),t("span",{class:"meta"},` ${e.host}`),...o.map(a=>t("span",{class:"meta note"},a)))))}function z(){let e=s.found.filter(a=>s.selected.has(a.host)&&!a.isConfigured&&!a.hasDuplicateName),n=t("input",{type:"text",id:"host",placeholder:"192.168.1.50",spellcheck:!1,autocomplete:"off"}),o=()=>{n.value.trim()!==""&&B(n.value.trim())};n.addEventListener("keydown",a=>{a.key==="Enter"&&o()});let r=e.length===1?"Set up 1 air conditioner":`Set up ${e.length} air conditioners`;return[b(),t("header",{},t("h1",{},"Find your air conditioners")),m(),s.found.length>0?t("ul",{class:"found-list"},...s.found.map(ie)):null,!s.isBusy&&s.found.length===0?t("p",{class:"empty"},"No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below."):null,t("details",{class:"manual",open:!s.isBusy&&s.found.length===0},t("summary",{},"Can't see your air conditioner?"),t("p",{class:"help"},"Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address."),t("div",{class:"inline-form"},t("label",{for:"host",class:"visually-hidden"},"Adapter address"),n,c("Look it up",o))),t("div",{class:"footer"},c("Search again",()=>{k()}),c("Cancel",w),c(r,()=>R(e.map(j)),"primary",e.length===0))]}function re(e){let n=e.controls===void 0||e.controls.fanSteps.length>0,o=e.controls===void 0||e.controls.hasSwing,r=e.hasSeparateFan?" on a fan of its own":"",a=e.fanControl==="hidden"?"fan speed not shown":`fan speed as ${e.fanControl==="steps"?"steps":"a percentage"}${r}`,i=e.swingControl==="hidden"?"swing not shown":"swing as switches",d=[n?a:"",o?i:""].filter(y=>y!=="").join(", "),p=[e.hasDryModeSwitch?"dry mode":"",e.hasFanModeSwitch?"fan mode":"",e.hasFanAutoSwitch?"auto fan":"",...e.specialModeSwitches].filter(y=>y!==""),h=p.length===0?"":`a switch for ${p.join(", ")}`;return d===""?h===""?"power, mode and temperature":h:h===""?d:`${d}, and ${h}`}function ae(e){let n={online:"Online",offline:"Not answering",stopped:"Not started"}[e.status],o=s.confirmingRemoval===e.name;return t("li",{class:"device"},t("div",{class:"device-main"},t("h3",{},e.name),t("p",{class:"meta"},t("span",{class:`status ${e.status}`},n),` at ${e.host}. In Matter: ${re(e)}.`),e.status!=="online"&&e.error?t("p",{class:"meta problem-text"},e.error):null),o?t("div",{class:"actions"},t("span",{class:"meta"},"Remove it from Matter too?"),c("Remove",()=>{Y(e.name)},"danger"),c("Keep",()=>{s.confirmingRemoval="",l()})):t("div",{class:"actions"},c(e.status==="online"?"Options":"Reconnect",()=>R([q(e)])),c("Remove",()=>{s.confirmingRemoval=e.name,l()})))}function J(){let e=s.setup?.devices??[];return[t("header",{},t("h1",{},"Daikin air conditioners"),s.setup?c("Add air conditioners",()=>{k()},"primary"):null),m(),e.length===0&&s.setup?t("p",{class:"empty"},"No air conditioner is set up yet. Add one to control it from Matter."):t("ul",{class:"devices"},...e.map(ae)),s.setup?null:c("Try again",()=>{E()},"primary")]}var F={apple:{title:"Apple Home",text:"Fan speed as a percentage, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan."},google:{title:"Google Home",text:"Fan speed as steps, swing switches and a dry mode switch."},full:{title:"Full",text:"Every control and every switch this unit can have, special modes included."},minimal:{title:"Minimal",text:"No fan control, no swing control and no switches. The fan and the swing stay as they are."}};function I(e,n){let o=e==="minimal";return{fanControl:o||n.fanSteps.length===0?"hidden":e==="apple"?"percent":"steps",fanSpeedWhenHidden:"keep",swingControl:o||!n.hasSwing?"hidden":"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!o&&n.hasDryMode,hasFanModeSwitch:(e==="apple"||e==="full")&&n.hasFanMode,hasFanAutoSwitch:(e==="apple"||e==="full")&&n.hasFanAuto,hasSeparateFan:!1,specialModeSwitches:e==="full"?[...n.specialModes]:[]}}function Q(e,n,o){let r=S(e);return o.fanSteps.length===0&&Object.assign(r,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),r.fanControl==="hidden"&&(r.hasSeparateFan=!1),o.hasSwing||Object.assign(r,{swingControl:"hidden",swingModeWhenHidden:"keep"}),r.specialModeSwitches=o.specialModes.filter(a=>r.specialModeSwitches.includes(a)),JSON.stringify(r)===JSON.stringify(I(n,o))}var le={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},ce={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},Z={powerful:{title:"Powerful",text:"Runs the unit at full power for a while."},econo:{title:"Econo",text:"Limits how much power the unit draws."},streamer:{title:"Streamer",text:"Runs the air purifying of the unit."}},C={steps:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect x="2" y="20" width="8" height="6"/><rect x="14" y="14" width="8" height="12"/><rect x="26" y="8" width="8" height="18"/><rect x="38" y="2" width="8" height="24"/></svg>',percent:'<svg viewBox="0 0 48 28" aria-hidden="true"><path d="M2 26 L46 2 L46 26 Z"/></svg>',hidden:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M6 14 H42" stroke-dasharray="3 5"/></svg>',switches:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19"/><path class="stroke" d="M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19"/></svg>'};function u(e,n){Object.assign(e,n),l()}function D(e,n,o,r,a,i,d){let p=`${e}-${n}`,h=t("input",{type:"radio",name:e,id:p,checked:n===o,onchange:()=>d(n)});return t("label",{class:"choice pictured",for:p},h,t("span",{class:"glyph",html:i}),t("span",{},t("strong",{},r),t("span",{class:"meta"},a)))}function A(e,n,o,r,a){let i=t("input",{type:"checkbox",id:e,checked:n,onchange:()=>a(i.checked)});return t("label",{class:"choice",for:e},i,t("span",{},t("strong",{},o),t("span",{class:"meta"},r)))}function de(e,n){return t("section",{class:"presets"},t("h2",{},"Start from a preset"),t("div",{class:"preset-list"},...Object.keys(F).map(o=>t("button",{type:"button",class:"preset","aria-pressed":String(Q(e,o,n)),onclick:()=>u(e,I(o,n))},t("strong",{},F[o].title),t("span",{class:"meta"},F[o].text)))),t("p",{class:"help"},"A preset fills in the choices below for what this unit can do. Change any of them afterwards."))}function pe(e,n){let o=e.capabilities?.fanSpeeds??[],r=n.fanSteps.length;if(r===0)return t("section",{},t("h2",{},"Fan speed"),t("p",{class:"help"},"This unit does not let apps set its fan speed, so there is nothing to choose."));let a=n.hasFanAuto&&!(e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep");return t("section",{},t("h2",{},"Fan speed"),t("div",{class:"choices"},D("fan","steps",e.fanControl,"Steps",`${r} speeds, one for each speed of the unit${o.includes("auto")?", and auto":""}.`,C.steps,i=>u(e,{fanControl:i})),D("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.",C.percent,i=>u(e,{fanControl:i})),D("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",C.hidden,i=>u(e,{fanControl:i}))),e.fanControl==="hidden"?t("div",{class:"field"},t("label",{for:"fan-fixed"},"When the plugin sends a command, the fan speed"),t("select",{id:"fan-fixed",onchange:i=>u(e,{fanSpeedWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.fanSpeedWhenHidden==="keep"},"stays as it is"),...o.map(i=>t("option",{value:i,selected:e.fanSpeedWhenHidden===i},`is set to ${le[i]}`)))):null,e.fanControl==="hidden"?null:A("separate-fan",e.hasSeparateFan,"Show the fan as a device of its own",`It appears as "${e.name} Fan", and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner. It always shows the speed the unit holds, also while the unit is off.`,i=>u(e,{hasSeparateFan:i})),a?A("fan-auto-switch",e.hasFanAutoSwitch,"Add a switch for auto fan speed",`It appears as "${e.name} Auto Fan". On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,i=>u(e,{hasFanAutoSwitch:i})):null)}function ue(e){let n=e.capabilities?.swingModes??[];if(n.length===0)return t("section",{},t("h2",{},"Swing"),t("p",{class:"help"},"This unit does not let apps move its louvers, so there is nothing to choose."));let o=n.includes("vertical"),r=n.includes("horizontal"),a="One switch, for the up and down swing.";return o&&r?a="Two switches: one for the up and down swing, one for left and right.":r&&(a="One switch, for the left and right swing."),t("section",{},t("h2",{},"Swing"),t("div",{class:"choices"},D("swing","switches",e.swingControl,"Switches",a,C.switches,i=>u(e,{swingControl:i})),D("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",C.hidden,i=>u(e,{swingControl:i}))),e.swingControl==="hidden"?t("div",{class:"field"},t("label",{for:"swing-fixed"},"When the plugin sends a command, the swing"),t("select",{id:"swing-fixed",onchange:i=>u(e,{swingModeWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.swingModeWhenHidden==="keep"},"stays as it is"),...n.map(i=>t("option",{value:i,selected:e.swingModeWhenHidden===i},`is set to: ${ce[i]}`)))):null)}function he(e,n){if(!n.hasDryMode&&!n.hasFanMode)return null;let o=e.capabilities?.modes??[];return t("section",{},t("h2",{},"Dry and fan modes"),t("p",{class:"help"},`These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${o.includes("cool")?"cool":"heat"}.`),t("div",{class:"choices"},n.hasDryMode?A("dry-mode-switch",e.hasDryModeSwitch,"Add a switch for dry mode",`It appears as "${e.name} Dry Mode".`,r=>u(e,{hasDryModeSwitch:r})):null,n.hasFanMode?A("fan-mode-switch",e.hasFanModeSwitch,"Add a switch for fan mode",`It appears as "${e.name} Fan Mode".`,r=>u(e,{hasFanModeSwitch:r})):null))}function fe(e,n){return n.specialModes.length===0?null:t("section",{},t("h2",{},"Special modes"),t("p",{class:"help"},"Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in."),t("div",{class:"choices"},...n.specialModes.map(o=>A(`special-${o}`,e.specialModeSwitches.includes(o),`Add a switch for ${Z[o].title}`,Z[o].text,r=>u(e,{specialModeSwitches:n.specialModes.filter(a=>a===o?r:e.specialModeSwitches.includes(a))})))))}function X(){let e=f();if(e===void 0)return[m()];let n=e.controls??O;return[b(),L(),H(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter, and so do the humidity and the outdoor temperature once the unit gives them: choose how the rest appears.`),de(e,n),pe(e,n),ue(e),he(e,n),fe(e,n),e.isNew?null:t("p",{class:"help"},"Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change."),m(),t("div",{class:"footer"},c("Cancel",w),c(e.isNew?`Add ${e.name}`:"Save options",()=>{U(e)},"primary"))]}function _(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let n of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let o=e.getPropertyValue(n).trim();o!==""&&document.documentElement.style.setProperty(`--host${n.slice(1)}`,o)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}W({loading:()=>[t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),"Loading\u2026")],home:J,find:z,connect:G,options:X,done:V});_();l();E();
|
|
1
|
+
async function g(e,n,o){try{let[r,a]=e.split(" "),i={method:r};n!==void 0&&(i.headers={"Content-Type":"application/json"},i.body=JSON.stringify(n));let d=await fetch(new URL(`api/${a}${o===void 0?"":`?${o}`}`,document.baseURI),i);if(d.status===204)return{ok:!0};if(d.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let p=await d.json();return"ok"in p?p:{ok:!1,error:p.error??`The plugin answered with status ${d.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}function S(e){let{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:y}=e;return{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:[...y]}}var ee={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!1,hasFanModeSwitch:!1,hasFanAutoSwitch:!1,hasSeparateFan:!1,specialModeSwitches:[]};function q(e){return{adapter:e,name:e.name??"",isNew:!0,...S(ee)}}function I(e){let{capabilities:n,controls:o,name:r,macAddress:a,host:i,protocol:d,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y}=e;return{adapter:{host:i,protocol:d,name:r,macAddress:a,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y},name:r,isNew:!1,capabilities:n,controls:o,...S(e)}}var O={fanSteps:[],hasFanAuto:!1,hasSwing:!1,hasDryMode:!1,hasFanMode:!1,specialModes:[]};var s={screen:"loading",setup:void 0,found:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",confirmingRemoval:""};function f(){return s.queue[0]}function v(e){return e.name===""?e.adapter.host:e.name}function $(e){return(s.setup?.devices??[]).some(n=>n.macAddress===e.macAddress)}var P;function W(e){P=e}function l(){let e=document.getElementById("app");if(e===null||P===void 0)return;let n=`${s.screen}:${f()?.adapter.host??""}`,o=e.dataset.form===n,r=document.activeElement instanceof HTMLElement?document.activeElement.id:"",a=new Map;for(let i of e.querySelectorAll("input[type=text], input[type=password]"))i.id!==""&&a.set(i.id,i.value);if(e.replaceChildren(...P[s.screen]().filter(i=>i!==!1&&i!==null&&i!==void 0)),e.dataset.form=n,!!o){for(let[i,d]of a){let p=document.getElementById(i);p instanceof HTMLInputElement&&(p.value=d)}r!==""&&document.getElementById(r)?.focus()}}async function x(e,n){s.isBusy=!0,s.busyText=e,s.error="",l();try{return await n()}finally{s.isBusy=!1,l()}}async function E(){let e=await g("GET state");if(!e.ok){s.error=e.error,s.screen="home",l();return}if(s.setup=e.state,e.state.devices.length===0){await k();return}s.screen="home",l()}async function T(){let e=await g("GET state");e.ok&&(s.setup=e.state)}function w(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",T().then(l),l()}async function k(){s.screen="find";let e=await x("Searching your network. This takes about three seconds\u2026",async()=>g("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,l();return}await T();let n=s.found.filter(o=>!e.devices.some(r=>r.host===o.host)&&s.selected.has(o.host)).map(o=>({...o,isConfigured:$(o)}));s.found=[...e.devices,...n];for(let o of e.devices)!o.isConfigured&&!o.hasDuplicateName&&s.selected.add(o.host);l()}async function B(e){let n=await x(`Looking for an adapter at ${e}\u2026`,async()=>g("POST identify",{host:e}));if(!n.ok){s.error=n.error,l();return}let o=$(n.device),r=n.device.requiresRegistration===!0;s.found=[...s.found.filter(a=>a.host!==n.device.host),{...n.device,isConfigured:o,hasDuplicateName:!1,needsRegistration:r}],o||s.selected.add(n.device.host),l()}function R(e){s.queue=e,s.saved=[],s.handled=[],N()}async function N(){let e=f();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await T(),l();return}s.screen="connect",s.error="",e.probe=void 0,l(),await M(e)}async function M(e,n){let r={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:n},a=n===void 0?`Checking ${v(e)}\u2026`:`Registering with ${v(e)}\u2026`,i=await x(a,async()=>g(n===void 0?"POST probe":"POST register",r));if(!i.ok){s.error=i.error,l();return}if(e.probe=i.result,e.adapter=i.result.device,i.result.status==="ready"){e.capabilities=i.result.capabilities,e.controls=i.result.controls;let d=i.result.device.name??"";d!==""&&(e.name=d),e.name!==""&&(s.screen="options")}l()}function K(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:v(e),isSaved:!1}),N()}async function U(e){let{adapter:n,name:o,localPassword:r}=e,a={...n,name:o,localPassword:r,...S(e)},i=await x(`Saving ${o}\u2026`,async()=>g("POST device",{device:a}));if(!i.ok){s.error=i.error,l();return}s.setup=i.state,s.saved.push(o),s.handled.push({label:o,isSaved:!0}),s.queue.shift(),await N()}async function Y(e){let n=await x(`Removing ${e.name}\u2026`,async()=>g("DELETE device",void 0,`macAddress=${encodeURIComponent(e.macAddress)}`));s.confirmingRemoval="",n.ok||(s.error=n.error),await T(),l()}function t(e,n={},...o){let r=document.createElement(e);for(let[a,i]of Object.entries(n))i===void 0||i===!1||(a==="class"?r.className=String(i):a==="html"?r.innerHTML=String(i):a.startsWith("on")&&typeof i=="function"?r.addEventListener(a.slice(2),i):a in r?r[a]=i:r.setAttribute(a,String(i)));for(let a of o)a!==!1&&a!==null&&a!==void 0&&r.append(a);return r}function c(e,n,o="quiet",r=!1){return t("button",{type:"button",class:`button ${o}`,onclick:n,disabled:r||s.isBusy},e)}function m(){return s.isBusy?t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),s.busyText):s.error!==""?t("p",{class:"notice problem",role:"alert"},s.error):null}function L(){let e=f();if(e===void 0||!e.isNew||s.handled.length+s.queue.length<2)return null;let n=s.screen==="connect"?"Connecting":"Choosing options";return t("ol",{class:"units","aria-label":"Air conditioners in this setup"},...s.handled.map(o=>t("li",{class:o.isSaved?"done":"skipped"},t("strong",{},o.label),t("span",{class:"meta"},o.isSaved?"Added":"Skipped"))),...s.queue.map((o,r)=>t("li",{class:r===0?"current":"ahead","aria-current":r===0?"true":void 0},t("strong",{},v(o)),t("span",{class:"meta"},r===0?n:"Waiting"))))}function H(e,n){return t("header",{class:"unit-header"},t("h1",{},v(e)),t("p",{class:"meta"},n))}function b(){if(s.queue.length>0&&!s.queue.some(r=>r.isNew))return null;let e=["Find","Connect","Options"],o=["find","connect","options"].indexOf(s.screen);return t("ol",{class:"steps"},...e.map((r,a)=>{let i="ahead";return a<o?i="done":a===o&&(i="current"),t("li",{class:i,"aria-current":a===o?"step":void 0},r)}))}function te(e){let n=t("input",{type:"text",id:"device-name",maxLength:32,autocomplete:"off"}),o=()=>{let r=n.value.trim();if(r!==""){if(s.setup?.devices.some(a=>a.name===r)){s.error=`"${r}" is already the name of another air conditioner. Pick a different one.`,l();return}e.name=r,s.error="",s.screen="options",l()}};return t("div",{class:"field"},t("label",{for:"device-name"},"Name this air conditioner"),t("p",{class:"help"},"Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead."),t("div",{class:"inline-form"},n,c("Continue",o,"primary")))}function ne(e,n){let o=t("input",{type:"password",id:"lock-code",inputMode:"numeric",autocomplete:"off",maxLength:64});return t("div",{class:"field"},t("label",{for:"lock-code"},"Child lock code"),t("p",{class:"help"},"The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter."),n?t("p",{class:"problem-text"},"The adapter did not accept that code."):null,t("div",{class:"inline-form"},o,c("Check the code",()=>{o.value!==""&&(e.localPassword=o.value,M(e))},"primary")))}function oe(e,n){let o=t("input",{type:"text",id:"adapter-key",autocomplete:"off",spellcheck:!1,maxLength:64});return t("div",{class:"field"},t("label",{for:"adapter-key"},"KEY on the adapter"),t("p",{class:"help"},"This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored."),n?t("p",{class:"problem-text"},"The adapter did not accept that KEY."):null,t("div",{class:"inline-form"},o,c("Register",()=>{o.value.trim()!==""&&M(e,o.value.trim())},"primary")))}function se(e){let n=e.probe?.status;return n==="ready"&&e.name===""?te(e):n==="localPasswordRequired"||n==="localPasswordRejected"?ne(e,n==="localPasswordRejected"):n==="registrationRequired"||n==="registrationRejected"?oe(e,n==="registrationRejected"):n==="unreachable"?t("p",{class:"notice problem",role:"alert"},`${e.name} did not answer at ${e.adapter.host}. Check that it has power and is on the network, then try again.`):n==="failed"?t("p",{class:"notice problem",role:"alert"},`${e.name} answered with an error: ${e.probe?.message??"unknown"}`):null}function G(){let e=f();if(e===void 0)return[m()];let n=e.probe?.status==="unreachable"||e.probe?.status==="failed"||s.error!==""&&e.name!=="";return[b(),L(),H(e,`Connecting to its adapter at ${e.adapter.host}.`),m(),s.isBusy?null:se(e),t("div",{class:"footer"},n?c("Try again",()=>{M(e)}):null,c(e.isNew&&s.queue.length>1?"Skip this one":"Cancel",e.isNew&&s.queue.length>1?K:w))]}function V(){let e=s.saved;return[t("header",{},t("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} air conditioners are set up`)),t("p",{class:"lead"},"They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page."),t("div",{class:"footer"},c("Done",w,"primary"))]}function ie(e){let n=!e.isConfigured&&!e.hasDuplicateName,o=[];e.isConfigured&&o.push("Already set up."),e.hasDuplicateName&&o.push("Another adapter has the same name. Give each its own name in its Daikin app, then search again."),e.name===void 0&&o.push("It has not told its name yet. If it has none, you will name it after connecting."),n&&e.needsRegistration?o.push("Asks for the KEY printed on its label."):n&&e.requiresRegistration&&o.push("Already registered with this plugin, so it will not ask for its KEY."),n&&e.requiresLocalPassword&&o.push("Asks for its child lock code.");let r=t("input",{type:"checkbox",id:`found-${e.host}`,checked:n&&s.selected.has(e.host),disabled:!n,onchange:()=>{r.checked?s.selected.add(e.host):s.selected.delete(e.host),l()}});return t("li",{class:n?"found":"found unavailable"},t("label",{for:`found-${e.host}`},r,t("span",{},t("strong",{},e.name??"Unnamed adapter"),t("span",{class:"meta"},` ${e.host}`),...o.map(a=>t("span",{class:"meta note"},a)))))}function z(){let e=s.found.filter(a=>s.selected.has(a.host)&&!a.isConfigured&&!a.hasDuplicateName),n=t("input",{type:"text",id:"host",placeholder:"192.168.1.50",spellcheck:!1,autocomplete:"off"}),o=()=>{n.value.trim()!==""&&B(n.value.trim())};n.addEventListener("keydown",a=>{a.key==="Enter"&&o()});let r=e.length===1?"Set up 1 air conditioner":`Set up ${e.length} air conditioners`;return[b(),t("header",{},t("h1",{},"Find your air conditioners")),m(),s.found.length>0?t("ul",{class:"found-list"},...s.found.map(ie)):null,!s.isBusy&&s.found.length===0?t("p",{class:"empty"},"No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below."):null,t("details",{class:"manual",open:!s.isBusy&&s.found.length===0},t("summary",{},"Can't see your air conditioner?"),t("p",{class:"help"},"Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address."),t("div",{class:"inline-form"},t("label",{for:"host",class:"visually-hidden"},"Adapter address"),n,c("Look it up",o))),t("div",{class:"footer"},c("Search again",()=>{k()}),c("Cancel",w),c(r,()=>R(e.map(q)),"primary",e.length===0))]}function re(e){let n=e.controls===void 0||e.controls.fanSteps.length>0,o=e.controls===void 0||e.controls.hasSwing,r=e.hasSeparateFan?" on a fan of its own":"",a=e.fanControl==="hidden"?"fan speed not shown":`fan speed as ${e.fanControl==="steps"?"steps":"a percentage"}${r}`,i=e.swingControl==="hidden"?"swing not shown":"swing as switches",d=[n?a:"",o?i:""].filter(y=>y!=="").join(", "),p=[e.hasDryModeSwitch?"dry mode":"",e.hasFanModeSwitch?"fan mode":"",e.hasFanAutoSwitch?"auto fan":"",...e.specialModeSwitches].filter(y=>y!==""),h=p.length===0?"":`a switch for ${p.join(", ")}`;return d===""?h===""?"power, mode and temperature":h:h===""?d:`${d}, and ${h}`}function ae(e){let n={online:"Online",offline:"Not answering",stopped:"Not started"}[e.status],o=s.confirmingRemoval===e.macAddress;return t("li",{class:"device"},t("div",{class:"device-main"},t("h3",{},e.name),t("p",{class:"meta"},t("span",{class:`status ${e.status}`},n),` at ${e.host}. In Matter: ${re(e)}.`),e.status!=="online"&&e.error?t("p",{class:"meta problem-text"},e.error):null),o?t("div",{class:"actions"},t("span",{class:"meta"},"Remove it from Matter too?"),c("Remove",()=>{Y(e)},"danger"),c("Keep",()=>{s.confirmingRemoval="",l()})):t("div",{class:"actions"},c(e.status==="online"?"Options":"Reconnect",()=>R([I(e)])),c("Remove",()=>{s.confirmingRemoval=e.macAddress,l()})))}function J(){let e=s.setup?.devices??[];return[t("header",{},t("h1",{},"Daikin air conditioners"),s.setup?c("Add air conditioners",()=>{k()},"primary"):null),m(),e.length===0&&s.setup?t("p",{class:"empty"},"No air conditioner is set up yet. Add one to control it from Matter."):t("ul",{class:"devices"},...e.map(ae)),s.setup?null:c("Try again",()=>{E()},"primary")]}var F={apple:{title:"Apple Home",text:"Fan speed as a percentage, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan."},google:{title:"Google Home",text:"Fan speed as steps, swing switches and a dry mode switch."},full:{title:"Full",text:"Every control and every switch this unit can have, special modes included."},minimal:{title:"Minimal",text:"No fan control, no swing control and no switches. The fan and the swing stay as they are."}};function j(e,n){let o=e==="minimal";return{fanControl:o||n.fanSteps.length===0?"hidden":e==="apple"?"percent":"steps",fanSpeedWhenHidden:"keep",swingControl:o||!n.hasSwing?"hidden":"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!o&&n.hasDryMode,hasFanModeSwitch:(e==="apple"||e==="full")&&n.hasFanMode,hasFanAutoSwitch:(e==="apple"||e==="full")&&n.hasFanAuto,hasSeparateFan:!1,specialModeSwitches:e==="full"?[...n.specialModes]:[]}}function Q(e,n,o){let r=S(e);return o.fanSteps.length===0&&Object.assign(r,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),r.fanControl==="hidden"&&(r.hasSeparateFan=!1),o.hasSwing||Object.assign(r,{swingControl:"hidden",swingModeWhenHidden:"keep"}),r.specialModeSwitches=o.specialModes.filter(a=>r.specialModeSwitches.includes(a)),JSON.stringify(r)===JSON.stringify(j(n,o))}var le={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},ce={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},Z={powerful:{title:"Powerful",text:"Runs the unit at full power for a while."},econo:{title:"Econo",text:"Limits how much power the unit draws."},streamer:{title:"Streamer",text:"Runs the air purifying of the unit."}},C={steps:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect x="2" y="20" width="8" height="6"/><rect x="14" y="14" width="8" height="12"/><rect x="26" y="8" width="8" height="18"/><rect x="38" y="2" width="8" height="24"/></svg>',percent:'<svg viewBox="0 0 48 28" aria-hidden="true"><path d="M2 26 L46 2 L46 26 Z"/></svg>',hidden:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M6 14 H42" stroke-dasharray="3 5"/></svg>',switches:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19"/><path class="stroke" d="M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19"/></svg>'};function u(e,n){Object.assign(e,n),l()}function D(e,n,o,r,a,i,d){let p=`${e}-${n}`,h=t("input",{type:"radio",name:e,id:p,checked:n===o,onchange:()=>d(n)});return t("label",{class:"choice pictured",for:p},h,t("span",{class:"glyph",html:i}),t("span",{},t("strong",{},r),t("span",{class:"meta"},a)))}function A(e,n,o,r,a){let i=t("input",{type:"checkbox",id:e,checked:n,onchange:()=>a(i.checked)});return t("label",{class:"choice",for:e},i,t("span",{},t("strong",{},o),t("span",{class:"meta"},r)))}function de(e,n){return t("section",{class:"presets"},t("h2",{},"Start from a preset"),t("div",{class:"preset-list"},...Object.keys(F).map(o=>t("button",{type:"button",class:"preset","aria-pressed":String(Q(e,o,n)),onclick:()=>u(e,j(o,n))},t("strong",{},F[o].title),t("span",{class:"meta"},F[o].text)))),t("p",{class:"help"},"A preset fills in the choices below for what this unit can do. Change any of them afterwards."))}function pe(e,n){let o=e.capabilities?.fanSpeeds??[],r=n.fanSteps.length;if(r===0)return t("section",{},t("h2",{},"Fan speed"),t("p",{class:"help"},"This unit does not let apps set its fan speed, so there is nothing to choose."));let a=n.hasFanAuto&&!(e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep");return t("section",{},t("h2",{},"Fan speed"),t("div",{class:"choices"},D("fan","steps",e.fanControl,"Steps",`${r} speeds, one for each speed of the unit${o.includes("auto")?", and auto":""}.`,C.steps,i=>u(e,{fanControl:i})),D("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.",C.percent,i=>u(e,{fanControl:i})),D("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",C.hidden,i=>u(e,{fanControl:i}))),e.fanControl==="hidden"?t("div",{class:"field"},t("label",{for:"fan-fixed"},"When the plugin sends a command, the fan speed"),t("select",{id:"fan-fixed",onchange:i=>u(e,{fanSpeedWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.fanSpeedWhenHidden==="keep"},"stays as it is"),...o.map(i=>t("option",{value:i,selected:e.fanSpeedWhenHidden===i},`is set to ${le[i]}`)))):null,e.fanControl==="hidden"?null:A("separate-fan",e.hasSeparateFan,"Show the fan as a device of its own",`It appears as "${e.name} Fan", and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner. It always shows the speed the unit holds, also while the unit is off.`,i=>u(e,{hasSeparateFan:i})),a?A("fan-auto-switch",e.hasFanAutoSwitch,"Add a switch for auto fan speed",`It appears as "${e.name} Auto Fan". On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,i=>u(e,{hasFanAutoSwitch:i})):null)}function ue(e){let n=e.capabilities?.swingModes??[];if(n.length===0)return t("section",{},t("h2",{},"Swing"),t("p",{class:"help"},"This unit does not let apps move its louvers, so there is nothing to choose."));let o=n.includes("vertical"),r=n.includes("horizontal"),a="One switch, for the up and down swing.";return o&&r?a="Two switches: one for the up and down swing, one for left and right.":r&&(a="One switch, for the left and right swing."),t("section",{},t("h2",{},"Swing"),t("div",{class:"choices"},D("swing","switches",e.swingControl,"Switches",a,C.switches,i=>u(e,{swingControl:i})),D("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",C.hidden,i=>u(e,{swingControl:i}))),e.swingControl==="hidden"?t("div",{class:"field"},t("label",{for:"swing-fixed"},"When the plugin sends a command, the swing"),t("select",{id:"swing-fixed",onchange:i=>u(e,{swingModeWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.swingModeWhenHidden==="keep"},"stays as it is"),...n.map(i=>t("option",{value:i,selected:e.swingModeWhenHidden===i},`is set to: ${ce[i]}`)))):null)}function he(e,n){if(!n.hasDryMode&&!n.hasFanMode)return null;let o=e.capabilities?.modes??[];return t("section",{},t("h2",{},"Dry and fan modes"),t("p",{class:"help"},`These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${o.includes("cool")?"cool":"heat"}.`),t("div",{class:"choices"},n.hasDryMode?A("dry-mode-switch",e.hasDryModeSwitch,"Add a switch for dry mode",`It appears as "${e.name} Dry Mode".`,r=>u(e,{hasDryModeSwitch:r})):null,n.hasFanMode?A("fan-mode-switch",e.hasFanModeSwitch,"Add a switch for fan mode",`It appears as "${e.name} Fan Mode".`,r=>u(e,{hasFanModeSwitch:r})):null))}function fe(e,n){return n.specialModes.length===0?null:t("section",{},t("h2",{},"Special modes"),t("p",{class:"help"},"Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in."),t("div",{class:"choices"},...n.specialModes.map(o=>A(`special-${o}`,e.specialModeSwitches.includes(o),`Add a switch for ${Z[o].title}`,Z[o].text,r=>u(e,{specialModeSwitches:n.specialModes.filter(a=>a===o?r:e.specialModeSwitches.includes(a))})))))}function X(){let e=f();if(e===void 0)return[m()];let n=e.controls??O;return[b(),L(),H(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter, and so do the humidity and the outdoor temperature once the unit gives them: choose how the rest appears.`),de(e,n),pe(e,n),ue(e),he(e,n),fe(e,n),e.isNew?null:t("p",{class:"help"},"Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change."),m(),t("div",{class:"footer"},c("Cancel",w),c(e.isNew?`Add ${e.name}`:"Save options",()=>{U(e)},"primary"))]}function _(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let n of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let o=e.getPropertyValue(n).trim();o!==""&&document.documentElement.style.setProperty(`--host${n.slice(1)}`,o)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}W({loading:()=>[t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),"Loading\u2026")],home:J,find:z,connect:G,options:X,done:V});_();l();E();
|
|
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, hasSeparateFan, specialModeSwitches } = source;\n return {\n fanControl,\n fanSpeedWhenHidden,\n swingControl,\n swingModeWhenHidden,\n hasDryModeSwitch,\n hasFanModeSwitch,\n hasFanAutoSwitch,\n hasSeparateFan,\n specialModeSwitches: [...specialModeSwitches],\n };\n}\n\n// What a new device starts with. What a stored device without a setting gets is the default of `parseDeviceConfig`\n// in the plugin, which stands for how the plugin behaved before that setting existed: the two may differ.\nconst defaultExposure: Exposure = {\n fanControl: 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: false,\n hasFanModeSwitch: false,\n hasFanAutoSwitch: false,\n hasSeparateFan: false,\n specialModeSwitches: [],\n};\n\nexport function draftFromAdapter(adapter: Adapter): Draft {\n return { adapter, name: adapter.name ?? '', isNew: true, ...pickExposure(defaultExposure) };\n}\n\nexport function draftFromStored(device: StoredDevice): Draft {\n const { capabilities, controls, name, macAddress, host, protocol, requiresRegistration, usesHttps, requiresLocalPassword } = device;\n const adapter: Adapter = { host, protocol, name, macAddress, requiresRegistration, usesHttps, requiresLocalPassword };\n return { adapter, name, isNew: false, capabilities, controls, ...pickExposure(device) };\n}\n\n// What a unit the plugin told nothing about can have: no control and no switch.\nexport const noControls: AvailableControls = { fanSteps: [], hasFanAuto: false, hasSwing: false, hasDryMode: false, hasFanMode: false, specialModes: [] };\n", "/**\n * @file apps/frontend/src/state.ts\n * @description What the setup page knows at any moment, and what it reads from that.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Capabilities, Exposure, FoundAdapter, ProbeResult, SetupState } from './api.js';\n\n/** One air conditioner on its way through Connect and Options. */\nexport interface Draft extends Exposure {\n adapter: Adapter;\n name: string;\n isNew: boolean;\n localPassword?: string;\n capabilities?: Capabilities;\n /** What the plugin says the unit leaves to choose in Options. */\n controls?: AvailableControls;\n probe?: ProbeResult;\n}\n\nexport type ScreenName = 'loading' | 'home' | 'find' | 'connect' | 'options' | 'done';\n\nexport const view = {\n screen: 'loading' as ScreenName,\n setup: undefined as SetupState | undefined,\n found: [] as FoundAdapter[],\n selected: new Set<string>(),\n queue: [] as Draft[],\n saved: [] as string[],\n /** The units this run is done with, in the order it went through them. */\n handled: [] as { label: string; isSaved: boolean }[],\n isBusy: false,\n busyText: '',\n error: '',\n confirmingRemoval: '',\n};\n\nexport function currentDraft(): Draft | undefined {\n return view.queue[0];\n}\n\n/** What a unit is called on the page: its name, or its address until it has one. */\nexport function draftLabel(draft: Draft): string {\n return draft.name === '' ? draft.adapter.host : draft.name;\n}\n\n/** Whether an adapter is that of a stored device, which is known by its MAC address. */\nexport function isStored(adapter: Adapter): boolean {\n return (view.setup?.devices ?? []).some((device) => device.macAddress === adapter.macAddress);\n}\n", "/**\n * @file apps/frontend/src/render.ts\n * @description Draws the screen the setup page is on. The screens are handed in by app.ts, so that they and the\n * actions can call for a redraw without this file depending on them.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Child } from './dom.js';\nimport { currentDraft, type ScreenName, view } from './state.js';\n\nexport type Screens = Record<ScreenName, () => Child[]>;\n\nlet screens: Screens | undefined;\n\n/** Tell the page what each screen draws. */\nexport function setScreens(all: Screens): void {\n screens = all;\n}\n\nexport function render(): void {\n const root = document.getElementById('app');\n if (root === null || screens === undefined) return;\n // Keep what the user is typing when the same form redraws around it, and never carry it over to another form.\n const form = `${view.screen}:${currentDraft()?.adapter.host ?? ''}`;\n const isSameForm = root.dataset.form === form;\n const activeId = document.activeElement instanceof HTMLElement ? document.activeElement.id : '';\n const typed = new Map<string, string>();\n for (const input of root.querySelectorAll<HTMLInputElement>('input[type=text], input[type=password]')) {\n if (input.id !== '') typed.set(input.id, input.value);\n }\n root.replaceChildren(...screens[view.screen]().filter((child): child is Node | string => child !== false && child !== null && child !== undefined));\n root.dataset.form = form;\n if (!isSameForm) return;\n for (const [id, value] of typed) {\n const input = document.getElementById(id);\n if (input instanceof HTMLInputElement) input.value = value;\n }\n if (activeId !== '') document.getElementById(activeId)?.focus();\n}\n", "/**\n * @file apps/frontend/src/actions.ts\n * @description What the setup page does: each action calls the plugin, changes the state of the page and redraws it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { api } 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(name: string): Promise<void> {\n const reply = await busy(`Removing ${name}\u2026`, async () => api('DELETE device', undefined, `name=${encodeURIComponent(name)}`));\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 the DAIKIN Mobile Controller 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 Mobile Controller app. 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 the DAIKIN Mobile Controller app, then search again.');\n if (adapter.name === undefined) notes.push('It has not told its name yet. If it has none, you will name it after connecting.');\n if (isSelectable && adapter.needsRegistration) notes.push('Asks for the KEY printed on its label.');\n else if (isSelectable && adapter.requiresRegistration) notes.push('Already registered with this plugin, so it will not ask for its KEY.');\n if (isSelectable && adapter.requiresLocalPassword) notes.push('Asks for its child lock code.');\n const checkbox = h('input', {\n type: 'checkbox',\n id: `found-${adapter.host}`,\n checked: isSelectable && view.selected.has(adapter.host),\n disabled: !isSelectable,\n onchange: () => {\n if (checkbox.checked) view.selected.add(adapter.host);\n else view.selected.delete(adapter.host);\n render();\n },\n });\n return h(\n 'li',\n { class: isSelectable ? 'found' : 'found unavailable' },\n h(\n 'label',\n { for: `found-${adapter.host}` },\n checkbox,\n h('span', {}, h('strong', {}, adapter.name ?? 'Unnamed adapter'), h('span', { class: 'meta' }, ` ${adapter.host}`), ...notes.map((note) => h('span', { class: 'meta note' }, note))),\n ),\n );\n}\n\nexport function findScreen(): Child[] {\n const chosen = view.found.filter((adapter) => view.selected.has(adapter.host) && !adapter.isConfigured && !adapter.hasDuplicateName);\n const host = h('input', { type: 'text', id: 'host', placeholder: '192.168.1.50', spellcheck: false, autocomplete: 'off' });\n const lookUpHost = (): void => {\n if (host.value.trim() !== '') void lookUp(host.value.trim());\n };\n host.addEventListener('keydown', (event) => {\n if (event.key === 'Enter') lookUpHost();\n });\n const continueLabel = chosen.length === 1 ? 'Set up 1 air conditioner' : `Set up ${chosen.length} air conditioners`;\n return [\n steps(),\n h('header', {}, h('h1', {}, 'Find your air conditioners')),\n notice(),\n view.found.length > 0 ? h('ul', { class: 'found-list' }, ...view.found.map(foundRow)) : null,\n !view.isBusy && view.found.length === 0\n ? h('p', { class: 'empty' }, 'No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below.')\n : null,\n h(\n 'details',\n { class: 'manual', open: !view.isBusy && view.found.length === 0 },\n h('summary', {}, \"Can't see your air conditioner?\"),\n h('p', { class: 'help' }, 'Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address.'),\n h('div', { class: 'inline-form' }, h('label', { for: 'host', class: 'visually-hidden' }, 'Adapter address'), host, button('Look it up', lookUpHost)),\n ),\n h(\n 'div',\n { class: 'footer' },\n button('Search again', () => void search()),\n button('Cancel', goHome),\n button(continueLabel, () => startQueue(chosen.map(draftFromAdapter)), 'primary', chosen.length === 0),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/home.ts\n * @description The home screen: the air conditioners that are set up, and the way to add more.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState, removeDevice, search, startQueue } from '../actions.js';\nimport type { StoredDevice } from '../api.js';\nimport { button, notice } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromStored } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction describeExposure(device: StoredDevice): string {\n // What the unit cannot do is left out, whatever is stored for it. A unit the plugin has not asked yet is described as stored.\n const hasFan = device.controls === undefined || device.controls.fanSteps.length > 0;\n const hasSwing = device.controls === undefined || device.controls.hasSwing;\n const fanPlace = device.hasSeparateFan ? ' on a fan of its own' : '';\n const fan = device.fanControl === 'hidden' ? 'fan speed not shown' : `fan speed as ${device.fanControl === 'steps' ? 'steps' : 'a percentage'}${fanPlace}`;\n const swing = device.swingControl === 'hidden' ? 'swing not shown' : 'swing as switches';\n const controls = [hasFan ? fan : '', hasSwing ? swing : ''].filter((text) => text !== '').join(', ');\n const switches = [device.hasDryModeSwitch ? 'dry mode' : '', device.hasFanModeSwitch ? 'fan mode' : '', device.hasFanAutoSwitch ? 'auto fan' : '', ...device.specialModeSwitches].filter(\n (name) => name !== '',\n );\n const extra = switches.length === 0 ? '' : `a switch for ${switches.join(', ')}`;\n if (controls === '') return extra === '' ? 'power, mode and temperature' : extra;\n return extra === '' ? controls : `${controls}, and ${extra}`;\n}\n\nfunction deviceRow(device: StoredDevice): HTMLElement {\n const statusText = { online: 'Online', offline: 'Not answering', stopped: 'Not started' }[device.status];\n const isConfirming = view.confirmingRemoval === device.name;\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.name), '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.name;\n render();\n }),\n ),\n );\n}\n\nexport function homeScreen(): Child[] {\n const devices = view.setup?.devices ?? [];\n return [\n h('header', {}, h('h1', {}, 'Daikin air conditioners'), view.setup ? button('Add air conditioners', () => void search(), 'primary') : null),\n notice(),\n devices.length === 0 && view.setup\n ? h('p', { class: 'empty' }, 'No air conditioner is set up yet. Add one to control it from Matter.')\n : h('ul', { class: 'devices' }, ...devices.map(deviceRow)),\n view.setup ? null : button('Try again', () => void loadState(), 'primary'),\n ];\n}\n", "/**\n * @file apps/frontend/src/presets.ts\n * @description The presets of the Options step: each fills in the choices of how a device appears in Matter.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { AvailableControls, Exposure } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport type { Draft } from './state.js';\n\nexport type PresetName = 'apple' | 'google' | 'full' | 'minimal';\n\nexport const presets: Record<PresetName, { title: string; text: string }> = {\n apple: { title: 'Apple Home', text: 'Fan speed as a percentage, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan.' },\n google: { title: 'Google Home', text: 'Fan speed as steps, swing switches and a dry mode switch.' },\n full: { title: 'Full', text: 'Every control and every switch this unit can have, special modes included.' },\n minimal: { title: 'Minimal', text: 'No fan control, no swing control and no switches. The fan and the swing stay as they are.' },\n};\n\n/** The choices a preset makes for a unit: never a switch or a control the unit has no use for. */\nexport function presetExposure(name: PresetName, controls: AvailableControls): Exposure {\n const isMinimal = name === 'minimal';\n return {\n fanControl: isMinimal || controls.fanSteps.length === 0 ? 'hidden' : name === 'apple' ? 'percent' : 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: isMinimal || !controls.hasSwing ? 'hidden' : 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: !isMinimal && controls.hasDryMode,\n hasFanModeSwitch: (name === 'apple' || name === 'full') && controls.hasFanMode,\n hasFanAutoSwitch: (name === 'apple' || name === 'full') && controls.hasFanAuto,\n // No preset takes the fan off the air conditioner: that is for an app that shows no fan speed there.\n hasSeparateFan: false,\n specialModeSwitches: name === 'full' ? [...controls.specialModes] : [],\n };\n}\n\n/** Whether the choices in a draft are exactly those of a preset, leaving aside what the unit has no use for. */\nexport function matchesPreset(draft: Draft, name: PresetName, controls: AvailableControls): boolean {\n const chosen = pickExposure(draft);\n if (controls.fanSteps.length === 0) Object.assign(chosen, { fanControl: 'hidden', fanSpeedWhenHidden: 'keep' });\n // The choice shows nowhere while the fan is hidden.\n if (chosen.fanControl === 'hidden') chosen.hasSeparateFan = false;\n if (!controls.hasSwing) Object.assign(chosen, { swingControl: 'hidden', swingModeWhenHidden: 'keep' });\n chosen.specialModeSwitches = controls.specialModes.filter((mode) => chosen.specialModeSwitches.includes(mode));\n return JSON.stringify(chosen) === JSON.stringify(presetExposure(name, controls));\n}\n", "/**\n * @file apps/frontend/src/screens/options.ts\n * @description The Options step: how the fan, the swing and the extra modes of a unit appear in Matter, one section each.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, saveDraft } from '../actions.js';\nimport type { AvailableControls, Exposure, FanSpeed, SpecialMode, SwingMode } from '../api.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { noControls } from '../exposure.js';\nimport { matchesPreset, presetExposure, type PresetName, presets } from '../presets.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft } from '../state.js';\n\nconst fanSpeedLabels: Record<FanSpeed, string> = {\n auto: 'Auto',\n silent: 'Silent',\n level1: 'Level 1',\n level2: 'Level 2',\n level3: 'Level 3',\n level4: 'Level 4',\n level5: 'Level 5',\n};\n\nconst swingModeLabels: Record<SwingMode, string> = {\n off: 'No swing',\n vertical: 'Up and down',\n horizontal: 'Left and right',\n both: 'Up, down, left and right',\n};\n\nconst specialModeTexts: Record<SpecialMode, { title: string; text: string }> = {\n powerful: { title: 'Powerful', text: 'Runs the unit at full power for a while.' },\n econo: { title: 'Econo', text: 'Limits how much power the unit draws.' },\n streamer: { title: 'Streamer', text: 'Runs the air purifying of the unit.' },\n};\n\n// Static drawings, so innerHTML is safe for them. Everything that comes from the network goes through text nodes.\nconst glyphs = {\n steps:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect x=\"2\" y=\"20\" width=\"8\" height=\"6\"/><rect x=\"14\" y=\"14\" width=\"8\" height=\"12\"/><rect x=\"26\" y=\"8\" width=\"8\" height=\"18\"/><rect x=\"38\" y=\"2\" width=\"8\" height=\"24\"/></svg>',\n percent: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path d=\"M2 26 L46 2 L46 26 Z\"/></svg>',\n hidden: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M6 14 H42\" stroke-dasharray=\"3 5\"/></svg>',\n switches:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19\"/><path class=\"stroke\" d=\"M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19\"/></svg>',\n};\n\nfunction update(draft: Draft, change: Partial<Exposure>): void {\n Object.assign(draft, change);\n render();\n}\n\nfunction choice<T extends string>(group: string, value: T, current: T, title: string, text: string, glyph: string, onpick: (value: T) => void): HTMLElement {\n const id = `${group}-${value}`;\n const input = h('input', { type: 'radio', name: group, id, checked: value === current, onchange: () => onpick(value) });\n return h('label', { class: 'choice pictured', for: id }, input, h('span', { class: 'glyph', html: glyph }), h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction toggle(id: string, isOn: boolean, title: string, text: string, onpick: (isOn: boolean) => void): HTMLElement {\n const input = h('input', { type: 'checkbox', id, checked: isOn, onchange: () => onpick(input.checked) });\n return h('label', { class: 'choice', for: id }, input, h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction presetSection(draft: Draft, controls: AvailableControls): HTMLElement {\n return h(\n 'section',\n { class: 'presets' },\n h('h2', {}, 'Start from a preset'),\n h(\n 'div',\n { class: 'preset-list' },\n ...(Object.keys(presets) as PresetName[]).map((name) =>\n h(\n 'button',\n { 'type': 'button', 'class': 'preset', 'aria-pressed': String(matchesPreset(draft, name, controls)), 'onclick': () => update(draft, presetExposure(name, controls)) },\n h('strong', {}, presets[name].title),\n h('span', { class: 'meta' }, presets[name].text),\n ),\n ),\n ),\n h('p', { class: 'help' }, 'A preset fills in the choices below for what this unit can do. Change any of them afterwards.'),\n );\n}\n\nfunction fanSection(draft: Draft, controls: AvailableControls): HTMLElement {\n const fanSpeeds = draft.capabilities?.fanSpeeds ?? [];\n const stepCount = controls.fanSteps.length;\n if (stepCount === 0) return h('section', {}, h('h2', {}, 'Fan speed'), h('p', { class: 'help' }, 'This unit does not let apps set its fan speed, so there is nothing to choose.'));\n // A fixed fan speed is sent with every command, so a switch for auto would not hold.\n const canSwitchFanAuto = controls.hasFanAuto && !(draft.fanControl === 'hidden' && draft.fanSpeedWhenHidden !== 'keep');\n return h(\n 'section',\n {},\n h('h2', {}, 'Fan speed'),\n h(\n 'div',\n { class: 'choices' },\n choice('fan', 'steps', draft.fanControl, 'Steps', `${stepCount} speeds, one for each speed of the unit${fanSpeeds.includes('auto') ? ', and auto' : ''}.`, glyphs.steps, (fanControl) =>\n update(draft, { fanControl }),\n ),\n choice('fan', 'percent', draft.fanControl, 'Percentage', 'A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.', glyphs.percent, (fanControl) => update(draft, { fanControl })),\n choice('fan', 'hidden', draft.fanControl, 'Not shown in Matter', 'No fan control. Pick below what the plugin does with the fan speed instead.', glyphs.hidden, (fanControl) =>\n update(draft, { fanControl }),\n ),\n ),\n draft.fanControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'fan-fixed' }, 'When the plugin sends a command, the fan speed'),\n h(\n 'select',\n { id: 'fan-fixed', onchange: (event: Event) => update(draft, { fanSpeedWhenHidden: (event.target as HTMLSelectElement).value as Exposure['fanSpeedWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.fanSpeedWhenHidden === 'keep' }, 'stays as it is'),\n ...fanSpeeds.map((speed) => h('option', { value: speed, selected: draft.fanSpeedWhenHidden === speed }, `is set to ${fanSpeedLabels[speed]}`)),\n ),\n )\n : null,\n draft.fanControl === 'hidden'\n ? null\n : toggle(\n 'separate-fan',\n draft.hasSeparateFan,\n 'Show the fan as a device of its own',\n `It appears as \"${draft.name} Fan\", and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner. It always shows the speed the unit holds, also while the unit is off.`,\n (hasSeparateFan) => update(draft, { hasSeparateFan }),\n ),\n canSwitchFanAuto\n ? toggle(\n 'fan-auto-switch',\n draft.hasFanAutoSwitch,\n 'Add a switch for auto fan speed',\n `It appears as \"${draft.name} Auto Fan\". On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,\n (hasFanAutoSwitch) => update(draft, { hasFanAutoSwitch }),\n )\n : null,\n );\n}\n\nfunction swingSection(draft: Draft): HTMLElement {\n const swingModes = draft.capabilities?.swingModes ?? [];\n if (swingModes.length === 0) return h('section', {}, h('h2', {}, 'Swing'), h('p', { class: 'help' }, 'This unit does not let apps move its louvers, so there is nothing to choose.'));\n const hasVertical = swingModes.includes('vertical');\n const hasHorizontal = swingModes.includes('horizontal');\n let switchText = 'One switch, for the up and down swing.';\n if (hasVertical && hasHorizontal) switchText = 'Two switches: one for the up and down swing, one for left and right.';\n else if (hasHorizontal) switchText = 'One switch, for the left and right swing.';\n return h(\n 'section',\n {},\n h('h2', {}, 'Swing'),\n h(\n 'div',\n { class: 'choices' },\n choice('swing', 'switches', draft.swingControl, 'Switches', switchText, glyphs.switches, (swingControl) => update(draft, { swingControl })),\n choice('swing', 'hidden', draft.swingControl, 'Not shown in Matter', 'No swing control. Pick below what the plugin does with the swing instead.', glyphs.hidden, (swingControl) =>\n update(draft, { swingControl }),\n ),\n ),\n draft.swingControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'swing-fixed' }, 'When the plugin sends a command, the swing'),\n h(\n 'select',\n { id: 'swing-fixed', onchange: (event: Event) => update(draft, { swingModeWhenHidden: (event.target as HTMLSelectElement).value as Exposure['swingModeWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.swingModeWhenHidden === 'keep' }, 'stays as it is'),\n ...swingModes.map((mode) => h('option', { value: mode, selected: draft.swingModeWhenHidden === mode }, `is set to: ${swingModeLabels[mode]}`)),\n ),\n )\n : null,\n );\n}\n\nfunction modeSection(draft: Draft, controls: AvailableControls): Child {\n if (!controls.hasDryMode && !controls.hasFanMode) return null;\n const modes = draft.capabilities?.modes ?? [];\n return h(\n 'section',\n {},\n h('h2', {}, 'Dry and fan modes'),\n h(\n 'p',\n { class: 'help' },\n `These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${modes.includes('cool') ? 'cool' : 'heat'}.`,\n ),\n h(\n 'div',\n { class: 'choices' },\n controls.hasDryMode\n ? toggle('dry-mode-switch', draft.hasDryModeSwitch, 'Add a switch for dry mode', `It appears as \"${draft.name} Dry Mode\".`, (hasDryModeSwitch) => update(draft, { hasDryModeSwitch }))\n : null,\n controls.hasFanMode\n ? toggle('fan-mode-switch', draft.hasFanModeSwitch, 'Add a switch for fan mode', `It appears as \"${draft.name} Fan Mode\".`, (hasFanModeSwitch) => update(draft, { hasFanModeSwitch }))\n : null,\n ),\n );\n}\n\nfunction specialSection(draft: Draft, controls: AvailableControls): Child {\n if (controls.specialModes.length === 0) return null;\n return h(\n 'section',\n {},\n h('h2', {}, 'Special modes'),\n h('p', { class: 'help' }, 'Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in.'),\n h(\n 'div',\n { class: 'choices' },\n ...controls.specialModes.map((mode) =>\n toggle(`special-${mode}`, draft.specialModeSwitches.includes(mode), `Add a switch for ${specialModeTexts[mode].title}`, specialModeTexts[mode].text, (isOn) =>\n update(draft, { specialModeSwitches: controls.specialModes.filter((other) => (other === mode ? isOn : draft.specialModeSwitches.includes(other))) }),\n ),\n ),\n ),\n );\n}\n\nexport function optionsScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const controls = draft.controls ?? noControls;\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Options for the air conditioner at ${draft.adapter.host}. Power, mode and temperature always appear in Matter, and so do the humidity and the outdoor temperature once the unit gives them: choose how the rest appears.`),\n presetSection(draft, controls),\n fanSection(draft, controls),\n swingSection(draft),\n modeSection(draft, controls),\n specialSection(draft, controls),\n draft.isNew ? null : h('p', { class: 'help' }, 'Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change.'),\n notice(),\n h('div', { class: 'footer' }, button('Cancel', goHome), button(draft.isNew ? `Add ${draft.name}` : 'Save options', () => void saveDraft(draft), 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/theme.ts\n * @description Makes the setup page look like the Matterbridge page it is shown in.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\n/** Take the colours and the font of the Matterbridge page this one is shown in. */\nexport function adoptHostTheme(): void {\n try {\n if (window.parent === window) return;\n const host = getComputedStyle(window.parent.document.body);\n for (const name of ['--primary-color', '--div-bg-color', '--div-text-color', '--main-bg-color', '--main-label-color', '--div-border-color', '--main-light-color']) {\n const value = host.getPropertyValue(name).trim();\n if (value !== '') document.documentElement.style.setProperty(`--host${name.slice(1)}`, value);\n }\n document.documentElement.style.setProperty('--host-font', host.fontFamily);\n } catch {\n // Opened on its own or from another origin: the page keeps its own colours.\n }\n}\n", "/**\n * @file apps/frontend/src/app.ts\n * @description The guided setup page of the Matterbridge Daikin Local plugin. Matterbridge serves it at\n * /plugins/matterbridge-daikin-local/ and it talks to the plugin through api/<path> below that address.\n * This file starts the page: the rest is in the files next to it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState } from './actions.js';\nimport { h } from './dom.js';\nimport { render, setScreens } from './render.js';\nimport { connectScreen } from './screens/connect.js';\nimport { doneScreen } from './screens/done.js';\nimport { findScreen } from './screens/find.js';\nimport { homeScreen } from './screens/home.js';\nimport { optionsScreen } from './screens/options.js';\nimport { adoptHostTheme } from './theme.js';\n\nsetScreens({\n loading: () => [h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), 'Loading\u2026')],\n home: homeScreen,\n find: findScreen,\n connect: connectScreen,\n options: optionsScreen,\n done: doneScreen,\n});\n\nadoptHostTheme();\nrender();\nvoid loadState();\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * @file apps/frontend/src/api.ts\n * @description What the plugin's API sends and takes, and the call to it: see src/setup/setupApi.ts in the plugin.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { DaikinCapabilities, DaikinDevice } from 'daikin-local-control';\n\nimport type { DaikinDeviceExposure } from '../../../src/config.js';\nimport type { DiscoveredDevice, SetupDevice, SetupReplies, SetupReply } from '../../../src/setup/setupApi.js';\n\n// The types are the plugin's own, so the page and the plugin cannot drift apart. They are only types: nothing of the\n// plugin is loaded in the browser. The page has its own words for some of them.\nexport type { FanSpeed, SpecialMode, SwingMode } from 'daikin-local-control';\nexport type { AvailableControls } from '../../../src/controls.js';\nexport type { ProbeResult } from '../../../src/setup/probe.js';\nexport type { SetupState } from '../../../src/setup/setupApi.js';\n\nexport type Capabilities = DaikinCapabilities;\nexport type Adapter = DaikinDevice;\nexport type Exposure = DaikinDeviceExposure;\nexport type StoredDevice = SetupDevice;\nexport type FoundAdapter = DiscoveredDevice;\n\n/** A request the plugin serves, by its method and its path after `api/`. */\nexport type Route = keyof SetupReplies;\n\n/** The reply to a request, with what the plugin sends back when the request worked. */\nexport type Reply<R extends Route> = ({ ok: true } & SetupReplies[R]) | Extract<SetupReply, { ok: false }>;\n\n/**\n * Call the plugin.\n *\n * @param route The method and the path, such as `POST probe`.\n * @param body The JSON body.\n * @param query The query string, without its `?`.\n * @returns The reply, with `ok: false` and the reason when the call did not work.\n */\nexport async function api<R extends Route>(route: R, body?: unknown, query?: string): Promise<Reply<R>> {\n try {\n const [method, path] = route.split(' ');\n const init: RequestInit = { method };\n if (body !== undefined) {\n init.headers = { 'Content-Type': 'application/json' };\n init.body = JSON.stringify(body);\n }\n const response = await fetch(new URL(`api/${path}${query === undefined ? '' : `?${query}`}`, document.baseURI), init);\n if (response.status === 204) return { ok: true } as Reply<R>;\n if (response.status === 503) return { ok: false, error: 'The plugin is not running. Start it in Matterbridge, then reload this page.' };\n const reply = (await response.json()) as Reply<R> | { error?: string };\n if ('ok' in reply) return reply;\n return { ok: false, error: reply.error ?? `The plugin answered with status ${response.status}` };\n } catch {\n return { ok: false, error: 'Matterbridge did not answer. Check that it is running, then try again.' };\n }\n}\n", "/**\n * @file apps/frontend/src/exposure.ts\n * @description How a device appears in Matter: the choices a draft starts with.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Exposure, StoredDevice } from './api.js';\nimport type { Draft } from './state.js';\n\n/** Copy the choices of how a device appears in Matter, and nothing else. */\nexport function pickExposure(source: Exposure): Exposure {\n const { fanControl, fanSpeedWhenHidden, swingControl, swingModeWhenHidden, hasDryModeSwitch, hasFanModeSwitch, hasFanAutoSwitch, hasSeparateFan, specialModeSwitches } = source;\n return {\n fanControl,\n fanSpeedWhenHidden,\n swingControl,\n swingModeWhenHidden,\n hasDryModeSwitch,\n hasFanModeSwitch,\n hasFanAutoSwitch,\n hasSeparateFan,\n specialModeSwitches: [...specialModeSwitches],\n };\n}\n\n// What a new device starts with. What a stored device without a setting gets is the default of `parseDeviceConfig`\n// in the plugin, which stands for how the plugin behaved before that setting existed: the two may differ.\nconst defaultExposure: Exposure = {\n fanControl: 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: false,\n hasFanModeSwitch: false,\n hasFanAutoSwitch: false,\n hasSeparateFan: false,\n specialModeSwitches: [],\n};\n\nexport function draftFromAdapter(adapter: Adapter): Draft {\n return { adapter, name: adapter.name ?? '', isNew: true, ...pickExposure(defaultExposure) };\n}\n\nexport function draftFromStored(device: StoredDevice): Draft {\n const { capabilities, controls, name, macAddress, host, protocol, requiresRegistration, usesHttps, requiresLocalPassword } = device;\n const adapter: Adapter = { host, protocol, name, macAddress, requiresRegistration, usesHttps, requiresLocalPassword };\n return { adapter, name, isNew: false, capabilities, controls, ...pickExposure(device) };\n}\n\n// What a unit the plugin told nothing about can have: no control and no switch.\nexport const noControls: AvailableControls = { fanSteps: [], hasFanAuto: false, hasSwing: false, hasDryMode: false, hasFanMode: false, specialModes: [] };\n", "/**\n * @file apps/frontend/src/state.ts\n * @description What the setup page knows at any moment, and what it reads from that.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Adapter, AvailableControls, Capabilities, Exposure, FoundAdapter, ProbeResult, SetupState } from './api.js';\n\n/** One air conditioner on its way through Connect and Options. */\nexport interface Draft extends Exposure {\n adapter: Adapter;\n name: string;\n isNew: boolean;\n localPassword?: string;\n capabilities?: Capabilities;\n /** What the plugin says the unit leaves to choose in Options. */\n controls?: AvailableControls;\n probe?: ProbeResult;\n}\n\nexport type ScreenName = 'loading' | 'home' | 'find' | 'connect' | 'options' | 'done';\n\nexport const view = {\n screen: 'loading' as ScreenName,\n setup: undefined as SetupState | undefined,\n found: [] as FoundAdapter[],\n selected: new Set<string>(),\n queue: [] as Draft[],\n saved: [] as string[],\n /** The units this run is done with, in the order it went through them. */\n handled: [] as { label: string; isSaved: boolean }[],\n isBusy: false,\n busyText: '',\n error: '',\n confirmingRemoval: '',\n};\n\nexport function currentDraft(): Draft | undefined {\n return view.queue[0];\n}\n\n/** What a unit is called on the page: its name, or its address until it has one. */\nexport function draftLabel(draft: Draft): string {\n return draft.name === '' ? draft.adapter.host : draft.name;\n}\n\n/** Whether an adapter is that of a stored device, which is known by its MAC address. */\nexport function isStored(adapter: Adapter): boolean {\n return (view.setup?.devices ?? []).some((device) => device.macAddress === adapter.macAddress);\n}\n", "/**\n * @file apps/frontend/src/render.ts\n * @description Draws the screen the setup page is on. The screens are handed in by app.ts, so that they and the\n * actions can call for a redraw without this file depending on them.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { Child } from './dom.js';\nimport { currentDraft, type ScreenName, view } from './state.js';\n\nexport type Screens = Record<ScreenName, () => Child[]>;\n\nlet screens: Screens | undefined;\n\n/** Tell the page what each screen draws. */\nexport function setScreens(all: Screens): void {\n screens = all;\n}\n\nexport function render(): void {\n const root = document.getElementById('app');\n if (root === null || screens === undefined) return;\n // Keep what the user is typing when the same form redraws around it, and never carry it over to another form.\n const form = `${view.screen}:${currentDraft()?.adapter.host ?? ''}`;\n const isSameForm = root.dataset.form === form;\n const activeId = document.activeElement instanceof HTMLElement ? document.activeElement.id : '';\n const typed = new Map<string, string>();\n for (const input of root.querySelectorAll<HTMLInputElement>('input[type=text], input[type=password]')) {\n if (input.id !== '') typed.set(input.id, input.value);\n }\n root.replaceChildren(...screens[view.screen]().filter((child): child is Node | string => child !== false && child !== null && child !== undefined));\n root.dataset.form = form;\n if (!isSameForm) return;\n for (const [id, value] of typed) {\n const input = document.getElementById(id);\n if (input instanceof HTMLInputElement) input.value = value;\n }\n if (activeId !== '') document.getElementById(activeId)?.focus();\n}\n", "/**\n * @file apps/frontend/src/actions.ts\n * @description What the setup page does: each action calls the plugin, changes the state of the page and redraws it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { api, type StoredDevice } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport { render } from './render.js';\nimport { currentDraft, type Draft, draftLabel, isStored, view } from './state.js';\n\n/**\n * Run a call while the page shows that it is working.\n *\n * @param text What the page says meanwhile.\n * @param task The call.\n * @returns What the call returned.\n */\nasync function busy<T>(text: string, task: () => Promise<T>): Promise<T> {\n view.isBusy = true;\n view.busyText = text;\n view.error = '';\n render();\n try {\n return await task();\n } finally {\n view.isBusy = false;\n render();\n }\n}\n\nexport async function loadState(): Promise<void> {\n const reply = await api('GET state');\n if (!reply.ok) {\n view.error = reply.error;\n view.screen = 'home';\n render();\n return;\n }\n view.setup = reply.state;\n // With nothing set up yet, the page goes straight to finding the air conditioners.\n if (reply.state.devices.length === 0) {\n await search();\n return;\n }\n view.screen = 'home';\n render();\n}\n\nasync function refreshState(): Promise<void> {\n const reply = await api('GET state');\n if (reply.ok) view.setup = reply.state;\n}\n\nexport function goHome(): void {\n view.queue = [];\n view.saved = [];\n view.handled = [];\n view.error = '';\n view.screen = 'home';\n void refreshState().then(render);\n render();\n}\n\nexport async function search(): Promise<void> {\n view.screen = 'find';\n const reply = await busy('Searching your network. This takes about three seconds\u2026', async () => api('POST discover'));\n if (!reply.ok) {\n view.error = `The search did not run: ${reply.error}`;\n render();\n return;\n }\n // Keep the adapters added by address: a search does not find them. One of them may have been set up since.\n await refreshState();\n const byAddress = view.found\n .filter((adapter) => !reply.devices.some((device) => device.host === adapter.host) && view.selected.has(adapter.host))\n .map((adapter) => ({ ...adapter, isConfigured: isStored(adapter) }));\n view.found = [...reply.devices, ...byAddress];\n for (const adapter of reply.devices) {\n if (!adapter.isConfigured && !adapter.hasDuplicateName) view.selected.add(adapter.host);\n }\n render();\n}\n\nexport async function lookUp(host: string): Promise<void> {\n const reply = await busy(`Looking for an adapter at ${host}\u2026`, async () => api('POST identify', { host }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n const isConfigured = isStored(reply.device);\n // Whether the adapter knows this plugin already shows in the Connect step: an address alone does not tell.\n const needsRegistration = reply.device.requiresRegistration === true;\n view.found = [...view.found.filter((adapter) => adapter.host !== reply.device.host), { ...reply.device, isConfigured, hasDuplicateName: false, needsRegistration }];\n if (!isConfigured) view.selected.add(reply.device.host);\n render();\n}\n\nexport function startQueue(drafts: Draft[]): void {\n view.queue = drafts;\n view.saved = [];\n view.handled = [];\n void enterConnect();\n}\n\nasync function enterConnect(): Promise<void> {\n const draft = currentDraft();\n if (draft === undefined) {\n view.screen = view.saved.length > 0 ? 'done' : 'home';\n await refreshState();\n render();\n return;\n }\n view.screen = 'connect';\n view.error = '';\n draft.probe = undefined;\n render();\n await check(draft);\n}\n\nexport async function check(draft: Draft, key?: string): Promise<void> {\n const device = { ...draft.adapter, name: draft.name === '' ? undefined : draft.name };\n const body = { device, localPassword: draft.localPassword, key };\n const text = key === undefined ? `Checking ${draftLabel(draft)}\u2026` : `Registering with ${draftLabel(draft)}\u2026`;\n const reply = await busy(text, async () => api(key === undefined ? 'POST probe' : 'POST register', body));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n draft.probe = reply.result;\n draft.adapter = reply.result.device;\n if (reply.result.status === 'ready') {\n draft.capabilities = reply.result.capabilities;\n draft.controls = reply.result.controls;\n // The name the adapter tells is the name of the device. Only an adapter that has none is named here.\n const told = reply.result.device.name ?? '';\n if (told !== '') draft.name = told;\n if (draft.name !== '') view.screen = 'options';\n }\n render();\n}\n\nexport function skipDraft(): void {\n const skipped = view.queue.shift();\n if (skipped !== undefined) view.handled.push({ label: draftLabel(skipped), isSaved: false });\n void enterConnect();\n}\n\nexport async function saveDraft(draft: Draft): Promise<void> {\n const { adapter, name, localPassword } = draft;\n const device = { ...adapter, name, localPassword, ...pickExposure(draft) };\n const reply = await busy(`Saving ${name}\u2026`, async () => api('POST device', { device }));\n if (!reply.ok) {\n view.error = reply.error;\n render();\n return;\n }\n view.setup = reply.state;\n view.saved.push(name);\n view.handled.push({ label: name, isSaved: true });\n view.queue.shift();\n await enterConnect();\n}\n\nexport async function removeDevice(device: StoredDevice): Promise<void> {\n const reply = await busy(`Removing ${device.name}\u2026`, async () => api('DELETE device', undefined, `macAddress=${encodeURIComponent(device.macAddress)}`));\n view.confirmingRemoval = '';\n if (!reply.ok) view.error = reply.error;\n await refreshState();\n render();\n}\n", "/**\n * @file apps/frontend/src/dom.ts\n * @description The one helper the setup page builds its elements with.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nexport type Child = Node | string | false | null | undefined;\n\n/**\n * Create an element.\n *\n * @param tag The tag name.\n * @param props `class`, `on<event>` listeners, and anything else as a property when the element has it, else an attribute.\n * @param children Nodes or text; false, null and undefined are skipped.\n * @returns The element.\n */\nexport function h<K extends keyof HTMLElementTagNameMap>(tag: K, props: Record<string, unknown> = {}, ...children: Child[]): HTMLElementTagNameMap[K] {\n const element = document.createElement(tag);\n for (const [key, value] of Object.entries(props)) {\n if (value === undefined || value === false) continue;\n if (key === 'class') element.className = String(value);\n else if (key === 'html') element.innerHTML = String(value);\n else if (key.startsWith('on') && typeof value === 'function') element.addEventListener(key.slice(2), value as EventListener);\n else if (key in element) (element as unknown as Record<string, unknown>)[key] = value;\n else element.setAttribute(key, String(value));\n }\n for (const child of children) {\n if (child !== false && child !== null && child !== undefined) element.append(child);\n }\n return element;\n}\n", "/**\n * @file apps/frontend/src/components.ts\n * @description The pieces more than one screen of the setup page is made of.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { type Child, h } from './dom.js';\nimport { currentDraft, type Draft, draftLabel, type ScreenName, view } from './state.js';\n\nexport function button(label: string, onclick: () => void, kind: 'primary' | 'quiet' | 'danger' = 'quiet', disabled = false): HTMLButtonElement {\n return h('button', { type: 'button', class: `button ${kind}`, onclick, disabled: disabled || view.isBusy }, label);\n}\n\nexport function notice(): Child {\n if (view.isBusy) return h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), view.busyText);\n if (view.error !== '') return h('p', { class: 'notice problem', role: 'alert' }, view.error);\n return null;\n}\n\n/**\n * The units of this run, with the one the page is on marked: shown when a run sets up more than one, so that it is\n * plain which unit the Connect and Options steps are about.\n */\nexport function unitStrip(): Child {\n const current = currentDraft();\n if (current === undefined || !current.isNew || view.handled.length + view.queue.length < 2) return null;\n const doing = view.screen === 'connect' ? 'Connecting' : 'Choosing options';\n return h(\n 'ol',\n { 'class': 'units', 'aria-label': 'Air conditioners in this setup' },\n ...view.handled.map((unit) => h('li', { class: unit.isSaved ? 'done' : 'skipped' }, h('strong', {}, unit.label), h('span', { class: 'meta' }, unit.isSaved ? 'Added' : 'Skipped'))),\n ...view.queue.map((draft, index) =>\n h('li', { 'class': index === 0 ? 'current' : 'ahead', 'aria-current': index === 0 ? 'true' : undefined }, h('strong', {}, draftLabel(draft)), h('span', { class: 'meta' }, index === 0 ? doing : 'Waiting')),\n ),\n );\n}\n\n/** The heading of a step that is about one unit: the unit first, then what the step does with it. */\nexport function unitHeader(draft: Draft, text: string): HTMLElement {\n return h('header', { class: 'unit-header' }, h('h1', {}, draftLabel(draft)), h('p', { class: 'meta' }, text));\n}\n\nexport function steps(): Child {\n if (view.queue.length > 0 && !view.queue.some((draft) => draft.isNew)) return null;\n const names = ['Find', 'Connect', 'Options'];\n const screens: ScreenName[] = ['find', 'connect', 'options'];\n const current = screens.indexOf(view.screen);\n return h(\n 'ol',\n { class: 'steps' },\n ...names.map((name, index) => {\n let state = 'ahead';\n if (index < current) state = 'done';\n else if (index === current) state = 'current';\n return h('li', { 'class': state, 'aria-current': index === current ? 'step' : undefined }, name);\n }),\n );\n}\n", "/**\n * @file apps/frontend/src/screens/connect.ts\n * @description The Connect step: the page checks an adapter and asks for what the adapter wants before it answers.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { check, goHome, skipDraft } from '../actions.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft, view } from '../state.js';\n\n/** Asks for the name of a unit whose adapter has none. */\nfunction nameForm(draft: Draft): HTMLElement {\n const name = h('input', { type: 'text', id: 'device-name', maxLength: 32, autocomplete: 'off' });\n const submit = (): void => {\n const value = name.value.trim();\n if (value === '') return;\n if (view.setup?.devices.some((device) => device.name === value)) {\n view.error = `\"${value}\" is already the name of another air conditioner. Pick a different one.`;\n render();\n return;\n }\n draft.name = value;\n view.error = '';\n view.screen = 'options';\n render();\n };\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'device-name' }, 'Name this air conditioner'),\n h(\n 'p',\n { class: 'help' },\n 'Its adapter has no name of its own. This is the name it gets in Matter, up to 32 characters. Name the adapter in its Daikin app later and the plugin takes that name instead.',\n ),\n h('div', { class: 'inline-form' }, name, button('Continue', submit, 'primary')),\n );\n}\n\nfunction lockCodeForm(draft: Draft, isRejected: boolean): HTMLElement {\n const code = h('input', { type: 'password', id: 'lock-code', inputMode: 'numeric', autocomplete: 'off', maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'lock-code' }, 'Child lock code'),\n h('p', { class: 'help' }, 'The 4-digit code set under Child lock in the Daikin app the adapter was set up with. It is not printed on the adapter.'),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that code.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n code,\n button(\n 'Check the code',\n () => {\n if (code.value === '') return;\n draft.localPassword = code.value;\n void check(draft);\n },\n 'primary',\n ),\n ),\n );\n}\n\nfunction keyForm(draft: Draft, isRejected: boolean): HTMLElement {\n const key = h('input', { type: 'text', id: 'adapter-key', autocomplete: 'off', spellcheck: false, maxLength: 64 });\n return h(\n 'div',\n { class: 'field' },\n h('label', { for: 'adapter-key' }, 'KEY on the adapter'),\n h(\n 'p',\n { class: 'help' },\n 'This adapter only answers apps it has registered. The KEY is printed on its label, next to the SSID, and in the operation manual. It is used once and not stored.',\n ),\n isRejected ? h('p', { class: 'problem-text' }, 'The adapter did not accept that KEY.') : null,\n h(\n 'div',\n { class: 'inline-form' },\n key,\n button(\n 'Register',\n () => {\n if (key.value.trim() !== '') void check(draft, key.value.trim());\n },\n 'primary',\n ),\n ),\n );\n}\n\n/** What the adapter wants before the page can go on, or why the page cannot: nothing while the check has no answer. */\nfunction credentialForm(draft: Draft): Child {\n const status = draft.probe?.status;\n if (status === 'ready' && draft.name === '') return nameForm(draft);\n if (status === 'localPasswordRequired' || status === 'localPasswordRejected') return lockCodeForm(draft, status === 'localPasswordRejected');\n if (status === 'registrationRequired' || status === 'registrationRejected') return keyForm(draft, status === 'registrationRejected');\n if (status === 'unreachable') {\n return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} did not answer at ${draft.adapter.host}. Check that it has power and is on the network, then try again.`);\n }\n if (status === 'failed') return h('p', { class: 'notice problem', role: 'alert' }, `${draft.name} answered with an error: ${draft.probe?.message ?? 'unknown'}`);\n return null;\n}\n\nexport function connectScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const canRetry = draft.probe?.status === 'unreachable' || draft.probe?.status === 'failed' || (view.error !== '' && draft.name !== '');\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Connecting to its adapter at ${draft.adapter.host}.`),\n notice(),\n view.isBusy ? null : credentialForm(draft),\n h(\n 'div',\n { class: 'footer' },\n canRetry ? button('Try again', () => void check(draft)) : null,\n button(draft.isNew && view.queue.length > 1 ? 'Skip this one' : 'Cancel', draft.isNew && view.queue.length > 1 ? skipDraft : goHome),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/done.ts\n * @description The screen a run ends on: what it set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome } from '../actions.js';\nimport { button } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { view } from '../state.js';\n\nexport function doneScreen(): Child[] {\n const names = view.saved;\n return [\n h('header', {}, h('h1', {}, names.length === 1 ? `${names[0]} is set up` : `${names.length} air conditioners are set up`)),\n h('p', { class: 'lead' }, 'They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page.'),\n h('div', { class: 'footer' }, button('Done', goHome, 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/find.ts\n * @description The Find step: the adapters a search found or an address led to, and which of them to set up.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, lookUp, search, startQueue } from '../actions.js';\nimport type { FoundAdapter } from '../api.js';\nimport { button, notice, steps } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromAdapter } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction foundRow(adapter: FoundAdapter): HTMLElement {\n const isSelectable = !adapter.isConfigured && !adapter.hasDuplicateName;\n const notes: string[] = [];\n if (adapter.isConfigured) notes.push('Already set up.');\n if (adapter.hasDuplicateName) notes.push('Another adapter has the same name. Give each its own name in its Daikin app, then search again.');\n if (adapter.name === undefined) notes.push('It has not told its name yet. If it has none, you will name it after connecting.');\n if (isSelectable && adapter.needsRegistration) notes.push('Asks for the KEY printed on its label.');\n else if (isSelectable && adapter.requiresRegistration) notes.push('Already registered with this plugin, so it will not ask for its KEY.');\n if (isSelectable && adapter.requiresLocalPassword) notes.push('Asks for its child lock code.');\n const checkbox = h('input', {\n type: 'checkbox',\n id: `found-${adapter.host}`,\n checked: isSelectable && view.selected.has(adapter.host),\n disabled: !isSelectable,\n onchange: () => {\n if (checkbox.checked) view.selected.add(adapter.host);\n else view.selected.delete(adapter.host);\n render();\n },\n });\n return h(\n 'li',\n { class: isSelectable ? 'found' : 'found unavailable' },\n h(\n 'label',\n { for: `found-${adapter.host}` },\n checkbox,\n h('span', {}, h('strong', {}, adapter.name ?? 'Unnamed adapter'), h('span', { class: 'meta' }, ` ${adapter.host}`), ...notes.map((note) => h('span', { class: 'meta note' }, note))),\n ),\n );\n}\n\nexport function findScreen(): Child[] {\n const chosen = view.found.filter((adapter) => view.selected.has(adapter.host) && !adapter.isConfigured && !adapter.hasDuplicateName);\n const host = h('input', { type: 'text', id: 'host', placeholder: '192.168.1.50', spellcheck: false, autocomplete: 'off' });\n const lookUpHost = (): void => {\n if (host.value.trim() !== '') void lookUp(host.value.trim());\n };\n host.addEventListener('keydown', (event) => {\n if (event.key === 'Enter') lookUpHost();\n });\n const continueLabel = chosen.length === 1 ? 'Set up 1 air conditioner' : `Set up ${chosen.length} air conditioners`;\n return [\n steps(),\n h('header', {}, h('h1', {}, 'Find your air conditioners')),\n notice(),\n view.found.length > 0 ? h('ul', { class: 'found-list' }, ...view.found.map(foundRow)) : null,\n !view.isBusy && view.found.length === 0\n ? h('p', { class: 'empty' }, 'No adapter answered. A search only reaches adapters on the same network segment as Matterbridge: add the others by address below.')\n : null,\n h(\n 'details',\n { class: 'manual', open: !view.isBusy && view.found.length === 0 },\n h('summary', {}, \"Can't see your air conditioner?\"),\n h('p', { class: 'help' }, 'Enter the address of its adapter. The plugin reads the name of the adapter there and follows it to a new address like any other, as long as a search can reach it. An adapter on another subnet or VLAN keeps this address.'),\n h('div', { class: 'inline-form' }, h('label', { for: 'host', class: 'visually-hidden' }, 'Adapter address'), host, button('Look it up', lookUpHost)),\n ),\n h(\n 'div',\n { class: 'footer' },\n button('Search again', () => void search()),\n button('Cancel', goHome),\n button(continueLabel, () => startQueue(chosen.map(draftFromAdapter)), 'primary', chosen.length === 0),\n ),\n ];\n}\n", "/**\n * @file apps/frontend/src/screens/home.ts\n * @description The home screen: the air conditioners that are set up, and the way to add more.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState, removeDevice, search, startQueue } from '../actions.js';\nimport type { StoredDevice } from '../api.js';\nimport { button, notice } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { draftFromStored } from '../exposure.js';\nimport { render } from '../render.js';\nimport { view } from '../state.js';\n\nfunction describeExposure(device: StoredDevice): string {\n // What the unit cannot do is left out, whatever is stored for it. A unit the plugin has not asked yet is described as stored.\n const hasFan = device.controls === undefined || device.controls.fanSteps.length > 0;\n const hasSwing = device.controls === undefined || device.controls.hasSwing;\n const fanPlace = device.hasSeparateFan ? ' on a fan of its own' : '';\n const fan = device.fanControl === 'hidden' ? 'fan speed not shown' : `fan speed as ${device.fanControl === 'steps' ? 'steps' : 'a percentage'}${fanPlace}`;\n const swing = device.swingControl === 'hidden' ? 'swing not shown' : 'swing as switches';\n const controls = [hasFan ? fan : '', hasSwing ? swing : ''].filter((text) => text !== '').join(', ');\n const switches = [device.hasDryModeSwitch ? 'dry mode' : '', device.hasFanModeSwitch ? 'fan mode' : '', device.hasFanAutoSwitch ? 'auto fan' : '', ...device.specialModeSwitches].filter(\n (name) => name !== '',\n );\n const extra = switches.length === 0 ? '' : `a switch for ${switches.join(', ')}`;\n if (controls === '') return extra === '' ? 'power, mode and temperature' : extra;\n return extra === '' ? controls : `${controls}, and ${extra}`;\n}\n\nfunction deviceRow(device: StoredDevice): HTMLElement {\n const statusText = { online: 'Online', offline: 'Not answering', stopped: 'Not started' }[device.status];\n const isConfirming = view.confirmingRemoval === device.macAddress;\n return h(\n 'li',\n { class: 'device' },\n h(\n 'div',\n { class: 'device-main' },\n h('h3', {}, device.name),\n h('p', { class: 'meta' }, h('span', { class: `status ${device.status}` }, statusText), ` at ${device.host}. In Matter: ${describeExposure(device)}.`),\n device.status !== 'online' && device.error ? h('p', { class: 'meta problem-text' }, device.error) : null,\n ),\n isConfirming\n ? h(\n 'div',\n { class: 'actions' },\n h('span', { class: 'meta' }, 'Remove it from Matter too?'),\n button('Remove', () => void removeDevice(device), 'danger'),\n button('Keep', () => {\n view.confirmingRemoval = '';\n render();\n }),\n )\n : h(\n 'div',\n { class: 'actions' },\n // Either way the unit is asked first what it accepts now, so the options offered are the ones it has today.\n button(device.status === 'online' ? 'Options' : 'Reconnect', () => startQueue([draftFromStored(device)])),\n button('Remove', () => {\n view.confirmingRemoval = device.macAddress;\n render();\n }),\n ),\n );\n}\n\nexport function homeScreen(): Child[] {\n const devices = view.setup?.devices ?? [];\n return [\n h('header', {}, h('h1', {}, 'Daikin air conditioners'), view.setup ? button('Add air conditioners', () => void search(), 'primary') : null),\n notice(),\n devices.length === 0 && view.setup\n ? h('p', { class: 'empty' }, 'No air conditioner is set up yet. Add one to control it from Matter.')\n : h('ul', { class: 'devices' }, ...devices.map(deviceRow)),\n view.setup ? null : button('Try again', () => void loadState(), 'primary'),\n ];\n}\n", "/**\n * @file apps/frontend/src/presets.ts\n * @description The presets of the Options step: each fills in the choices of how a device appears in Matter.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport type { AvailableControls, Exposure } from './api.js';\nimport { pickExposure } from './exposure.js';\nimport type { Draft } from './state.js';\n\nexport type PresetName = 'apple' | 'google' | 'full' | 'minimal';\n\nexport const presets: Record<PresetName, { title: string; text: string }> = {\n apple: { title: 'Apple Home', text: 'Fan speed as a percentage, and a switch for everything Home has no control for: swing, dry mode, fan mode and auto fan.' },\n google: { title: 'Google Home', text: 'Fan speed as steps, swing switches and a dry mode switch.' },\n full: { title: 'Full', text: 'Every control and every switch this unit can have, special modes included.' },\n minimal: { title: 'Minimal', text: 'No fan control, no swing control and no switches. The fan and the swing stay as they are.' },\n};\n\n/** The choices a preset makes for a unit: never a switch or a control the unit has no use for. */\nexport function presetExposure(name: PresetName, controls: AvailableControls): Exposure {\n const isMinimal = name === 'minimal';\n return {\n fanControl: isMinimal || controls.fanSteps.length === 0 ? 'hidden' : name === 'apple' ? 'percent' : 'steps',\n fanSpeedWhenHidden: 'keep',\n swingControl: isMinimal || !controls.hasSwing ? 'hidden' : 'switches',\n swingModeWhenHidden: 'keep',\n hasDryModeSwitch: !isMinimal && controls.hasDryMode,\n hasFanModeSwitch: (name === 'apple' || name === 'full') && controls.hasFanMode,\n hasFanAutoSwitch: (name === 'apple' || name === 'full') && controls.hasFanAuto,\n // No preset takes the fan off the air conditioner: that is for an app that shows no fan speed there.\n hasSeparateFan: false,\n specialModeSwitches: name === 'full' ? [...controls.specialModes] : [],\n };\n}\n\n/** Whether the choices in a draft are exactly those of a preset, leaving aside what the unit has no use for. */\nexport function matchesPreset(draft: Draft, name: PresetName, controls: AvailableControls): boolean {\n const chosen = pickExposure(draft);\n if (controls.fanSteps.length === 0) Object.assign(chosen, { fanControl: 'hidden', fanSpeedWhenHidden: 'keep' });\n // The choice shows nowhere while the fan is hidden.\n if (chosen.fanControl === 'hidden') chosen.hasSeparateFan = false;\n if (!controls.hasSwing) Object.assign(chosen, { swingControl: 'hidden', swingModeWhenHidden: 'keep' });\n chosen.specialModeSwitches = controls.specialModes.filter((mode) => chosen.specialModeSwitches.includes(mode));\n return JSON.stringify(chosen) === JSON.stringify(presetExposure(name, controls));\n}\n", "/**\n * @file apps/frontend/src/screens/options.ts\n * @description The Options step: how the fan, the swing and the extra modes of a unit appear in Matter, one section each.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { goHome, saveDraft } from '../actions.js';\nimport type { AvailableControls, Exposure, FanSpeed, SpecialMode, SwingMode } from '../api.js';\nimport { button, notice, steps, unitHeader, unitStrip } from '../components.js';\nimport { type Child, h } from '../dom.js';\nimport { noControls } from '../exposure.js';\nimport { matchesPreset, presetExposure, type PresetName, presets } from '../presets.js';\nimport { render } from '../render.js';\nimport { currentDraft, type Draft } from '../state.js';\n\nconst fanSpeedLabels: Record<FanSpeed, string> = {\n auto: 'Auto',\n silent: 'Silent',\n level1: 'Level 1',\n level2: 'Level 2',\n level3: 'Level 3',\n level4: 'Level 4',\n level5: 'Level 5',\n};\n\nconst swingModeLabels: Record<SwingMode, string> = {\n off: 'No swing',\n vertical: 'Up and down',\n horizontal: 'Left and right',\n both: 'Up, down, left and right',\n};\n\nconst specialModeTexts: Record<SpecialMode, { title: string; text: string }> = {\n powerful: { title: 'Powerful', text: 'Runs the unit at full power for a while.' },\n econo: { title: 'Econo', text: 'Limits how much power the unit draws.' },\n streamer: { title: 'Streamer', text: 'Runs the air purifying of the unit.' },\n};\n\n// Static drawings, so innerHTML is safe for them. Everything that comes from the network goes through text nodes.\nconst glyphs = {\n steps:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><rect x=\"2\" y=\"20\" width=\"8\" height=\"6\"/><rect x=\"14\" y=\"14\" width=\"8\" height=\"12\"/><rect x=\"26\" y=\"8\" width=\"8\" height=\"18\"/><rect x=\"38\" y=\"2\" width=\"8\" height=\"24\"/></svg>',\n percent: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path d=\"M2 26 L46 2 L46 26 Z\"/></svg>',\n hidden: '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M6 14 H42\" stroke-dasharray=\"3 5\"/></svg>',\n switches:\n '<svg viewBox=\"0 0 48 28\" aria-hidden=\"true\"><path class=\"stroke\" d=\"M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19\"/><path class=\"stroke\" d=\"M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19\"/></svg>',\n};\n\nfunction update(draft: Draft, change: Partial<Exposure>): void {\n Object.assign(draft, change);\n render();\n}\n\nfunction choice<T extends string>(group: string, value: T, current: T, title: string, text: string, glyph: string, onpick: (value: T) => void): HTMLElement {\n const id = `${group}-${value}`;\n const input = h('input', { type: 'radio', name: group, id, checked: value === current, onchange: () => onpick(value) });\n return h('label', { class: 'choice pictured', for: id }, input, h('span', { class: 'glyph', html: glyph }), h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction toggle(id: string, isOn: boolean, title: string, text: string, onpick: (isOn: boolean) => void): HTMLElement {\n const input = h('input', { type: 'checkbox', id, checked: isOn, onchange: () => onpick(input.checked) });\n return h('label', { class: 'choice', for: id }, input, h('span', {}, h('strong', {}, title), h('span', { class: 'meta' }, text)));\n}\n\nfunction presetSection(draft: Draft, controls: AvailableControls): HTMLElement {\n return h(\n 'section',\n { class: 'presets' },\n h('h2', {}, 'Start from a preset'),\n h(\n 'div',\n { class: 'preset-list' },\n ...(Object.keys(presets) as PresetName[]).map((name) =>\n h(\n 'button',\n { 'type': 'button', 'class': 'preset', 'aria-pressed': String(matchesPreset(draft, name, controls)), 'onclick': () => update(draft, presetExposure(name, controls)) },\n h('strong', {}, presets[name].title),\n h('span', { class: 'meta' }, presets[name].text),\n ),\n ),\n ),\n h('p', { class: 'help' }, 'A preset fills in the choices below for what this unit can do. Change any of them afterwards.'),\n );\n}\n\nfunction fanSection(draft: Draft, controls: AvailableControls): HTMLElement {\n const fanSpeeds = draft.capabilities?.fanSpeeds ?? [];\n const stepCount = controls.fanSteps.length;\n if (stepCount === 0) return h('section', {}, h('h2', {}, 'Fan speed'), h('p', { class: 'help' }, 'This unit does not let apps set its fan speed, so there is nothing to choose.'));\n // A fixed fan speed is sent with every command, so a switch for auto would not hold.\n const canSwitchFanAuto = controls.hasFanAuto && !(draft.fanControl === 'hidden' && draft.fanSpeedWhenHidden !== 'keep');\n return h(\n 'section',\n {},\n h('h2', {}, 'Fan speed'),\n h(\n 'div',\n { class: 'choices' },\n choice('fan', 'steps', draft.fanControl, 'Steps', `${stepCount} speeds, one for each speed of the unit${fanSpeeds.includes('auto') ? ', and auto' : ''}.`, glyphs.steps, (fanControl) =>\n update(draft, { fanControl }),\n ),\n choice('fan', 'percent', draft.fanControl, 'Percentage', 'A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.', glyphs.percent, (fanControl) => update(draft, { fanControl })),\n choice('fan', 'hidden', draft.fanControl, 'Not shown in Matter', 'No fan control. Pick below what the plugin does with the fan speed instead.', glyphs.hidden, (fanControl) =>\n update(draft, { fanControl }),\n ),\n ),\n draft.fanControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'fan-fixed' }, 'When the plugin sends a command, the fan speed'),\n h(\n 'select',\n { id: 'fan-fixed', onchange: (event: Event) => update(draft, { fanSpeedWhenHidden: (event.target as HTMLSelectElement).value as Exposure['fanSpeedWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.fanSpeedWhenHidden === 'keep' }, 'stays as it is'),\n ...fanSpeeds.map((speed) => h('option', { value: speed, selected: draft.fanSpeedWhenHidden === speed }, `is set to ${fanSpeedLabels[speed]}`)),\n ),\n )\n : null,\n draft.fanControl === 'hidden'\n ? null\n : toggle(\n 'separate-fan',\n draft.hasSeparateFan,\n 'Show the fan as a device of its own',\n `It appears as \"${draft.name} Fan\", and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner. It always shows the speed the unit holds, also while the unit is off.`,\n (hasSeparateFan) => update(draft, { hasSeparateFan }),\n ),\n canSwitchFanAuto\n ? toggle(\n 'fan-auto-switch',\n draft.hasFanAutoSwitch,\n 'Add a switch for auto fan speed',\n `It appears as \"${draft.name} Auto Fan\". On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,\n (hasFanAutoSwitch) => update(draft, { hasFanAutoSwitch }),\n )\n : null,\n );\n}\n\nfunction swingSection(draft: Draft): HTMLElement {\n const swingModes = draft.capabilities?.swingModes ?? [];\n if (swingModes.length === 0) return h('section', {}, h('h2', {}, 'Swing'), h('p', { class: 'help' }, 'This unit does not let apps move its louvers, so there is nothing to choose.'));\n const hasVertical = swingModes.includes('vertical');\n const hasHorizontal = swingModes.includes('horizontal');\n let switchText = 'One switch, for the up and down swing.';\n if (hasVertical && hasHorizontal) switchText = 'Two switches: one for the up and down swing, one for left and right.';\n else if (hasHorizontal) switchText = 'One switch, for the left and right swing.';\n return h(\n 'section',\n {},\n h('h2', {}, 'Swing'),\n h(\n 'div',\n { class: 'choices' },\n choice('swing', 'switches', draft.swingControl, 'Switches', switchText, glyphs.switches, (swingControl) => update(draft, { swingControl })),\n choice('swing', 'hidden', draft.swingControl, 'Not shown in Matter', 'No swing control. Pick below what the plugin does with the swing instead.', glyphs.hidden, (swingControl) =>\n update(draft, { swingControl }),\n ),\n ),\n draft.swingControl === 'hidden'\n ? h(\n 'div',\n { class: 'field' },\n h('label', { for: 'swing-fixed' }, 'When the plugin sends a command, the swing'),\n h(\n 'select',\n { id: 'swing-fixed', onchange: (event: Event) => update(draft, { swingModeWhenHidden: (event.target as HTMLSelectElement).value as Exposure['swingModeWhenHidden'] }) },\n h('option', { value: 'keep', selected: draft.swingModeWhenHidden === 'keep' }, 'stays as it is'),\n ...swingModes.map((mode) => h('option', { value: mode, selected: draft.swingModeWhenHidden === mode }, `is set to: ${swingModeLabels[mode]}`)),\n ),\n )\n : null,\n );\n}\n\nfunction modeSection(draft: Draft, controls: AvailableControls): Child {\n if (!controls.hasDryMode && !controls.hasFanMode) return null;\n const modes = draft.capabilities?.modes ?? [];\n return h(\n 'section',\n {},\n h('h2', {}, 'Dry and fan modes'),\n h(\n 'p',\n { class: 'help' },\n `These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${modes.includes('cool') ? 'cool' : 'heat'}.`,\n ),\n h(\n 'div',\n { class: 'choices' },\n controls.hasDryMode\n ? toggle('dry-mode-switch', draft.hasDryModeSwitch, 'Add a switch for dry mode', `It appears as \"${draft.name} Dry Mode\".`, (hasDryModeSwitch) => update(draft, { hasDryModeSwitch }))\n : null,\n controls.hasFanMode\n ? toggle('fan-mode-switch', draft.hasFanModeSwitch, 'Add a switch for fan mode', `It appears as \"${draft.name} Fan Mode\".`, (hasFanModeSwitch) => update(draft, { hasFanModeSwitch }))\n : null,\n ),\n );\n}\n\nfunction specialSection(draft: Draft, controls: AvailableControls): Child {\n if (controls.specialModes.length === 0) return null;\n return h(\n 'section',\n {},\n h('h2', {}, 'Special modes'),\n h('p', { class: 'help' }, 'Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in.'),\n h(\n 'div',\n { class: 'choices' },\n ...controls.specialModes.map((mode) =>\n toggle(`special-${mode}`, draft.specialModeSwitches.includes(mode), `Add a switch for ${specialModeTexts[mode].title}`, specialModeTexts[mode].text, (isOn) =>\n update(draft, { specialModeSwitches: controls.specialModes.filter((other) => (other === mode ? isOn : draft.specialModeSwitches.includes(other))) }),\n ),\n ),\n ),\n );\n}\n\nexport function optionsScreen(): Child[] {\n const draft = currentDraft();\n if (draft === undefined) return [notice()];\n const controls = draft.controls ?? noControls;\n return [\n steps(),\n unitStrip(),\n unitHeader(draft, `Options for the air conditioner at ${draft.adapter.host}. Power, mode and temperature always appear in Matter, and so do the humidity and the outdoor temperature once the unit gives them: choose how the rest appears.`),\n presetSection(draft, controls),\n fanSection(draft, controls),\n swingSection(draft),\n modeSection(draft, controls),\n specialSection(draft, controls),\n draft.isNew ? null : h('p', { class: 'help' }, 'Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change.'),\n notice(),\n h('div', { class: 'footer' }, button('Cancel', goHome), button(draft.isNew ? `Add ${draft.name}` : 'Save options', () => void saveDraft(draft), 'primary')),\n ];\n}\n", "/**\n * @file apps/frontend/src/theme.ts\n * @description Makes the setup page look like the Matterbridge page it is shown in.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\n/** Take the colours and the font of the Matterbridge page this one is shown in. */\nexport function adoptHostTheme(): void {\n try {\n if (window.parent === window) return;\n const host = getComputedStyle(window.parent.document.body);\n for (const name of ['--primary-color', '--div-bg-color', '--div-text-color', '--main-bg-color', '--main-label-color', '--div-border-color', '--main-light-color']) {\n const value = host.getPropertyValue(name).trim();\n if (value !== '') document.documentElement.style.setProperty(`--host${name.slice(1)}`, value);\n }\n document.documentElement.style.setProperty('--host-font', host.fontFamily);\n } catch {\n // Opened on its own or from another origin: the page keeps its own colours.\n }\n}\n", "/**\n * @file apps/frontend/src/app.ts\n * @description The guided setup page of the Matterbridge Daikin Local plugin. Matterbridge serves it at\n * /plugins/matterbridge-daikin-local/ and it talks to the plugin through api/<path> below that address.\n * This file starts the page: the rest is in the files next to it.\n * @author Calvin Che\n * @license Apache-2.0\n */\n\nimport { loadState } from './actions.js';\nimport { h } from './dom.js';\nimport { render, setScreens } from './render.js';\nimport { connectScreen } from './screens/connect.js';\nimport { doneScreen } from './screens/done.js';\nimport { findScreen } from './screens/find.js';\nimport { homeScreen } from './screens/home.js';\nimport { optionsScreen } from './screens/options.js';\nimport { adoptHostTheme } from './theme.js';\n\nsetScreens({\n loading: () => [h('p', { class: 'notice working', role: 'status' }, h('span', { class: 'spinner' }), 'Loading\u2026')],\n home: homeScreen,\n find: findScreen,\n connect: connectScreen,\n options: optionsScreen,\n done: doneScreen,\n});\n\nadoptHostTheme();\nrender();\nvoid loadState();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["api", "route", "body", "query", "method", "path", "init", "response", "reply", "pickExposure", "source", "fanControl", "fanSpeedWhenHidden", "swingControl", "swingModeWhenHidden", "hasDryModeSwitch", "hasFanModeSwitch", "hasFanAutoSwitch", "hasSeparateFan", "specialModeSwitches", "defaultExposure", "draftFromAdapter", "adapter", "draftFromStored", "device", "capabilities", "controls", "name", "macAddress", "host", "protocol", "requiresRegistration", "usesHttps", "requiresLocalPassword", "noControls", "view", "currentDraft", "draftLabel", "draft", "isStored", "adapter", "device", "screens", "setScreens", "all", "render", "root", "form", "view", "currentDraft", "isSameForm", "activeId", "typed", "input", "child", "id", "value", "busy", "text", "task", "view", "render", "loadState", "reply", "api", "search", "refreshState", "goHome", "byAddress", "adapter", "device", "isStored", "lookUp", "host", "isConfigured", "needsRegistration", "startQueue", "drafts", "enterConnect", "draft", "currentDraft", "check", "key", "body", "draftLabel", "told", "skipDraft", "skipped", "saveDraft", "name", "localPassword", "pickExposure", "removeDevice", "h", "tag", "props", "children", "element", "key", "value", "child", "button", "label", "onclick", "kind", "disabled", "h", "view", "notice", "unitStrip", "current", "currentDraft", "doing", "unit", "draft", "index", "draftLabel", "unitHeader", "text", "steps", "names", "name", "state", "nameForm", "draft", "name", "h", "submit", "value", "view", "device", "render", "button", "lockCodeForm", "isRejected", "code", "check", "keyForm", "key", "credentialForm", "status", "connectScreen", "currentDraft", "notice", "canRetry", "steps", "unitStrip", "unitHeader", "skipDraft", "goHome", "doneScreen", "names", "view", "h", "button", "goHome", "foundRow", "adapter", "isSelectable", "notes", "checkbox", "h", "view", "render", "note", "findScreen", "chosen", "host", "lookUpHost", "lookUp", "event", "continueLabel", "steps", "notice", "button", "search", "goHome", "startQueue", "draftFromAdapter", "describeExposure", "device", "hasFan", "hasSwing", "fanPlace", "fan", "swing", "controls", "text", "switches", "name", "extra", "deviceRow", "statusText", "isConfirming", "view", "h", "button", "removeDevice", "render", "startQueue", "draftFromStored", "homeScreen", "devices", "search", "notice", "loadState", "presets", "presetExposure", "name", "controls", "isMinimal", "matchesPreset", "draft", "chosen", "pickExposure", "mode", "fanSpeedLabels", "swingModeLabels", "specialModeTexts", "glyphs", "update", "draft", "change", "render", "choice", "group", "value", "current", "title", "text", "glyph", "onpick", "id", "input", "h", "toggle", "isOn", "presetSection", "controls", "presets", "name", "matchesPreset", "presetExposure", "fanSection", "fanSpeeds", "stepCount", "canSwitchFanAuto", "fanControl", "event", "speed", "hasSeparateFan", "hasFanAutoSwitch", "swingSection", "swingModes", "hasVertical", "hasHorizontal", "switchText", "swingControl", "mode", "modeSection", "modes", "hasDryModeSwitch", "hasFanModeSwitch", "specialSection", "other", "optionsScreen", "currentDraft", "notice", "noControls", "steps", "unitStrip", "unitHeader", "button", "goHome", "saveDraft", "adoptHostTheme", "host", "name", "value", "setScreens", "h", "homeScreen", "findScreen", "connectScreen", "optionsScreen", "doneScreen", "adoptHostTheme", "render", "loadState"]
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@ import { SPECIAL_MODES, } from 'daikin-local-control';
|
|
|
2
2
|
import { BridgedDeviceBasicInformation, FanControl, OnOff, Thermostat } from 'matterbridge/matter/clusters';
|
|
3
3
|
import { addReportedSensors, getSwitchableSpecialModes, hasFanAuto, noReportedSensors } from '../controls.js';
|
|
4
4
|
import { getErrorMessage } from '../errors.js';
|
|
5
|
-
import {
|
|
5
|
+
import { toOperatingMode } from '../mapping.js';
|
|
6
6
|
import { createAirConditionerEndpoint, createFanEndpoint, createOutdoorTemperatureEndpoint, createSwitchEndpoint } from './endpoints.js';
|
|
7
7
|
import { getAccessoryLayout } from './layout.js';
|
|
8
8
|
import { applySensors, applyState, readFanSpeed } from './matterState.js';
|
|
@@ -17,7 +17,6 @@ function ignore() {
|
|
|
17
17
|
}
|
|
18
18
|
export class DaikinAccessory {
|
|
19
19
|
name;
|
|
20
|
-
serial;
|
|
21
20
|
endpoint;
|
|
22
21
|
verticalSwitch;
|
|
23
22
|
horizontalSwitch;
|
|
@@ -54,8 +53,7 @@ export class DaikinAccessory {
|
|
|
54
53
|
this.log = options.log;
|
|
55
54
|
this.commandDelayInMs = options.commandDelayInMs ?? defaultCommandDelayInMs;
|
|
56
55
|
this.name = options.config.name;
|
|
57
|
-
this.
|
|
58
|
-
this.identity = { name: this.name, serial: this.serial, vendorId: options.vendorId };
|
|
56
|
+
this.identity = { name: this.name, serial: options.config.macAddress, vendorId: options.vendorId };
|
|
59
57
|
this.layout = getAccessoryLayout(this.config, this.capabilities, this.reportedSensors);
|
|
60
58
|
this.endpoint = this.createAirConditioner();
|
|
61
59
|
if (this.layout.hasSeparateFan)
|
package/dist/mapping.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { FAN_SPEEDS } from 'daikin-local-control';
|
|
3
2
|
import { FanControl, Thermostat } from 'matterbridge/matter/clusters';
|
|
4
3
|
import { maxDeviceNameLength } from './config.js';
|
|
@@ -97,9 +96,6 @@ export function resolveSwingMode(current, change, supported) {
|
|
|
97
96
|
}
|
|
98
97
|
return current;
|
|
99
98
|
}
|
|
100
|
-
export function getDeviceSerial(macAddress) {
|
|
101
|
-
return `DLC${createHash('sha256').update(macAddress).digest('hex').slice(0, 12).toUpperCase()}`;
|
|
102
|
-
}
|
|
103
99
|
export function composeDeviceName(baseName, suffix) {
|
|
104
100
|
const room = maxDeviceNameLength - suffix.length - 1;
|
|
105
101
|
return `${baseName.slice(0, Math.max(room, 1)).trimEnd()} ${suffix}`;
|
package/dist/module.js
CHANGED
|
@@ -118,14 +118,14 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
118
118
|
async getSetupState() {
|
|
119
119
|
const devices = [];
|
|
120
120
|
for (const { localPassword, ...device } of this.devices) {
|
|
121
|
-
const status = this.accessories.get(device.
|
|
121
|
+
const status = this.accessories.get(device.macAddress)?.getStatus();
|
|
122
122
|
const offlineOrStopped = status === undefined ? 'stopped' : 'offline';
|
|
123
123
|
const capabilities = await this.capabilityStore.read(device);
|
|
124
124
|
devices.push({
|
|
125
125
|
...device,
|
|
126
126
|
hasLocalPassword: localPassword !== undefined,
|
|
127
127
|
status: status?.isReachable === true ? 'online' : offlineOrStopped,
|
|
128
|
-
error: status === undefined ? this.startErrors.get(device.
|
|
128
|
+
error: status === undefined ? this.startErrors.get(device.macAddress) : status.lastError,
|
|
129
129
|
capabilities,
|
|
130
130
|
controls: capabilities === undefined ? undefined : getAvailableControls(capabilities),
|
|
131
131
|
});
|
|
@@ -158,7 +158,7 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
158
158
|
if (probed !== undefined) {
|
|
159
159
|
await this.capabilityStore.write(device, probed);
|
|
160
160
|
this.probedCapabilities.delete(device.macAddress);
|
|
161
|
-
this.checkedCapabilities.add(device.
|
|
161
|
+
this.checkedCapabilities.add(device.macAddress);
|
|
162
162
|
}
|
|
163
163
|
const sensors = this.probedSensors.get(device.macAddress);
|
|
164
164
|
if (sensors !== undefined) {
|
|
@@ -172,14 +172,14 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
172
172
|
if (!next.requiresLocalPassword)
|
|
173
173
|
delete next.localPassword;
|
|
174
174
|
const devices = stored === undefined ? [...this.devices, next] : this.devices.map((candidate) => (candidate === stored ? next : candidate));
|
|
175
|
-
const running = this.accessories.get(
|
|
175
|
+
const running = this.accessories.get(device.macAddress);
|
|
176
176
|
const hasNewControls = probed !== undefined && running !== undefined && !hasSameControls(running.capabilities, probed);
|
|
177
|
-
await this.applyDevices(devices, hasNewControls ?
|
|
177
|
+
await this.applyDevices(devices, hasNewControls ? device.macAddress : undefined);
|
|
178
178
|
}
|
|
179
|
-
async removeDevice(
|
|
180
|
-
if (!this.devices.some((device) => device.
|
|
179
|
+
async removeDevice(macAddress) {
|
|
180
|
+
if (!this.devices.some((device) => device.macAddress === macAddress))
|
|
181
181
|
return false;
|
|
182
|
-
await this.applyDevices(this.devices.filter((device) => device.
|
|
182
|
+
await this.applyDevices(this.devices.filter((device) => device.macAddress !== macAddress));
|
|
183
183
|
return true;
|
|
184
184
|
}
|
|
185
185
|
async updateAddresses() {
|
|
@@ -227,7 +227,7 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
227
227
|
async reconnectDevices() {
|
|
228
228
|
await this.enqueueChange(async () => {
|
|
229
229
|
for (const device of this.devices)
|
|
230
|
-
await this.accessories.get(device.
|
|
230
|
+
await this.accessories.get(device.macAddress)?.updateConnection(device, this.createClient(device));
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
233
|
startTimers() {
|
|
@@ -299,10 +299,10 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
299
299
|
this.config.devices = [...this.devices, ...this.unreadableDevices];
|
|
300
300
|
this.saveConfig(this.config);
|
|
301
301
|
}
|
|
302
|
-
async applyDevices(devices,
|
|
303
|
-
await this.enqueueChange(async () => this.reconcile(devices,
|
|
302
|
+
async applyDevices(devices, rebuiltMacAddress) {
|
|
303
|
+
await this.enqueueChange(async () => this.reconcile(devices, rebuiltMacAddress));
|
|
304
304
|
}
|
|
305
|
-
async reconcile(devices,
|
|
305
|
+
async reconcile(devices, rebuiltMacAddress) {
|
|
306
306
|
const previous = this.devices;
|
|
307
307
|
this.devices = devices;
|
|
308
308
|
this.storeConfig();
|
|
@@ -312,11 +312,11 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
312
312
|
}
|
|
313
313
|
for (const device of devices) {
|
|
314
314
|
const before = previous.find((candidate) => candidate.macAddress === device.macAddress);
|
|
315
|
-
const accessory =
|
|
315
|
+
const accessory = this.accessories.get(device.macAddress);
|
|
316
316
|
if (before === undefined || accessory === undefined) {
|
|
317
317
|
await this.startDevice(device);
|
|
318
318
|
}
|
|
319
|
-
else if (!hasSameExposure(before, device) || device.
|
|
319
|
+
else if (!hasSameExposure(before, device) || device.macAddress === rebuiltMacAddress) {
|
|
320
320
|
await this.stopDevice(before, false);
|
|
321
321
|
await this.startDevice(device);
|
|
322
322
|
}
|
|
@@ -331,7 +331,7 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
331
331
|
}
|
|
332
332
|
async startStoppedDevices() {
|
|
333
333
|
for (const device of this.devices) {
|
|
334
|
-
if (!this.accessories.has(device.
|
|
334
|
+
if (!this.accessories.has(device.macAddress))
|
|
335
335
|
await this.startDevice(device);
|
|
336
336
|
}
|
|
337
337
|
}
|
|
@@ -343,7 +343,7 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
343
343
|
if (capabilities === undefined) {
|
|
344
344
|
capabilities = await client.getCapabilities();
|
|
345
345
|
await this.capabilityStore.write(device, capabilities);
|
|
346
|
-
this.checkedCapabilities.add(device.
|
|
346
|
+
this.checkedCapabilities.add(device.macAddress);
|
|
347
347
|
}
|
|
348
348
|
const reportedSensors = knownSensors ?? (await this.capabilityStore.readSensors(device)) ?? noReportedSensors;
|
|
349
349
|
const accessory = new DaikinAccessory({ config: device, capabilities, reportedSensors, client, log: this.log, vendorId: this.matterbridge.aggregatorVendorId });
|
|
@@ -352,16 +352,16 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
352
352
|
registered.push(endpoint);
|
|
353
353
|
}
|
|
354
354
|
await accessory.clearReadings();
|
|
355
|
-
this.accessories.set(device.
|
|
356
|
-
this.startErrors.delete(device.
|
|
355
|
+
this.accessories.set(device.macAddress, accessory);
|
|
356
|
+
this.startErrors.delete(device.macAddress);
|
|
357
357
|
this.log.info(`"${device.name}" started at ${device.host} with ${accessory.endpoints.length} Matter endpoint(s)`);
|
|
358
358
|
void accessory.refresh();
|
|
359
359
|
}
|
|
360
360
|
catch (error) {
|
|
361
361
|
const message = getErrorMessage(error);
|
|
362
|
-
if (this.startErrors.get(device.
|
|
362
|
+
if (this.startErrors.get(device.macAddress) !== message)
|
|
363
363
|
this.log.warn(`"${device.name}" could not be started at ${device.host} and will be retried: ${message}`);
|
|
364
|
-
this.startErrors.set(device.
|
|
364
|
+
this.startErrors.set(device.macAddress, message);
|
|
365
365
|
for (const endpoint of registered)
|
|
366
366
|
await this.unregisterDevice(endpoint);
|
|
367
367
|
try {
|
|
@@ -373,7 +373,7 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
373
373
|
}
|
|
374
374
|
async exposeNewSensors() {
|
|
375
375
|
for (const device of this.devices) {
|
|
376
|
-
const reported = this.accessories.get(device.
|
|
376
|
+
const reported = this.accessories.get(device.macAddress)?.getNewlyReportedSensors();
|
|
377
377
|
if (reported === undefined)
|
|
378
378
|
continue;
|
|
379
379
|
this.log.notice(`"${device.name}" gave a reading it shows nowhere in Matter yet: it is built again in Matter`);
|
|
@@ -383,10 +383,10 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
async stopDevice(device, shouldForget) {
|
|
386
|
-
const { name } = device;
|
|
387
|
-
const accessory = this.accessories.get(
|
|
388
|
-
this.accessories.delete(
|
|
389
|
-
this.startErrors.delete(
|
|
386
|
+
const { name, macAddress } = device;
|
|
387
|
+
const accessory = this.accessories.get(macAddress);
|
|
388
|
+
this.accessories.delete(macAddress);
|
|
389
|
+
this.startErrors.delete(macAddress);
|
|
390
390
|
if (accessory !== undefined) {
|
|
391
391
|
await accessory.close();
|
|
392
392
|
for (const endpoint of accessory.endpoints)
|
|
@@ -395,13 +395,13 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
395
395
|
}
|
|
396
396
|
if (!shouldForget)
|
|
397
397
|
return;
|
|
398
|
-
this.checkedCapabilities.delete(
|
|
398
|
+
this.checkedCapabilities.delete(macAddress);
|
|
399
399
|
await this.capabilityStore.remove(device);
|
|
400
400
|
}
|
|
401
401
|
async checkCapabilities() {
|
|
402
402
|
for (const device of this.devices) {
|
|
403
|
-
const accessory = this.accessories.get(device.
|
|
404
|
-
if (accessory === undefined || this.checkedCapabilities.has(device.
|
|
403
|
+
const accessory = this.accessories.get(device.macAddress);
|
|
404
|
+
if (accessory === undefined || this.checkedCapabilities.has(device.macAddress) || !accessory.getStatus().isReachable)
|
|
405
405
|
continue;
|
|
406
406
|
let capabilities;
|
|
407
407
|
try {
|
|
@@ -410,13 +410,13 @@ export class DaikinPlatform extends MatterbridgeDynamicPlatform {
|
|
|
410
410
|
catch {
|
|
411
411
|
continue;
|
|
412
412
|
}
|
|
413
|
-
this.checkedCapabilities.add(device.
|
|
413
|
+
this.checkedCapabilities.add(device.macAddress);
|
|
414
414
|
await this.capabilityStore.write(device, capabilities);
|
|
415
415
|
if (hasSameControls(accessory.capabilities, capabilities))
|
|
416
416
|
continue;
|
|
417
417
|
this.log.notice(`"${device.name}" accepts other settings than when it was set up: it is built again in Matter`);
|
|
418
418
|
const fitted = fitToCapabilities(device, capabilities);
|
|
419
|
-
this.devices = this.devices.map((candidate) => (candidate.
|
|
419
|
+
this.devices = this.devices.map((candidate) => (candidate.macAddress === device.macAddress ? fitted : candidate));
|
|
420
420
|
this.storeConfig();
|
|
421
421
|
await this.stopDevice(device, false);
|
|
422
422
|
await this.startDevice(fitted);
|
|
@@ -3,7 +3,8 @@ export function getDiscoveredUpdate(stored, devices, found, log) {
|
|
|
3
3
|
const match = found.find((device) => normalizeMacAddress(device.macAddress) === stored.macAddress);
|
|
4
4
|
if (match === undefined)
|
|
5
5
|
return undefined;
|
|
6
|
-
|
|
6
|
+
const isUnconfirmedAirbase = stored.protocol === 'airbase' && match.protocol === 'classic';
|
|
7
|
+
if (match.protocol !== stored.protocol && !isUnconfirmedAirbase) {
|
|
7
8
|
log.warn(`The adapter of "${stored.name}" now speaks ${match.protocol} instead of ${stored.protocol}: remove the device and add it again`);
|
|
8
9
|
return undefined;
|
|
9
10
|
}
|
|
@@ -13,9 +14,9 @@ export function getDiscoveredUpdate(stored, devices, found, log) {
|
|
|
13
14
|
...stored,
|
|
14
15
|
name: isNameFree ? name : stored.name,
|
|
15
16
|
host: match.host,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
...(isUnconfirmedAirbase
|
|
18
|
+
? {}
|
|
19
|
+
: { requiresRegistration: match.requiresRegistration === true, usesHttps: match.usesHttps === true, requiresLocalPassword: match.requiresLocalPassword === true }),
|
|
19
20
|
};
|
|
20
21
|
return hasSameConnection(stored, updated) && updated.name === stored.name ? undefined : updated;
|
|
21
22
|
}
|
package/dist/setup/probe.js
CHANGED
|
@@ -85,6 +85,18 @@ async function identifyClassic(host, usesHttps, terminalUuid) {
|
|
|
85
85
|
await closeQuietly(client);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
async function identifyAirbase(host, terminalUuid) {
|
|
89
|
+
const client = createDaikinClient({ host, protocol: 'airbase' }, { terminalUuid, timeoutInMs: probeTimeoutInMs, retry: { delaysInMs: [] } });
|
|
90
|
+
try {
|
|
91
|
+
return { host, protocol: 'airbase', ...(await client.getDeviceInfo()) };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await closeQuietly(client);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
88
100
|
async function identifyDsiot(host, terminalUuid) {
|
|
89
101
|
const client = createDaikinClient({ host, protocol: 'dsiot' }, { terminalUuid, timeoutInMs: probeTimeoutInMs, retry: { delaysInMs: [] } });
|
|
90
102
|
try {
|
|
@@ -106,5 +118,10 @@ export async function identifyHost(host, terminalUuid) {
|
|
|
106
118
|
if (typeof overHttps !== 'number')
|
|
107
119
|
return overHttps;
|
|
108
120
|
}
|
|
121
|
+
if (overHttp === 404) {
|
|
122
|
+
const airbase = await identifyAirbase(host, terminalUuid);
|
|
123
|
+
if (airbase !== undefined)
|
|
124
|
+
return airbase;
|
|
125
|
+
}
|
|
109
126
|
return identifyDsiot(host, terminalUuid);
|
|
110
127
|
}
|
package/dist/setup/setupApi.js
CHANGED
|
@@ -62,10 +62,10 @@ export async function handleSetupRequest(platform, method, path, query, body) {
|
|
|
62
62
|
if (method === 'POST' && path !== undefined)
|
|
63
63
|
return await handlePost(platform, path, toRecord(body));
|
|
64
64
|
if (method === 'DELETE' && path === 'device') {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
return fail('
|
|
68
|
-
return (await platform.removeDevice(
|
|
65
|
+
const macAddress = normalizeMacAddress(query?.macAddress);
|
|
66
|
+
if (macAddress === undefined)
|
|
67
|
+
return fail('Give the MAC address of the adapter of the device to remove');
|
|
68
|
+
return (await platform.removeDevice(macAddress)) ? { ok: true } : fail(`No device has the adapter ${macAddress}`);
|
|
69
69
|
}
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge-daikin-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"daikin-local-control": "0.
|
|
12
|
+
"daikin-local-control": "0.5.0",
|
|
13
13
|
"node-ansi-logger": "3.3.1",
|
|
14
14
|
"node-persist-manager": "2.1.1"
|
|
15
15
|
},
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"node_modules/daikin-local-control": {
|
|
21
|
-
"version": "0.
|
|
22
|
-
"resolved": "https://registry.npmjs.org/daikin-local-control/-/daikin-local-control-0.
|
|
23
|
-
"integrity": "sha512-
|
|
21
|
+
"version": "0.5.0",
|
|
22
|
+
"resolved": "https://registry.npmjs.org/daikin-local-control/-/daikin-local-control-0.5.0.tgz",
|
|
23
|
+
"integrity": "sha512-C3g9AG7RLjsoqZoc3eokKs/cHrIt0XLaj9NFMsSDfZyLOwwqGmKUDjnpQDqR+zfgrbv5mKNWCY+ox7E/ZeFLYg==",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"undici": "^8.11.2"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge-daikin-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Matterbridge Daikin Local",
|
|
5
5
|
"author": "https://github.com/cheziyi",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"*.schema.json"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"daikin-local-control": "0.
|
|
56
|
+
"daikin-local-control": "0.5.0",
|
|
57
57
|
"node-ansi-logger": "3.3.1",
|
|
58
58
|
"node-persist-manager": "2.1.1"
|
|
59
59
|
}
|