@mp-consulting/homebridge-daikin-cloud 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +40 -27
  2. package/config.schema.json +66 -12
  3. package/dist/src/accessories/air-conditioning-accessory.js +1 -1
  4. package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
  5. package/dist/src/accessories/altherma-accessory.js +1 -1
  6. package/dist/src/accessories/altherma-accessory.js.map +1 -1
  7. package/dist/src/accessories/base-accessory.d.ts +5 -4
  8. package/dist/src/accessories/base-accessory.d.ts.map +1 -1
  9. package/dist/src/accessories/base-accessory.js +9 -10
  10. package/dist/src/accessories/base-accessory.js.map +1 -1
  11. package/dist/src/api/daikin-api.d.ts +22 -6
  12. package/dist/src/api/daikin-api.d.ts.map +1 -1
  13. package/dist/src/api/daikin-api.js +49 -23
  14. package/dist/src/api/daikin-api.js.map +1 -1
  15. package/dist/src/api/daikin-controller.d.ts +4 -48
  16. package/dist/src/api/daikin-controller.d.ts.map +1 -1
  17. package/dist/src/api/daikin-controller.js +14 -84
  18. package/dist/src/api/daikin-controller.js.map +1 -1
  19. package/dist/src/api/daikin-mobile-oauth.d.ts +11 -40
  20. package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
  21. package/dist/src/api/daikin-mobile-oauth.js +36 -160
  22. package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
  23. package/dist/src/api/daikin-oauth.d.ts +10 -37
  24. package/dist/src/api/daikin-oauth.d.ts.map +1 -1
  25. package/dist/src/api/daikin-oauth.js +36 -166
  26. package/dist/src/api/daikin-oauth.js.map +1 -1
  27. package/dist/src/api/daikin-schemas.d.ts +20 -22
  28. package/dist/src/api/daikin-schemas.d.ts.map +1 -1
  29. package/dist/src/api/daikin-schemas.js +17 -49
  30. package/dist/src/api/daikin-schemas.js.map +1 -1
  31. package/dist/src/api/daikin-types.d.ts +11 -23
  32. package/dist/src/api/daikin-types.d.ts.map +1 -1
  33. package/dist/src/api/daikin-types.js.map +1 -1
  34. package/dist/src/api/daikin-websocket.d.ts +4 -5
  35. package/dist/src/api/daikin-websocket.d.ts.map +1 -1
  36. package/dist/src/api/daikin-websocket.js +62 -34
  37. package/dist/src/api/daikin-websocket.js.map +1 -1
  38. package/dist/src/api/http-defaults.d.ts +3 -2
  39. package/dist/src/api/http-defaults.d.ts.map +1 -1
  40. package/dist/src/api/http-defaults.js.map +1 -1
  41. package/dist/src/api/http-transport.d.ts +14 -2
  42. package/dist/src/api/http-transport.d.ts.map +1 -1
  43. package/dist/src/api/http-transport.js +32 -9
  44. package/dist/src/api/http-transport.js.map +1 -1
  45. package/dist/src/api/token-client.d.ts +82 -0
  46. package/dist/src/api/token-client.d.ts.map +1 -0
  47. package/dist/src/api/token-client.js +188 -0
  48. package/dist/src/api/token-client.js.map +1 -0
  49. package/dist/src/api/token-storage.d.ts +6 -0
  50. package/dist/src/api/token-storage.d.ts.map +1 -1
  51. package/dist/src/api/token-storage.js +27 -1
  52. package/dist/src/api/token-storage.js.map +1 -1
  53. package/dist/src/config/config-manager.d.ts +69 -125
  54. package/dist/src/config/config-manager.d.ts.map +1 -1
  55. package/dist/src/config/config-manager.js +288 -225
  56. package/dist/src/config/config-manager.js.map +1 -1
  57. package/dist/src/constants/api.constants.d.ts +6 -0
  58. package/dist/src/constants/api.constants.d.ts.map +1 -1
  59. package/dist/src/constants/api.constants.js +7 -1
  60. package/dist/src/constants/api.constants.js.map +1 -1
  61. package/dist/src/constants/auth.constants.d.ts +11 -0
  62. package/dist/src/constants/auth.constants.d.ts.map +1 -1
  63. package/dist/src/constants/auth.constants.js +12 -1
  64. package/dist/src/constants/auth.constants.js.map +1 -1
  65. package/dist/src/constants/device.constants.d.ts +1 -7
  66. package/dist/src/constants/device.constants.d.ts.map +1 -1
  67. package/dist/src/constants/device.constants.js +2 -11
  68. package/dist/src/constants/device.constants.js.map +1 -1
  69. package/dist/src/constants/time.constants.d.ts +0 -2
  70. package/dist/src/constants/time.constants.d.ts.map +1 -1
  71. package/dist/src/constants/time.constants.js +1 -3
  72. package/dist/src/constants/time.constants.js.map +1 -1
  73. package/dist/src/device/accessory-factory.d.ts +0 -5
  74. package/dist/src/device/accessory-factory.d.ts.map +1 -1
  75. package/dist/src/device/accessory-factory.js +0 -7
  76. package/dist/src/device/accessory-factory.js.map +1 -1
  77. package/dist/src/device/capability-detector.d.ts +9 -1
  78. package/dist/src/device/capability-detector.d.ts.map +1 -1
  79. package/dist/src/device/capability-detector.js +32 -28
  80. package/dist/src/device/capability-detector.js.map +1 -1
  81. package/dist/src/device/capability-docs.d.ts.map +1 -1
  82. package/dist/src/device/capability-docs.js +3 -0
  83. package/dist/src/device/capability-docs.js.map +1 -1
  84. package/dist/src/device/data-paths.d.ts +18 -0
  85. package/dist/src/device/data-paths.d.ts.map +1 -0
  86. package/dist/src/device/data-paths.js +20 -0
  87. package/dist/src/device/data-paths.js.map +1 -0
  88. package/dist/src/device/profiles/device-profile.d.ts +0 -5
  89. package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
  90. package/dist/src/device/profiles/device-profile.js +0 -21
  91. package/dist/src/device/profiles/device-profile.js.map +1 -1
  92. package/dist/src/features/base-feature.d.ts +53 -9
  93. package/dist/src/features/base-feature.d.ts.map +1 -1
  94. package/dist/src/features/base-feature.js +93 -40
  95. package/dist/src/features/base-feature.js.map +1 -1
  96. package/dist/src/features/feature-manager.d.ts +27 -15
  97. package/dist/src/features/feature-manager.d.ts.map +1 -1
  98. package/dist/src/features/feature-manager.js +27 -24
  99. package/dist/src/features/feature-manager.js.map +1 -1
  100. package/dist/src/features/index.d.ts +1 -0
  101. package/dist/src/features/index.d.ts.map +1 -1
  102. package/dist/src/features/index.js +1 -0
  103. package/dist/src/features/index.js.map +1 -1
  104. package/dist/src/features/modes/auto-fan-mode.feature.d.ts +16 -10
  105. package/dist/src/features/modes/auto-fan-mode.feature.d.ts.map +1 -1
  106. package/dist/src/features/modes/auto-fan-mode.feature.js +16 -40
  107. package/dist/src/features/modes/auto-fan-mode.feature.js.map +1 -1
  108. package/dist/src/features/modes/dry-operation-mode.feature.d.ts +12 -9
  109. package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
  110. package/dist/src/features/modes/dry-operation-mode.feature.js +11 -30
  111. package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
  112. package/dist/src/features/modes/econo-mode.feature.d.ts +12 -9
  113. package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
  114. package/dist/src/features/modes/econo-mode.feature.js +11 -31
  115. package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
  116. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +12 -9
  117. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
  118. package/dist/src/features/modes/fan-only-operation-mode.feature.js +11 -30
  119. package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
  120. package/dist/src/features/modes/firmware-update.feature.d.ts +7 -6
  121. package/dist/src/features/modes/firmware-update.feature.d.ts.map +1 -1
  122. package/dist/src/features/modes/firmware-update.feature.js +15 -22
  123. package/dist/src/features/modes/firmware-update.feature.js.map +1 -1
  124. package/dist/src/features/modes/holiday-mode.feature.d.ts +2 -1
  125. package/dist/src/features/modes/holiday-mode.feature.d.ts.map +1 -1
  126. package/dist/src/features/modes/holiday-mode.feature.js +5 -15
  127. package/dist/src/features/modes/holiday-mode.feature.js.map +1 -1
  128. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +15 -12
  129. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
  130. package/dist/src/features/modes/indoor-silent-mode.feature.js +15 -39
  131. package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
  132. package/dist/src/features/modes/oscillation-mode.feature.d.ts +5 -6
  133. package/dist/src/features/modes/oscillation-mode.feature.d.ts.map +1 -1
  134. package/dist/src/features/modes/oscillation-mode.feature.js +14 -36
  135. package/dist/src/features/modes/oscillation-mode.feature.js.map +1 -1
  136. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +12 -9
  137. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
  138. package/dist/src/features/modes/outdoor-silent-mode.feature.js +11 -31
  139. package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
  140. package/dist/src/features/modes/powerful-mode.feature.d.ts +14 -10
  141. package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
  142. package/dist/src/features/modes/powerful-mode.feature.js +13 -32
  143. package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
  144. package/dist/src/features/modes/streamer-mode.feature.d.ts +12 -9
  145. package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
  146. package/dist/src/features/modes/streamer-mode.feature.js +11 -31
  147. package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
  148. package/dist/src/features/on-off-data-point-feature.d.ts +43 -0
  149. package/dist/src/features/on-off-data-point-feature.d.ts.map +1 -0
  150. package/dist/src/features/on-off-data-point-feature.js +49 -0
  151. package/dist/src/features/on-off-data-point-feature.js.map +1 -0
  152. package/dist/src/platform.d.ts +56 -9
  153. package/dist/src/platform.d.ts.map +1 -1
  154. package/dist/src/platform.js +248 -128
  155. package/dist/src/platform.js.map +1 -1
  156. package/dist/src/services/climate-control/fan-speed.d.ts +45 -0
  157. package/dist/src/services/climate-control/fan-speed.d.ts.map +1 -0
  158. package/dist/src/services/climate-control/fan-speed.js +104 -0
  159. package/dist/src/services/climate-control/fan-speed.js.map +1 -0
  160. package/dist/src/services/climate-control/separate-fan.service.d.ts +45 -0
  161. package/dist/src/services/climate-control/separate-fan.service.d.ts.map +1 -0
  162. package/dist/src/services/climate-control/separate-fan.service.js +82 -0
  163. package/dist/src/services/climate-control/separate-fan.service.js.map +1 -0
  164. package/dist/src/services/climate-control/setpoint-resolver.d.ts +68 -0
  165. package/dist/src/services/climate-control/setpoint-resolver.d.ts.map +1 -0
  166. package/dist/src/services/climate-control/setpoint-resolver.js +170 -0
  167. package/dist/src/services/climate-control/setpoint-resolver.js.map +1 -0
  168. package/dist/src/services/climate-control/swing-controller.d.ts +28 -0
  169. package/dist/src/services/climate-control/swing-controller.d.ts.map +1 -0
  170. package/dist/src/services/climate-control/swing-controller.js +65 -0
  171. package/dist/src/services/climate-control/swing-controller.js.map +1 -0
  172. package/dist/src/services/climate-control.service.d.ts +39 -48
  173. package/dist/src/services/climate-control.service.d.ts.map +1 -1
  174. package/dist/src/services/climate-control.service.js +223 -554
  175. package/dist/src/services/climate-control.service.js.map +1 -1
  176. package/dist/src/services/hot-water-tank.service.d.ts +17 -8
  177. package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
  178. package/dist/src/services/hot-water-tank.service.js +102 -157
  179. package/dist/src/services/hot-water-tank.service.js.map +1 -1
  180. package/dist/src/types/daikin-enums.d.ts +0 -5
  181. package/dist/src/types/daikin-enums.d.ts.map +1 -1
  182. package/dist/src/types/daikin-enums.js +1 -7
  183. package/dist/src/types/daikin-enums.js.map +1 -1
  184. package/dist/src/types/device-capabilities.d.ts +1 -0
  185. package/dist/src/types/device-capabilities.d.ts.map +1 -1
  186. package/dist/src/utils/errors.d.ts +9 -0
  187. package/dist/src/utils/errors.d.ts.map +1 -0
  188. package/dist/src/utils/errors.js +25 -0
  189. package/dist/src/utils/errors.js.map +1 -0
  190. package/dist/src/utils/hap-write.d.ts +22 -0
  191. package/dist/src/utils/hap-write.d.ts.map +1 -0
  192. package/dist/src/utils/hap-write.js +33 -0
  193. package/dist/src/utils/hap-write.js.map +1 -0
  194. package/dist/src/utils/sleep.d.ts +5 -0
  195. package/dist/src/utils/sleep.d.ts.map +1 -0
  196. package/dist/src/utils/sleep.js +10 -0
  197. package/dist/src/utils/sleep.js.map +1 -0
  198. package/homebridge-ui/README.md +25 -2
  199. package/homebridge-ui/public/index.html +19 -17
  200. package/homebridge-ui/public/script.js +95 -32
  201. package/homebridge-ui/public/styles.css +109 -0
  202. package/homebridge-ui/server.js +321 -125
  203. package/package.json +2 -3
  204. package/dist/src/utils/log-context.d.ts +0 -105
  205. package/dist/src/utils/log-context.d.ts.map +0 -1
  206. package/dist/src/utils/log-context.js +0 -206
  207. package/dist/src/utils/log-context.js.map +0 -1
  208. package/dist/src/utils/update-mapper.d.ts +0 -59
  209. package/dist/src/utils/update-mapper.d.ts.map +0 -1
  210. package/dist/src/utils/update-mapper.js +0 -192
  211. package/dist/src/utils/update-mapper.js.map +0 -1
@@ -23,7 +23,7 @@ const Utils = {
23
23
 
24
24
  escapeHtml(text) {
25
25
  const div = document.createElement('div');
26
- div.textContent = text;
26
+ div.textContent = text === null || text === undefined ? '' : String(text);
27
27
  return div.innerHTML;
28
28
  },
29
29
 
@@ -379,7 +379,7 @@ const Wizard = {
379
379
  try {
380
380
  const validation = await homebridge.request('/config/validate', config);
381
381
  if (!validation.valid) {
382
- El.validationErrors.innerHTML = validation.errors.map(e => `<div>${e}</div>`).join('');
382
+ El.validationErrors.innerHTML = validation.errors.map(e => `<div>${Utils.escapeHtml(e)}</div>`).join('');
383
383
  DOM.show(El.validationErrors);
384
384
  UI.hideLoading();
385
385
  State.isValidating = false;
@@ -401,7 +401,7 @@ const Wizard = {
401
401
  Polling.start();
402
402
  this.goToStep(2);
403
403
  } catch (error) {
404
- El.validationErrors.innerHTML = `<div>${error.message}</div>`;
404
+ El.validationErrors.innerHTML = `<div>${Utils.escapeHtml(error.message)}</div>`;
405
405
  DOM.show(El.validationErrors);
406
406
  }
407
407
 
@@ -563,6 +563,37 @@ const Config = {
563
563
  },
564
564
  };
565
565
 
566
+ // ============================================================================
567
+ // Device List Request (shared)
568
+ // ============================================================================
569
+
570
+ /**
571
+ * Settings and Devices both need /devices/list on page load. Share one
572
+ * in-flight request so the page spends at most one API call; an explicit
573
+ * refresh bypasses both this and the server-side cache.
574
+ */
575
+ const DeviceList = {
576
+ _inFlight: null,
577
+ _mode: null,
578
+
579
+ fetch({ refresh = false } = {}) {
580
+ const mode = AuthMode.current;
581
+ if (!refresh && this._inFlight && this._mode === mode) {
582
+ return this._inFlight;
583
+ }
584
+
585
+ const request = homebridge.request('/devices/list', { mode, refresh })
586
+ .finally(() => {
587
+ if (this._inFlight === request) {
588
+ this._inFlight = null;
589
+ }
590
+ });
591
+ this._inFlight = request;
592
+ this._mode = mode;
593
+ return request;
594
+ },
595
+ };
596
+
566
597
  // ============================================================================
567
598
  // Settings Module
568
599
  // ============================================================================
@@ -571,6 +602,7 @@ const Settings = {
571
602
  devices: [],
572
603
  excludedIds: [],
573
604
  saveTimeout: null,
605
+ toggleListenerAttached: false,
574
606
 
575
607
  // Feature switches that honour the legacy "Show Extra Features" flag as their default.
576
608
  FEATURE_KEYS: [
@@ -642,10 +674,12 @@ const Settings = {
642
674
  populateForm(config) {
643
675
  const showAllLegacy = config.showExtraFeatures === true;
644
676
 
677
+ // Same rule as the plugin (resolveFeatures in config-manager): only a boolean
678
+ // overrides; absent or null follows showExtraFeatures.
645
679
  this.FEATURE_KEYS.forEach(key => {
646
680
  const el = $id(key);
647
681
  if (el) {
648
- el.checked = key in config ? config[key] === true : showAllLegacy;
682
+ el.checked = typeof config[key] === 'boolean' ? config[key] : showAllLegacy;
649
683
  }
650
684
  });
651
685
 
@@ -693,11 +727,11 @@ const Settings = {
693
727
  DOM.hide(empty);
694
728
 
695
729
  try {
696
- const result = await homebridge.request('/devices/list', { mode: AuthMode.current });
730
+ const result = await DeviceList.fetch();
697
731
  DOM.hide(loading);
698
732
 
699
733
  if (!result.success) {
700
- empty.innerHTML = `<p class="mb-0">${result.message || 'No devices found. Authenticate first to see your devices.'}</p>`;
734
+ empty.innerHTML = `<p class="mb-0">${Utils.escapeHtml(result.message || 'No devices found. Authenticate first to see your devices.')}</p>`;
701
735
  DOM.show(empty);
702
736
  return;
703
737
  }
@@ -711,18 +745,21 @@ const Settings = {
711
745
  this.devices = result.devices;
712
746
  list.innerHTML = this.devices.map((d, i) => this.renderDeviceToggle(d, i)).join('');
713
747
 
714
- list.addEventListener('change', (e) => {
715
- if (e.target.classList.contains('device-visibility-toggle')) {
716
- const idx = parseInt(e.target.dataset.index, 10);
717
- const device = this.devices[idx];
718
- if (device) {
719
- this.toggleDevice(device.id, e.target.checked, idx);
748
+ if (!this.toggleListenerAttached) {
749
+ this.toggleListenerAttached = true;
750
+ list.addEventListener('change', (e) => {
751
+ if (e.target.classList.contains('device-visibility-toggle')) {
752
+ const idx = parseInt(e.target.dataset.index, 10);
753
+ const device = this.devices[idx];
754
+ if (device) {
755
+ this.toggleDevice(device.id, e.target.checked, idx);
756
+ }
720
757
  }
721
- }
722
- });
758
+ });
759
+ }
723
760
  } catch (error) {
724
761
  DOM.hide(loading);
725
- empty.innerHTML = `<p class="mb-0">Failed to load: ${error.message}</p>`;
762
+ empty.innerHTML = `<p class="mb-0">Failed to load: ${Utils.escapeHtml(error.message)}</p>`;
726
763
  DOM.show(empty);
727
764
  }
728
765
  },
@@ -730,12 +767,12 @@ const Settings = {
730
767
  renderDeviceToggle(device, index) {
731
768
  const visible = !this.excludedIds.includes(device.id);
732
769
  return `
733
- <div class="list-group-item d-flex justify-content-between align-items-center">
734
- <div>
770
+ <div class="list-group-item d-flex justify-content-between align-items-center gap-3">
771
+ <div class="min-w-0">
735
772
  <div class="fw-medium">${Utils.escapeHtml(device.name)}</div>
736
- <small class="text-muted">${Utils.escapeHtml(device.id)}</small>
773
+ <small class="device-id text-muted">${Utils.escapeHtml(device.id)}</small>
737
774
  </div>
738
- <div class="d-flex align-items-center gap-2">
775
+ <div class="d-flex align-items-center gap-2 flex-shrink-0">
739
776
  <span class="device-toggle-label small ${visible ? 'visible' : 'hidden-label'}" data-label-index="${index}">${visible ? 'Visible' : 'Hidden'}</span>
740
777
  <div class="form-check form-switch mb-0">
741
778
  <input type="checkbox" class="form-check-input device-visibility-toggle" role="switch" data-index="${index}" ${visible ? 'checked' : ''}>
@@ -816,14 +853,14 @@ const Settings = {
816
853
  // ============================================================================
817
854
 
818
855
  const Devices = {
819
- async load() {
856
+ async load({ refresh = false } = {}) {
820
857
  DOM.show(El.devicesLoading);
821
858
  El.devicesList.innerHTML = '';
822
859
  DOM.hide(El.devicesEmpty);
823
860
  DOM.hide(El.devicesError);
824
861
 
825
862
  try {
826
- const result = await homebridge.request('/devices/list', { mode: AuthMode.current });
863
+ const result = await DeviceList.fetch({ refresh });
827
864
  DOM.hide(El.devicesLoading);
828
865
 
829
866
  if (!result.success) {
@@ -862,7 +899,7 @@ const Devices = {
862
899
  const powerOn = device.powerState === 'on';
863
900
  const mode = device.operationMode ? Utils.capitalize(device.operationMode) : '-';
864
901
  const features = device.features?.length
865
- ? device.features.map(f => `<span class="badge bg-secondary bg-opacity-50 me-1">${f}</span>`).join('')
902
+ ? device.features.map(f => `<span class="badge bg-secondary bg-opacity-50">${Utils.escapeHtml(f)}</span>`).join('')
866
903
  : '';
867
904
 
868
905
  const meta = [
@@ -874,18 +911,22 @@ const Devices = {
874
911
 
875
912
  return `
876
913
  <div class="list-group-item">
877
- <div class="d-flex align-items-center">
878
- <span class="device-power ${powerOn ? 'power-on' : 'power-off'} me-2">${powerOn ? 'ON' : 'OFF'}</span>
879
- <span class="fw-semibold">${Utils.escapeHtml(device.name)}</span>
880
- ${features ? `<div class="ms-2">${features}</div>` : ''}
881
- <span class="device-status ${online ? 'online' : 'offline'} ms-auto">${online ? 'Online' : 'Offline'}</span>
914
+ <div class="device-header">
915
+ <span class="device-power ${powerOn ? 'power-on' : 'power-off'}">${powerOn ? 'ON' : 'OFF'}</span>
916
+ <span class="device-name fw-semibold">${Utils.escapeHtml(device.name)}</span>
917
+ ${features ? `<div class="device-features">${features}</div>` : ''}
918
+ <span class="device-status ${online ? 'online' : 'offline'}">${online ? 'Online' : 'Offline'}</span>
882
919
  </div>
883
920
  <div class="device-meta mt-1">${meta}</div>
884
921
  </div>`;
885
922
  },
886
923
 
887
- refresh() {
888
- this.load();
924
+ async refresh() {
925
+ // Bypass the shared request and the server cache, then let the Settings
926
+ // device toggles reuse the same fresh request.
927
+ const pending = this.load({ refresh: true });
928
+ Settings.loadDeviceToggles();
929
+ await pending;
889
930
  },
890
931
  };
891
932
 
@@ -894,11 +935,16 @@ const Devices = {
894
935
  // ============================================================================
895
936
 
896
937
  const RateLimit = {
938
+ MODE_LABELS: { developer_portal: 'Developer Portal', mobile_app: 'Mobile App' },
939
+
897
940
  async check() {
898
941
  const display = El.rateLimitDisplay;
942
+ const updated = El.rateLimitUpdated;
899
943
  display.textContent = 'Checking...';
944
+ DOM.hide(updated);
900
945
 
901
946
  try {
947
+ // Reads the status the running plugin last recorded; no API call is made.
902
948
  const result = await homebridge.request('/api/rate-limit', { mode: AuthMode.current });
903
949
  if (result.success && result.rateLimit) {
904
950
  const { limitDay, remainingDay, limitMinute, remainingMinute } = result.rateLimit;
@@ -906,7 +952,8 @@ const RateLimit = {
906
952
  if (remainingMinute !== undefined) {
907
953
  text += text ? `, ${remainingMinute}/${limitMinute}/min` : `${remainingMinute}/${limitMinute}/min`;
908
954
  }
909
- display.textContent = text || 'No rate limit headers';
955
+ display.textContent = text || 'No rate limit headers in the last response';
956
+ this.showUpdated(updated, result);
910
957
  } else {
911
958
  display.textContent = result.message || 'No info';
912
959
  }
@@ -914,6 +961,21 @@ const RateLimit = {
914
961
  display.textContent = 'Error: ' + error.message;
915
962
  }
916
963
  },
964
+
965
+ showUpdated(el, result) {
966
+ if (!el || !result.updatedAt) {
967
+ return;
968
+ }
969
+ const parts = [`as of ${new Date(result.updatedAt).toLocaleString()}`];
970
+ if (typeof result.ageMs === 'number') {
971
+ parts.push(`(${Utils.formatTime(result.ageMs)} ago)`);
972
+ }
973
+ if (result.mode && result.mode !== AuthMode.current) {
974
+ parts.push(`— recorded in ${this.MODE_LABELS[result.mode] || result.mode} mode`);
975
+ }
976
+ el.textContent = parts.join(' ');
977
+ DOM.show(el);
978
+ },
917
979
  };
918
980
 
919
981
  // ============================================================================
@@ -976,8 +1038,8 @@ const MobileAuth = {
976
1038
  if (result.success) {
977
1039
  await this.saveCredentials(email, password);
978
1040
  success.innerHTML = `
979
- <strong>Success!</strong> Found ${result.deviceCount || 0} device(s).<br>
980
- Rate limit: ${result.rateLimit?.remainingDay || '?'}/${result.rateLimit?.limitDay || '3000'}/day<br>
1041
+ <strong>Success!</strong> Found ${Utils.escapeHtml(result.deviceCount || 0)} device(s).<br>
1042
+ Rate limit: ${Utils.escapeHtml(result.rateLimit?.remainingDay ?? '?')}/${Utils.escapeHtml(result.rateLimit?.limitDay ?? '3000')}/day<br>
981
1043
  <small>Restart Homebridge to apply.</small>`;
982
1044
  DOM.show(success);
983
1045
  setTimeout(() => {
@@ -1154,6 +1216,7 @@ function cacheElements() {
1154
1216
  devicesError: $id('devices-error'),
1155
1217
  settingsStatus: $id('settings-status'),
1156
1218
  rateLimitDisplay: $id('rate-limit-display'),
1219
+ rateLimitUpdated: $id('rate-limit-updated'),
1157
1220
  loading: $id('loading'),
1158
1221
  globalError: $id('global-error'),
1159
1222
  };
@@ -105,18 +105,88 @@
105
105
  color: var(--bs-danger-text-emphasis);
106
106
  }
107
107
 
108
+ /* Device header row: power, name, features, status */
109
+ .device-header {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ align-items: center;
113
+ gap: 0.25rem 0.5rem;
114
+ }
115
+
116
+ .device-name {
117
+ min-width: 0;
118
+ overflow-wrap: anywhere;
119
+ }
120
+
121
+ .device-features {
122
+ display: flex;
123
+ flex-wrap: wrap;
124
+ gap: 0.25rem;
125
+ }
126
+
127
+ .device-header .device-status {
128
+ margin-left: auto;
129
+ flex-shrink: 0;
130
+ }
131
+
108
132
  /* Device meta row */
109
133
  .device-meta {
134
+ display: flex;
135
+ flex-wrap: wrap;
110
136
  font-size: 0.75rem;
111
137
  color: var(--bs-tertiary-color);
112
138
  opacity: 0.55;
113
139
  }
114
140
 
141
+ .device-meta-item {
142
+ white-space: nowrap;
143
+ }
144
+
115
145
  .device-meta-sep {
116
146
  margin: 0 0.4rem;
117
147
  opacity: 0.4;
118
148
  }
119
149
 
150
+ .device-id {
151
+ font-family: var(--bs-font-monospace);
152
+ font-size: 0.75rem;
153
+ overflow-wrap: anywhere;
154
+ }
155
+
156
+ .min-w-0 {
157
+ min-width: 0;
158
+ }
159
+
160
+ /* Header status (token expiry + badge) */
161
+ .mp-header {
162
+ flex-wrap: wrap;
163
+ }
164
+
165
+ .header-status {
166
+ white-space: nowrap;
167
+ }
168
+
169
+ /* Tabs: keep on one row, scroll horizontally if they still don't fit */
170
+ .app-tabs {
171
+ flex-wrap: nowrap;
172
+ overflow-x: auto;
173
+ scrollbar-width: none;
174
+ }
175
+
176
+ .app-tabs::-webkit-scrollbar {
177
+ display: none;
178
+ }
179
+
180
+ .app-tabs .nav-link {
181
+ white-space: nowrap;
182
+ }
183
+
184
+ /* Lists inside a card body: align with the card's own padding */
185
+ .card-body > .list-group-flush > .list-group-item {
186
+ padding-left: 0;
187
+ padding-right: 0;
188
+ }
189
+
120
190
  /* Device Toggle Labels */
121
191
  .device-toggle-label.visible {
122
192
  color: var(--bs-success);
@@ -238,3 +308,42 @@
238
308
  #token-expires.text-danger {
239
309
  color: var(--bs-danger) !important;
240
310
  }
311
+
312
+ /* Mobile */
313
+ @media (max-width: 575.98px) {
314
+ .container-fluid {
315
+ padding: 0.75rem !important;
316
+ }
317
+
318
+ .mp-header-title {
319
+ font-size: 1.25rem;
320
+ }
321
+
322
+ /* Status drops to its own line under the title */
323
+ .header-status {
324
+ width: 100%;
325
+ margin-left: 0 !important;
326
+ justify-content: space-between;
327
+ }
328
+
329
+ .app-tabs .nav-link {
330
+ padding: 0.375rem 0.625rem;
331
+ font-size: 0.875rem;
332
+ }
333
+
334
+ /* Name takes the row, features move under it */
335
+ .device-name {
336
+ flex: 1 1 0;
337
+ }
338
+
339
+ .device-features {
340
+ order: 1;
341
+ width: 100%;
342
+ }
343
+ }
344
+
345
+ @media (max-width: 359.98px) {
346
+ .app-tabs .nav-link {
347
+ padding: 0.375rem 0.4rem;
348
+ }
349
+ }