homebridge-roborock-matter 3.7.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.8.0
4
+
5
+ **The settings page follows Homebridge's dark theme, and the icon is in the header.**
6
+
7
+ The page stayed white inside a dark Homebridge, in every version. It was not reading the operating system's setting — that would have been wrong too. Homebridge UI reaches into this plugin's iframe and puts classes on our own `<body>`: `dark-mode` when the user picks a dark theme, `config-ui-x-<theme>` when they pick a light one. The stylesheet had `color-scheme: light` hard-coded and one set of colours, so there was nothing to follow it with.
8
+
9
+ Every colour is now a token, the dark set is declared once, and `index.js` decides which applies and writes it to `<html data-theme>`. Homebridge's own choice wins over the OS, because someone who picked light in Homebridge on a dark Mac meant light; the OS is the fallback for a page opened outside Homebridge. A MutationObserver keeps up when the theme changes while the page is open — the switch is one screen away, and the parent applies it by mutating our body rather than reloading us.
10
+
11
+ Two details worth writing down. Homebridge also assigns `body.style.backgroundColor = "#242424 !important"`; the CSSOM rejects a value carrying `!important`, so that line has never done anything and the class is the only signal there is. And the QR tile stays white in both themes on purpose — a camera cannot read a code without a light quiet zone. It is the one colour on the page that is not a token, and the test names it so the exemption cannot grow.
12
+
13
+ **The plugin's icon now sits beside the title**, as on the other plugins' settings pages. It is `assets/icon.png` byte for byte rather than a redrawing or a resize, and a test holds the two files identical, so the tile here and the tile on the Homebridge plugin list cannot drift apart.
14
+
15
+ 787 tests, up from 773. Verified red against 3.7.1: 14 of 14 fail.
16
+
3
17
  ## 3.7.1
4
18
 
5
19
  **The Matter Pairing section told you to pair the wrong thing first.**
Binary file
@@ -1,11 +1,26 @@
1
1
  <link rel="stylesheet" href="styles.css" />
2
2
 
3
3
  <main class="container">
4
- <header>
5
- <h1>Roborock Vacuum</h1>
6
- <p class="subtitle">
7
- Configure account access, discovery filters, and connection diagnostics.
8
- </p>
4
+ <header class="page-header">
5
+ <!-- The plugin's own icon, byte for byte the file in assets/ that npm and
6
+ the Homebridge plugin page show. Not a redrawing: a second copy of a
7
+ logo drifts from the first the moment either is touched, and then the
8
+ tile here and the tile on the plugin list stop matching. A test holds
9
+ the two files identical. -->
10
+ <img
11
+ class="brand-mark"
12
+ src="icon.png"
13
+ alt=""
14
+ width="54"
15
+ height="54"
16
+ decoding="async"
17
+ />
18
+ <div class="brand-text">
19
+ <h1>Roborock Vacuum</h1>
20
+ <p class="subtitle">
21
+ Configure account access, discovery filters, and connection diagnostics.
22
+ </p>
23
+ </div>
9
24
  </header>
10
25
 
11
26
  <section class="panel">
@@ -1647,7 +1647,80 @@ async function updatePluginConfig(patch) {
1647
1647
  await window.homebridge.savePluginConfig();
1648
1648
  }
1649
1649
 
1650
+ /**
1651
+ * Follow Homebridge's theme, not the operating system's.
1652
+ *
1653
+ * Homebridge UI reaches into this iframe's document and puts classes on our
1654
+ * body: `dark-mode` (plus `config-ui-x-dark-mode-<theme>`) when the user has
1655
+ * picked a dark theme, and `config-ui-x-<theme>` when they have picked a light
1656
+ * one. It also tries to force the background with
1657
+ * `body.style.backgroundColor = "#242424 !important"`, which the CSSOM rejects
1658
+ * because a property value may not carry `!important` — so that line does
1659
+ * nothing and the class is the only signal there is.
1660
+ *
1661
+ * The page used to read neither, so it stayed white inside a dark Homebridge.
1662
+ *
1663
+ * Precedence, and the order matters: Homebridge's own choice wins whenever it
1664
+ * has expressed one, because a user who picked light in Homebridge on a dark
1665
+ * Mac meant light. Only when no Homebridge class is present at all — the page
1666
+ * opened on its own, or before the parent has painted — do we fall back to the
1667
+ * OS preference, so the first frame is never the wrong colour.
1668
+ */
1669
+ const DARK_CLASS = "dark-mode";
1670
+ const HOMEBRIDGE_THEME_CLASS = /^config-ui-x-/;
1671
+
1672
+ function homebridgeHasChosenATheme() {
1673
+ return Array.from(document.body.classList).some((name) =>
1674
+ HOMEBRIDGE_THEME_CLASS.test(name)
1675
+ );
1676
+ }
1677
+
1678
+ function prefersDark() {
1679
+ return (
1680
+ typeof window.matchMedia === "function" &&
1681
+ window.matchMedia("(prefers-color-scheme: dark)").matches
1682
+ );
1683
+ }
1684
+
1685
+ function applyTheme() {
1686
+ const dark = document.body.classList.contains(DARK_CLASS)
1687
+ ? true
1688
+ : homebridgeHasChosenATheme()
1689
+ ? false
1690
+ : prefersDark();
1691
+ document.documentElement.dataset.theme = dark ? "dark" : "light";
1692
+ }
1693
+
1694
+ /**
1695
+ * Keep following it after the first paint.
1696
+ *
1697
+ * The theme can change while this page is open — the switch is in the same UI,
1698
+ * one screen away — and the parent applies it by mutating our body's class
1699
+ * list rather than reloading us. Without the observer the settings page is the
1700
+ * one thing in Homebridge that stays the old colour until it is reopened.
1701
+ */
1702
+ function watchTheme() {
1703
+ applyTheme();
1704
+
1705
+ new MutationObserver(applyTheme).observe(document.body, {
1706
+ attributes: true,
1707
+ attributeFilter: ["class"],
1708
+ });
1709
+
1710
+ if (typeof window.matchMedia === "function") {
1711
+ const query = window.matchMedia("(prefers-color-scheme: dark)");
1712
+ // Only relevant while Homebridge has not chosen for us; applyTheme decides.
1713
+ if (typeof query.addEventListener === "function") {
1714
+ query.addEventListener("change", applyTheme);
1715
+ }
1716
+ }
1717
+ }
1718
+
1650
1719
  function init() {
1720
+ // Before anything else: a page that is briefly the wrong colour is the first
1721
+ // thing the eye catches.
1722
+ watchTheme();
1723
+
1651
1724
  // The markup ships in the off state, but say it once here too: loadConfig()
1652
1725
  // can fail or find no config, and the alternative is a loud orange pairing
1653
1726
  // callout for a feature that is switched off.
@@ -4,6 +4,17 @@
4
4
  also sent a request to Google from a local admin interface. The body rule
5
5
  below already falls back to the system stack. */
6
6
 
7
+ /* Every colour on this page comes from a token below, and the dark set is
8
+ declared exactly once. That is a rule, not a preference: the page renders
9
+ inside Homebridge's own iframe, and a single hard-coded hex is all it takes
10
+ to put light-on-light or dark-on-dark somewhere nobody looks. The rule is
11
+ enforced by __tests__/the-settings-page-follows-homebridge-s-theme.test.js.
12
+
13
+ Which set applies is decided by index.js and written to
14
+ `<html data-theme>`, because Homebridge's theme is not the operating
15
+ system's: the UI reaches into this iframe and puts `dark-mode` on our body
16
+ when the user picks a dark theme, whatever the OS is set to. See
17
+ applyTheme() there for the precedence. */
7
18
  :root {
8
19
  color-scheme: light;
9
20
  --bg: #f4f6f9;
@@ -13,15 +24,72 @@
13
24
  --text: #17242f;
14
25
  --heading: #0c1a24;
15
26
  --muted: #5a6b7b;
27
+ --placeholder: #8a97a5;
16
28
  --primary: #0e7a6a;
17
29
  --primary-strong: #0b6357;
18
30
  --primary-text: #ffffff;
31
+ --primary-wash: rgba(14, 122, 106, 0.06);
32
+ --primary-ring: rgba(14, 122, 106, 0.16);
33
+ --primary-glow: rgba(14, 122, 106, 0.14);
34
+ --row-hover: #f1f7f5;
19
35
  --secondary: #eef2f5;
20
36
  --border: #d9e0e7;
21
37
  --shadow: 0 6px 18px rgba(15, 32, 44, 0.07);
22
38
  --warning: #9a5b00;
23
39
  --danger: #b42323;
24
40
  --link: #0e7a6a;
41
+
42
+ /* Status colours, as a face and a soft backing each. */
43
+ --good-line: #16a34a;
44
+ --good-bg: #e6f6ec;
45
+ --good-border: #b5e3c5;
46
+ --good-text: #14683a;
47
+ --warn-line: #d97706;
48
+ --warn-bg: #fdf3df;
49
+ --warn-border: #f0d9a8;
50
+ --warn-text: #8a5a00;
51
+ --danger-line: #dc2626;
52
+ --callout-border: #d9822b;
53
+ --callout-bg: rgba(217, 130, 43, 0.08);
54
+ }
55
+
56
+ /* The dark set. One block, same names, no extra rules — anything that needs
57
+ to change in dark mode changes here or it is not a token yet. */
58
+ :root[data-theme="dark"] {
59
+ color-scheme: dark;
60
+ --bg: #1c1c1c;
61
+ --panel: #242424;
62
+ --panel-soft: #2a2a2a;
63
+ --input: #1f1f1f;
64
+ --text: #e4e6e8;
65
+ --heading: #f5f7f8;
66
+ --muted: #a2adb6;
67
+ --placeholder: #7c8790;
68
+ --primary: #3fb39c;
69
+ --primary-strong: #56c7b0;
70
+ --primary-text: #10201d;
71
+ --primary-wash: rgba(63, 179, 156, 0.08);
72
+ --primary-ring: rgba(63, 179, 156, 0.28);
73
+ --primary-glow: rgba(0, 0, 0, 0.45);
74
+ --row-hover: #303634;
75
+ --secondary: #333333;
76
+ --border: #3a3a3a;
77
+ --shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
78
+ --warning: #e0a458;
79
+ --danger: #f0736e;
80
+ --link: #56c7b0;
81
+
82
+ --good-line: #3fbf76;
83
+ --good-bg: rgba(63, 191, 118, 0.14);
84
+ --good-border: rgba(63, 191, 118, 0.42);
85
+ --good-text: #7fdca6;
86
+ --warn-line: #e0a458;
87
+ --warn-bg: rgba(224, 164, 88, 0.14);
88
+ --warn-border: rgba(224, 164, 88, 0.42);
89
+ --warn-text: #edc088;
90
+ --danger-line: #f0736e;
91
+ --callout-border: #d9822b;
92
+ --callout-bg: rgba(217, 130, 43, 0.14);
25
93
  }
26
94
 
27
95
  * {
@@ -31,11 +99,7 @@
31
99
  body {
32
100
  margin: 0;
33
101
  font-family: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
34
- background: linear-gradient(
35
- 180deg,
36
- rgba(14, 122, 106, 0.06),
37
- transparent 260px
38
- ),
102
+ background: linear-gradient(180deg, var(--primary-wash), transparent 260px),
39
103
  var(--bg);
40
104
  color: var(--text);
41
105
  }
@@ -48,6 +112,28 @@ body {
48
112
  gap: 18px;
49
113
  }
50
114
 
115
+ /* The icon sits beside the title, the way it does on the other plugins'
116
+ settings pages. */
117
+ .page-header {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 16px;
121
+ }
122
+
123
+ .brand-mark {
124
+ flex: 0 0 auto;
125
+ width: 54px;
126
+ height: 54px;
127
+ /* The artwork carries its own rounded tile; this only matches the corner so
128
+ the edges do not show against the page. */
129
+ border-radius: 12px;
130
+ display: block;
131
+ }
132
+
133
+ .brand-text {
134
+ min-width: 0;
135
+ }
136
+
51
137
  /* Explicit heading colors: Homebridge UI injects its own theme stylesheet
52
138
  into custom-UI iframes, so headings must not rely on inheritance or they
53
139
  can end up dark-on-dark / light-on-light. */
@@ -146,11 +232,11 @@ select:focus,
146
232
  input:focus {
147
233
  outline: none;
148
234
  border-color: var(--primary);
149
- box-shadow: 0 0 0 3px rgba(14, 122, 106, 0.16);
235
+ box-shadow: 0 0 0 3px var(--primary-ring);
150
236
  }
151
237
 
152
238
  input::placeholder {
153
- color: #8a97a5;
239
+ color: var(--placeholder);
154
240
  }
155
241
 
156
242
  .checkbox {
@@ -235,7 +321,7 @@ button {
235
321
 
236
322
  button:hover {
237
323
  transform: translateY(-1px);
238
- box-shadow: 0 10px 20px rgba(14, 122, 106, 0.14);
324
+ box-shadow: 0 10px 20px var(--primary-glow);
239
325
  }
240
326
 
241
327
  button:active {
@@ -292,15 +378,15 @@ select:focus-visible {
292
378
  }
293
379
 
294
380
  .toast.success {
295
- border-left-color: #16a34a;
381
+ border-left-color: var(--good-line);
296
382
  }
297
383
 
298
384
  .toast.error {
299
- border-left-color: #dc2626;
385
+ border-left-color: var(--danger-line);
300
386
  }
301
387
 
302
388
  .toast.warning {
303
- border-left-color: #d97706;
389
+ border-left-color: var(--warn-line);
304
390
  }
305
391
 
306
392
  .diagnostics-summary {
@@ -363,7 +449,7 @@ select:focus-visible {
363
449
 
364
450
  .device-row:hover {
365
451
  border-color: var(--primary);
366
- background: #f1f7f5;
452
+ background: var(--row-hover);
367
453
  }
368
454
 
369
455
  .device-row input {
@@ -403,11 +489,11 @@ select:focus-visible {
403
489
  }
404
490
 
405
491
  .local-test-card.good {
406
- border-color: rgba(22, 163, 74, 0.45);
492
+ border-color: var(--good-border);
407
493
  }
408
494
 
409
495
  .local-test-card.warn {
410
- border-color: rgba(217, 119, 6, 0.45);
496
+ border-color: var(--warn-border);
411
497
  }
412
498
 
413
499
  .device-header {
@@ -450,10 +536,14 @@ select:focus-visible {
450
536
  height: 180px;
451
537
  border-radius: 8px;
452
538
  border: 1px solid var(--border);
539
+ /* Deliberately white in both themes, and the one colour on this page that
540
+ is not a token. A QR code needs a light quiet zone to scan; a camera
541
+ pointed at a dark tile gets nothing. */
453
542
  background: #ffffff;
454
543
  }
455
544
 
456
545
  .qr-placeholder {
546
+ background: var(--panel-soft);
457
547
  color: var(--muted);
458
548
  display: grid;
459
549
  place-items: center;
@@ -514,15 +604,15 @@ select:focus-visible {
514
604
  }
515
605
 
516
606
  .pill.good {
517
- background: #e6f6ec;
518
- border-color: #b5e3c5;
519
- color: #14683a;
607
+ background: var(--good-bg);
608
+ border-color: var(--good-border);
609
+ color: var(--good-text);
520
610
  }
521
611
 
522
612
  .pill.warn {
523
- background: #fdf3df;
524
- border-color: #f0d9a8;
525
- color: #8a5a00;
613
+ background: var(--warn-bg);
614
+ border-color: var(--warn-border);
615
+ color: var(--warn-text);
526
616
  }
527
617
 
528
618
  .status-pill {
@@ -537,15 +627,15 @@ select:focus-visible {
537
627
  }
538
628
 
539
629
  .status-pill.good {
540
- background: #e4f5ef;
541
- border-color: #a9dfcd;
542
- color: #0c6b52;
630
+ background: var(--good-bg);
631
+ border-color: var(--good-border);
632
+ color: var(--good-text);
543
633
  }
544
634
 
545
635
  .status-pill.warn {
546
- background: #fdf3df;
547
- border-color: #edd39c;
548
- color: #8a5a00;
636
+ background: var(--warn-bg);
637
+ border-color: var(--warn-border);
638
+ color: var(--warn-text);
549
639
  }
550
640
 
551
641
  .diagnostic-device dl,
@@ -586,6 +676,11 @@ select:focus-visible {
586
676
  font-size: 1.85rem;
587
677
  }
588
678
 
679
+ .brand-mark {
680
+ width: 44px;
681
+ height: 44px;
682
+ }
683
+
589
684
  .container {
590
685
  padding: 22px 16px;
591
686
  }
@@ -636,10 +731,10 @@ select:focus-visible {
636
731
  .pairing-callout {
637
732
  margin: 4px 0 16px 26px;
638
733
  padding: 12px 14px;
639
- border: 1px solid #d9822b;
734
+ border: 1px solid var(--callout-border);
640
735
  border-left-width: 4px;
641
736
  border-radius: 6px;
642
- background: rgba(217, 130, 43, 0.08);
737
+ background: var(--callout-bg);
643
738
  }
644
739
 
645
740
  .pairing-callout h3 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "3.7.1",
3
+ "version": "3.8.0",
4
4
  "description": "The most complete Roborock plugin for Apple Home. Supports the entire Roborock lineup — from the classic S-series to the new 2025 Q7 series that no other plugin can control. Sign in with your Roborock account and get native start/stop, room cleaning, suction levels, battery, and live 'cleaning in the kitchen' room tracking. Verified by Homebridge.",
5
5
  "license": "MIT",
6
6
  "author": {