matterbridge-daikin-local 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ 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
+
7
18
  ## [0.2.0] - 2026-10-13
8
19
 
9
20
  ### Added
package/README.md CHANGED
@@ -201,9 +201,9 @@ The table is read from the README, the settings and the source of matterbridge-d
201
201
 
202
202
  ## How devices are stored
203
203
 
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:
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:
205
205
 
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 stays the same device in Matter, under its new name.
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
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.
208
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.
209
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.
@@ -1,4 +1,4 @@
1
- async function g(e,n,o){try{let[r,a]=e.split(" "),i={method:r};n!==void 0&&(i.headers={"Content-Type":"application/json"},i.body=JSON.stringify(n));let d=await fetch(new URL(`api/${a}${o===void 0?"":`?${o}`}`,document.baseURI),i);if(d.status===204)return{ok:!0};if(d.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let p=await d.json();return"ok"in p?p:{ok:!1,error:p.error??`The plugin answered with status ${d.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}function S(e){let{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:y}=e;return{fanControl:n,fanSpeedWhenHidden:o,swingControl:r,swingModeWhenHidden:a,hasDryModeSwitch:i,hasFanModeSwitch:d,hasFanAutoSwitch:p,hasSeparateFan:h,specialModeSwitches:[...y]}}var ee={fanControl:"steps",fanSpeedWhenHidden:"keep",swingControl:"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!1,hasFanModeSwitch:!1,hasFanAutoSwitch:!1,hasSeparateFan:!1,specialModeSwitches:[]};function q(e){return{adapter:e,name:e.name??"",isNew:!0,...S(ee)}}function I(e){let{capabilities:n,controls:o,name:r,macAddress:a,host:i,protocol:d,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y}=e;return{adapter:{host:i,protocol:d,name:r,macAddress:a,requiresRegistration:p,usesHttps:h,requiresLocalPassword:y},name:r,isNew:!1,capabilities:n,controls:o,...S(e)}}var O={fanSteps:[],hasFanAuto:!1,hasSwing:!1,hasDryMode:!1,hasFanMode:!1,specialModes:[]};var s={screen:"loading",setup:void 0,found:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",confirmingRemoval:""};function f(){return s.queue[0]}function v(e){return e.name===""?e.adapter.host:e.name}function $(e){return(s.setup?.devices??[]).some(n=>n.macAddress===e.macAddress)}var P;function W(e){P=e}function l(){let e=document.getElementById("app");if(e===null||P===void 0)return;let n=`${s.screen}:${f()?.adapter.host??""}`,o=e.dataset.form===n,r=document.activeElement instanceof HTMLElement?document.activeElement.id:"",a=new Map;for(let i of e.querySelectorAll("input[type=text], input[type=password]"))i.id!==""&&a.set(i.id,i.value);if(e.replaceChildren(...P[s.screen]().filter(i=>i!==!1&&i!==null&&i!==void 0)),e.dataset.form=n,!!o){for(let[i,d]of a){let p=document.getElementById(i);p instanceof HTMLInputElement&&(p.value=d)}r!==""&&document.getElementById(r)?.focus()}}async function x(e,n){s.isBusy=!0,s.busyText=e,s.error="",l();try{return await n()}finally{s.isBusy=!1,l()}}async function E(){let e=await g("GET state");if(!e.ok){s.error=e.error,s.screen="home",l();return}if(s.setup=e.state,e.state.devices.length===0){await k();return}s.screen="home",l()}async function T(){let e=await g("GET state");e.ok&&(s.setup=e.state)}function w(){s.queue=[],s.saved=[],s.handled=[],s.error="",s.screen="home",T().then(l),l()}async function k(){s.screen="find";let e=await x("Searching your network. This takes about three seconds\u2026",async()=>g("POST discover"));if(!e.ok){s.error=`The search did not run: ${e.error}`,l();return}await T();let n=s.found.filter(o=>!e.devices.some(r=>r.host===o.host)&&s.selected.has(o.host)).map(o=>({...o,isConfigured:$(o)}));s.found=[...e.devices,...n];for(let o of e.devices)!o.isConfigured&&!o.hasDuplicateName&&s.selected.add(o.host);l()}async function B(e){let n=await x(`Looking for an adapter at ${e}\u2026`,async()=>g("POST identify",{host:e}));if(!n.ok){s.error=n.error,l();return}let o=$(n.device),r=n.device.requiresRegistration===!0;s.found=[...s.found.filter(a=>a.host!==n.device.host),{...n.device,isConfigured:o,hasDuplicateName:!1,needsRegistration:r}],o||s.selected.add(n.device.host),l()}function R(e){s.queue=e,s.saved=[],s.handled=[],N()}async function N(){let e=f();if(e===void 0){s.screen=s.saved.length>0?"done":"home",await T(),l();return}s.screen="connect",s.error="",e.probe=void 0,l(),await M(e)}async function M(e,n){let r={device:{...e.adapter,name:e.name===""?void 0:e.name},localPassword:e.localPassword,key:n},a=n===void 0?`Checking ${v(e)}\u2026`:`Registering with ${v(e)}\u2026`,i=await x(a,async()=>g(n===void 0?"POST probe":"POST register",r));if(!i.ok){s.error=i.error,l();return}if(e.probe=i.result,e.adapter=i.result.device,i.result.status==="ready"){e.capabilities=i.result.capabilities,e.controls=i.result.controls;let d=i.result.device.name??"";d!==""&&(e.name=d),e.name!==""&&(s.screen="options")}l()}function K(){let e=s.queue.shift();e!==void 0&&s.handled.push({label:v(e),isSaved:!1}),N()}async function U(e){let{adapter:n,name:o,localPassword:r}=e,a={...n,name:o,localPassword:r,...S(e)},i=await x(`Saving ${o}\u2026`,async()=>g("POST device",{device:a}));if(!i.ok){s.error=i.error,l();return}s.setup=i.state,s.saved.push(o),s.handled.push({label:o,isSaved:!0}),s.queue.shift(),await N()}async function Y(e){let n=await x(`Removing ${e}\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 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.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([I(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 j(e,n){let o=e==="minimal";return{fanControl:o||n.fanSteps.length===0?"hidden":e==="apple"?"percent":"steps",fanSpeedWhenHidden:"keep",swingControl:o||!n.hasSwing?"hidden":"switches",swingModeWhenHidden:"keep",hasDryModeSwitch:!o&&n.hasDryMode,hasFanModeSwitch:(e==="apple"||e==="full")&&n.hasFanMode,hasFanAutoSwitch:(e==="apple"||e==="full")&&n.hasFanAuto,hasSeparateFan:!1,specialModeSwitches:e==="full"?[...n.specialModes]:[]}}function Q(e,n,o){let r=S(e);return o.fanSteps.length===0&&Object.assign(r,{fanControl:"hidden",fanSpeedWhenHidden:"keep"}),r.fanControl==="hidden"&&(r.hasSeparateFan=!1),o.hasSwing||Object.assign(r,{swingControl:"hidden",swingModeWhenHidden:"keep"}),r.specialModeSwitches=o.specialModes.filter(a=>r.specialModeSwitches.includes(a)),JSON.stringify(r)===JSON.stringify(j(n,o))}var le={auto:"Auto",silent:"Silent",level1:"Level 1",level2:"Level 2",level3:"Level 3",level4:"Level 4",level5:"Level 5"},ce={off:"No swing",vertical:"Up and down",horizontal:"Left and right",both:"Up, down, left and right"},Z={powerful:{title:"Powerful",text:"Runs the unit at full power for a while."},econo:{title:"Econo",text:"Limits how much power the unit draws."},streamer:{title:"Streamer",text:"Runs the air purifying of the unit."}},C={steps:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect x="2" y="20" width="8" height="6"/><rect x="14" y="14" width="8" height="12"/><rect x="26" y="8" width="8" height="18"/><rect x="38" y="2" width="8" height="24"/></svg>',percent:'<svg viewBox="0 0 48 28" aria-hidden="true"><path d="M2 26 L46 2 L46 26 Z"/></svg>',hidden:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M6 14 H42" stroke-dasharray="3 5"/></svg>',switches:'<svg viewBox="0 0 48 28" aria-hidden="true"><path class="stroke" d="M12 4 V24 M7 9 L12 4 L17 9 M7 19 L12 24 L17 19"/><path class="stroke" d="M26 14 H46 M31 9 L26 14 L31 19 M41 9 L46 14 L41 19"/></svg>'};function u(e,n){Object.assign(e,n),l()}function D(e,n,o,r,a,i,d){let p=`${e}-${n}`,h=t("input",{type:"radio",name:e,id:p,checked:n===o,onchange:()=>d(n)});return t("label",{class:"choice pictured",for:p},h,t("span",{class:"glyph",html:i}),t("span",{},t("strong",{},r),t("span",{class:"meta"},a)))}function A(e,n,o,r,a){let i=t("input",{type:"checkbox",id:e,checked:n,onchange:()=>a(i.checked)});return t("label",{class:"choice",for:e},i,t("span",{},t("strong",{},o),t("span",{class:"meta"},r)))}function de(e,n){return t("section",{class:"presets"},t("h2",{},"Start from a preset"),t("div",{class:"preset-list"},...Object.keys(F).map(o=>t("button",{type:"button",class:"preset","aria-pressed":String(Q(e,o,n)),onclick:()=>u(e,j(o,n))},t("strong",{},F[o].title),t("span",{class:"meta"},F[o].text)))),t("p",{class:"help"},"A preset fills in the choices below for what this unit can do. Change any of them afterwards."))}function pe(e,n){let o=e.capabilities?.fanSpeeds??[],r=n.fanSteps.length;if(r===0)return t("section",{},t("h2",{},"Fan speed"),t("p",{class:"help"},"This unit does not let apps set its fan speed, so there is nothing to choose."));let a=n.hasFanAuto&&!(e.fanControl==="hidden"&&e.fanSpeedWhenHidden!=="keep");return t("section",{},t("h2",{},"Fan speed"),t("div",{class:"choices"},D("fan","steps",e.fanControl,"Steps",`${r} speeds, one for each speed of the unit${o.includes("auto")?", and auto":""}.`,C.steps,i=>u(e,{fanControl:i})),D("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed of the unit.",C.percent,i=>u(e,{fanControl:i})),D("fan","hidden",e.fanControl,"Not shown in Matter","No fan control. Pick below what the plugin does with the fan speed instead.",C.hidden,i=>u(e,{fanControl:i}))),e.fanControl==="hidden"?t("div",{class:"field"},t("label",{for:"fan-fixed"},"When the plugin sends a command, the fan speed"),t("select",{id:"fan-fixed",onchange:i=>u(e,{fanSpeedWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.fanSpeedWhenHidden==="keep"},"stays as it is"),...o.map(i=>t("option",{value:i,selected:e.fanSpeedWhenHidden===i},`is set to ${le[i]}`)))):null,e.fanControl==="hidden"?null:A("separate-fan",e.hasSeparateFan,"Show the fan as a device of its own",`It appears as "${e.name} Fan", and the air conditioner has no fan speed of its own. For apps that show no fan speed on an air conditioner. It always shows the speed the unit holds, also while the unit is off.`,i=>u(e,{hasSeparateFan:i})),a?A("fan-auto-switch",e.hasFanAutoSwitch,"Add a switch for auto fan speed",`It appears as "${e.name} Auto Fan". On sets the fan to auto, off puts it back at the speed it last ran at. For apps that show no auto fan mode.`,i=>u(e,{hasFanAutoSwitch:i})):null)}function ue(e){let n=e.capabilities?.swingModes??[];if(n.length===0)return t("section",{},t("h2",{},"Swing"),t("p",{class:"help"},"This unit does not let apps move its louvers, so there is nothing to choose."));let o=n.includes("vertical"),r=n.includes("horizontal"),a="One switch, for the up and down swing.";return o&&r?a="Two switches: one for the up and down swing, one for left and right.":r&&(a="One switch, for the left and right swing."),t("section",{},t("h2",{},"Swing"),t("div",{class:"choices"},D("swing","switches",e.swingControl,"Switches",a,C.switches,i=>u(e,{swingControl:i})),D("swing","hidden",e.swingControl,"Not shown in Matter","No swing control. Pick below what the plugin does with the swing instead.",C.hidden,i=>u(e,{swingControl:i}))),e.swingControl==="hidden"?t("div",{class:"field"},t("label",{for:"swing-fixed"},"When the plugin sends a command, the swing"),t("select",{id:"swing-fixed",onchange:i=>u(e,{swingModeWhenHidden:i.target.value})},t("option",{value:"keep",selected:e.swingModeWhenHidden==="keep"},"stays as it is"),...n.map(i=>t("option",{value:i,selected:e.swingModeWhenHidden===i},`is set to: ${ce[i]}`)))):null)}function he(e,n){if(!n.hasDryMode&&!n.hasFanMode)return null;let o=e.capabilities?.modes??[];return t("section",{},t("h2",{},"Dry and fan modes"),t("p",{class:"help"},`These are always among the modes of the air conditioner in Matter, but some apps, such as Apple Home, only offer cool, heat and auto. A switch makes the mode reachable there: on puts the unit in that mode, off puts it back in ${o.includes("cool")?"cool":"heat"}.`),t("div",{class:"choices"},n.hasDryMode?A("dry-mode-switch",e.hasDryModeSwitch,"Add a switch for dry mode",`It appears as "${e.name} Dry Mode".`,r=>u(e,{hasDryModeSwitch:r})):null,n.hasFanMode?A("fan-mode-switch",e.hasFanModeSwitch,"Add a switch for fan mode",`It appears as "${e.name} Fan Mode".`,r=>u(e,{hasFanModeSwitch:r})):null))}function fe(e,n){return n.specialModes.length===0?null:t("section",{},t("h2",{},"Special modes"),t("p",{class:"help"},"Matter has no control for these, so each can get a switch of its own. A switch only takes effect in the modes this unit offers that special mode in."),t("div",{class:"choices"},...n.specialModes.map(o=>A(`special-${o}`,e.specialModeSwitches.includes(o),`Add a switch for ${Z[o].title}`,Z[o].text,r=>u(e,{specialModeSwitches:n.specialModes.filter(a=>a===o?r:e.specialModeSwitches.includes(a))})))))}function X(){let e=f();if(e===void 0)return[m()];let n=e.controls??O;return[b(),L(),H(e,`Options for the air conditioner at ${e.adapter.host}. Power, mode and temperature always appear in Matter, and so do the humidity and the outdoor temperature once the unit gives them: choose how the rest appears.`),de(e,n),pe(e,n),ue(e),he(e,n),fe(e,n),e.isNew?null:t("p",{class:"help"},"Changing any of these rebuilds this air conditioner in Matter. Your Matter app may take a moment to show the change."),m(),t("div",{class:"footer"},c("Cancel",w),c(e.isNew?`Add ${e.name}`:"Save options",()=>{U(e)},"primary"))]}function _(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let n of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let o=e.getPropertyValue(n).trim();o!==""&&document.documentElement.style.setProperty(`--host${n.slice(1)}`,o)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}W({loading:()=>[t("p",{class:"notice working",role:"status"},t("span",{class:"spinner"}),"Loading\u2026")],home:J,find:z,connect:G,options:X,done:V});_();l();E();
1
+ async function 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 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.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": "AAuCA,eAAsBA,EAAqBC,EAAUC,EAAgBC,EAAmC,CACtG,GAAI,CACF,GAAM,CAACC,EAAQC,CAAI,EAAIJ,EAAM,MAAM,GAAG,EAChCK,EAAoB,CAAE,OAAAF,CAAO,EAC/BF,IAAS,SACXI,EAAK,QAAU,CAAE,eAAgB,kBAAmB,EACpDA,EAAK,KAAO,KAAK,UAAUJ,CAAI,GAEjC,IAAMK,EAAW,MAAM,MAAM,IAAI,IAAI,OAAOF,CAAI,GAAGF,IAAU,OAAY,GAAK,IAAIA,CAAK,EAAE,GAAI,SAAS,OAAO,EAAGG,CAAI,EACpH,GAAIC,EAAS,SAAW,IAAK,MAAO,CAAE,GAAI,EAAK,EAC/C,GAAIA,EAAS,SAAW,IAAK,MAAO,CAAE,GAAI,GAAO,MAAO,6EAA8E,EACtI,IAAMC,EAAS,MAAMD,EAAS,KAAK,EACnC,MAAI,OAAQC,EAAcA,EACnB,CAAE,GAAI,GAAO,MAAOA,EAAM,OAAS,mCAAmCD,EAAS,MAAM,EAAG,CACjG,MAAQ,CACN,MAAO,CAAE,GAAI,GAAO,MAAO,wEAAyE,CACtG,CACF,CC7CO,SAASE,EAAaC,EAA4B,CACvD,GAAM,CAAE,WAAAC,EAAY,mBAAAC,EAAoB,aAAAC,EAAc,oBAAAC,EAAqB,iBAAAC,EAAkB,iBAAAC,EAAkB,iBAAAC,EAAkB,eAAAC,EAAgB,oBAAAC,CAAoB,EAAIT,EACzK,MAAO,CACL,WAAAC,EACA,mBAAAC,EACA,aAAAC,EACA,oBAAAC,EACA,iBAAAC,EACA,iBAAAC,EACA,iBAAAC,EACA,eAAAC,EACA,oBAAqB,CAAC,GAAGC,CAAmB,CAC9C,CACF,CAIA,IAAMC,GAA4B,CAChC,WAAY,QACZ,mBAAoB,OACpB,aAAc,WACd,oBAAqB,OACrB,iBAAkB,GAClB,iBAAkB,GAClB,iBAAkB,GAClB,eAAgB,GAChB,oBAAqB,CAAC,CACxB,EAEO,SAASC,EAAiBC,EAAyB,CACxD,MAAO,CAAE,QAAAA,EAAS,KAAMA,EAAQ,MAAQ,GAAI,MAAO,GAAM,GAAGb,EAAaW,EAAe,CAAE,CAC5F,CAEO,SAASG,EAAgBC,EAA6B,CAC3D,GAAM,CAAE,aAAAC,EAAc,SAAAC,EAAU,KAAAC,EAAM,WAAAC,EAAY,KAAAC,EAAM,SAAAC,EAAU,qBAAAC,EAAsB,UAAAC,EAAW,sBAAAC,CAAsB,EAAIT,EAE7H,MAAO,CAAE,QADgB,CAAE,KAAAK,EAAM,SAAAC,EAAU,KAAAH,EAAM,WAAAC,EAAY,qBAAAG,EAAsB,UAAAC,EAAW,sBAAAC,CAAsB,EAClG,KAAAN,EAAM,MAAO,GAAO,aAAAF,EAAc,SAAAC,EAAU,GAAGjB,EAAae,CAAM,CAAE,CACxF,CAGO,IAAMU,EAAgC,CAAE,SAAU,CAAC,EAAG,WAAY,GAAO,SAAU,GAAO,WAAY,GAAO,WAAY,GAAO,aAAc,CAAC,CAAE,EC5BjJ,IAAMC,EAAO,CAClB,OAAQ,UACR,MAAO,OACP,MAAO,CAAC,EACR,SAAU,IAAI,IACd,MAAO,CAAC,EACR,MAAO,CAAC,EAER,QAAS,CAAC,EACV,OAAQ,GACR,SAAU,GACV,MAAO,GACP,kBAAmB,EACrB,EAEO,SAASC,GAAkC,CAChD,OAAOD,EAAK,MAAM,CAAC,CACrB,CAGO,SAASE,EAAWC,EAAsB,CAC/C,OAAOA,EAAM,OAAS,GAAKA,EAAM,QAAQ,KAAOA,EAAM,IACxD,CAGO,SAASC,EAASC,EAA2B,CAClD,OAAQL,EAAK,OAAO,SAAW,CAAC,GAAG,KAAMM,GAAWA,EAAO,aAAeD,EAAQ,UAAU,CAC9F,CCrCA,IAAIE,EAGG,SAASC,EAAWC,EAAoB,CAC7CF,EAAUE,CACZ,CAEO,SAASC,GAAe,CAC7B,IAAMC,EAAO,SAAS,eAAe,KAAK,EAC1C,GAAIA,IAAS,MAAQJ,IAAY,OAAW,OAE5C,IAAMK,EAAO,GAAGC,EAAK,MAAM,IAAIC,EAAa,GAAG,QAAQ,MAAQ,EAAE,GAC3DC,EAAaJ,EAAK,QAAQ,OAASC,EACnCI,EAAW,SAAS,yBAAyB,YAAc,SAAS,cAAc,GAAK,GACvFC,EAAQ,IAAI,IAClB,QAAWC,KAASP,EAAK,iBAAmC,wCAAwC,EAC9FO,EAAM,KAAO,IAAID,EAAM,IAAIC,EAAM,GAAIA,EAAM,KAAK,EAItD,GAFAP,EAAK,gBAAgB,GAAGJ,EAAQM,EAAK,MAAM,EAAE,EAAE,OAAQM,GAAkCA,IAAU,IAASA,IAAU,MAAQA,IAAU,MAAS,CAAC,EAClJR,EAAK,QAAQ,KAAOC,EAChB,EAACG,EACL,QAAW,CAACK,EAAIC,CAAK,IAAKJ,EAAO,CAC/B,IAAMC,EAAQ,SAAS,eAAeE,CAAE,EACpCF,aAAiB,mBAAkBA,EAAM,MAAQG,EACvD,CACIL,IAAa,IAAI,SAAS,eAAeA,CAAQ,GAAG,MAAM,EAChE,CCpBA,eAAeM,EAAQC,EAAcC,EAAoC,CACvEC,EAAK,OAAS,GACdA,EAAK,SAAWF,EAChBE,EAAK,MAAQ,GACbC,EAAO,EACP,GAAI,CACF,OAAO,MAAMF,EAAK,CACpB,QAAE,CACAC,EAAK,OAAS,GACdC,EAAO,CACT,CACF,CAEA,eAAsBC,GAA2B,CAC/C,IAAMC,EAAQ,MAAMC,EAAI,WAAW,EACnC,GAAI,CAACD,EAAM,GAAI,CACbH,EAAK,MAAQG,EAAM,MACnBH,EAAK,OAAS,OACdC,EAAO,EACP,MACF,CAGA,GAFAD,EAAK,MAAQG,EAAM,MAEfA,EAAM,MAAM,QAAQ,SAAW,EAAG,CACpC,MAAME,EAAO,EACb,MACF,CACAL,EAAK,OAAS,OACdC,EAAO,CACT,CAEA,eAAeK,GAA8B,CAC3C,IAAMH,EAAQ,MAAMC,EAAI,WAAW,EAC/BD,EAAM,KAAIH,EAAK,MAAQG,EAAM,MACnC,CAEO,SAASI,GAAe,CAC7BP,EAAK,MAAQ,CAAC,EACdA,EAAK,MAAQ,CAAC,EACdA,EAAK,QAAU,CAAC,EAChBA,EAAK,MAAQ,GACbA,EAAK,OAAS,OACTM,EAAa,EAAE,KAAKL,CAAM,EAC/BA,EAAO,CACT,CAEA,eAAsBI,GAAwB,CAC5CL,EAAK,OAAS,OACd,IAAMG,EAAQ,MAAMN,EAAK,+DAA2D,SAAYO,EAAI,eAAe,CAAC,EACpH,GAAI,CAACD,EAAM,GAAI,CACbH,EAAK,MAAQ,2BAA2BG,EAAM,KAAK,GACnDF,EAAO,EACP,MACF,CAEA,MAAMK,EAAa,EACnB,IAAME,EAAYR,EAAK,MACpB,OAAQS,GAAY,CAACN,EAAM,QAAQ,KAAMO,GAAWA,EAAO,OAASD,EAAQ,IAAI,GAAKT,EAAK,SAAS,IAAIS,EAAQ,IAAI,CAAC,EACpH,IAAKA,IAAa,CAAE,GAAGA,EAAS,aAAcE,EAASF,CAAO,CAAE,EAAE,EACrET,EAAK,MAAQ,CAAC,GAAGG,EAAM,QAAS,GAAGK,CAAS,EAC5C,QAAWC,KAAWN,EAAM,QACtB,CAACM,EAAQ,cAAgB,CAACA,EAAQ,kBAAkBT,EAAK,SAAS,IAAIS,EAAQ,IAAI,EAExFR,EAAO,CACT,CAEA,eAAsBW,EAAOC,EAA6B,CACxD,IAAMV,EAAQ,MAAMN,EAAK,6BAA6BgB,CAAI,SAAK,SAAYT,EAAI,gBAAiB,CAAE,KAAAS,CAAK,CAAC,CAAC,EACzG,GAAI,CAACV,EAAM,GAAI,CACbH,EAAK,MAAQG,EAAM,MACnBF,EAAO,EACP,MACF,CACA,IAAMa,EAAeH,EAASR,EAAM,MAAM,EAEpCY,EAAoBZ,EAAM,OAAO,uBAAyB,GAChEH,EAAK,MAAQ,CAAC,GAAGA,EAAK,MAAM,OAAQS,GAAYA,EAAQ,OAASN,EAAM,OAAO,IAAI,EAAG,CAAE,GAAGA,EAAM,OAAQ,aAAAW,EAAc,iBAAkB,GAAO,kBAAAC,CAAkB,CAAC,EAC7JD,GAAcd,EAAK,SAAS,IAAIG,EAAM,OAAO,IAAI,EACtDF,EAAO,CACT,CAEO,SAASe,EAAWC,EAAuB,CAChDjB,EAAK,MAAQiB,EACbjB,EAAK,MAAQ,CAAC,EACdA,EAAK,QAAU,CAAC,EACXkB,EAAa,CACpB,CAEA,eAAeA,GAA8B,CAC3C,IAAMC,EAAQC,EAAa,EAC3B,GAAID,IAAU,OAAW,CACvBnB,EAAK,OAASA,EAAK,MAAM,OAAS,EAAI,OAAS,OAC/C,MAAMM,EAAa,EACnBL,EAAO,EACP,MACF,CACAD,EAAK,OAAS,UACdA,EAAK,MAAQ,GACbmB,EAAM,MAAQ,OACdlB,EAAO,EACP,MAAMoB,EAAMF,CAAK,CACnB,CAEA,eAAsBE,EAAMF,EAAcG,EAA6B,CAErE,IAAMC,EAAO,CAAE,OADA,CAAE,GAAGJ,EAAM,QAAS,KAAMA,EAAM,OAAS,GAAK,OAAYA,EAAM,IAAK,EAC7D,cAAeA,EAAM,cAAe,IAAAG,CAAI,EACzDxB,EAAOwB,IAAQ,OAAY,YAAYE,EAAWL,CAAK,CAAC,SAAM,oBAAoBK,EAAWL,CAAK,CAAC,SACnGhB,EAAQ,MAAMN,EAAKC,EAAM,SAAYM,EAAIkB,IAAQ,OAAY,aAAe,gBAAiBC,CAAI,CAAC,EACxG,GAAI,CAACpB,EAAM,GAAI,CACbH,EAAK,MAAQG,EAAM,MACnBF,EAAO,EACP,MACF,CAGA,GAFAkB,EAAM,MAAQhB,EAAM,OACpBgB,EAAM,QAAUhB,EAAM,OAAO,OACzBA,EAAM,OAAO,SAAW,QAAS,CACnCgB,EAAM,aAAehB,EAAM,OAAO,aAClCgB,EAAM,SAAWhB,EAAM,OAAO,SAE9B,IAAMsB,EAAOtB,EAAM,OAAO,OAAO,MAAQ,GACrCsB,IAAS,KAAIN,EAAM,KAAOM,GAC1BN,EAAM,OAAS,KAAInB,EAAK,OAAS,UACvC,CACAC,EAAO,CACT,CAEO,SAASyB,GAAkB,CAChC,IAAMC,EAAU3B,EAAK,MAAM,MAAM,EAC7B2B,IAAY,QAAW3B,EAAK,QAAQ,KAAK,CAAE,MAAOwB,EAAWG,CAAO,EAAG,QAAS,EAAM,CAAC,EACtFT,EAAa,CACpB,CAEA,eAAsBU,EAAUT,EAA6B,CAC3D,GAAM,CAAE,QAAAV,EAAS,KAAAoB,EAAM,cAAAC,CAAc,EAAIX,EACnCT,EAAS,CAAE,GAAGD,EAAS,KAAAoB,EAAM,cAAAC,EAAe,GAAGC,EAAaZ,CAAK,CAAE,EACnEhB,EAAQ,MAAMN,EAAK,UAAUgC,CAAI,SAAK,SAAYzB,EAAI,cAAe,CAAE,OAAAM,CAAO,CAAC,CAAC,EACtF,GAAI,CAACP,EAAM,GAAI,CACbH,EAAK,MAAQG,EAAM,MACnBF,EAAO,EACP,MACF,CACAD,EAAK,MAAQG,EAAM,MACnBH,EAAK,MAAM,KAAK6B,CAAI,EACpB7B,EAAK,QAAQ,KAAK,CAAE,MAAO6B,EAAM,QAAS,EAAK,CAAC,EAChD7B,EAAK,MAAM,MAAM,EACjB,MAAMkB,EAAa,CACrB,CAEA,eAAsBc,EAAaH,EAA6B,CAC9D,IAAM1B,EAAQ,MAAMN,EAAK,YAAYgC,CAAI,SAAK,SAAYzB,EAAI,gBAAiB,OAAW,QAAQ,mBAAmByB,CAAI,CAAC,EAAE,CAAC,EAC7H7B,EAAK,kBAAoB,GACpBG,EAAM,KAAIH,EAAK,MAAQG,EAAM,OAClC,MAAMG,EAAa,EACnBL,EAAO,CACT,CC5JO,SAASgC,EAAyCC,EAAQC,EAAiC,CAAC,KAAMC,EAA6C,CACpJ,IAAMC,EAAU,SAAS,cAAcH,CAAG,EAC1C,OAAW,CAACI,EAAKC,CAAK,IAAK,OAAO,QAAQJ,CAAK,EACzCI,IAAU,QAAaA,IAAU,KACjCD,IAAQ,QAASD,EAAQ,UAAY,OAAOE,CAAK,EAC5CD,IAAQ,OAAQD,EAAQ,UAAY,OAAOE,CAAK,EAChDD,EAAI,WAAW,IAAI,GAAK,OAAOC,GAAU,WAAYF,EAAQ,iBAAiBC,EAAI,MAAM,CAAC,EAAGC,CAAsB,EAClHD,KAAOD,EAAUA,EAA+CC,CAAG,EAAIC,EAC3EF,EAAQ,aAAaC,EAAK,OAAOC,CAAK,CAAC,GAE9C,QAAWC,KAASJ,EACdI,IAAU,IAASA,IAAU,MAAQA,IAAU,QAAWH,EAAQ,OAAOG,CAAK,EAEpF,OAAOH,CACT,CCrBO,SAASI,EAAOC,EAAeC,EAAqBC,EAAuC,QAASC,EAAW,GAA0B,CAC9I,OAAOC,EAAE,SAAU,CAAE,KAAM,SAAU,MAAO,UAAUF,CAAI,GAAI,QAAAD,EAAS,SAAUE,GAAYE,EAAK,MAAO,EAAGL,CAAK,CACnH,CAEO,SAASM,GAAgB,CAC9B,OAAID,EAAK,OAAeD,EAAE,IAAK,CAAE,MAAO,iBAAkB,KAAM,QAAS,EAAGA,EAAE,OAAQ,CAAE,MAAO,SAAU,CAAC,EAAGC,EAAK,QAAQ,EACtHA,EAAK,QAAU,GAAWD,EAAE,IAAK,CAAE,MAAO,iBAAkB,KAAM,OAAQ,EAAGC,EAAK,KAAK,EACpF,IACT,CAMO,SAASE,GAAmB,CACjC,IAAMC,EAAUC,EAAa,EAC7B,GAAID,IAAY,QAAa,CAACA,EAAQ,OAASH,EAAK,QAAQ,OAASA,EAAK,MAAM,OAAS,EAAG,OAAO,KACnG,IAAMK,EAAQL,EAAK,SAAW,UAAY,aAAe,mBACzD,OAAOD,EACL,KACA,CAAE,MAAS,QAAS,aAAc,gCAAiC,EACnE,GAAGC,EAAK,QAAQ,IAAKM,GAASP,EAAE,KAAM,CAAE,MAAOO,EAAK,QAAU,OAAS,SAAU,EAAGP,EAAE,SAAU,CAAC,EAAGO,EAAK,KAAK,EAAGP,EAAE,OAAQ,CAAE,MAAO,MAAO,EAAGO,EAAK,QAAU,QAAU,SAAS,CAAC,CAAC,EAClL,GAAGN,EAAK,MAAM,IAAI,CAACO,EAAOC,IACxBT,EAAE,KAAM,CAAE,MAASS,IAAU,EAAI,UAAY,QAAS,eAAgBA,IAAU,EAAI,OAAS,MAAU,EAAGT,EAAE,SAAU,CAAC,EAAGU,EAAWF,CAAK,CAAC,EAAGR,EAAE,OAAQ,CAAE,MAAO,MAAO,EAAGS,IAAU,EAAIH,EAAQ,SAAS,CAAC,CAC7M,CACF,CACF,CAGO,SAASK,EAAWH,EAAcI,EAA2B,CAClE,OAAOZ,EAAE,SAAU,CAAE,MAAO,aAAc,EAAGA,EAAE,KAAM,CAAC,EAAGU,EAAWF,CAAK,CAAC,EAAGR,EAAE,IAAK,CAAE,MAAO,MAAO,EAAGY,CAAI,CAAC,CAC9G,CAEO,SAASC,GAAe,CAC7B,GAAIZ,EAAK,MAAM,OAAS,GAAK,CAACA,EAAK,MAAM,KAAMO,GAAUA,EAAM,KAAK,EAAG,OAAO,KAC9E,IAAMM,EAAQ,CAAC,OAAQ,UAAW,SAAS,EAErCV,EADwB,CAAC,OAAQ,UAAW,SAAS,EACnC,QAAQH,EAAK,MAAM,EAC3C,OAAOD,EACL,KACA,CAAE,MAAO,OAAQ,EACjB,GAAGc,EAAM,IAAI,CAACC,EAAMN,IAAU,CAC5B,IAAIO,EAAQ,QACZ,OAAIP,EAAQL,EAASY,EAAQ,OACpBP,IAAUL,IAASY,EAAQ,WAC7BhB,EAAE,KAAM,CAAE,MAASgB,EAAO,eAAgBP,IAAUL,EAAU,OAAS,MAAU,EAAGW,CAAI,CACjG,CAAC,CACH,CACF,CC5CA,SAASE,GAASC,EAA2B,CAC3C,IAAMC,EAAOC,EAAE,QAAS,CAAE,KAAM,OAAQ,GAAI,cAAe,UAAW,GAAI,aAAc,KAAM,CAAC,EACzFC,EAAS,IAAY,CACzB,IAAMC,EAAQH,EAAK,MAAM,KAAK,EAC9B,GAAIG,IAAU,GACd,IAAIC,EAAK,OAAO,QAAQ,KAAMC,GAAWA,EAAO,OAASF,CAAK,EAAG,CAC/DC,EAAK,MAAQ,IAAID,CAAK,0EACtBG,EAAO,EACP,MACF,CACAP,EAAM,KAAOI,EACbC,EAAK,MAAQ,GACbA,EAAK,OAAS,UACdE,EAAO,EACT,EACA,OAAOL,EACL,MACA,CAAE,MAAO,OAAQ,EACjBA,EAAE,QAAS,CAAE,IAAK,aAAc,EAAG,2BAA2B,EAC9DA,EACE,IACA,CAAE,MAAO,MAAO,EAChB,+KACF,EACAA,EAAE,MAAO,CAAE,MAAO,aAAc,EAAGD,EAAMO,EAAO,WAAYL,EAAQ,SAAS,CAAC,CAChF,CACF,CAEA,SAASM,GAAaT,EAAcU,EAAkC,CACpE,IAAMC,EAAOT,EAAE,QAAS,CAAE,KAAM,WAAY,GAAI,YAAa,UAAW,UAAW,aAAc,MAAO,UAAW,EAAG,CAAC,EACvH,OAAOA,EACL,MACA,CAAE,MAAO,OAAQ,EACjBA,EAAE,QAAS,CAAE,IAAK,WAAY,EAAG,iBAAiB,EAClDA,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,wHAAwH,EAClJQ,EAAaR,EAAE,IAAK,CAAE,MAAO,cAAe,EAAG,uCAAuC,EAAI,KAC1FA,EACE,MACA,CAAE,MAAO,aAAc,EACvBS,EACAH,EACE,iBACA,IAAM,CACAG,EAAK,QAAU,KACnBX,EAAM,cAAgBW,EAAK,MACtBC,EAAMZ,CAAK,EAClB,EACA,SACF,CACF,CACF,CACF,CAEA,SAASa,GAAQb,EAAcU,EAAkC,CAC/D,IAAMI,EAAMZ,EAAE,QAAS,CAAE,KAAM,OAAQ,GAAI,cAAe,aAAc,MAAO,WAAY,GAAO,UAAW,EAAG,CAAC,EACjH,OAAOA,EACL,MACA,CAAE,MAAO,OAAQ,EACjBA,EAAE,QAAS,CAAE,IAAK,aAAc,EAAG,oBAAoB,EACvDA,EACE,IACA,CAAE,MAAO,MAAO,EAChB,mKACF,EACAQ,EAAaR,EAAE,IAAK,CAAE,MAAO,cAAe,EAAG,sCAAsC,EAAI,KACzFA,EACE,MACA,CAAE,MAAO,aAAc,EACvBY,EACAN,EACE,WACA,IAAM,CACAM,EAAI,MAAM,KAAK,IAAM,IAASF,EAAMZ,EAAOc,EAAI,MAAM,KAAK,CAAC,CACjE,EACA,SACF,CACF,CACF,CACF,CAGA,SAASC,GAAef,EAAqB,CAC3C,IAAMgB,EAAShB,EAAM,OAAO,OAC5B,OAAIgB,IAAW,SAAWhB,EAAM,OAAS,GAAWD,GAASC,CAAK,EAC9DgB,IAAW,yBAA2BA,IAAW,wBAAgCP,GAAaT,EAAOgB,IAAW,uBAAuB,EACvIA,IAAW,wBAA0BA,IAAW,uBAA+BH,GAAQb,EAAOgB,IAAW,sBAAsB,EAC/HA,IAAW,cACNd,EAAE,IAAK,CAAE,MAAO,iBAAkB,KAAM,OAAQ,EAAG,GAAGF,EAAM,IAAI,sBAAsBA,EAAM,QAAQ,IAAI,kEAAkE,EAE/KgB,IAAW,SAAiBd,EAAE,IAAK,CAAE,MAAO,iBAAkB,KAAM,OAAQ,EAAG,GAAGF,EAAM,IAAI,4BAA4BA,EAAM,OAAO,SAAW,SAAS,EAAE,EACxJ,IACT,CAEO,SAASiB,GAAyB,CACvC,IAAMjB,EAAQkB,EAAa,EAC3B,GAAIlB,IAAU,OAAW,MAAO,CAACmB,EAAO,CAAC,EACzC,IAAMC,EAAWpB,EAAM,OAAO,SAAW,eAAiBA,EAAM,OAAO,SAAW,UAAaK,EAAK,QAAU,IAAML,EAAM,OAAS,GACnI,MAAO,CACLqB,EAAM,EACNC,EAAU,EACVC,EAAWvB,EAAO,gCAAgCA,EAAM,QAAQ,IAAI,GAAG,EACvEmB,EAAO,EACPd,EAAK,OAAS,KAAOU,GAAef,CAAK,EACzCE,EACE,MACA,CAAE,MAAO,QAAS,EAClBkB,EAAWZ,EAAO,YAAa,IAAG,CAAQI,EAAMZ,CAAK,EAAC,EAAI,KAC1DQ,EAAOR,EAAM,OAASK,EAAK,MAAM,OAAS,EAAI,gBAAkB,SAAUL,EAAM,OAASK,EAAK,MAAM,OAAS,EAAImB,EAAYC,CAAM,CACrI,CACF,CACF,CChHO,SAASC,GAAsB,CACpC,IAAMC,EAAQC,EAAK,MACnB,MAAO,CACLC,EAAE,SAAU,CAAC,EAAGA,EAAE,KAAM,CAAC,EAAGF,EAAM,SAAW,EAAI,GAAGA,EAAM,CAAC,CAAC,aAAe,GAAGA,EAAM,MAAM,8BAA8B,CAAC,EACzHE,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,6HAA6H,EACvJA,EAAE,MAAO,CAAE,MAAO,QAAS,EAAGC,EAAO,OAAQC,EAAQ,SAAS,CAAC,CACjE,CACF,CCJA,SAASC,GAASC,EAAoC,CACpD,IAAMC,EAAe,CAACD,EAAQ,cAAgB,CAACA,EAAQ,iBACjDE,EAAkB,CAAC,EACrBF,EAAQ,cAAcE,EAAM,KAAK,iBAAiB,EAClDF,EAAQ,kBAAkBE,EAAM,KAAK,iGAAiG,EACtIF,EAAQ,OAAS,QAAWE,EAAM,KAAK,kFAAkF,EACzHD,GAAgBD,EAAQ,kBAAmBE,EAAM,KAAK,wCAAwC,EACzFD,GAAgBD,EAAQ,sBAAsBE,EAAM,KAAK,sEAAsE,EACpID,GAAgBD,EAAQ,uBAAuBE,EAAM,KAAK,+BAA+B,EAC7F,IAAMC,EAAWC,EAAE,QAAS,CAC1B,KAAM,WACN,GAAI,SAASJ,EAAQ,IAAI,GACzB,QAASC,GAAgBI,EAAK,SAAS,IAAIL,EAAQ,IAAI,EACvD,SAAU,CAACC,EACX,SAAU,IAAM,CACVE,EAAS,QAASE,EAAK,SAAS,IAAIL,EAAQ,IAAI,EAC/CK,EAAK,SAAS,OAAOL,EAAQ,IAAI,EACtCM,EAAO,CACT,CACF,CAAC,EACD,OAAOF,EACL,KACA,CAAE,MAAOH,EAAe,QAAU,mBAAoB,EACtDG,EACE,QACA,CAAE,IAAK,SAASJ,EAAQ,IAAI,EAAG,EAC/BG,EACAC,EAAE,OAAQ,CAAC,EAAGA,EAAE,SAAU,CAAC,EAAGJ,EAAQ,MAAQ,iBAAiB,EAAGI,EAAE,OAAQ,CAAE,MAAO,MAAO,EAAG,IAAIJ,EAAQ,IAAI,EAAE,EAAG,GAAGE,EAAM,IAAKK,GAASH,EAAE,OAAQ,CAAE,MAAO,WAAY,EAAGG,CAAI,CAAC,CAAC,CACrL,CACF,CACF,CAEO,SAASC,GAAsB,CACpC,IAAMC,EAASJ,EAAK,MAAM,OAAQL,GAAYK,EAAK,SAAS,IAAIL,EAAQ,IAAI,GAAK,CAACA,EAAQ,cAAgB,CAACA,EAAQ,gBAAgB,EAC7HU,EAAON,EAAE,QAAS,CAAE,KAAM,OAAQ,GAAI,OAAQ,YAAa,eAAgB,WAAY,GAAO,aAAc,KAAM,CAAC,EACnHO,EAAa,IAAY,CACzBD,EAAK,MAAM,KAAK,IAAM,IAASE,EAAOF,EAAK,MAAM,KAAK,CAAC,CAC7D,EACAA,EAAK,iBAAiB,UAAYG,GAAU,CACtCA,EAAM,MAAQ,SAASF,EAAW,CACxC,CAAC,EACD,IAAMG,EAAgBL,EAAO,SAAW,EAAI,2BAA6B,UAAUA,EAAO,MAAM,oBAChG,MAAO,CACLM,EAAM,EACNX,EAAE,SAAU,CAAC,EAAGA,EAAE,KAAM,CAAC,EAAG,4BAA4B,CAAC,EACzDY,EAAO,EACPX,EAAK,MAAM,OAAS,EAAID,EAAE,KAAM,CAAE,MAAO,YAAa,EAAG,GAAGC,EAAK,MAAM,IAAIN,EAAQ,CAAC,EAAI,KACxF,CAACM,EAAK,QAAUA,EAAK,MAAM,SAAW,EAClCD,EAAE,IAAK,CAAE,MAAO,OAAQ,EAAG,mIAAmI,EAC9J,KACJA,EACE,UACA,CAAE,MAAO,SAAU,KAAM,CAACC,EAAK,QAAUA,EAAK,MAAM,SAAW,CAAE,EACjED,EAAE,UAAW,CAAC,EAAG,iCAAiC,EAClDA,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,6NAA6N,EACvPA,EAAE,MAAO,CAAE,MAAO,aAAc,EAAGA,EAAE,QAAS,CAAE,IAAK,OAAQ,MAAO,iBAAkB,EAAG,iBAAiB,EAAGM,EAAMO,EAAO,aAAcN,CAAU,CAAC,CACrJ,EACAP,EACE,MACA,CAAE,MAAO,QAAS,EAClBa,EAAO,eAAgB,IAAG,CAAQC,EAAO,EAAC,EAC1CD,EAAO,SAAUE,CAAM,EACvBF,EAAOH,EAAe,IAAMM,EAAWX,EAAO,IAAIY,CAAgB,CAAC,EAAG,UAAWZ,EAAO,SAAW,CAAC,CACtG,CACF,CACF,CCjEA,SAASa,GAAiBC,EAA8B,CAEtD,IAAMC,EAASD,EAAO,WAAa,QAAaA,EAAO,SAAS,SAAS,OAAS,EAC5EE,EAAWF,EAAO,WAAa,QAAaA,EAAO,SAAS,SAC5DG,EAAWH,EAAO,eAAiB,uBAAyB,GAC5DI,EAAMJ,EAAO,aAAe,SAAW,sBAAwB,gBAAgBA,EAAO,aAAe,QAAU,QAAU,cAAc,GAAGG,CAAQ,GAClJE,EAAQL,EAAO,eAAiB,SAAW,kBAAoB,oBAC/DM,EAAW,CAACL,EAASG,EAAM,GAAIF,EAAWG,EAAQ,EAAE,EAAE,OAAQE,GAASA,IAAS,EAAE,EAAE,KAAK,IAAI,EAC7FC,EAAW,CAACR,EAAO,iBAAmB,WAAa,GAAIA,EAAO,iBAAmB,WAAa,GAAIA,EAAO,iBAAmB,WAAa,GAAI,GAAGA,EAAO,mBAAmB,EAAE,OAC/KS,GAASA,IAAS,EACrB,EACMC,EAAQF,EAAS,SAAW,EAAI,GAAK,gBAAgBA,EAAS,KAAK,IAAI,CAAC,GAC9E,OAAIF,IAAa,GAAWI,IAAU,GAAK,8BAAgCA,EACpEA,IAAU,GAAKJ,EAAW,GAAGA,CAAQ,SAASI,CAAK,EAC5D,CAEA,SAASC,GAAUX,EAAmC,CACpD,IAAMY,EAAa,CAAE,OAAQ,SAAU,QAAS,gBAAiB,QAAS,aAAc,EAAEZ,EAAO,MAAM,EACjGa,EAAeC,EAAK,oBAAsBd,EAAO,KACvD,OAAOe,EACL,KACA,CAAE,MAAO,QAAS,EAClBA,EACE,MACA,CAAE,MAAO,aAAc,EACvBA,EAAE,KAAM,CAAC,EAAGf,EAAO,IAAI,EACvBe,EAAE,IAAK,CAAE,MAAO,MAAO,EAAGA,EAAE,OAAQ,CAAE,MAAO,UAAUf,EAAO,MAAM,EAAG,EAAGY,CAAU,EAAG,OAAOZ,EAAO,IAAI,gBAAgBD,GAAiBC,CAAM,CAAC,GAAG,EACpJA,EAAO,SAAW,UAAYA,EAAO,MAAQe,EAAE,IAAK,CAAE,MAAO,mBAAoB,EAAGf,EAAO,KAAK,EAAI,IACtG,EACAa,EACIE,EACE,MACA,CAAE,MAAO,SAAU,EACnBA,EAAE,OAAQ,CAAE,MAAO,MAAO,EAAG,4BAA4B,EACzDC,EAAO,SAAU,IAAG,CAAQC,EAAajB,EAAO,IAAI,GAAG,QAAQ,EAC/DgB,EAAO,OAAQ,IAAM,CACnBF,EAAK,kBAAoB,GACzBI,EAAO,CACT,CAAC,CACH,EACAH,EACE,MACA,CAAE,MAAO,SAAU,EAEnBC,EAAOhB,EAAO,SAAW,SAAW,UAAY,YAAa,IAAMmB,EAAW,CAACC,EAAgBpB,CAAM,CAAC,CAAC,CAAC,EACxGgB,EAAO,SAAU,IAAM,CACrBF,EAAK,kBAAoBd,EAAO,KAChCkB,EAAO,CACT,CAAC,CACH,CACN,CACF,CAEO,SAASG,GAAsB,CACpC,IAAMC,EAAUR,EAAK,OAAO,SAAW,CAAC,EACxC,MAAO,CACLC,EAAE,SAAU,CAAC,EAAGA,EAAE,KAAM,CAAC,EAAG,yBAAyB,EAAGD,EAAK,MAAQE,EAAO,uBAAwB,IAAG,CAAQO,EAAO,GAAG,SAAS,EAAI,IAAI,EAC1IC,EAAO,EACPF,EAAQ,SAAW,GAAKR,EAAK,MACzBC,EAAE,IAAK,CAAE,MAAO,OAAQ,EAAG,sEAAsE,EACjGA,EAAE,KAAM,CAAE,MAAO,SAAU,EAAG,GAAGO,EAAQ,IAAIX,EAAS,CAAC,EAC3DG,EAAK,MAAQ,KAAOE,EAAO,YAAa,IAAG,CAAQS,EAAU,GAAG,SAAS,CAC3E,CACF,CCjEO,IAAMC,EAA+D,CAC1E,MAAO,CAAE,MAAO,aAAc,KAAM,yHAA0H,EAC9J,OAAQ,CAAE,MAAO,cAAe,KAAM,2DAA4D,EAClG,KAAM,CAAE,MAAO,OAAQ,KAAM,4EAA6E,EAC1G,QAAS,CAAE,MAAO,UAAW,KAAM,2FAA4F,CACjI,EAGO,SAASC,EAAeC,EAAkBC,EAAuC,CACtF,IAAMC,EAAYF,IAAS,UAC3B,MAAO,CACL,WAAYE,GAAaD,EAAS,SAAS,SAAW,EAAI,SAAWD,IAAS,QAAU,UAAY,QACpG,mBAAoB,OACpB,aAAcE,GAAa,CAACD,EAAS,SAAW,SAAW,WAC3D,oBAAqB,OACrB,iBAAkB,CAACC,GAAaD,EAAS,WACzC,kBAAmBD,IAAS,SAAWA,IAAS,SAAWC,EAAS,WACpE,kBAAmBD,IAAS,SAAWA,IAAS,SAAWC,EAAS,WAEpE,eAAgB,GAChB,oBAAqBD,IAAS,OAAS,CAAC,GAAGC,EAAS,YAAY,EAAI,CAAC,CACvE,CACF,CAGO,SAASE,EAAcC,EAAcJ,EAAkBC,EAAsC,CAClG,IAAMI,EAASC,EAAaF,CAAK,EACjC,OAAIH,EAAS,SAAS,SAAW,GAAG,OAAO,OAAOI,EAAQ,CAAE,WAAY,SAAU,mBAAoB,MAAO,CAAC,EAE1GA,EAAO,aAAe,WAAUA,EAAO,eAAiB,IACvDJ,EAAS,UAAU,OAAO,OAAOI,EAAQ,CAAE,aAAc,SAAU,oBAAqB,MAAO,CAAC,EACrGA,EAAO,oBAAsBJ,EAAS,aAAa,OAAQM,GAASF,EAAO,oBAAoB,SAASE,CAAI,CAAC,EACtG,KAAK,UAAUF,CAAM,IAAM,KAAK,UAAUN,EAAeC,EAAMC,CAAQ,CAAC,CACjF,CC9BA,IAAMO,GAA2C,CAC/C,KAAM,OACN,OAAQ,SACR,OAAQ,UACR,OAAQ,UACR,OAAQ,UACR,OAAQ,UACR,OAAQ,SACV,EAEMC,GAA6C,CACjD,IAAK,WACL,SAAU,cACV,WAAY,iBACZ,KAAM,0BACR,EAEMC,EAAyE,CAC7E,SAAU,CAAE,MAAO,WAAY,KAAM,0CAA2C,EAChF,MAAO,CAAE,MAAO,QAAS,KAAM,uCAAwC,EACvE,SAAU,CAAE,MAAO,WAAY,KAAM,qCAAsC,CAC7E,EAGMC,EAAS,CACb,MACE,6NACF,QAAS,qFACT,OAAQ,gHACR,SACE,0MACJ,EAEA,SAASC,EAAOC,EAAcC,EAAiC,CAC7D,OAAO,OAAOD,EAAOC,CAAM,EAC3BC,EAAO,CACT,CAEA,SAASC,EAAyBC,EAAeC,EAAUC,EAAYC,EAAeC,EAAcC,EAAeC,EAAyC,CAC1J,IAAMC,EAAK,GAAGP,CAAK,IAAIC,CAAK,GACtBO,EAAQC,EAAE,QAAS,CAAE,KAAM,QAAS,KAAMT,EAAO,GAAAO,EAAI,QAASN,IAAUC,EAAS,SAAU,IAAMI,EAAOL,CAAK,CAAE,CAAC,EACtH,OAAOQ,EAAE,QAAS,CAAE,MAAO,kBAAmB,IAAKF,CAAG,EAAGC,EAAOC,EAAE,OAAQ,CAAE,MAAO,QAAS,KAAMJ,CAAM,CAAC,EAAGI,EAAE,OAAQ,CAAC,EAAGA,EAAE,SAAU,CAAC,EAAGN,CAAK,EAAGM,EAAE,OAAQ,CAAE,MAAO,MAAO,EAAGL,CAAI,CAAC,CAAC,CACvL,CAEA,SAASM,EAAOH,EAAYI,EAAeR,EAAeC,EAAcE,EAA8C,CACpH,IAAME,EAAQC,EAAE,QAAS,CAAE,KAAM,WAAY,GAAAF,EAAI,QAASI,EAAM,SAAU,IAAML,EAAOE,EAAM,OAAO,CAAE,CAAC,EACvG,OAAOC,EAAE,QAAS,CAAE,MAAO,SAAU,IAAKF,CAAG,EAAGC,EAAOC,EAAE,OAAQ,CAAC,EAAGA,EAAE,SAAU,CAAC,EAAGN,CAAK,EAAGM,EAAE,OAAQ,CAAE,MAAO,MAAO,EAAGL,CAAI,CAAC,CAAC,CAClI,CAEA,SAASQ,GAAchB,EAAciB,EAA0C,CAC7E,OAAOJ,EACL,UACA,CAAE,MAAO,SAAU,EACnBA,EAAE,KAAM,CAAC,EAAG,qBAAqB,EACjCA,EACE,MACA,CAAE,MAAO,aAAc,EACvB,GAAI,OAAO,KAAKK,CAAO,EAAmB,IAAKC,GAC7CN,EACE,SACA,CAAE,KAAQ,SAAU,MAAS,SAAU,eAAgB,OAAOO,EAAcpB,EAAOmB,EAAMF,CAAQ,CAAC,EAAG,QAAW,IAAMlB,EAAOC,EAAOqB,EAAeF,EAAMF,CAAQ,CAAC,CAAE,EACpKJ,EAAE,SAAU,CAAC,EAAGK,EAAQC,CAAI,EAAE,KAAK,EACnCN,EAAE,OAAQ,CAAE,MAAO,MAAO,EAAGK,EAAQC,CAAI,EAAE,IAAI,CACjD,CACF,CACF,EACAN,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,+FAA+F,CAC3H,CACF,CAEA,SAASS,GAAWtB,EAAciB,EAA0C,CAC1E,IAAMM,EAAYvB,EAAM,cAAc,WAAa,CAAC,EAC9CwB,EAAYP,EAAS,SAAS,OACpC,GAAIO,IAAc,EAAG,OAAOX,EAAE,UAAW,CAAC,EAAGA,EAAE,KAAM,CAAC,EAAG,WAAW,EAAGA,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,+EAA+E,CAAC,EAEjL,IAAMY,EAAmBR,EAAS,YAAc,EAAEjB,EAAM,aAAe,UAAYA,EAAM,qBAAuB,QAChH,OAAOa,EACL,UACA,CAAC,EACDA,EAAE,KAAM,CAAC,EAAG,WAAW,EACvBA,EACE,MACA,CAAE,MAAO,SAAU,EACnBV,EAAO,MAAO,QAASH,EAAM,WAAY,QAAS,GAAGwB,CAAS,0CAA0CD,EAAU,SAAS,MAAM,EAAI,aAAe,EAAE,IAAKzB,EAAO,MAAQ4B,GACxK3B,EAAOC,EAAO,CAAE,WAAA0B,CAAW,CAAC,CAC9B,EACAvB,EAAO,MAAO,UAAWH,EAAM,WAAY,aAAc,oFAAqFF,EAAO,QAAU4B,GAAe3B,EAAOC,EAAO,CAAE,WAAA0B,CAAW,CAAC,CAAC,EAC3MvB,EAAO,MAAO,SAAUH,EAAM,WAAY,sBAAuB,8EAA+EF,EAAO,OAAS4B,GAC9J3B,EAAOC,EAAO,CAAE,WAAA0B,CAAW,CAAC,CAC9B,CACF,EACA1B,EAAM,aAAe,SACjBa,EACE,MACA,CAAE,MAAO,OAAQ,EACjBA,EAAE,QAAS,CAAE,IAAK,WAAY,EAAG,gDAAgD,EACjFA,EACE,SACA,CAAE,GAAI,YAAa,SAAWc,GAAiB5B,EAAOC,EAAO,CAAE,mBAAqB2B,EAAM,OAA6B,KAAwC,CAAC,CAAE,EAClKd,EAAE,SAAU,CAAE,MAAO,OAAQ,SAAUb,EAAM,qBAAuB,MAAO,EAAG,gBAAgB,EAC9F,GAAGuB,EAAU,IAAKK,GAAUf,EAAE,SAAU,CAAE,MAAOe,EAAO,SAAU5B,EAAM,qBAAuB4B,CAAM,EAAG,aAAajC,GAAeiC,CAAK,CAAC,EAAE,CAAC,CAC/I,CACF,EACA,KACJ5B,EAAM,aAAe,SACjB,KACAc,EACE,eACAd,EAAM,eACN,sCACA,kBAAkBA,EAAM,IAAI,2LAC3B6B,GAAmB9B,EAAOC,EAAO,CAAE,eAAA6B,CAAe,CAAC,CACtD,EACJJ,EACIX,EACE,kBACAd,EAAM,iBACN,kCACA,kBAAkBA,EAAM,IAAI,0HAC3B8B,GAAqB/B,EAAOC,EAAO,CAAE,iBAAA8B,CAAiB,CAAC,CAC1D,EACA,IACN,CACF,CAEA,SAASC,GAAa/B,EAA2B,CAC/C,IAAMgC,EAAahC,EAAM,cAAc,YAAc,CAAC,EACtD,GAAIgC,EAAW,SAAW,EAAG,OAAOnB,EAAE,UAAW,CAAC,EAAGA,EAAE,KAAM,CAAC,EAAG,OAAO,EAAGA,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,8EAA8E,CAAC,EACpL,IAAMoB,EAAcD,EAAW,SAAS,UAAU,EAC5CE,EAAgBF,EAAW,SAAS,YAAY,EAClDG,EAAa,yCACjB,OAAIF,GAAeC,EAAeC,EAAa,uEACtCD,IAAeC,EAAa,6CAC9BtB,EACL,UACA,CAAC,EACDA,EAAE,KAAM,CAAC,EAAG,OAAO,EACnBA,EACE,MACA,CAAE,MAAO,SAAU,EACnBV,EAAO,QAAS,WAAYH,EAAM,aAAc,WAAYmC,EAAYrC,EAAO,SAAWsC,GAAiBrC,EAAOC,EAAO,CAAE,aAAAoC,CAAa,CAAC,CAAC,EAC1IjC,EAAO,QAAS,SAAUH,EAAM,aAAc,sBAAuB,4EAA6EF,EAAO,OAASsC,GAChKrC,EAAOC,EAAO,CAAE,aAAAoC,CAAa,CAAC,CAChC,CACF,EACApC,EAAM,eAAiB,SACnBa,EACE,MACA,CAAE,MAAO,OAAQ,EACjBA,EAAE,QAAS,CAAE,IAAK,aAAc,EAAG,4CAA4C,EAC/EA,EACE,SACA,CAAE,GAAI,cAAe,SAAWc,GAAiB5B,EAAOC,EAAO,CAAE,oBAAsB2B,EAAM,OAA6B,KAAyC,CAAC,CAAE,EACtKd,EAAE,SAAU,CAAE,MAAO,OAAQ,SAAUb,EAAM,sBAAwB,MAAO,EAAG,gBAAgB,EAC/F,GAAGgC,EAAW,IAAKK,GAASxB,EAAE,SAAU,CAAE,MAAOwB,EAAM,SAAUrC,EAAM,sBAAwBqC,CAAK,EAAG,cAAczC,GAAgByC,CAAI,CAAC,EAAE,CAAC,CAC/I,CACF,EACA,IACN,CACF,CAEA,SAASC,GAAYtC,EAAciB,EAAoC,CACrE,GAAI,CAACA,EAAS,YAAc,CAACA,EAAS,WAAY,OAAO,KACzD,IAAMsB,EAAQvC,EAAM,cAAc,OAAS,CAAC,EAC5C,OAAOa,EACL,UACA,CAAC,EACDA,EAAE,KAAM,CAAC,EAAG,mBAAmB,EAC/BA,EACE,IACA,CAAE,MAAO,MAAO,EAChB,qOAAqO0B,EAAM,SAAS,MAAM,EAAI,OAAS,MAAM,GAC/Q,EACA1B,EACE,MACA,CAAE,MAAO,SAAU,EACnBI,EAAS,WACLH,EAAO,kBAAmBd,EAAM,iBAAkB,4BAA6B,kBAAkBA,EAAM,IAAI,cAAgBwC,GAAqBzC,EAAOC,EAAO,CAAE,iBAAAwC,CAAiB,CAAC,CAAC,EACnL,KACJvB,EAAS,WACLH,EAAO,kBAAmBd,EAAM,iBAAkB,4BAA6B,kBAAkBA,EAAM,IAAI,cAAgByC,GAAqB1C,EAAOC,EAAO,CAAE,iBAAAyC,CAAiB,CAAC,CAAC,EACnL,IACN,CACF,CACF,CAEA,SAASC,GAAe1C,EAAciB,EAAoC,CACxE,OAAIA,EAAS,aAAa,SAAW,EAAU,KACxCJ,EACL,UACA,CAAC,EACDA,EAAE,KAAM,CAAC,EAAG,eAAe,EAC3BA,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,sJAAsJ,EAChLA,EACE,MACA,CAAE,MAAO,SAAU,EACnB,GAAGI,EAAS,aAAa,IAAKoB,GAC5BvB,EAAO,WAAWuB,CAAI,GAAIrC,EAAM,oBAAoB,SAASqC,CAAI,EAAG,oBAAoBxC,EAAiBwC,CAAI,EAAE,KAAK,GAAIxC,EAAiBwC,CAAI,EAAE,KAAOtB,GACpJhB,EAAOC,EAAO,CAAE,oBAAqBiB,EAAS,aAAa,OAAQ0B,GAAWA,IAAUN,EAAOtB,EAAOf,EAAM,oBAAoB,SAAS2C,CAAK,CAAE,CAAE,CAAC,CACrJ,CACF,CACF,CACF,CACF,CAEO,SAASC,GAAyB,CACvC,IAAM5C,EAAQ6C,EAAa,EAC3B,GAAI7C,IAAU,OAAW,MAAO,CAAC8C,EAAO,CAAC,EACzC,IAAM7B,EAAWjB,EAAM,UAAY+C,EACnC,MAAO,CACLC,EAAM,EACNC,EAAU,EACVC,EAAWlD,EAAO,sCAAsCA,EAAM,QAAQ,IAAI,kKAAkK,EAC5OgB,GAAchB,EAAOiB,CAAQ,EAC7BK,GAAWtB,EAAOiB,CAAQ,EAC1Bc,GAAa/B,CAAK,EAClBsC,GAAYtC,EAAOiB,CAAQ,EAC3ByB,GAAe1C,EAAOiB,CAAQ,EAC9BjB,EAAM,MAAQ,KAAOa,EAAE,IAAK,CAAE,MAAO,MAAO,EAAG,sHAAsH,EACrKiC,EAAO,EACPjC,EAAE,MAAO,CAAE,MAAO,QAAS,EAAGsC,EAAO,SAAUC,CAAM,EAAGD,EAAOnD,EAAM,MAAQ,OAAOA,EAAM,IAAI,GAAK,eAAgB,IAAG,CAAQqD,EAAUrD,CAAK,GAAG,SAAS,CAAC,CAC5J,CACF,CCtOO,SAASsD,GAAuB,CACrC,GAAI,CACF,GAAI,OAAO,SAAW,OAAQ,OAC9B,IAAMC,EAAO,iBAAiB,OAAO,OAAO,SAAS,IAAI,EACzD,QAAWC,IAAQ,CAAC,kBAAmB,iBAAkB,mBAAoB,kBAAmB,qBAAsB,qBAAsB,oBAAoB,EAAG,CACjK,IAAMC,EAAQF,EAAK,iBAAiBC,CAAI,EAAE,KAAK,EAC3CC,IAAU,IAAI,SAAS,gBAAgB,MAAM,YAAY,SAASD,EAAK,MAAM,CAAC,CAAC,GAAIC,CAAK,CAC9F,CACA,SAAS,gBAAgB,MAAM,YAAY,cAAeF,EAAK,UAAU,CAC3E,MAAQ,CAER,CACF,CCDAG,EAAW,CACT,QAAS,IAAM,CAACC,EAAE,IAAK,CAAE,MAAO,iBAAkB,KAAM,QAAS,EAAGA,EAAE,OAAQ,CAAE,MAAO,SAAU,CAAC,EAAG,eAAU,CAAC,EAChH,KAAMC,EACN,KAAMC,EACN,QAASC,EACT,QAASC,EACT,KAAMC,CACR,CAAC,EAEDC,EAAe,EACfC,EAAO,EACFC,EAAU",
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 { getDeviceSerial, toOperatingMode } from '../mapping.js';
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.serial = getDeviceSerial(options.config.macAddress);
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.name)?.getStatus();
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.name) : status.lastError,
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.name);
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(stored?.name ?? next.name);
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 ? next.name : undefined);
177
+ await this.applyDevices(devices, hasNewControls ? device.macAddress : undefined);
178
178
  }
179
- async removeDevice(name) {
180
- if (!this.devices.some((device) => device.name === name))
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.name !== name));
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.name)?.updateConnection(device, this.createClient(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, rebuiltName) {
303
- await this.enqueueChange(async () => this.reconcile(devices, rebuiltName));
302
+ async applyDevices(devices, rebuiltMacAddress) {
303
+ await this.enqueueChange(async () => this.reconcile(devices, rebuiltMacAddress));
304
304
  }
305
- async reconcile(devices, rebuiltName) {
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 = before === undefined ? undefined : this.accessories.get(before.name);
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.name === rebuiltName) {
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.name))
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.name);
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.name, accessory);
356
- this.startErrors.delete(device.name);
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.name) !== message)
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.name, message);
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.name)?.getNewlyReportedSensors();
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(name);
388
- this.accessories.delete(name);
389
- this.startErrors.delete(name);
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(name);
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.name);
404
- if (accessory === undefined || this.checkedCapabilities.has(device.name) || !accessory.getStatus().isReachable)
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.name);
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.name === device.name ? fitted : 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);
@@ -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 name = normalizeDeviceName(query?.name);
66
- if (name === undefined)
67
- return fail('Name the device to remove');
68
- return (await platform.removeDevice(name)) ? { ok: true } : fail(`No device is named "${name}"`);
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
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge-daikin-local",
3
- "version": "0.2.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.2.0",
9
+ "version": "0.3.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "daikin-local-control": "0.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-daikin-local",
3
- "version": "0.2.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",