matterbridge-dyson-care 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +29 -16
- package/apps/frontend/build/app.js +1 -1
- package/apps/frontend/build/app.js.map +3 -3
- package/apps/frontend/build/styles.css +0 -1
- package/dist/accessory/dysonAccessory.js +12 -2
- package/dist/config.js +33 -1
- package/dist/module.js +22 -9
- package/dist/platform/machineSync.js +1 -0
- package/dist/setup/probe.js +23 -0
- package/dist/setup/setupApi.js +4 -1
- package/npm-shrinkwrap.json +45 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- [setup]: Set a machine to the Dyson cloud only in the Connection of its options, so that each connection skips the search of the network, which takes up to 3 seconds when the machine is not there. The Connect step searches the network for a new machine beside its check through the cloud, and starts one it does not find on the cloud only.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- [devices]: Find each machine on the local network by the service it announces over mDNS, with dyson-control 0.8.2, in place of its name `<serialNumber>.local`. The search runs again on each connection, so the plugin follows a machine its router gives a new IP address, and it needs no Avahi on Linux. A machine the search does not find in 3 seconds is reached through the Dyson cloud. A host given in the setup page is still used in place of the search.
|
|
16
|
+
- [setup]: The Connection field of the options leaves the host empty to search the network, and a machine online through the cloud alone says that it was not found on the network, or did not answer at the host given.
|
|
17
|
+
- [deps]: Update dyson-control to 0.8.2.
|
|
18
|
+
|
|
19
|
+
## [0.3.0] - 2026-10-20
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- [devices]: Reach each machine on the local network first, at `<serialNumber>.local`, and through the Dyson cloud when it does not answer there, with dyson-control 0.8.1. The password of each machine's broker comes from the machine list of the account and is stored with the device, never shown in the setup page. A device stored by an earlier version gets it at the next read of the machine list, at the start.
|
|
24
|
+
- [setup]: Add a host to the options of a machine, for a network where its own name does not resolve, such as a Docker container: an IP address or a host name. A new host connects the machine again without building it again in Matter.
|
|
25
|
+
- [setup]: Show whether a machine is online on the network or through the Dyson cloud, and say so when it did not answer on the network. The log says which one each connection goes through when it changes.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- [deps]: Update dyson-control to 0.8.1, which also signs in an account the Dyson cloud reports with the authentication method `EMAIL_PWD_2FA`.
|
|
30
|
+
|
|
7
31
|
## [0.2.0] - 2026-10-20
|
|
8
32
|
|
|
9
33
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
Control your Dyson purifiers, fans, heaters and humidifiers from Apple Home, Google Home, Alexa, Home Assistant and other Matter apps.
|
|
14
14
|
|
|
15
|
-
It is a [Matterbridge](https://matterbridge.io) plugin built on [dyson-control](https://www.npmjs.com/package/dyson-control), which reaches each machine through the Dyson cloud with your Dyson account, the way the MyDyson app does. It covers the environmental care machines dyson-control knows: see its [machines](https://github.com/cheziyi/dyson-control#machines) and its [coverage and limitations](https://github.com/cheziyi/dyson-control#coverage--limitations).
|
|
15
|
+
It is a [Matterbridge](https://matterbridge.io) plugin built on [dyson-control](https://www.npmjs.com/package/dyson-control), which reaches each machine on your local network first, and through the Dyson cloud with your Dyson account when it does not answer there, the way the MyDyson app does. It covers the environmental care machines dyson-control knows: see its [machines](https://github.com/cheziyi/dyson-control#machines) and its [coverage and limitations](https://github.com/cheziyi/dyson-control#coverage--limitations).
|
|
16
16
|
|
|
17
17
|
## Why this plugin
|
|
18
18
|
|
|
@@ -22,7 +22,8 @@ It is a [Matterbridge](https://matterbridge.io) plugin built on [dyson-control](
|
|
|
22
22
|
- **It reaches more of the machine than the power and the fan.** Besides the fan speed, the auto mode, the oscillation and the night mode, you get the oscillation angle, the tilt, the heating, the humidifying, find and follow, the filters, and switches for the child lock, the jet focus, the UV light, the ioniser and the like: see the [comparison](#compared-with-other-dyson-plugins).
|
|
23
23
|
- **It shows the air quality.** A purifier gets an air quality sensor with the air quality of its worst pollutant and each pollutant it measures: PM2.5, PM10, VOC, NO₂, formaldehyde and CO₂.
|
|
24
24
|
- **You name each machine.** The MyDyson app names a machine after its room, which several machines can share: the setup page starts from that name and lets you give each machine a name of its own in Matter. A machine whose firmware gains a control gets it.
|
|
25
|
-
- **It
|
|
25
|
+
- **It talks to your machines on your network.** Each machine is reached on the local network first, which answers quicker and keeps working while the Dyson cloud or the internet is down, and through the Dyson cloud when it does not answer there. There is no address or password to type: see [Connection](#connection).
|
|
26
|
+
- **It says when a machine stops answering.** After two reads in a row fail, or when its connection does not open again, your Matter app shows the machine as unreachable, and it comes back by itself as soon as the machine answers again.
|
|
26
27
|
- **Nothing needs a restart.** Sign in, add, change and remove machines while Matterbridge keeps running.
|
|
27
28
|
|
|
28
29
|
## Requirements
|
|
@@ -30,7 +31,7 @@ It is a [Matterbridge](https://matterbridge.io) plugin built on [dyson-control](
|
|
|
30
31
|
- Matterbridge 3.10.10 or later.
|
|
31
32
|
- Node.js 22.13 or later (22, 24 or 26). Node.js 20 is not supported, because dyson-control needs Node.js 22.
|
|
32
33
|
- A Dyson account that signs in with an email address, with your machines added to it in the MyDyson app. The plugin finds the machines of that account: it does not create an account or add a machine itself, so set each machine up in the MyDyson app first.
|
|
33
|
-
- A route to the internet
|
|
34
|
+
- A route to the internet, to sign in and read the machine list of the account. A machine on the same network as Matterbridge is reached there, and through the Dyson cloud otherwise: see [Connection](#connection).
|
|
34
35
|
|
|
35
36
|
## Installation
|
|
36
37
|
|
|
@@ -55,9 +56,9 @@ Then it walks through three steps:
|
|
|
55
56
|
|
|
56
57
|
1. **Find.** The page lists the machines of your account and marks those already set up. It says why it cannot control one: a robot vacuum or another machine that is not a purifier, a fan, a heater or a humidifier, a machine of a type dyson-control does not know yet, or one reached through a Dyson cloud service it cannot connect to yet.
|
|
57
58
|
2. **Connect.** For each machine you picked, the plugin checks that it answers through the Dyson cloud, and reads which sensors it has.
|
|
58
|
-
3. **Options.** The name the machine gets in Matter, starting from its room name in the MyDyson app, whether it appears as an air purifier or a fan, and how the fan, the oscillation or the tilt, the night mode, the airflow direction, the find and follow, the heating, the humidifying, the clean cycle, the [more controls](#more-controls) and the readings appear in Matter, from what the machine can do, and whether they are one device or several. Start from a [preset](#presets) or choose each one yourself.
|
|
59
|
+
3. **Options.** The name the machine gets in Matter, starting from its room name in the MyDyson app, whether it appears as an air purifier or a fan, and how the fan, the oscillation or the tilt, the night mode, the airflow direction, the find and follow, the heating, the humidifying, the clean cycle, the [more controls](#more-controls) and the readings appear in Matter, from what the machine can do, and whether they are one device or several. Start from a [preset](#presets) or choose each one yourself. Last, whether the machine is reached on your network first or through the Dyson cloud only, and the host it is reached at on your network, left empty to search the network for it: see [Connection](#connection).
|
|
59
60
|
|
|
60
|
-
Afterwards the same page lists your machines with their status: online, connecting while a machine just added or rebuilt connects through the cloud
|
|
61
|
+
Afterwards the same page lists your machines with their status: online on your network or through the Dyson cloud, connecting while a machine just added or rebuilt connects, not answering, or not started. A machine online through the cloud alone says that it was not found on your network, or did not answer at the host given. Under each machine it shows how it appears in Matter, the life left in its filters, its clean cycle, such as when the next one is due or how long one that runs has left, how long the drying of its wick has left, and the faults it reports, such as a filter to replace or an empty tank. From there you can add more, change the options of one, reconnect one that stopped answering, remove one, switch to another account, sign in again when the Dyson cloud no longer accepts the sign-in, or sign out. None of this needs a Matterbridge restart.
|
|
61
62
|
|
|
62
63
|
<table>
|
|
63
64
|
<tr>
|
|
@@ -86,8 +87,8 @@ Afterwards the same page lists your machines with their status: online, connecti
|
|
|
86
87
|
<br><b>Options.</b> The humidifying and the clean cycle of a humidifier.
|
|
87
88
|
</td>
|
|
88
89
|
<td valign="top">
|
|
89
|
-
<img src="screenshots/home.png" alt="The home page lists four machines with their status, how each appears in Matter, the life left in their filters, a clean cycle due, the faults they report and one that is not answering">
|
|
90
|
-
<br><b>Afterwards.</b> Your machines, their status, their filters and their faults.
|
|
90
|
+
<img src="screenshots/home.png" alt="The home page lists four machines with their status, two online on the network and one through the Dyson cloud, how each appears in Matter, the life left in their filters, a clean cycle due, the faults they report and one that is not answering">
|
|
91
|
+
<br><b>Afterwards.</b> Your machines, their status and connection, their filters and their faults.
|
|
91
92
|
</td>
|
|
92
93
|
</tr>
|
|
93
94
|
</table>
|
|
@@ -357,19 +358,31 @@ Matter names a device, not its parts, so what a part is called is up to the Matt
|
|
|
357
358
|
|
|
358
359
|
Changing the choice builds the machine again in Matter, so a Matter app takes its parts for new devices: they lose the rooms, the names and the automations they had there.
|
|
359
360
|
|
|
361
|
+
## Connection
|
|
362
|
+
|
|
363
|
+
Each machine runs a broker of its own on the local network, which the MyDyson app talks to when it is on the same network. The plugin connects to it first, and to the Dyson cloud's when it does not answer, as [dyson-control](https://github.com/cheziyi/dyson-control#lan-connection) does given an account:
|
|
364
|
+
|
|
365
|
+
- **The search.** A machine announces its broker over mDNS, and the plugin finds it there on each connection, a reconnect included, so it follows a machine its router gives a new IP address. It needs no Avahi on Linux. Multicast does not reach a Docker container on the default bridge network: run Matterbridge with `--network host`, or enter the IP address of the machine in its options in the setup page, ideally one your router keeps for it. A machine given a host is no longer searched for, and a new host connects the machine again, without building it again in Matter.
|
|
366
|
+
- **The password.** The machine list of the account gives the password of each machine's broker, which is stored with the device and never shown in the setup page. A device stored by an earlier version gets it at the next read of the machine list, at the start. Until then, and for a machine the cloud gives none for, it is reached through the cloud alone.
|
|
367
|
+
- **The fallback.** Each connection, a reconnect included, tries the network first and opens the cloud's when the search finds nothing in 3 seconds, or the machine's own broker does not answer, refuses the password or its host does not resolve. A host given that does not answer costs up to 10 seconds before the cloud. A reconnect tries the network again, so a machine that fell back to the cloud goes back to the network once it answers there.
|
|
368
|
+
- **The cloud only.** A machine can be set to the Dyson cloud alone in its options, so that each connection skips the search, for a machine that is never on the network of Matterbridge. The **Connect** step of the guided setup searches the network for a new machine beside its check through the cloud, and starts one it does not find on the cloud only. Choose your network in its options to search for it again.
|
|
369
|
+
- **The status.** The setup page shows whether each machine is online on your network or through the Dyson cloud, and the log says which one each connection goes through when it changes.
|
|
370
|
+
|
|
371
|
+
The Dyson account is still needed: it gives the machine list and the password, and signing out stops every machine, on the network as well. The **Connect** step of the guided setup checks each machine through the cloud.
|
|
372
|
+
|
|
360
373
|
## Compared with other Dyson plugins
|
|
361
374
|
|
|
362
375
|
One other Matterbridge plugin controls Dyson purifiers, fans and heaters: [matterbridge-dyson-robot](https://github.com/thoukydides/matterbridge-dyson-robot), which despite its name covers these machines as well as the robot vacuums.
|
|
363
376
|
|
|
364
|
-
This plugin is the one for a humidifier, and the one that reaches the oscillation angle, the jet focus, the child lock, the UV light, the ioniser and the other controls Matter has no place for, as switches or mode selects. It also signs in and adds machines without a restart, and fits each control to the Matter app you use. matterbridge-dyson-robot has strengths of its own: it controls the robot vacuums, it can reach a machine over the local network
|
|
377
|
+
This plugin is the one for a humidifier, and the one that reaches the oscillation angle, the jet focus, the child lock, the UV light, the ioniser and the other controls Matter has no place for, as switches or mode selects. It also signs in and adds machines without a restart, and fits each control to the Matter app you use. matterbridge-dyson-robot has strengths of its own: it controls the robot vacuums, it can reach a machine over the local network without the cloud at all, and it has been released for longer.
|
|
365
378
|
|
|
366
379
|
| | matterbridge-dyson-care (this plugin) | [matterbridge-dyson-robot](https://github.com/thoukydides/matterbridge-dyson-robot) |
|
|
367
380
|
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
368
|
-
| Connection | The
|
|
381
|
+
| Connection | The local network first, with the address and the password of each machine found for you, and the Dyson cloud when the machine does not answer there. A MyDyson account either way | The Dyson cloud with a MyDyson account, the default. Or the local network, with the address of each machine and its credentials from the account, from its label or from [opendyson](https://github.com/libdyson-wg/opendyson), but not for recent products |
|
|
369
382
|
| Machines | The purifiers, fans, heaters and humidifiers dyson-control knows: see its [machines](https://github.com/cheziyi/dyson-control#machines). Not robot vacuums | Robot vacuums, and the Pure Cool Link, Pure Cool, Purifier Cool, Hot+Cool, Humidify+Cool, Big+Quiet, Cool and Hot+Cool fans and HushJet purifiers. Many of these are checked against logs of the machine only |
|
|
370
383
|
| Built on | [dyson-control](https://github.com/cheziyi/dyson-control), a library by the same author that works on its own | Its own cloud and MQTT code |
|
|
371
384
|
| Adding a machine | Sign in and pick the machines in a guided setup page. No restart | Sign in from the plugin settings, then a restart. Every machine of the account, with a white list and a black list |
|
|
372
|
-
| A machine that stops answering | Shown as unreachable after two failed reads, or when its connection
|
|
385
|
+
| A machine that stops answering | Shown as unreachable after two failed reads, or when its connection does not open again | Shown as unreachable when its connection closes, or when the machine says it is leaving |
|
|
373
386
|
| Device types | An air purifier or a fan, as you choose, with a thermostat, the switches, the mode selects and the sensors as parts of it or as devices of their own | An air purifier for every machine, the fans included, with an air quality sensor and a thermostat as devices of their own. Temperature and humidity sensors, and one composed device, are optional |
|
|
374
387
|
| Fan speed and auto | Steps or a percentage, and the auto mode as a fan mode, a switch or a step | Speeds 1 to 10, and the Auto fan mode |
|
|
375
388
|
| Oscillation | The rocking of the fan control, a switch, or a mode select with the angles the machine sweeps. The tilt of the Big+Quiet the same way | The rocking of the fan control, on and off. The tilt of the Big+Quiet as up and down rocking. No angles |
|
|
@@ -392,13 +405,13 @@ Outside Matterbridge, [homebridge-dyson-pure-cool](https://github.com/lukasroegn
|
|
|
392
405
|
|
|
393
406
|
The plugin knows each machine by its **serial number**, which is also the serial number it has in Matter. A thermostat, a switch, a mode select or a sensor that is a device of its own has that serial number with a dash and a letter after it, such as `NK6-EU-MHA0000A-N` for the night mode switch and `NK6-EU-MHA0000A-T` for the thermostat:
|
|
394
407
|
|
|
395
|
-
- **The firmware is kept up to date.** The plugin reads the machine list of your account when it starts, when you sign in, every 15 minutes and when you open the setup page. It takes the firmware, the variant, the model, the product name
|
|
408
|
+
- **The firmware is kept up to date.** The plugin reads the machine list of your account when it starts, when you sign in, every 15 minutes and when you open the setup page. It takes the firmware, the variant, the model, the product name, the room name and the password of its broker on the network each machine has now, and starts again a machine that could not start. A machine the cloud now reports with another type, or on a Dyson cloud service dyson-control cannot connect to, keeps what was stored, and a warning is logged.
|
|
396
409
|
- **Each device has the name you give it in the setup page**, of up to 32 characters, which starts as the room name of the MyDyson app, followed by a number when another device has that name already, such as `Bedroom 2`. A machine without a room name starts with none, for you to type. A new room name in the MyDyson app does not change it: the options of the device show the new one beside its product name and serial number, as soon as you open the setup page again. No two devices share a name, because the name is what Matter shows and what its switches and sensors are named after, shortened where the whole would pass 32 characters.
|
|
397
410
|
- **A new name builds the device again in Matter**, 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.
|
|
398
411
|
- **What each machine can do** is read from its type, its model, its variant and its firmware capabilities, as the cloud reports them. When a firmware update gives a machine other controls, it is built again in Matter and a setting it no longer takes is reset.
|
|
399
412
|
- **The readings each machine has given** are kept in the plugin's storage, so that its sensors are there from the start after a restart. Removing a machine in the setup page removes them too.
|
|
400
413
|
|
|
401
|
-
The account and the devices are stored in the plugin configuration, which Matterbridge keeps in `matterbridge-dyson-care.config.json`. The account is stored as the sign-in token Dyson gives the plugin, with its email address and country, and never the password. The devices are added, changed and removed in the setup page only: the Matterbridge config editor does not list them.
|
|
414
|
+
The account and the devices are stored in the plugin configuration, which Matterbridge keeps in `matterbridge-dyson-care.config.json`. The account is stored as the sign-in token Dyson gives the plugin, with its email address and country, and never the password. Each device is stored with the password of its machine's broker on the network, as the machine list gives it. The devices are added, changed and removed in the setup page only: the Matterbridge config editor does not list them.
|
|
402
415
|
|
|
403
416
|
A stored device without a valid name, serial number or type is ignored and logged. It stays in the configuration as it is, until a device with its name or its serial number is added in the setup page. A stored device with the name or the serial number of one before it is ignored and logged too, and left out of the configuration at its next save. A setting whose stored value is not valid falls back to its default, which is logged as well, and a field this version does not know, such as one of a later version, is left out at the next save.
|
|
404
417
|
|
|
@@ -412,12 +425,12 @@ The settings also have Matterbridge's own `debug` and `unregisterOnShutdown`, fo
|
|
|
412
425
|
|
|
413
426
|
## Good to know
|
|
414
427
|
|
|
415
|
-
- **It needs the Dyson cloud.**
|
|
428
|
+
- **It needs the Dyson cloud less.** A machine that answers on the network keeps working while the Dyson cloud or the internet is down, a restart included. Signing in, the machine list, the check in the setup page and a machine that does not answer on the network need the cloud: see [Connection](#connection).
|
|
416
429
|
- **The sign-in can expire.** How long the cloud accepts a sign-in is not known. When it stops accepting it, the plugin logs a warning and the setup page asks you to sign in again. Signing out leaves the machines in Matter, unreachable, with their rooms and automations, until you sign in again.
|
|
417
|
-
- **A change on the machine shows up at once.** A machine sends its state
|
|
430
|
+
- **A change on the machine shows up at once.** A machine sends its state when it changes, from its remote or the MyDyson app, and the plugin writes it to Matter as it comes.
|
|
418
431
|
- <a id="power"></a>**Controls turn a machine on, as in the MyDyson app.** dyson-control sends the power on with nearly every control, as the MyDyson app does, so changing almost anything on a machine that is off turns it on. The continuous monitoring, the water hardness, the temperature unit, the air quality target and the wick drying leave it off, and so does turning off find and follow or an oscillation that sweeps angles. On a machine of the first generation, only the power, a fan speed and the auto mode turn it on. Turning the heating or the humidifying off on a machine that is off turns it on too, with the fan alone.
|
|
419
432
|
- **Commands are grouped.** A Matter app often changes several things at once. The plugin waits until no change has come for 300 milliseconds and sends the changes to the machine as one command.
|
|
420
|
-
- **A device is reported as unreachable** after two reads in a row fail, or when its connection
|
|
433
|
+
- **A device is reported as unreachable** after two reads in a row fail, or when its connection closes and does not open again on the network or through the cloud. It is reachable again on the first read that works, or the first state the machine sends.
|
|
421
434
|
- **The setup page is as open as Matterbridge itself.** Anyone who can reach the Matterbridge frontend can use it, and so control the machines of the signed-in account. It never returns the stored sign-in token.
|
|
422
435
|
- **Faults are logged.** The plugin asks a machine for its faults when it first reads it and at the first read after each 15 minutes, and takes those it sends as they change. It logs each change, and the setup page shows them.
|
|
423
436
|
- **Not in Matter:** the clean cycle, how long the drying of a wick has left and the faults, which the setup page shows instead, the sleep timer, and the temperature unit of a machine without a thermostat. A machine on the RabbitMQ broker of the Dyson cloud, and an account that signs in with a mobile number, are not supported by dyson-control.
|
|
@@ -459,7 +472,7 @@ To see the setup page without a Dyson account, `npm run screenshots` serves it a
|
|
|
459
472
|
|
|
460
473
|
An independent, community project for interoperability and home automation. It is not affiliated with, authorized, or endorsed by Dyson. All product names and trademarks are the property of their respective owners.
|
|
461
474
|
|
|
462
|
-
The plugin talks to the Dyson cloud with the account you sign in with, as an ordinary client of the cloud's own endpoints. Its use may be subject to Dyson's terms of service.
|
|
475
|
+
The plugin talks to the Dyson cloud with the account you sign in with, as an ordinary client of the cloud's own endpoints, and to each machine on your network with the password the cloud gives for it. Its use may be subject to Dyson's terms of service.
|
|
463
476
|
|
|
464
477
|
## License
|
|
465
478
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
async function y(e,t,n){try{let[s,r]=e.split(" "),a={method:s};t!==void 0&&(a.headers={"Content-Type":"application/json"},a.body=JSON.stringify(t));let h=await fetch(new URL(`api/${r}${n===void 0?"":`?${n}`}`,document.baseURI),a);if(h.status===204)return{ok:!0};if(h.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let l=await h.json();return"ok"in l?l:{ok:!1,error:l.error??`The plugin answered with status ${h.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}var i={screen:"loading",setup:void 0,countries:[],signIn:{email:"",country:""},machines:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",info:"",confirmingRemoval:"",confirmingSignOut:!1};function b(){return i.queue[0]}function $(e){return e.name===""?e.machine.serialNumber:e.name}function z(e){return e.productName??`Dyson machine of type ${e.type}`}function j(e){let t=`${z(e)} ${e.serialNumber}`;return e.name===void 0?t:`${t}, named "${e.name}" in the MyDyson app`}var G=["hasChildLockSwitch","hasUvLightSwitch","hasIoniserSwitch","hasJetFocusSwitch","hasIndirectAirflowSwitch","hasAirQualityTargetSelect","hasWickDryingSwitch","hasWickDryingSpeedSelect","hasWaterHardnessSelect"];function L(e){let{deviceType:t,fanControl:n,fanAutoControl:s,oscillationControl:r,nightModeControl:a,airflowDirectionControl:h,heatControl:l,humidificationControl:p,findAndFollowControl:v,hasChildLockSwitch:C,hasUvLightSwitch:D,hasIoniserSwitch:P,hasJetFocusSwitch:m,hasIndirectAirflowSwitch:T,hasAirQualityTargetSelect:xe,hasWickDryingSwitch:ke,hasWickDryingSpeedSelect:Me,hasWaterHardnessSelect:De,hasContinuousMonitoringSwitch:Te,hasAirQualitySensor:$e,hasTemperatureSensor:Ee,hasHumiditySensor:Ie,isComposed:Ne}=e;return{deviceType:t,fanControl:n,fanAutoControl:s,oscillationControl:r,nightModeControl:a,airflowDirectionControl:h,heatControl:l,humidificationControl:p,findAndFollowControl:v,hasChildLockSwitch:C,hasUvLightSwitch:D,hasIoniserSwitch:P,hasJetFocusSwitch:m,hasIndirectAirflowSwitch:T,hasAirQualityTargetSelect:xe,hasWickDryingSwitch:ke,hasWickDryingSpeedSelect:Me,hasWaterHardnessSelect:De,hasContinuousMonitoringSwitch:Te,hasAirQualitySensor:$e,hasTemperatureSensor:Ee,hasHumiditySensor:Ie,isComposed:Ne}}function Y(e,t){let n=L(e);t.hasAutoMode||(n.fanAutoControl="native");let s=t.oscillationAngles.length===0&&t.tiltAngles.length===0;t.hasOscillation&&n.oscillationControl==="select"&&s&&(n.oscillationControl="switch"),t.hasHeat||(n.heatControl="hidden"),t.hasHumidification||(n.humidificationControl="hidden"),t.hasFindAndFollow||(n.findAndFollowControl="hidden");for(let r of G)n[r]&&=t.settingControls.includes(r);return n.hasContinuousMonitoringSwitch&&=t.hasContinuousMonitoring,n.hasAirQualitySensor&&=t.hasAirQualitySensor,n}var He={deviceType:"airPurifier",fanControl:"steps",fanAutoControl:"native",oscillationControl:"native",nightModeControl:"native",airflowDirectionControl:"native",heatControl:"thermostat",humidificationControl:"select",findAndFollowControl:"hidden",hasChildLockSwitch:!1,hasUvLightSwitch:!1,hasIoniserSwitch:!1,hasJetFocusSwitch:!1,hasIndirectAirflowSwitch:!1,hasAirQualityTargetSelect:!1,hasWickDryingSwitch:!1,hasWickDryingSpeedSelect:!1,hasWaterHardnessSelect:!1,hasContinuousMonitoringSwitch:!1,hasAirQualitySensor:!0,hasTemperatureSensor:!0,hasHumiditySensor:!0,isComposed:!0},Le=32;function Re(e,t){if(e===""||!t.has(e))return e;for(let n=2;;n++){let s=` ${n}`,r=`${e.slice(0,Le-s.length).trimEnd()}${s}`;if(!t.has(r))return r}}function oe(e){let t=new Set((i.setup?.devices??[]).map(n=>n.name));return e.map(n=>{let s=Re(n.name??"",t);return t.add(s),{machine:n,name:s,isNew:!0,...L(He)}})}function ie(e){let{capabilities:t,controls:n,reportedSensors:s,name:r,roomName:a,serialNumber:h,type:l,variant:p,mqttRootTopicLevel:v,model:C,productName:D,firmwareVersion:P,firmwareCapabilities:m}=e;return{machine:{serialNumber:h,type:l,category:"environmentalCare",name:a,variant:p,mqttRootTopicLevel:v,model:C,productName:D,firmwareVersion:P,firmwareCapabilities:m},name:r,isNew:!1,capabilities:t,controls:n,reportedSensors:s,...L(e)}}var se={temperature:!1,humidity:!1,airQuality:!1,pm25:!1,pm10:!1,volatileOrganicCompounds:!1,nitrogenDioxide:!1,formaldehyde:!1,carbonDioxide:!1},re={hasAutoMode:!1,hasOscillation:!1,oscillationAngles:[],tiltAngles:[],hasFindAndFollow:!1,hasAirflowDirection:!1,hasHeat:!1,hasHumidification:!1,hasAutoHumidification:!1,settingControls:[],hasFilterLife:!1,hasContinuousMonitoring:!1,hasAirQualitySensor:!1};var q={apple:{title:"Apple Home",text:"Fan speed as a percentage with its auto mode, a switch for the oscillation, the night mode, the find and follow, the jet focus, the UV light and the ioniser, with a sensor for the air quality, the temperature and the humidity, each a device of its own."},google:{title:"Google Home",text:"Fan speed as steps, a switch for the auto mode, the oscillation, the night mode, the find and follow, the jet focus, the UV light and the ioniser, with a sensor for the air quality, the temperature and the humidity, each a device of its own."},matter:{title:"Full Matter support",text:"For apps that show every Matter control: the auto mode, the oscillation, the night mode and the airflow direction on the fan control itself, the target humidity, the find and follow and the settings of the machine as mode selects and switches, and every sensor, all as one device."}};function R(e,t){let n=e==="matter",s=n?"native":"switch";return Y({deviceType:t.hasAirQualitySensor?"airPurifier":"fan",fanControl:e==="apple"?"percent":"steps",fanAutoControl:e==="google"?"switch":"native",oscillationControl:s,nightModeControl:s,airflowDirectionControl:"native",heatControl:"thermostat",humidificationControl:n?"select":"switches",findAndFollowControl:n?"select":"switch",hasChildLockSwitch:n,hasUvLightSwitch:!0,hasIoniserSwitch:!0,hasJetFocusSwitch:!0,hasIndirectAirflowSwitch:!0,hasAirQualityTargetSelect:n,hasWickDryingSwitch:n,hasWickDryingSpeedSelect:n,hasWaterHardnessSelect:n,hasContinuousMonitoringSwitch:n,hasAirQualitySensor:!0,hasTemperatureSensor:!0,hasHumiditySensor:!0,isComposed:n},t)}function ae(e,t,n){return JSON.stringify(Y(e,n))===JSON.stringify(R(t,n))}var Z;function le(e){Z=e}function c(){let e=document.getElementById("app");if(e===null||Z===void 0)return;let t=`${i.screen}:${b()?.machine.serialNumber??""}`,n=e.dataset.form===t,s=document.activeElement,r=s instanceof HTMLElement?s.id:"",a=s instanceof HTMLInputElement&&(s.type==="text"||s.type==="password")?{start:s.selectionStart,end:s.selectionEnd}:void 0,h=new Map;for(let p of e.querySelectorAll("input[type=text], input[type=email], input[type=password]"))p.id!==""&&h.set(p.id,p.value);if(e.replaceChildren(...Z[i.screen]().filter(p=>p!==!1&&p!==null&&p!==void 0)),e.dataset.form=t,!n)return;for(let[p,v]of h){let C=document.getElementById(p);C instanceof HTMLInputElement&&(C.value=v)}if(r==="")return;let l=document.getElementById(r);l?.focus(),a!==void 0&&l instanceof HTMLInputElement&&l.setSelectionRange(a.start,a.end)}async function A(e,t){i.isBusy=!0,i.busyText=e,i.error="",i.info="",c();try{return await t()}finally{i.isBusy=!1,c()}}async function W(){let e=await y("GET state");if(!e.ok){i.error=e.error,i.screen="home",c();return}if(F(e.state),e.state.account===void 0){await E();return}if(e.state.devices.length===0&&!e.state.account.needsSignIn){await I();return}i.screen="home",c(),Fe()}async function Fe(){let e=await y("POST sync");!e.ok||JSON.stringify(e.state)===JSON.stringify(i.setup)||(F(e.state),c())}var Oe=2e3,X;function F(e){i.setup=e,!(X!==void 0||!e.devices.some(t=>t.status==="connecting"))&&(X=window.setTimeout(()=>{X=void 0,x().then(c)},Oe))}async function x(){let e=await y("GET state");e.ok&&F(e.state)}function S(){i.queue=[],i.saved=[],i.handled=[],i.error="",i.info="",i.screen="home",x().then(c),c()}async function E(){let e=i.setup?.account;if(i.signIn={email:e?.email??"",country:e?.country??""},i.error="",i.info="",i.screen="signIn",c(),i.countries.length>0)return;let t=await y("GET countries");t.ok&&(i.countries=t.countries),c()}async function _(e,t){i.signIn={email:e,country:t};let n=await A(`Asking Dyson to email a code to ${e}\u2026`,async()=>y("POST login-code",{email:e,country:t}));if(!n.ok){i.error=n.error,c();return}i.signIn={email:e,country:t,challengeId:n.challengeId},c()}async function ce(e,t){let{email:n,country:s,challengeId:r}=i.signIn,a=await A("Signing in\u2026",async()=>y("POST login",{email:n,country:s,challengeId:r,password:e,otpCode:t}));if(!a.ok){i.error=a.error,c();return}F(a.state),i.signIn={email:n,country:s},a.state.devices.length===0?await I():S()}async function he(){let e=await A("Signing out\u2026",async()=>y("DELETE account"));i.confirmingSignOut=!1,e.ok||(i.error=e.error),await x(),c()}async function I(){i.screen="find";let e=await A("Reading the machines of your Dyson account\u2026",async()=>y("POST machines"));if(!e.ok){i.error=`The machines could not be read: ${e.error}`,c();return}await x(),i.machines=e.machines;for(let t of e.machines)!t.isConfigured&&t.support==="supported"&&i.selected.add(t.serialNumber);c()}function Q(e){i.queue=e,i.saved=[],i.handled=[],ee()}async function ee(){let e=b();if(e===void 0){i.screen=i.saved.length>0?"done":"home",await x(),c();return}i.screen="connect",i.error="",i.info="",e.probe=void 0,c(),await te(e)}async function te(e){let t=await A(`Connecting to ${$(e)} through the Dyson cloud\u2026`,async()=>y("POST probe",{machine:e.machine}));if(!t.ok){i.error=t.error,c();return}if(e.probe=t.result,t.result.status==="signInRequired"&&await x(),t.result.status==="ready"){let n=e.controls===void 0;e.capabilities=t.result.capabilities,e.controls=t.result.controls,e.reportedSensors=t.result.reportedSensors,e.isNew&&n&&e.controls!==void 0&&Object.assign(e,R("apple",e.controls)),i.screen="options"}c()}function ue(){let e=i.queue.shift();e!==void 0&&i.handled.push({label:$(e),isSaved:!1}),ee()}async function pe(e){let t=e.name.trim(),n=Pe(e,t);if(n!==""){i.error=n,c();return}let{machine:s}=e,r={...s,name:t,roomName:s.name,...L(e)},a=await A(`Saving ${t}\u2026`,async()=>y("POST device",{device:r}));if(!a.ok){i.error=a.error,c();return}F(a.state),i.saved.push(t),i.handled.push({label:t,isSaved:!0}),i.queue.shift(),await ee()}function Pe(e,t){return t===""?"Give the machine a name: it is the name it gets in Matter.":(i.setup?.devices??[]).some(s=>s.name===t&&s.serialNumber!==e.machine.serialNumber)?`"${t}" is already the name of another machine. Pick a different one.`:""}var ne="Matterbridge's own list of devices shows the change once you reload its page.";async function de(e){let t=await A(`Removing ${e.name}\u2026`,async()=>y("DELETE device",void 0,`serialNumber=${encodeURIComponent(e.serialNumber)}`));i.confirmingRemoval="",t.ok?i.info=`${e.name} is removed. ${ne}`:i.error=t.error,await x(),c()}function o(e,t={},...n){let s=document.createElement(e);for(let[r,a]of Object.entries(t))a===void 0||a===!1||(r==="class"?s.className=String(a):r==="html"?s.innerHTML=String(a):r.startsWith("on")&&typeof a=="function"?s.addEventListener(r.slice(2),a):r in s?s[r]=a:s.setAttribute(r,String(a)));for(let r of n)r!==!1&&r!==null&&r!==void 0&&s.append(r);return s}function u(e,t,n="quiet",s=!1){return o("button",{type:"button",class:`button ${n}`,onclick:t,disabled:s||i.isBusy},e)}function w(){return i.isBusy?o("p",{class:"notice working",role:"status"},o("span",{class:"spinner"}),i.busyText):i.error!==""?o("p",{class:"notice problem",role:"alert"},i.error):i.info!==""?o("p",{class:"notice",role:"status"},i.info):null}function U(){let e=b();if(e===void 0||!e.isNew||i.handled.length+i.queue.length<2)return null;let t=i.screen==="connect"?"Connecting":"Choosing options";return o("ol",{class:"units","aria-label":"Machines in this setup"},...i.handled.map(n=>o("li",{class:n.isSaved?"done":"skipped"},o("strong",{},n.label),o("span",{class:"meta"},n.isSaved?"Added":"Skipped"))),...i.queue.map((n,s)=>o("li",{class:s===0?"current":"ahead","aria-current":s===0?"true":void 0},o("strong",{},$(n)),o("span",{class:"meta"},s===0?t:"Waiting"))))}function B(e,t){return o("header",{class:"unit-header"},o("h1",{},$(e)),o("p",{class:"meta"},t))}function N(){if(i.queue.length>0&&!i.queue.some(s=>s.isNew))return null;let e=["Find","Connect","Options"],n=["find","connect","options"].indexOf(i.screen);return o("ol",{class:"steps"},...e.map((s,r)=>{let a="ahead";return r<n?a="done":r===n&&(a="current"),o("li",{class:a,"aria-current":r===n?"step":void 0},s)}))}function je(e){let t=e.probe?.status,n=e.name===""?"The machine":e.name;return t==="signInRequired"?o("div",{class:"field"},o("p",{class:"notice problem",role:"alert"},"The Dyson cloud no longer accepts the sign-in of this plugin."),u("Sign in again",()=>{E()},"primary")):t==="unreachable"?o("p",{class:"notice problem",role:"alert"},`${n} did not answer. Check that it is plugged in and shows as online in the MyDyson app, then try again.`):t==="unsupported"?o("p",{class:"notice problem",role:"alert"},`The plugin cannot control ${n}: ${e.probe?.message??"unknown"}`):t==="failed"?o("p",{class:"notice problem",role:"alert"},`${n} answered with an error: ${e.probe?.message??"unknown"}`):null}function fe(){let e=b();if(e===void 0)return[w()];let t=e.probe?.status==="unreachable"||e.probe?.status==="failed"||i.error!=="",n=e.isNew&&i.queue.length>1;return[N(),U(),B(e,`Connecting to the ${j(e.machine)} through the Dyson cloud.`),w(),i.isBusy?null:je(e),o("div",{class:"footer"},t?u("Try again",()=>{te(e)}):null,u(n?"Skip this one":"Cancel",n?ue:S))]}function me(){let e=i.saved;return[o("header",{},o("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} machines are set up`)),o("p",{class:"lead"},"They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page."),o("p",{class:"help"},ne),o("div",{class:"footer"},u("Done",S,"primary"))]}function ge(e){return!e.isConfigured&&e.support==="supported"}function qe(e){let t=[];return e.isConfigured&&t.push("Already set up."),e.support==="otherCategory"&&t.push("Not a purifier, fan, heater or humidifier: the plugin does not control it."),e.support==="unknownType"&&t.push(`The plugin does not know machines of type ${e.type} yet.`),e.support==="otherBroker"&&t.push("It is reached through a Dyson cloud service the plugin cannot connect to yet."),t}function We(e){let t=ge(e),n=`machine-${e.serialNumber}`,s=o("input",{type:"checkbox",id:n,checked:t&&i.selected.has(e.serialNumber),disabled:!t,onchange:()=>{s.checked?i.selected.add(e.serialNumber):i.selected.delete(e.serialNumber),c()}});return o("li",{class:t?"found":"found unavailable"},o("label",{for:n},s,o("span",{},o("strong",{},e.name??"No room name"),o("span",{class:"meta"},` ${z(e)}, ${e.serialNumber}`),...qe(e).map(r=>o("span",{class:"meta note"},r)))))}function ye(){let e=i.machines.filter(n=>i.selected.has(n.serialNumber)&&ge(n)),t=e.length===1?"Set up 1 machine":`Set up ${e.length} machines`;return[N(),o("header",{},o("h1",{},"Your Dyson machines")),w(),i.machines.length>0?o("ul",{class:"found-list"},...i.machines.map(We)):null,!i.isBusy&&i.machines.length===0&&i.error===""?o("p",{class:"empty"},"Your Dyson account has no machine yet. Add your machines in the MyDyson app, then read the list again."):null,o("div",{class:"footer"},u("Read the list again",()=>{I()}),u("Cancel",S),u(t,()=>Q(oe(e)),"primary",e.length===0))]}var Qe={hasChildLockSwitch:"child lock",hasUvLightSwitch:"UV light",hasIoniserSwitch:"ioniser",hasJetFocusSwitch:"jet focus",hasIndirectAirflowSwitch:"indirect airflow",hasAirQualityTargetSelect:"air quality target",hasWickDryingSwitch:"wick drying",hasWickDryingSpeedSelect:"wick drying speed",hasWaterHardnessSelect:"water hardness"},O={native:"on the fan",switch:"as a switch",select:"as a mode select",hidden:""};function Ue(e){let t=e.controls,n=m=>t===void 0||t[m],s=t!==void 0&&t.tiltAngles.length>0,r=e.deviceType==="airPurifier"?"an air purifier":"a fan",a={native:"",switch:"",firstStep:" with auto first",lastStep:" with auto last"}[e.fanAutoControl],h=[`${r} with speed ${e.fanControl==="steps"?"steps":"as a percentage"}${n("hasAutoMode")?a:""}`],l=new Map,p=(m,T)=>{m!==""&&l.set(m,[...l.get(m)??[],T])};n("hasAutoMode")&&e.fanAutoControl==="switch"&&p(O.switch,"the auto mode"),n("hasOscillation")&&p(O[e.oscillationControl],s?"the tilt":"the oscillation"),n("hasFindAndFollow")&&p(O[e.findAndFollowControl],"the find and follow"),p(O[e.nightModeControl],"the night mode"),n("hasAirflowDirection")&&p(O[e.airflowDirectionControl],"the airflow direction");for(let[m,T]of l)h.push(`${T.join(", ")} ${m}`);n("hasHeat")&&e.heatControl!=="hidden"&&h.push(e.heatControl==="thermostat"?"a thermostat":"a heat switch"),n("hasHumidification")&&e.humidificationControl!=="hidden"&&h.push(e.humidificationControl==="select"?"a target humidity select":"humidify switches");let v=G.filter(m=>e[m]&&(t===void 0||t.settingControls.includes(m))).map(m=>Qe[m]);v.length>0&&h.push(`a control for the ${v.join(", ")}`),n("hasFilterLife")&&e.deviceType==="airPurifier"&&h.push("the filter life");let{reportedSensors:C}=e,D=[n("hasAirQualitySensor")&&e.hasAirQualitySensor&&C.airQuality?"air quality":"",e.hasTemperatureSensor&&C.temperature?"temperature":"",e.hasHumiditySensor&&C.humidity?"humidity":""].filter(m=>m!=="");return D.length>0&&h.push(`a sensor for the ${D.join(", ")}`),`${h.length===1?h[0]:`${h.slice(0,-1).join(", ")} and ${h[h.length-1]}`}${e.isComposed?", all as one device":""}`}var Be={filter:"Filter",outerFilter:"HEPA filter",innerFilter:"Carbon filter"};function Ke(e){let t=e.remainingTimeOfActiveInMinutes;if(e.state==="active")return t===null?"Cleaning":`Cleaning, ${t} min left`;if(e.state==="paused")return"Clean cycle paused";if(e.state==="incomplete")return"Clean cycle not finished";if(e.state==="setup")return"Clean cycle being set up";let n=e.remainingTimeToNextInHours;return n===null?"":n<=0?"Clean cycle due":n<48?`Next clean cycle in ${n} hours`:`Next clean cycle in ${Math.round(n/24)} days`}function Je(e){let t=e.filters.map(({filter:a,lifeInPercent:h})=>`${Be[a]} ${h} %`).join(", "),n=e.cleanCycle===void 0?"":Ke(e.cleanCycle),s=e.wickDrying?.remainingTimeInMinutes,r="";return e.wickDrying!==void 0&&(r=s==null?"Drying its wick":`Drying its wick, ${s} min left`),[t,n,r].filter(a=>a!=="").map(a=>`${a}.`).join(" ")}function Ve(e){return e.replaceAll(/([a-z])([A-Z])/g,"$1 $2").toLowerCase()}function ze(e){if(e===void 0)return[];let t=Je(e),n=e.faults??[];return[t===""?null:o("p",{class:"meta"},t),n.length===0?null:o("p",{class:"meta problem-text"},`The machine reports: ${n.map(Ve).join(", ")}.`)]}function Ge(e){let t={online:"Online",connecting:"Connecting\u2026",offline:"Not answering",stopped:"Not started"}[e.status],n=i.confirmingRemoval===e.serialNumber,s=e.productName??`type ${e.type}`;return o("li",{class:"device"},o("div",{class:"device-main"},o("h3",{},e.name),o("p",{class:"meta"},o("span",{class:`status ${e.status}`},t),` ${s}, ${e.serialNumber}. In Matter: ${Ue(e)}.`),...ze(e.report),e.status!=="online"&&e.error?o("p",{class:"meta problem-text"},e.error):null),n?o("div",{class:"actions"},o("span",{class:"meta"},"Remove it from Matter too?"),u("Remove",()=>{de(e)},"danger"),u("Keep",()=>{i.confirmingRemoval="",c()})):o("div",{class:"actions"},u(e.status==="online"?"Options":"Reconnect",()=>Q([ie(e)]),"quiet",i.setup?.account===void 0),u("Remove",()=>{i.confirmingRemoval=e.serialNumber,c()})))}function Ye(){let e=i.setup?.account;if(i.setup===void 0)return null;if(e===void 0)return o("section",{class:"account"},o("p",{class:"notice problem",role:"alert"},"No Dyson account is signed in, so the machines below are not connected. They stay in Matter until you sign in again."),u("Sign in",()=>{E()},"primary"));let t=i.confirmingSignOut?[o("span",{class:"meta"},"The machines stay in Matter, unreachable until you sign in again."),u("Sign out",()=>{he()},"danger"),u("Stay signed in",()=>{i.confirmingSignOut=!1,c()})]:[u(e.needsSignIn?"Sign in again":"Switch account",()=>{E()},e.needsSignIn?"primary":"quiet"),u("Sign out",()=>{i.confirmingSignOut=!0,c()})];return o("section",{class:"account"},o("p",{class:"meta"},`Signed in to Dyson as ${e.email}.`),e.needsSignIn?o("p",{class:"notice problem",role:"alert"},"The Dyson cloud no longer accepts this sign-in. Sign in again to keep the machines connected."):null,o("div",{class:"actions"},...t))}function we(){let e=i.setup?.devices??[],t=i.setup?.account!==void 0&&!i.setup.account.needsSignIn;return[o("header",{},o("h1",{},"Dyson machines"),t?u("Add machines",()=>{I()},"primary"):null),w(),Ye(),e.length===0&&i.setup?o("p",{class:"empty"},"No machine is set up yet. Add one to control it from Matter."):o("ul",{class:"devices"},...e.map(Ge)),i.setup?null:u("Try again",()=>{W()},"primary")]}var K={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>',purifier:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect class="stroke" x="16" y="2" width="16" height="24" rx="3"/><path class="stroke" d="M20 8 H28 M20 13 H28 M20 18 H28"/></svg>',fan:'<svg viewBox="0 0 48 28" aria-hidden="true"><circle class="stroke" cx="24" cy="12" r="9"/><path class="stroke" d="M24 21 V26 M18 26 H30"/></svg>'},H="Few apps show a mode select: Home Assistant does, Apple Home does not.",V="Not every app shows it there: Apple Home and Google Home are not known to.";function f(e,t,n){return e.isComposed?n?`It appears as a part of "${e.name}", tagged "${t}"`:`It appears as a part of "${e.name}"`:`It appears as "${e.name} ${t}"`}function g(e,t){Object.assign(e,t),c()}function J(e,t,n,s,r,a,h){let l=`${e}-${t}`,p=o("input",{type:"radio",name:e,id:l,checked:t===n,onchange:()=>h(t)});return o("label",{class:"choice pictured",for:l},p,o("span",{class:"glyph",html:a}),o("span",{},o("strong",{},s),o("span",{class:"meta"},r)))}function d(e,t,n,s,r,a){let h=`${e}-${t}`,l=o("input",{type:"radio",name:e,id:h,checked:t===n,onchange:()=>a(t)});return o("label",{class:"choice",for:h},l,o("span",{},o("strong",{},s),o("span",{class:"meta"},r)))}function k(e,t,n,s,r){let a=o("input",{type:"checkbox",id:e,checked:t,onchange:()=>r(a.checked)});return o("label",{class:"choice",for:e},a,o("span",{},o("strong",{},n),o("span",{class:"meta"},s)))}function M(e,t,...n){let s=`${e.toLowerCase().replaceAll(" ","-")}-title`;return o("section",{},o("h2",{id:s},e),t===""?null:o("p",{class:"help"},t),o("div",{class:"choices",role:"radiogroup","aria-labelledby":s},...n))}function Ze(e){let t=o("input",{type:"text",id:"device-name",value:e.name,maxLength:32,autocomplete:"off",spellcheck:!1});t.addEventListener("input",r=>{e.name=t.value.trim(),r.isComposing||c()});let n=e.machine.name,s=n===void 0?"It has no room name in the MyDyson app.":`Its room name in the MyDyson app is "${n}", which other machines in that room share.`;return o("section",{},o("h2",{},o("label",{for:"device-name"},"Name in Matter")),o("p",{class:"help"},`${s} The name here is what your Matter apps show, up to 32 characters, and no two machines of this plugin share one. A new room name in the MyDyson app does not change it.`),o("div",{class:"inline-form"},t))}function Xe(e,t){return o("section",{class:"presets"},o("h2",{},"Start from a preset"),o("div",{class:"preset-list"},...Object.keys(q).map(n=>o("button",{type:"button",class:"preset","aria-pressed":String(ae(e,n,t)),onclick:()=>g(e,R(n,t))},o("strong",{},q[n].title),o("span",{class:"meta"},q[n].text)))),o("p",{class:"help"},"A preset fills in the choices below for what this machine can do. Change any of them afterwards."))}function _e(e,t){let n=r=>g(e,{deviceType:r}),s=t.hasFilterLife?" and the life left in its filters, which a Matter app can reset after a new one is fitted":"";return o("section",{},o("h2",{},"Appears as"),o("div",{class:"choices"},J("device-type","airPurifier",e.deviceType,"An air purifier",`The Matter device type of a purifier, with its fan speed${s}. Apple Home, Google Home and Home Assistant show it.`,K.purifier,n),J("device-type","fan",e.deviceType,"A fan",`The Matter device type every app shows, with the same fan speed${t.hasFilterLife?", but no filters":""}. For apps that show no air purifier.`,K.fan,n)))}function et(e,t){let n=l=>g(e,{fanControl:l}),s=l=>g(e,{fanAutoControl:l}),r=t.hasAutoMode?e.fanAutoControl:"native",h=r==="firstStep"||r==="lastStep"?"Eleven steps: the ten speeds and auto.":"Ten steps, one for each speed.";return o("section",{},o("h2",{},"Fan speed"),o("div",{class:"choices"},J("fan","steps",e.fanControl,"Steps",`${h} A slider shows them as a percentage too.`,K.steps,n),J("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed: 10 % is speed 1 and 100 % speed 10.",K.percent,n)),t.hasAutoMode?o("div",{class:"field"},o("h3",{id:"fan-auto-title"},"Auto mode"),o("p",{class:"help"},"In its auto mode the machine picks the fan speed from the air it measures. Matter has it as the fan mode Auto, which not every app shows."),o("div",{class:"choices",role:"radiogroup","aria-labelledby":"fan-auto-title"},d("fan-auto","native",r,"Native fan mode only","Auto is a mode of the fan speed, beside its speeds, as Matter has it.",s),d("fan-auto","switch",r,"A switch",`${f(e,"Auto Mode",!0)}. On turns the auto mode on, off goes back to the speed the fan last ran at. Auto stays a fan mode as well.`,s),d("fan-auto","firstStep",r,"The first step","Auto comes below speed 1: step 1, or the bottom of the slider.",s),d("fan-auto","lastStep",r,"The last step","Auto comes above speed 10: the top step, or 100 %.",s))):null)}function tt(e){let t=e.map(n=>n==="breeze"?"breeze":`${n}\xB0`);return t.length===1?t[0]:`${t.slice(0,-1).join(", ")} and ${t[t.length-1]}`}function nt(e){let t=n=>g(e,{oscillationControl:n});return M("Tilt","The machine tilts to 0\xB0, 25\xB0 or 50\xB0, or breezes: it tilts up and down. Tilting turns a machine that is off on, as in the MyDyson app.",d("oscillation","native",e.oscillationControl,"Native rocking",`The up and down rocking of the fan control, as Matter has it: on breezes, off goes back to the angle it was tilted to. ${V}`,t),d("oscillation","switch",e.oscillationControl,"A switch",`${f(e,"Oscillation",!0)}. On breezes, off goes back to the angle it was tilted to. It takes the place of the rocking of the fan control, which Apple Home mishandles.`,t),d("oscillation","select",e.oscillationControl,"A mode select",`${f(e,"Tilt",!0)}, and picks 0\xB0, 25\xB0, 50\xB0 or breeze. It takes the place of the rocking of the fan control, which Apple Home mishandles. ${H}`,t))}function ot(e,t){if(!t.hasOscillation)return null;if(t.tiltAngles.length>0)return nt(e);let n=a=>g(e,{oscillationControl:a}),s=t.oscillationAngles.length>0,r=e.oscillationControl==="select"&&!s?"switch":e.oscillationControl;return M("Oscillation","",d("oscillation","native",r,"Native rocking",`The left and right rocking of the fan control, as Matter has it. ${V}`,n),d("oscillation","switch",r,"A switch",`${f(e,"Oscillation",!0)}. On sweeps the angle set in the MyDyson app. It takes the place of the rocking of the fan control, which Apple Home mishandles.`,n),s?d("oscillation","select",r,"A mode select",`${f(e,"Oscillation",!0)}, and picks off or one of ${tt(t.oscillationAngles)}. It takes the place of the rocking of the fan control, which Apple Home mishandles. ${H}`,n):null)}function it(e){let t=n=>g(e,{nightModeControl:n});return M("Night mode","In its night mode the machine runs quieter and dims its display.",d("night-mode","native",e.nightModeControl,"Native sleep wind only",`The sleep wind of the fan control, as Matter has it. ${V}`,t),d("night-mode","switch",e.nightModeControl,"A switch",`${f(e,"Night Mode",!0)}. The night mode stays the sleep wind of the fan control as well.`,t))}function st(e,t){if(!t.hasAirflowDirection)return null;let n=s=>g(e,{airflowDirectionControl:s});return M("Airflow direction","The machine blows its air out of the front, or out of the back to circulate the air without cooling you.",d("airflow","native",e.airflowDirectionControl,"Native airflow direction only",`The airflow direction of the fan control, as Matter has it: forward is the front. ${V}`,n),d("airflow","switch",e.airflowDirectionControl,"A switch",`${f(e,"Front Airflow",!0)}. On blows the air out of the front, off out of the back. The airflow direction of the fan control stays as well.`,n))}function rt(e,t){if(!t.hasFindAndFollow)return null;let n=s=>g(e,{findAndFollowControl:s});return M("Find and follow","The machine finds the people in the room and turns to follow them. It runs only while the machine is on, so turning it on turns the machine on.",d("find-and-follow","switch",e.findAndFollowControl,"A switch",`${f(e,"Find and Follow",!0)}. On follows the people the machine finds.`,n),d("find-and-follow","select",e.findAndFollowControl,"A mode select",`${f(e,"Find and Follow",!0)}, and picks off, follow, or scan to look for people. ${H}`,n),d("find-and-follow","hidden",e.findAndFollowControl,"Not shown in Matter","Find and follow stays as the MyDyson app sets it.",n))}function at(e,t){if(!t.hasHeat)return null;let n=s=>g(e,{heatControl:s});return M("Heating","The machine heats only while it is on, so turning the heating on turns the machine on.",d("heat","thermostat",e.heatControl,"A thermostat",`${f(e,"Heater",!1)}: it heats or is off, with a setpoint from 1 to 37 \xB0C. Off leaves the fan running.`,n),d("heat","switch",e.heatControl,"A switch",`${f(e,"Heat",!0)}. It heats to the setpoint set in the MyDyson app.`,n),d("heat","hidden",e.heatControl,"Not shown in Matter","The heating stays as the MyDyson app sets it.",n))}function lt(e,t){if(!t.hasHumidification)return null;let n=a=>g(e,{humidificationControl:a}),s=t.hasAutoHumidification?' With a second switch, "Auto Humidify", for the machine to pick the target itself.':"",r=t.hasAutoHumidification?"off, auto, or a target from 30 to 70 %":"off, or a target from 30 to 70 %";return M("Humidifying","Matter has no humidifier, so the humidifying takes a switch or a mode select. It runs only while the machine is on, so turning it on turns the machine on.",d("humidify","switches",e.humidificationControl,"Switches",`${f(e,"Humidify",!0)}, and humidifies to the target set in the MyDyson app.${s}`,n),d("humidify","select",e.humidificationControl,"A mode select",`${f(e,"Target Humidity",!0)}, and picks ${r}. ${H}`,n),d("humidify","hidden",e.humidificationControl,"Not shown in Matter","The humidifying stays as the MyDyson app sets it.",n))}var Se={hasChildLockSwitch:{label:"Child Lock",text:"On locks the buttons of the machine.",isSelect:!1},hasUvLightSwitch:{label:"UV Light",text:"On turns the UV light of the machine on.",isSelect:!1},hasIoniserSwitch:{label:"Ioniser",text:"On turns the ioniser of the machine on.",isSelect:!1},hasJetFocusSwitch:{label:"Jet Focus",text:"On focuses the air into one stream, and off diffuses it.",isSelect:!1},hasIndirectAirflowSwitch:{label:"Indirect Airflow",text:"On tilts the nozzle to 45\xB0, and off points it straight.",isSelect:!1},hasAirQualityTargetSelect:{label:"Air Quality Target",text:"picks the air quality the auto mode aims for: good, sensitive or very sensitive.",isSelect:!0},hasWickDryingSwitch:{label:"Wick Drying",text:"On dries the wick of the humidifier.",isSelect:!1},hasWickDryingSpeedSelect:{label:"Wick Drying Speed",text:"picks how fast the wick dries: slow, normal or fast, in 60, 45 or 30 minutes.",isSelect:!0}};function ct(e,t){let n=Object.keys(Se).filter(s=>t.settingControls.includes(s));return n.length===0?null:o("section",{},o("h2",{},"More controls"),o("p",{class:"help"},"Controls of this machine that Matter has no place for. As in the MyDyson app, most of them turn a machine that is off on: the air quality target and the wick drying do not."),o("div",{class:"choices"},...n.map(s=>{let{label:r,text:a,isSelect:h}=Se[s],l=`Add a ${h?"mode select":"switch"} for the ${r.toLowerCase().replace("uv","UV")}`,p=h?`${f(e,r,!0)}, and ${a} ${H}`:`${f(e,r,!0)}. ${a}`;return k(s,e[s],l,p,v=>g(e,{[s]:v}))})))}function ht(e,t){return t.settingControls.includes("hasWaterHardnessSelect")?o("section",{},o("h2",{},"Clean cycle"),o("p",{class:"help"},"The machine asks for a clean cycle as often as the hardness of its water says: the harder the water, the sooner. The setup page shows when the next one is due."),o("div",{class:"choices"},k("water-hardness",e.hasWaterHardnessSelect,"Add a mode select for the water hardness",`${f(e,"Water Hardness",!0)}, and picks soft, medium or hard. ${H}`,n=>g(e,{hasWaterHardnessSelect:n})))):null}var ut=[["pm25","PM2.5"],["pm10","PM10"],["volatileOrganicCompounds","VOC"],["nitrogenDioxide","NO\u2082"],["formaldehyde","formaldehyde"],["carbonDioxide","CO\u2082"]];function ve(e,t="and"){return e.length===1?e[0]:`${e.slice(0,-1).join(", ")} ${t} ${e[e.length-1]}`}function pt(e,t){let n=e.reportedSensors??se,s=ut.filter(([l])=>n[l]).map(([,l])=>l),r=s.length===0?"each pollutant the machine measures, once it gives it":`the ${ve(s)} the machine measures`,a=[n.temperature?"":"room temperature",n.humidity?"":"humidity"].filter(l=>l!==""),h=a.length===0?"Each sensor shows what the machine measures.":`This machine has given no ${ve(a,"or")} reading, so there is no sensor for ${a.length===1?"it":"them"}. If it gives one later, open its options again.`;return o("section",{},o("h2",{},"Readings"),o("p",{class:"help"},h),o("div",{class:"choices"},t.hasAirQualitySensor?k("air-quality",e.hasAirQualitySensor,"Add an air quality sensor",`${f(e,"Air Quality",!1)}, with the air quality of its worst pollutant and ${r}.`,l=>g(e,{hasAirQualitySensor:l})):null,n.temperature?k("temperature",e.hasTemperatureSensor,"Add a temperature sensor",`${f(e,"Temperature",!1)}, with the room temperature the machine measures.`,l=>g(e,{hasTemperatureSensor:l})):null,n.humidity?k("humidity",e.hasHumiditySensor,"Add a humidity sensor",`${f(e,"Humidity",!1)}, with the room humidity the machine measures.`,l=>g(e,{hasHumiditySensor:l})):null,t.hasContinuousMonitoring?k("continuous-monitoring",e.hasContinuousMonitoringSwitch,"Add a switch for the continuous monitoring",`${f(e,"Continuous Monitoring",!0)}. On keeps the machine measuring the air while it is off, so that the readings stay fresh.`,l=>g(e,{hasContinuousMonitoringSwitch:l})):null))}function dt(e){let t=e.deviceType==="airPurifier"?"the air purifier":"the fan";return o("section",{},o("h2",{},"One device or several"),o("p",{class:"help"},`The thermostat, the switches, the mode selects and the sensors can be parts of ${t} in Matter, or devices of their own beside it.`),o("div",{class:"choices"},k("composed",e.isComposed,`Make them parts of ${t}`,`Your Matter app has one device, "${e.name}", and they move between rooms with it. A part has no name of its own in Matter, so an app may show the switches as numbered outlets, and an app that shows no parts shows ${t} alone. Turned off, each is a device of its own with its own name, which your app puts in a room by itself.`,n=>g(e,{isComposed:n}))))}function Ce(){let e=b();if(e===void 0)return[w()];let t=e.controls??re;return[N(),U(),B(e,`Options for the ${j(e.machine)}. Power and fan speed always appear in Matter: choose how the rest appears.`),Ze(e),Xe(e,t),_e(e,t),et(e,t),ot(e,t),it(e),st(e,t),rt(e,t),at(e,t),lt(e,t),ht(e,t),ct(e,t),pt(e,t),dt(e),e.isNew?null:o("p",{class:"help"},"Changing any of these rebuilds this machine in Matter. Your Matter app may take a moment to show the change."),w(),o("div",{class:"footer"},u("Cancel",S),u(e.isNew?`Add ${e.name}`:"Save options",()=>{pe(e)},"primary"))]}var ft="An account in mainland China is on a Dyson cloud of its own: pick China for it.";function mt(){let e=i.signIn.country===""?"US":i.signIn.country,t=i.countries.length===0?o("input",{type:"text",id:"country",value:e,maxLength:2,autocomplete:"country",spellcheck:!1}):o("select",{id:"country"},...i.countries.map(n=>o("option",{value:n,selected:n===e},gt(n))));return o("div",{class:"field"},o("label",{for:"country"},"Country of the account"),o("p",{class:"help"},ft),t)}function gt(e){try{return new Intl.DisplayNames(void 0,{type:"region"}).of(e)??e}catch{return e}}function yt(){let e=o("input",{type:"email",id:"email",value:i.signIn.email,autocomplete:"username",spellcheck:!1}),t=()=>{let n=document.getElementById("country"),s=n instanceof HTMLInputElement||n instanceof HTMLSelectElement?n.value.trim().toUpperCase():"";e.value.trim()===""||s===""||_(e.value.trim(),s)};return[o("div",{class:"field"},o("label",{for:"email"},"Email address"),o("p",{class:"help"},"The address you sign in to the MyDyson app with. Dyson emails it a code."),e),mt(),o("div",{class:"footer"},u("Cancel",S),u("Email me a code",t,"primary"))]}function wt(){let e=o("input",{type:"text",id:"otp-code",inputMode:"numeric",autocomplete:"one-time-code",spellcheck:!1,maxLength:16}),t=o("input",{type:"password",id:"password",autocomplete:"current-password",maxLength:256}),n=()=>{e.value.trim()===""||t.value===""||ce(t.value,e.value.trim())};return t.addEventListener("keydown",s=>{s.key==="Enter"&&n()}),[o("p",{class:"lead"},`Dyson emailed a code to ${i.signIn.email}. It can take a minute to arrive.`),o("div",{class:"field"},o("label",{for:"otp-code"},"Code from the email"),e),o("div",{class:"field"},o("label",{for:"password"},"Password of the account"),o("p",{class:"help"},"It is used once to sign in and is not stored."),t),o("div",{class:"footer"},u("Use another address",()=>{i.signIn={email:i.signIn.email,country:i.signIn.country},i.error="",c()}),u("Send a new code",()=>{_(i.signIn.email,i.signIn.country)}),u("Sign in",n,"primary"))]}function be(){let e=i.setup?.account!==void 0;return[o("header",{},o("h1",{},e?"Sign in to Dyson again":"Sign in to Dyson")),o("p",{class:"lead"},"Dyson machines are reached through the Dyson cloud, with your Dyson account. The plugin keeps the sign-in Dyson gives it in its configuration, and never your password."),w(),...i.signIn.challengeId===void 0?yt():wt()]}function Ae(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let t of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let n=e.getPropertyValue(t).trim();n!==""&&document.documentElement.style.setProperty(`--host${t.slice(1)}`,n)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}le({loading:()=>[o("p",{class:"notice working",role:"status"},o("span",{class:"spinner"}),"Loading\u2026")],home:we,signIn:be,find:ye,connect:fe,options:Ce,done:me});Ae();c();W();
|
|
1
|
+
async function y(e,t,n){try{let[s,r]=e.split(" "),a={method:s};t!==void 0&&(a.headers={"Content-Type":"application/json"},a.body=JSON.stringify(t));let l=await fetch(new URL(`api/${r}${n===void 0?"":`?${n}`}`,document.baseURI),a);if(l.status===204)return{ok:!0};if(l.status===503)return{ok:!1,error:"The plugin is not running. Start it in Matterbridge, then reload this page."};let c=await l.json();return"ok"in c?c:{ok:!1,error:c.error??`The plugin answered with status ${l.status}`}}catch{return{ok:!1,error:"Matterbridge did not answer. Check that it is running, then try again."}}}var i={screen:"loading",setup:void 0,countries:[],signIn:{email:"",country:""},machines:[],selected:new Set,queue:[],saved:[],handled:[],isBusy:!1,busyText:"",error:"",info:"",confirmingRemoval:"",confirmingSignOut:!1};function b(){return i.queue[0]}function $(e){return e.name===""?e.machine.serialNumber:e.name}function G(e){return e.productName??`Dyson machine of type ${e.type}`}function j(e){let t=`${G(e)} ${e.serialNumber}`;return e.name===void 0?t:`${t}, named "${e.name}" in the MyDyson app`}var Y=["hasChildLockSwitch","hasUvLightSwitch","hasIoniserSwitch","hasJetFocusSwitch","hasIndirectAirflowSwitch","hasAirQualityTargetSelect","hasWickDryingSwitch","hasWickDryingSpeedSelect","hasWaterHardnessSelect"];function L(e){let{deviceType:t,fanControl:n,fanAutoControl:s,oscillationControl:r,nightModeControl:a,airflowDirectionControl:l,heatControl:c,humidificationControl:d,findAndFollowControl:v,hasChildLockSwitch:C,hasUvLightSwitch:T,hasIoniserSwitch:P,hasJetFocusSwitch:m,hasIndirectAirflowSwitch:k,hasAirQualityTargetSelect:z,hasWickDryingSwitch:ie,hasWickDryingSpeedSelect:Me,hasWaterHardnessSelect:De,hasContinuousMonitoringSwitch:Te,hasAirQualitySensor:$e,hasTemperatureSensor:Ie,hasHumiditySensor:Ee,isComposed:He}=e;return{deviceType:t,fanControl:n,fanAutoControl:s,oscillationControl:r,nightModeControl:a,airflowDirectionControl:l,heatControl:c,humidificationControl:d,findAndFollowControl:v,hasChildLockSwitch:C,hasUvLightSwitch:T,hasIoniserSwitch:P,hasJetFocusSwitch:m,hasIndirectAirflowSwitch:k,hasAirQualityTargetSelect:z,hasWickDryingSwitch:ie,hasWickDryingSpeedSelect:Me,hasWaterHardnessSelect:De,hasContinuousMonitoringSwitch:Te,hasAirQualitySensor:$e,hasTemperatureSensor:Ie,hasHumiditySensor:Ee,isComposed:He}}function Z(e,t){let n=L(e);t.hasAutoMode||(n.fanAutoControl="native");let s=t.oscillationAngles.length===0&&t.tiltAngles.length===0;t.hasOscillation&&n.oscillationControl==="select"&&s&&(n.oscillationControl="switch"),t.hasHeat||(n.heatControl="hidden"),t.hasHumidification||(n.humidificationControl="hidden"),t.hasFindAndFollow||(n.findAndFollowControl="hidden");for(let r of Y)n[r]&&=t.settingControls.includes(r);return n.hasContinuousMonitoringSwitch&&=t.hasContinuousMonitoring,n.hasAirQualitySensor&&=t.hasAirQualitySensor,n}var Ne={deviceType:"airPurifier",fanControl:"steps",fanAutoControl:"native",oscillationControl:"native",nightModeControl:"native",airflowDirectionControl:"native",heatControl:"thermostat",humidificationControl:"select",findAndFollowControl:"hidden",hasChildLockSwitch:!1,hasUvLightSwitch:!1,hasIoniserSwitch:!1,hasJetFocusSwitch:!1,hasIndirectAirflowSwitch:!1,hasAirQualityTargetSelect:!1,hasWickDryingSwitch:!1,hasWickDryingSpeedSelect:!1,hasWaterHardnessSelect:!1,hasContinuousMonitoringSwitch:!1,hasAirQualitySensor:!0,hasTemperatureSensor:!0,hasHumiditySensor:!0,isComposed:!0},Le=32;function Oe(e,t){if(e===""||!t.has(e))return e;for(let n=2;;n++){let s=` ${n}`,r=`${e.slice(0,Le-s.length).trimEnd()}${s}`;if(!t.has(r))return r}}function se(e){let t=new Set((i.setup?.devices??[]).map(n=>n.name));return e.map(n=>{let s=Oe(n.name??"",t);return t.add(s),{machine:n,name:s,host:"",isCloudOnly:!1,isNew:!0,...L(Ne)}})}function re(e){let{capabilities:t,controls:n,reportedSensors:s,name:r,roomName:a,host:l,isCloudOnly:c,serialNumber:d,type:v,variant:C,mqttRootTopicLevel:T,model:P,productName:m,firmwareVersion:k,firmwareCapabilities:z}=e;return{machine:{serialNumber:d,type:v,category:"environmentalCare",name:a,variant:C,mqttRootTopicLevel:T,model:P,productName:m,firmwareVersion:k,firmwareCapabilities:z},name:r,host:l??"",isCloudOnly:c??!1,isNew:!1,capabilities:t,controls:n,reportedSensors:s,...L(e)}}var ae={temperature:!1,humidity:!1,airQuality:!1,pm25:!1,pm10:!1,volatileOrganicCompounds:!1,nitrogenDioxide:!1,formaldehyde:!1,carbonDioxide:!1},le={hasAutoMode:!1,hasOscillation:!1,oscillationAngles:[],tiltAngles:[],hasFindAndFollow:!1,hasAirflowDirection:!1,hasHeat:!1,hasHumidification:!1,hasAutoHumidification:!1,settingControls:[],hasFilterLife:!1,hasContinuousMonitoring:!1,hasAirQualitySensor:!1};var q={apple:{title:"Apple Home",text:"Fan speed as a percentage with its auto mode, a switch for the oscillation, the night mode, the find and follow, the jet focus, the UV light and the ioniser, with a sensor for the air quality, the temperature and the humidity, each a device of its own."},google:{title:"Google Home",text:"Fan speed as steps, a switch for the auto mode, the oscillation, the night mode, the find and follow, the jet focus, the UV light and the ioniser, with a sensor for the air quality, the temperature and the humidity, each a device of its own."},matter:{title:"Full Matter support",text:"For apps that show every Matter control: the auto mode, the oscillation, the night mode and the airflow direction on the fan control itself, the target humidity, the find and follow and the settings of the machine as mode selects and switches, and every sensor, all as one device."}};function O(e,t){let n=e==="matter",s=n?"native":"switch";return Z({deviceType:t.hasAirQualitySensor?"airPurifier":"fan",fanControl:e==="apple"?"percent":"steps",fanAutoControl:e==="google"?"switch":"native",oscillationControl:s,nightModeControl:s,airflowDirectionControl:"native",heatControl:"thermostat",humidificationControl:n?"select":"switches",findAndFollowControl:n?"select":"switch",hasChildLockSwitch:n,hasUvLightSwitch:!0,hasIoniserSwitch:!0,hasJetFocusSwitch:!0,hasIndirectAirflowSwitch:!0,hasAirQualityTargetSelect:n,hasWickDryingSwitch:n,hasWickDryingSpeedSelect:n,hasWaterHardnessSelect:n,hasContinuousMonitoringSwitch:n,hasAirQualitySensor:!0,hasTemperatureSensor:!0,hasHumiditySensor:!0,isComposed:n},t)}function ce(e,t,n){return JSON.stringify(Z(e,n))===JSON.stringify(O(t,n))}var X;function he(e){X=e}function h(){let e=document.getElementById("app");if(e===null||X===void 0)return;let t=`${i.screen}:${b()?.machine.serialNumber??""}`,n=e.dataset.form===t,s=document.activeElement,r=s instanceof HTMLElement?s.id:"",a=s instanceof HTMLInputElement&&(s.type==="text"||s.type==="password")?{start:s.selectionStart,end:s.selectionEnd}:void 0,l=new Map;for(let d of e.querySelectorAll("input[type=text], input[type=email], input[type=password]"))d.id!==""&&l.set(d.id,d.value);if(e.replaceChildren(...X[i.screen]().filter(d=>d!==!1&&d!==null&&d!==void 0)),e.dataset.form=t,!n)return;for(let[d,v]of l){let C=document.getElementById(d);C instanceof HTMLInputElement&&(C.value=v)}if(r==="")return;let c=document.getElementById(r);c?.focus(),a!==void 0&&c instanceof HTMLInputElement&&c.setSelectionRange(a.start,a.end)}async function A(e,t){i.isBusy=!0,i.busyText=e,i.error="",i.info="",h();try{return await t()}finally{i.isBusy=!1,h()}}async function W(){let e=await y("GET state");if(!e.ok){i.error=e.error,i.screen="home",h();return}if(R(e.state),e.state.account===void 0){await I();return}if(e.state.devices.length===0&&!e.state.account.needsSignIn){await E();return}i.screen="home",h(),Re()}async function Re(){let e=await y("POST sync");!e.ok||JSON.stringify(e.state)===JSON.stringify(i.setup)||(R(e.state),h())}var Fe=2e3,_;function R(e){i.setup=e,!(_!==void 0||!e.devices.some(t=>t.status==="connecting"))&&(_=window.setTimeout(()=>{_=void 0,x().then(h)},Fe))}async function x(){let e=await y("GET state");e.ok&&R(e.state)}function S(){i.queue=[],i.saved=[],i.handled=[],i.error="",i.info="",i.screen="home",x().then(h),h()}async function I(){let e=i.setup?.account;if(i.signIn={email:e?.email??"",country:e?.country??""},i.error="",i.info="",i.screen="signIn",h(),i.countries.length>0)return;let t=await y("GET countries");t.ok&&(i.countries=t.countries),h()}async function ee(e,t){i.signIn={email:e,country:t};let n=await A(`Asking Dyson to email a code to ${e}\u2026`,async()=>y("POST login-code",{email:e,country:t}));if(!n.ok){i.error=n.error,h();return}i.signIn={email:e,country:t,challengeId:n.challengeId},h()}async function ue(e,t){let{email:n,country:s,challengeId:r}=i.signIn,a=await A("Signing in\u2026",async()=>y("POST login",{email:n,country:s,challengeId:r,password:e,otpCode:t}));if(!a.ok){i.error=a.error,h();return}R(a.state),i.signIn={email:n,country:s},a.state.devices.length===0?await E():S()}async function pe(){let e=await A("Signing out\u2026",async()=>y("DELETE account"));i.confirmingSignOut=!1,e.ok||(i.error=e.error),await x(),h()}async function E(){i.screen="find";let e=await A("Reading the machines of your Dyson account\u2026",async()=>y("POST machines"));if(!e.ok){i.error=`The machines could not be read: ${e.error}`,h();return}await x(),i.machines=e.machines;for(let t of e.machines)!t.isConfigured&&t.support==="supported"&&i.selected.add(t.serialNumber);h()}function Q(e){i.queue=e,i.saved=[],i.handled=[],te()}async function te(){let e=b();if(e===void 0){i.screen=i.saved.length>0?"done":"home",await x(),h();return}i.screen="connect",i.error="",i.info="",e.probe=void 0,h(),await ne(e)}async function ne(e){let t=await A(`Connecting to ${$(e)} through the Dyson cloud, and looking for it on your network\u2026`,async()=>y("POST probe",{machine:e.machine}));if(!t.ok){i.error=t.error,h();return}if(e.probe=t.result,t.result.status==="signInRequired"&&await x(),t.result.status==="ready"){let n=e.controls===void 0;e.capabilities=t.result.capabilities,e.controls=t.result.controls,e.reportedSensors=t.result.reportedSensors,e.isNew&&n&&e.controls!==void 0&&Object.assign(e,O("apple",e.controls)),e.isNew&&n&&t.result.isFoundOnLan===!1&&(e.isCloudOnly=!0),i.screen="options"}h()}function de(){let e=i.queue.shift();e!==void 0&&i.handled.push({label:$(e),isSaved:!1}),te()}async function fe(e){let t=e.name.trim(),n=Pe(e,t);if(n!==""){i.error=n,h();return}let{machine:s}=e,r=e.isCloudOnly||e.host===""?void 0:e.host,a={...s,name:t,roomName:s.name,host:r,isCloudOnly:e.isCloudOnly||void 0,...L(e)},l=await A(`Saving ${t}\u2026`,async()=>y("POST device",{device:a}));if(!l.ok){i.error=l.error,h();return}R(l.state),i.saved.push(t),i.handled.push({label:t,isSaved:!0}),i.queue.shift(),await te()}function Pe(e,t){return t===""?"Give the machine a name: it is the name it gets in Matter.":(i.setup?.devices??[]).some(s=>s.name===t&&s.serialNumber!==e.machine.serialNumber)?`"${t}" is already the name of another machine. Pick a different one.`:""}var oe="Matterbridge's own list of devices shows the change once you reload its page.";async function me(e){let t=await A(`Removing ${e.name}\u2026`,async()=>y("DELETE device",void 0,`serialNumber=${encodeURIComponent(e.serialNumber)}`));i.confirmingRemoval="",t.ok?i.info=`${e.name} is removed. ${oe}`:i.error=t.error,await x(),h()}function o(e,t={},...n){let s=document.createElement(e);for(let[r,a]of Object.entries(t))a===void 0||a===!1||(r==="class"?s.className=String(a):r==="html"?s.innerHTML=String(a):r.startsWith("on")&&typeof a=="function"?s.addEventListener(r.slice(2),a):r in s?s[r]=a:s.setAttribute(r,String(a)));for(let r of n)r!==!1&&r!==null&&r!==void 0&&s.append(r);return s}function u(e,t,n="quiet",s=!1){return o("button",{type:"button",class:`button ${n}`,onclick:t,disabled:s||i.isBusy},e)}function w(){return i.isBusy?o("p",{class:"notice working",role:"status"},o("span",{class:"spinner"}),i.busyText):i.error!==""?o("p",{class:"notice problem",role:"alert"},i.error):i.info!==""?o("p",{class:"notice",role:"status"},i.info):null}function U(){let e=b();if(e===void 0||!e.isNew||i.handled.length+i.queue.length<2)return null;let t=i.screen==="connect"?"Connecting":"Choosing options";return o("ol",{class:"units","aria-label":"Machines in this setup"},...i.handled.map(n=>o("li",{class:n.isSaved?"done":"skipped"},o("strong",{},n.label),o("span",{class:"meta"},n.isSaved?"Added":"Skipped"))),...i.queue.map((n,s)=>o("li",{class:s===0?"current":"ahead","aria-current":s===0?"true":void 0},o("strong",{},$(n)),o("span",{class:"meta"},s===0?t:"Waiting"))))}function B(e,t){return o("header",{class:"unit-header"},o("h1",{},$(e)),o("p",{class:"meta"},t))}function H(){if(i.queue.length>0&&!i.queue.some(s=>s.isNew))return null;let e=["Find","Connect","Options"],n=["find","connect","options"].indexOf(i.screen);return o("ol",{class:"steps"},...e.map((s,r)=>{let a="ahead";return r<n?a="done":r===n&&(a="current"),o("li",{class:a,"aria-current":r===n?"step":void 0},s)}))}function je(e){let t=e.probe?.status,n=e.name===""?"The machine":e.name;return t==="signInRequired"?o("div",{class:"field"},o("p",{class:"notice problem",role:"alert"},"The Dyson cloud no longer accepts the sign-in of this plugin."),u("Sign in again",()=>{I()},"primary")):t==="unreachable"?o("p",{class:"notice problem",role:"alert"},`${n} did not answer. Check that it is plugged in and shows as online in the MyDyson app, then try again.`):t==="unsupported"?o("p",{class:"notice problem",role:"alert"},`The plugin cannot control ${n}: ${e.probe?.message??"unknown"}`):t==="failed"?o("p",{class:"notice problem",role:"alert"},`${n} answered with an error: ${e.probe?.message??"unknown"}`):null}function ge(){let e=b();if(e===void 0)return[w()];let t=e.probe?.status==="unreachable"||e.probe?.status==="failed"||i.error!=="",n=e.isNew&&i.queue.length>1;return[H(),U(),B(e,`Connecting to the ${j(e.machine)} through the Dyson cloud.`),w(),i.isBusy?null:je(e),o("div",{class:"footer"},t?u("Try again",()=>{ne(e)}):null,u(n?"Skip this one":"Cancel",n?de:S))]}function ye(){let e=i.saved;return[o("header",{},o("h1",{},e.length===1?`${e[0]} is set up`:`${e.length} machines are set up`)),o("p",{class:"lead"},"They now appear in the Matter apps paired with Matterbridge. If Matterbridge is not paired yet, pair it from its home page."),o("p",{class:"help"},oe),o("div",{class:"footer"},u("Done",S,"primary"))]}function we(e){return!e.isConfigured&&e.support==="supported"}function qe(e){let t=[];return e.isConfigured&&t.push("Already set up."),e.support==="otherCategory"&&t.push("Not a purifier, fan, heater or humidifier: the plugin does not control it."),e.support==="unknownType"&&t.push(`The plugin does not know machines of type ${e.type} yet.`),e.support==="otherBroker"&&t.push("It is reached through a Dyson cloud service the plugin cannot connect to yet."),t}function We(e){let t=we(e),n=`machine-${e.serialNumber}`,s=o("input",{type:"checkbox",id:n,checked:t&&i.selected.has(e.serialNumber),disabled:!t,onchange:()=>{s.checked?i.selected.add(e.serialNumber):i.selected.delete(e.serialNumber),h()}});return o("li",{class:t?"found":"found unavailable"},o("label",{for:n},s,o("span",{},o("strong",{},e.name??"No room name"),o("span",{class:"meta"},` ${G(e)}, ${e.serialNumber}`),...qe(e).map(r=>o("span",{class:"meta note"},r)))))}function Se(){let e=i.machines.filter(n=>i.selected.has(n.serialNumber)&&we(n)),t=e.length===1?"Set up 1 machine":`Set up ${e.length} machines`;return[H(),o("header",{},o("h1",{},"Your Dyson machines")),w(),i.machines.length>0?o("ul",{class:"found-list"},...i.machines.map(We)):null,!i.isBusy&&i.machines.length===0&&i.error===""?o("p",{class:"empty"},"Your Dyson account has no machine yet. Add your machines in the MyDyson app, then read the list again."):null,o("div",{class:"footer"},u("Read the list again",()=>{E()}),u("Cancel",S),u(t,()=>Q(se(e)),"primary",e.length===0))]}var Qe={hasChildLockSwitch:"child lock",hasUvLightSwitch:"UV light",hasIoniserSwitch:"ioniser",hasJetFocusSwitch:"jet focus",hasIndirectAirflowSwitch:"indirect airflow",hasAirQualityTargetSelect:"air quality target",hasWickDryingSwitch:"wick drying",hasWickDryingSpeedSelect:"wick drying speed",hasWaterHardnessSelect:"water hardness"},F={native:"on the fan",switch:"as a switch",select:"as a mode select",hidden:""};function Ue(e){let t=e.controls,n=m=>t===void 0||t[m],s=t!==void 0&&t.tiltAngles.length>0,r=e.deviceType==="airPurifier"?"an air purifier":"a fan",a={native:"",switch:"",firstStep:" with auto first",lastStep:" with auto last"}[e.fanAutoControl],l=[`${r} with speed ${e.fanControl==="steps"?"steps":"as a percentage"}${n("hasAutoMode")?a:""}`],c=new Map,d=(m,k)=>{m!==""&&c.set(m,[...c.get(m)??[],k])};n("hasAutoMode")&&e.fanAutoControl==="switch"&&d(F.switch,"the auto mode"),n("hasOscillation")&&d(F[e.oscillationControl],s?"the tilt":"the oscillation"),n("hasFindAndFollow")&&d(F[e.findAndFollowControl],"the find and follow"),d(F[e.nightModeControl],"the night mode"),n("hasAirflowDirection")&&d(F[e.airflowDirectionControl],"the airflow direction");for(let[m,k]of c)l.push(`${k.join(", ")} ${m}`);n("hasHeat")&&e.heatControl!=="hidden"&&l.push(e.heatControl==="thermostat"?"a thermostat":"a heat switch"),n("hasHumidification")&&e.humidificationControl!=="hidden"&&l.push(e.humidificationControl==="select"?"a target humidity select":"humidify switches");let v=Y.filter(m=>e[m]&&(t===void 0||t.settingControls.includes(m))).map(m=>Qe[m]);v.length>0&&l.push(`a control for the ${v.join(", ")}`),n("hasFilterLife")&&e.deviceType==="airPurifier"&&l.push("the filter life");let{reportedSensors:C}=e,T=[n("hasAirQualitySensor")&&e.hasAirQualitySensor&&C.airQuality?"air quality":"",e.hasTemperatureSensor&&C.temperature?"temperature":"",e.hasHumiditySensor&&C.humidity?"humidity":""].filter(m=>m!=="");return T.length>0&&l.push(`a sensor for the ${T.join(", ")}`),`${l.length===1?l[0]:`${l.slice(0,-1).join(", ")} and ${l[l.length-1]}`}${e.isComposed?", all as one device":""}`}var Be={filter:"Filter",outerFilter:"HEPA filter",innerFilter:"Carbon filter"};function Ke(e){let t=e.remainingTimeOfActiveInMinutes;if(e.state==="active")return t===null?"Cleaning":`Cleaning, ${t} min left`;if(e.state==="paused")return"Clean cycle paused";if(e.state==="incomplete")return"Clean cycle not finished";if(e.state==="setup")return"Clean cycle being set up";let n=e.remainingTimeToNextInHours;return n===null?"":n<=0?"Clean cycle due":n<48?`Next clean cycle in ${n} hours`:`Next clean cycle in ${Math.round(n/24)} days`}function Je(e){let t=e.filters.map(({filter:a,lifeInPercent:l})=>`${Be[a]} ${l} %`).join(", "),n=e.cleanCycle===void 0?"":Ke(e.cleanCycle),s=e.wickDrying?.remainingTimeInMinutes,r="";return e.wickDrying!==void 0&&(r=s==null?"Drying its wick":`Drying its wick, ${s} min left`),[t,n,r].filter(a=>a!=="").map(a=>`${a}.`).join(" ")}function Ve(e){return e.replaceAll(/([a-z])([A-Z])/g,"$1 $2").toLowerCase()}function ze(e){if(e===void 0)return[];let t=Je(e),n=e.faults??[];return[t===""?null:o("p",{class:"meta"},t),n.length===0?null:o("p",{class:"meta problem-text"},`The machine reports: ${n.map(Ve).join(", ")}.`)]}var Ge={local:" on your network",remote:" through the Dyson cloud"};function Ye(e){return e.status!=="online"||e.hostType!=="remote"||!e.isTriedOnLan?null:e.host!==void 0?o("p",{class:"meta"},`It did not answer on your network at ${e.host}, so the plugin reaches it through the Dyson cloud. Check the host in its options.`):o("p",{class:"meta"},"It was not found on your network, so the plugin reaches it through the Dyson cloud. Where the search does not reach it, such as in a Docker container, enter its IP address in its options.")}function Ze(e){let n={online:`Online${e.hostType===void 0?"":Ge[e.hostType]}`,connecting:"Connecting\u2026",offline:"Not answering",stopped:"Not started"}[e.status],s=i.confirmingRemoval===e.serialNumber,r=e.productName??`type ${e.type}`;return o("li",{class:"device"},o("div",{class:"device-main"},o("h3",{},e.name),o("p",{class:"meta"},o("span",{class:`status ${e.status}`},n),` ${r}, ${e.serialNumber}. In Matter: ${Ue(e)}.`),Ye(e),...ze(e.report),e.status!=="online"&&e.error?o("p",{class:"meta problem-text"},e.error):null),s?o("div",{class:"actions"},o("span",{class:"meta"},"Remove it from Matter too?"),u("Remove",()=>{me(e)},"danger"),u("Keep",()=>{i.confirmingRemoval="",h()})):o("div",{class:"actions"},u(e.status==="online"?"Options":"Reconnect",()=>Q([re(e)]),"quiet",i.setup?.account===void 0),u("Remove",()=>{i.confirmingRemoval=e.serialNumber,h()})))}function Xe(){let e=i.setup?.account;if(i.setup===void 0)return null;if(e===void 0)return o("section",{class:"account"},o("p",{class:"notice problem",role:"alert"},"No Dyson account is signed in, so the machines below are not connected. They stay in Matter until you sign in again."),u("Sign in",()=>{I()},"primary"));let t=i.confirmingSignOut?[o("span",{class:"meta"},"The machines stay in Matter, unreachable until you sign in again."),u("Sign out",()=>{pe()},"danger"),u("Stay signed in",()=>{i.confirmingSignOut=!1,h()})]:[u(e.needsSignIn?"Sign in again":"Switch account",()=>{I()},e.needsSignIn?"primary":"quiet"),u("Sign out",()=>{i.confirmingSignOut=!0,h()})];return o("section",{class:"account"},o("p",{class:"meta"},`Signed in to Dyson as ${e.email}.`),e.needsSignIn?o("p",{class:"notice problem",role:"alert"},"The Dyson cloud no longer accepts this sign-in. Sign in again to keep the machines connected."):null,o("div",{class:"actions"},...t))}function ve(){let e=i.setup?.devices??[],t=i.setup?.account!==void 0&&!i.setup.account.needsSignIn;return[o("header",{},o("h1",{},"Dyson machines"),t?u("Add machines",()=>{E()},"primary"):null),w(),Xe(),e.length===0&&i.setup?o("p",{class:"empty"},"No machine is set up yet. Add one to control it from Matter."):o("ul",{class:"devices"},...e.map(Ze)),i.setup?null:u("Try again",()=>{W()},"primary")]}var K={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>',purifier:'<svg viewBox="0 0 48 28" aria-hidden="true"><rect class="stroke" x="16" y="2" width="16" height="24" rx="3"/><path class="stroke" d="M20 8 H28 M20 13 H28 M20 18 H28"/></svg>',fan:'<svg viewBox="0 0 48 28" aria-hidden="true"><circle class="stroke" cx="24" cy="12" r="9"/><path class="stroke" d="M24 21 V26 M18 26 H30"/></svg>'},N="Few apps show a mode select: Home Assistant does, Apple Home does not.",V="Not every app shows it there: Apple Home and Google Home are not known to.";function f(e,t,n){return e.isComposed?n?`It appears as a part of "${e.name}", tagged "${t}"`:`It appears as a part of "${e.name}"`:`It appears as "${e.name} ${t}"`}function g(e,t){Object.assign(e,t),h()}function J(e,t,n,s,r,a,l){let c=`${e}-${t}`,d=o("input",{type:"radio",name:e,id:c,checked:t===n,onchange:()=>l(t)});return o("label",{class:"choice pictured",for:c},d,o("span",{class:"glyph",html:a}),o("span",{},o("strong",{},s),o("span",{class:"meta"},r)))}function p(e,t,n,s,r,a){let l=`${e}-${t}`,c=o("input",{type:"radio",name:e,id:l,checked:t===n,onchange:()=>a(t)});return o("label",{class:"choice",for:l},c,o("span",{},o("strong",{},s),o("span",{class:"meta"},r)))}function M(e,t,n,s,r){let a=o("input",{type:"checkbox",id:e,checked:t,onchange:()=>r(a.checked)});return o("label",{class:"choice",for:e},a,o("span",{},o("strong",{},n),o("span",{class:"meta"},s)))}function D(e,t,...n){let s=`${e.toLowerCase().replaceAll(" ","-")}-title`;return o("section",{},o("h2",{id:s},e),t===""?null:o("p",{class:"help"},t),o("div",{class:"choices",role:"radiogroup","aria-labelledby":s},...n))}function _e(e){let t=o("input",{type:"text",id:"device-name",value:e.name,maxLength:32,autocomplete:"off",spellcheck:!1});t.addEventListener("input",r=>{e.name=t.value.trim(),r.isComposing||h()});let n=e.machine.name,s=n===void 0?"It has no room name in the MyDyson app.":`Its room name in the MyDyson app is "${n}", which other machines in that room share.`;return o("section",{},o("h2",{},o("label",{for:"device-name"},"Name in Matter")),o("p",{class:"help"},`${s} The name here is what your Matter apps show, up to 32 characters, and no two machines of this plugin share one. A new room name in the MyDyson app does not change it.`),o("div",{class:"inline-form"},t))}function et(e,t){return o("section",{class:"presets"},o("h2",{},"Start from a preset"),o("div",{class:"preset-list"},...Object.keys(q).map(n=>o("button",{type:"button",class:"preset","aria-pressed":String(ce(e,n,t)),onclick:()=>g(e,O(n,t))},o("strong",{},q[n].title),o("span",{class:"meta"},q[n].text)))),o("p",{class:"help"},"A preset fills in the choices below for what this machine can do. Change any of them afterwards."))}function tt(e,t){let n=r=>g(e,{deviceType:r}),s=t.hasFilterLife?" and the life left in its filters, which a Matter app can reset after a new one is fitted":"";return o("section",{},o("h2",{},"Appears as"),o("div",{class:"choices"},J("device-type","airPurifier",e.deviceType,"An air purifier",`The Matter device type of a purifier, with its fan speed${s}. Apple Home, Google Home and Home Assistant show it.`,K.purifier,n),J("device-type","fan",e.deviceType,"A fan",`The Matter device type every app shows, with the same fan speed${t.hasFilterLife?", but no filters":""}. For apps that show no air purifier.`,K.fan,n)))}function nt(e,t){let n=c=>g(e,{fanControl:c}),s=c=>g(e,{fanAutoControl:c}),r=t.hasAutoMode?e.fanAutoControl:"native",l=r==="firstStep"||r==="lastStep"?"Eleven steps: the ten speeds and auto.":"Ten steps, one for each speed.";return o("section",{},o("h2",{},"Fan speed"),o("div",{class:"choices"},J("fan","steps",e.fanControl,"Steps",`${l} A slider shows them as a percentage too.`,K.steps,n),J("fan","percent",e.fanControl,"Percentage","A slider from 0 to 100, which the plugin rounds to the nearest speed: 10 % is speed 1 and 100 % speed 10.",K.percent,n)),t.hasAutoMode?o("div",{class:"field"},o("h3",{id:"fan-auto-title"},"Auto mode"),o("p",{class:"help"},"In its auto mode the machine picks the fan speed from the air it measures. Matter has it as the fan mode Auto, which not every app shows."),o("div",{class:"choices",role:"radiogroup","aria-labelledby":"fan-auto-title"},p("fan-auto","native",r,"Native fan mode only","Auto is a mode of the fan speed, beside its speeds, as Matter has it.",s),p("fan-auto","switch",r,"A switch",`${f(e,"Auto Mode",!0)}. On turns the auto mode on, off goes back to the speed the fan last ran at. Auto stays a fan mode as well.`,s),p("fan-auto","firstStep",r,"The first step","Auto comes below speed 1: step 1, or the bottom of the slider.",s),p("fan-auto","lastStep",r,"The last step","Auto comes above speed 10: the top step, or 100 %.",s))):null)}function ot(e){let t=e.map(n=>n==="breeze"?"breeze":`${n}\xB0`);return t.length===1?t[0]:`${t.slice(0,-1).join(", ")} and ${t[t.length-1]}`}function it(e){let t=n=>g(e,{oscillationControl:n});return D("Tilt","The machine tilts to 0\xB0, 25\xB0 or 50\xB0, or breezes: it tilts up and down. Tilting turns a machine that is off on, as in the MyDyson app.",p("oscillation","native",e.oscillationControl,"Native rocking",`The up and down rocking of the fan control, as Matter has it: on breezes, off goes back to the angle it was tilted to. ${V}`,t),p("oscillation","switch",e.oscillationControl,"A switch",`${f(e,"Oscillation",!0)}. On breezes, off goes back to the angle it was tilted to. It takes the place of the rocking of the fan control, which Apple Home mishandles.`,t),p("oscillation","select",e.oscillationControl,"A mode select",`${f(e,"Tilt",!0)}, and picks 0\xB0, 25\xB0, 50\xB0 or breeze. It takes the place of the rocking of the fan control, which Apple Home mishandles. ${N}`,t))}function st(e,t){if(!t.hasOscillation)return null;if(t.tiltAngles.length>0)return it(e);let n=a=>g(e,{oscillationControl:a}),s=t.oscillationAngles.length>0,r=e.oscillationControl==="select"&&!s?"switch":e.oscillationControl;return D("Oscillation","",p("oscillation","native",r,"Native rocking",`The left and right rocking of the fan control, as Matter has it. ${V}`,n),p("oscillation","switch",r,"A switch",`${f(e,"Oscillation",!0)}. On sweeps the angle set in the MyDyson app. It takes the place of the rocking of the fan control, which Apple Home mishandles.`,n),s?p("oscillation","select",r,"A mode select",`${f(e,"Oscillation",!0)}, and picks off or one of ${ot(t.oscillationAngles)}. It takes the place of the rocking of the fan control, which Apple Home mishandles. ${N}`,n):null)}function rt(e){let t=n=>g(e,{nightModeControl:n});return D("Night mode","In its night mode the machine runs quieter and dims its display.",p("night-mode","native",e.nightModeControl,"Native sleep wind only",`The sleep wind of the fan control, as Matter has it. ${V}`,t),p("night-mode","switch",e.nightModeControl,"A switch",`${f(e,"Night Mode",!0)}. The night mode stays the sleep wind of the fan control as well.`,t))}function at(e,t){if(!t.hasAirflowDirection)return null;let n=s=>g(e,{airflowDirectionControl:s});return D("Airflow direction","The machine blows its air out of the front, or out of the back to circulate the air without cooling you.",p("airflow","native",e.airflowDirectionControl,"Native airflow direction only",`The airflow direction of the fan control, as Matter has it: forward is the front. ${V}`,n),p("airflow","switch",e.airflowDirectionControl,"A switch",`${f(e,"Front Airflow",!0)}. On blows the air out of the front, off out of the back. The airflow direction of the fan control stays as well.`,n))}function lt(e,t){if(!t.hasFindAndFollow)return null;let n=s=>g(e,{findAndFollowControl:s});return D("Find and follow","The machine finds the people in the room and turns to follow them. It runs only while the machine is on, so turning it on turns the machine on.",p("find-and-follow","switch",e.findAndFollowControl,"A switch",`${f(e,"Find and Follow",!0)}. On follows the people the machine finds.`,n),p("find-and-follow","select",e.findAndFollowControl,"A mode select",`${f(e,"Find and Follow",!0)}, and picks off, follow, or scan to look for people. ${N}`,n),p("find-and-follow","hidden",e.findAndFollowControl,"Not shown in Matter","Find and follow stays as the MyDyson app sets it.",n))}function ct(e,t){if(!t.hasHeat)return null;let n=s=>g(e,{heatControl:s});return D("Heating","The machine heats only while it is on, so turning the heating on turns the machine on.",p("heat","thermostat",e.heatControl,"A thermostat",`${f(e,"Heater",!1)}: it heats or is off, with a setpoint from 1 to 37 \xB0C. Off leaves the fan running.`,n),p("heat","switch",e.heatControl,"A switch",`${f(e,"Heat",!0)}. It heats to the setpoint set in the MyDyson app.`,n),p("heat","hidden",e.heatControl,"Not shown in Matter","The heating stays as the MyDyson app sets it.",n))}function ht(e,t){if(!t.hasHumidification)return null;let n=a=>g(e,{humidificationControl:a}),s=t.hasAutoHumidification?' With a second switch, "Auto Humidify", for the machine to pick the target itself.':"",r=t.hasAutoHumidification?"off, auto, or a target from 30 to 70 %":"off, or a target from 30 to 70 %";return D("Humidifying","Matter has no humidifier, so the humidifying takes a switch or a mode select. It runs only while the machine is on, so turning it on turns the machine on.",p("humidify","switches",e.humidificationControl,"Switches",`${f(e,"Humidify",!0)}, and humidifies to the target set in the MyDyson app.${s}`,n),p("humidify","select",e.humidificationControl,"A mode select",`${f(e,"Target Humidity",!0)}, and picks ${r}. ${N}`,n),p("humidify","hidden",e.humidificationControl,"Not shown in Matter","The humidifying stays as the MyDyson app sets it.",n))}var Ce={hasChildLockSwitch:{label:"Child Lock",text:"On locks the buttons of the machine.",isSelect:!1},hasUvLightSwitch:{label:"UV Light",text:"On turns the UV light of the machine on.",isSelect:!1},hasIoniserSwitch:{label:"Ioniser",text:"On turns the ioniser of the machine on.",isSelect:!1},hasJetFocusSwitch:{label:"Jet Focus",text:"On focuses the air into one stream, and off diffuses it.",isSelect:!1},hasIndirectAirflowSwitch:{label:"Indirect Airflow",text:"On tilts the nozzle to 45\xB0, and off points it straight.",isSelect:!1},hasAirQualityTargetSelect:{label:"Air Quality Target",text:"picks the air quality the auto mode aims for: good, sensitive or very sensitive.",isSelect:!0},hasWickDryingSwitch:{label:"Wick Drying",text:"On dries the wick of the humidifier.",isSelect:!1},hasWickDryingSpeedSelect:{label:"Wick Drying Speed",text:"picks how fast the wick dries: slow, normal or fast, in 60, 45 or 30 minutes.",isSelect:!0}};function ut(e,t){let n=Object.keys(Ce).filter(s=>t.settingControls.includes(s));return n.length===0?null:o("section",{},o("h2",{},"More controls"),o("p",{class:"help"},"Controls of this machine that Matter has no place for. As in the MyDyson app, most of them turn a machine that is off on: the air quality target and the wick drying do not."),o("div",{class:"choices"},...n.map(s=>{let{label:r,text:a,isSelect:l}=Ce[s],c=`Add a ${l?"mode select":"switch"} for the ${r.toLowerCase().replace("uv","UV")}`,d=l?`${f(e,r,!0)}, and ${a} ${N}`:`${f(e,r,!0)}. ${a}`;return M(s,e[s],c,d,v=>g(e,{[s]:v}))})))}function pt(e,t){return t.settingControls.includes("hasWaterHardnessSelect")?o("section",{},o("h2",{},"Clean cycle"),o("p",{class:"help"},"The machine asks for a clean cycle as often as the hardness of its water says: the harder the water, the sooner. The setup page shows when the next one is due."),o("div",{class:"choices"},M("water-hardness",e.hasWaterHardnessSelect,"Add a mode select for the water hardness",`${f(e,"Water Hardness",!0)}, and picks soft, medium or hard. ${N}`,n=>g(e,{hasWaterHardnessSelect:n})))):null}var dt=[["pm25","PM2.5"],["pm10","PM10"],["volatileOrganicCompounds","VOC"],["nitrogenDioxide","NO\u2082"],["formaldehyde","formaldehyde"],["carbonDioxide","CO\u2082"]];function be(e,t="and"){return e.length===1?e[0]:`${e.slice(0,-1).join(", ")} ${t} ${e[e.length-1]}`}function ft(e,t){let n=e.reportedSensors??ae,s=dt.filter(([c])=>n[c]).map(([,c])=>c),r=s.length===0?"each pollutant the machine measures, once it gives it":`the ${be(s)} the machine measures`,a=[n.temperature?"":"room temperature",n.humidity?"":"humidity"].filter(c=>c!==""),l=a.length===0?"Each sensor shows what the machine measures.":`This machine has given no ${be(a,"or")} reading, so there is no sensor for ${a.length===1?"it":"them"}. If it gives one later, open its options again.`;return o("section",{},o("h2",{},"Readings"),o("p",{class:"help"},l),o("div",{class:"choices"},t.hasAirQualitySensor?M("air-quality",e.hasAirQualitySensor,"Add an air quality sensor",`${f(e,"Air Quality",!1)}, with the air quality of its worst pollutant and ${r}.`,c=>g(e,{hasAirQualitySensor:c})):null,n.temperature?M("temperature",e.hasTemperatureSensor,"Add a temperature sensor",`${f(e,"Temperature",!1)}, with the room temperature the machine measures.`,c=>g(e,{hasTemperatureSensor:c})):null,n.humidity?M("humidity",e.hasHumiditySensor,"Add a humidity sensor",`${f(e,"Humidity",!1)}, with the room humidity the machine measures.`,c=>g(e,{hasHumiditySensor:c})):null,t.hasContinuousMonitoring?M("continuous-monitoring",e.hasContinuousMonitoringSwitch,"Add a switch for the continuous monitoring",`${f(e,"Continuous Monitoring",!0)}. On keeps the machine measuring the air while it is off, so that the readings stay fresh.`,c=>g(e,{hasContinuousMonitoringSwitch:c})):null))}function mt(e){let t=e.probe?.isFoundOnLan;return t===void 0?"":t?" It answered on your network just now.":" It was not found on your network just now."}function gt(e){let t=o("input",{type:"text",id:"device-host",value:e.host,placeholder:"Found on your network",maxLength:253,autocomplete:"off",spellcheck:!1});return t.addEventListener("input",()=>{e.host=t.value.trim()}),o("div",{class:"field"},o("label",{for:"device-host"},"Host"),o("p",{class:"help"},"Where the search does not reach the machine, such as in a Docker container without host networking, enter its IP address, ideally one your router keeps for it. Leave it empty to search."),o("div",{class:"inline-form"},t))}function yt(e){let t=s=>{e.isCloudOnly=s==="cloud",h()},n=e.isCloudOnly?"cloud":"lan";return o("section",{},o("h2",{id:"connection-title"},"Connection"),o("p",{class:"help"},`How the plugin reaches the machine.${mt(e)}`),o("div",{class:"choices",role:"radiogroup","aria-labelledby":"connection-title"},p("connection","lan",n,"Your network, then the Dyson cloud","The plugin finds the machine on your network by the service it announces there, and reaches it through the Dyson cloud when it does not answer. It searches again on each connection, so it follows the machine to a new IP address.",t),p("connection","cloud",n,"The Dyson cloud only","For a machine that is not on the network of Matterbridge: each connection skips the search of up to 3 seconds.",t)),e.isCloudOnly?null:gt(e))}function wt(e){let t=e.deviceType==="airPurifier"?"the air purifier":"the fan";return o("section",{},o("h2",{},"One device or several"),o("p",{class:"help"},`The thermostat, the switches, the mode selects and the sensors can be parts of ${t} in Matter, or devices of their own beside it.`),o("div",{class:"choices"},M("composed",e.isComposed,`Make them parts of ${t}`,`Your Matter app has one device, "${e.name}", and they move between rooms with it. A part has no name of its own in Matter, so an app may show the switches as numbered outlets, and an app that shows no parts shows ${t} alone. Turned off, each is a device of its own with its own name, which your app puts in a room by itself.`,n=>g(e,{isComposed:n}))))}function ke(){let e=b();if(e===void 0)return[w()];let t=e.controls??le;return[H(),U(),B(e,`Options for the ${j(e.machine)}. Power and fan speed always appear in Matter: choose how the rest appears.`),_e(e),et(e,t),tt(e,t),nt(e,t),st(e,t),rt(e),at(e,t),lt(e,t),ct(e,t),ht(e,t),pt(e,t),ut(e,t),ft(e,t),wt(e),yt(e),e.isNew?null:o("p",{class:"help"},"Changing any of these but the connection rebuilds this machine in Matter. Your Matter app may take a moment to show the change."),w(),o("div",{class:"footer"},u("Cancel",S),u(e.isNew?`Add ${e.name}`:"Save options",()=>{fe(e)},"primary"))]}var St="An account in mainland China is on a Dyson cloud of its own: pick China for it.";function vt(){let e=i.signIn.country===""?"US":i.signIn.country,t=i.countries.length===0?o("input",{type:"text",id:"country",value:e,maxLength:2,autocomplete:"country",spellcheck:!1}):o("select",{id:"country"},...i.countries.map(n=>o("option",{value:n,selected:n===e},Ct(n))));return o("div",{class:"field"},o("label",{for:"country"},"Country of the account"),o("p",{class:"help"},St),t)}function Ct(e){try{return new Intl.DisplayNames(void 0,{type:"region"}).of(e)??e}catch{return e}}function bt(){let e=o("input",{type:"email",id:"email",value:i.signIn.email,autocomplete:"username",spellcheck:!1}),t=()=>{let n=document.getElementById("country"),s=n instanceof HTMLInputElement||n instanceof HTMLSelectElement?n.value.trim().toUpperCase():"";e.value.trim()===""||s===""||ee(e.value.trim(),s)};return[o("div",{class:"field"},o("label",{for:"email"},"Email address"),o("p",{class:"help"},"The address you sign in to the MyDyson app with. Dyson emails it a code."),e),vt(),o("div",{class:"footer"},u("Cancel",S),u("Email me a code",t,"primary"))]}function kt(){let e=o("input",{type:"text",id:"otp-code",inputMode:"numeric",autocomplete:"one-time-code",spellcheck:!1,maxLength:16}),t=o("input",{type:"password",id:"password",autocomplete:"current-password",maxLength:256}),n=()=>{e.value.trim()===""||t.value===""||ue(t.value,e.value.trim())};return t.addEventListener("keydown",s=>{s.key==="Enter"&&n()}),[o("p",{class:"lead"},`Dyson emailed a code to ${i.signIn.email}. It can take a minute to arrive.`),o("div",{class:"field"},o("label",{for:"otp-code"},"Code from the email"),e),o("div",{class:"field"},o("label",{for:"password"},"Password of the account"),o("p",{class:"help"},"It is used once to sign in and is not stored."),t),o("div",{class:"footer"},u("Use another address",()=>{i.signIn={email:i.signIn.email,country:i.signIn.country},i.error="",h()}),u("Send a new code",()=>{ee(i.signIn.email,i.signIn.country)}),u("Sign in",n,"primary"))]}function Ae(){let e=i.setup?.account!==void 0;return[o("header",{},o("h1",{},e?"Sign in to Dyson again":"Sign in to Dyson")),o("p",{class:"lead"},"Dyson machines are reached through the Dyson cloud, with your Dyson account. The plugin keeps the sign-in Dyson gives it in its configuration, and never your password."),w(),...i.signIn.challengeId===void 0?bt():kt()]}function xe(){try{if(window.parent===window)return;let e=getComputedStyle(window.parent.document.body);for(let t of["--primary-color","--div-bg-color","--div-text-color","--main-bg-color","--main-label-color","--div-border-color","--main-light-color"]){let n=e.getPropertyValue(t).trim();n!==""&&document.documentElement.style.setProperty(`--host${t.slice(1)}`,n)}document.documentElement.style.setProperty("--host-font",e.fontFamily)}catch{}}he({loading:()=>[o("p",{class:"notice working",role:"status"},o("span",{class:"spinner"}),"Loading\u2026")],home:ve,signIn:Ae,find:Se,connect:ge,options:ke,done:ye});xe();h();W();
|
|
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.
|