matterbridge-dyson-robot 0.1.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 (261) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +14 -0
  3. package/README.md +365 -0
  4. package/dist/async-eventemitter.d.ts +9 -0
  5. package/dist/async-eventemitter.d.ts.map +1 -0
  6. package/dist/async-eventemitter.js +35 -0
  7. package/dist/async-eventemitter.js.map +1 -0
  8. package/dist/check-configuration.d.ts +5 -0
  9. package/dist/check-configuration.d.ts.map +1 -0
  10. package/dist/check-configuration.js +35 -0
  11. package/dist/check-configuration.js.map +1 -0
  12. package/dist/check-versions.d.ts +3 -0
  13. package/dist/check-versions.d.ts.map +1 -0
  14. package/dist/check-versions.js +31 -0
  15. package/dist/check-versions.js.map +1 -0
  16. package/dist/config-types.d.ts +48 -0
  17. package/dist/config-types.d.ts.map +1 -0
  18. package/dist/config-types.js +4 -0
  19. package/dist/config-types.js.map +1 -0
  20. package/dist/decorator-changed.d.ts +16 -0
  21. package/dist/decorator-changed.d.ts.map +1 -0
  22. package/dist/decorator-changed.js +62 -0
  23. package/dist/decorator-changed.js.map +1 -0
  24. package/dist/dyson-360-msg-types.d.ts +113 -0
  25. package/dist/dyson-360-msg-types.d.ts.map +1 -0
  26. package/dist/dyson-360-msg-types.js +4 -0
  27. package/dist/dyson-360-msg-types.js.map +1 -0
  28. package/dist/dyson-360-types.d.ts +76 -0
  29. package/dist/dyson-360-types.d.ts.map +1 -0
  30. package/dist/dyson-360-types.js +72 -0
  31. package/dist/dyson-360-types.js.map +1 -0
  32. package/dist/dyson-air-msg-types.d.ts +99 -0
  33. package/dist/dyson-air-msg-types.d.ts.map +1 -0
  34. package/dist/dyson-air-msg-types.js +4 -0
  35. package/dist/dyson-air-msg-types.js.map +1 -0
  36. package/dist/dyson-air-sensor-types.d.ts +59 -0
  37. package/dist/dyson-air-sensor-types.d.ts.map +1 -0
  38. package/dist/dyson-air-sensor-types.js +4 -0
  39. package/dist/dyson-air-sensor-types.js.map +1 -0
  40. package/dist/dyson-air-state-types.d.ts +96 -0
  41. package/dist/dyson-air-state-types.d.ts.map +1 -0
  42. package/dist/dyson-air-state-types.js +4 -0
  43. package/dist/dyson-air-state-types.js.map +1 -0
  44. package/dist/dyson-air-types.d.ts +325 -0
  45. package/dist/dyson-air-types.d.ts.map +1 -0
  46. package/dist/dyson-air-types.js +382 -0
  47. package/dist/dyson-air-types.js.map +1 -0
  48. package/dist/dyson-device-360-base.d.ts +35 -0
  49. package/dist/dyson-device-360-base.d.ts.map +1 -0
  50. package/dist/dyson-device-360-base.js +233 -0
  51. package/dist/dyson-device-360-base.js.map +1 -0
  52. package/dist/dyson-device-360-commands.d.ts +7 -0
  53. package/dist/dyson-device-360-commands.d.ts.map +1 -0
  54. package/dist/dyson-device-360-commands.js +129 -0
  55. package/dist/dyson-device-360-commands.js.map +1 -0
  56. package/dist/dyson-device-360-faults-table.d.ts +20 -0
  57. package/dist/dyson-device-360-faults-table.d.ts.map +1 -0
  58. package/dist/dyson-device-360-faults-table.js +161 -0
  59. package/dist/dyson-device-360-faults-table.js.map +1 -0
  60. package/dist/dyson-device-360-faults.d.ts +10 -0
  61. package/dist/dyson-device-360-faults.d.ts.map +1 -0
  62. package/dist/dyson-device-360-faults.js +123 -0
  63. package/dist/dyson-device-360-faults.js.map +1 -0
  64. package/dist/dyson-device-360.d.ts +38 -0
  65. package/dist/dyson-device-360.d.ts.map +1 -0
  66. package/dist/dyson-device-360.js +45 -0
  67. package/dist/dyson-device-360.js.map +1 -0
  68. package/dist/dyson-device-air-base.d.ts +41 -0
  69. package/dist/dyson-device-air-base.d.ts.map +1 -0
  70. package/dist/dyson-device-air-base.js +446 -0
  71. package/dist/dyson-device-air-base.js.map +1 -0
  72. package/dist/dyson-device-air-heat.d.ts +52 -0
  73. package/dist/dyson-device-air-heat.d.ts.map +1 -0
  74. package/dist/dyson-device-air-heat.js +71 -0
  75. package/dist/dyson-device-air-heat.js.map +1 -0
  76. package/dist/dyson-device-air-quality.d.ts +7 -0
  77. package/dist/dyson-device-air-quality.d.ts.map +1 -0
  78. package/dist/dyson-device-air-quality.js +101 -0
  79. package/dist/dyson-device-air-quality.js.map +1 -0
  80. package/dist/dyson-device-air.d.ts +216 -0
  81. package/dist/dyson-device-air.d.ts.map +1 -0
  82. package/dist/dyson-device-air.js +80 -0
  83. package/dist/dyson-device-air.js.map +1 -0
  84. package/dist/dyson-device-base.d.ts +49 -0
  85. package/dist/dyson-device-base.d.ts.map +1 -0
  86. package/dist/dyson-device-base.js +46 -0
  87. package/dist/dyson-device-base.js.map +1 -0
  88. package/dist/dyson-device.d.ts +5 -0
  89. package/dist/dyson-device.d.ts.map +1 -0
  90. package/dist/dyson-device.js +43 -0
  91. package/dist/dyson-device.js.map +1 -0
  92. package/dist/dyson-mqtt-360.d.ts +16 -0
  93. package/dist/dyson-mqtt-360.d.ts.map +1 -0
  94. package/dist/dyson-mqtt-360.js +83 -0
  95. package/dist/dyson-mqtt-360.js.map +1 -0
  96. package/dist/dyson-mqtt-air.d.ts +48 -0
  97. package/dist/dyson-mqtt-air.d.ts.map +1 -0
  98. package/dist/dyson-mqtt-air.js +216 -0
  99. package/dist/dyson-mqtt-air.js.map +1 -0
  100. package/dist/dyson-mqtt-config.d.ts +6 -0
  101. package/dist/dyson-mqtt-config.d.ts.map +1 -0
  102. package/dist/dyson-mqtt-config.js +47 -0
  103. package/dist/dyson-mqtt-config.js.map +1 -0
  104. package/dist/dyson-mqtt-connect.d.ts +22 -0
  105. package/dist/dyson-mqtt-connect.d.ts.map +1 -0
  106. package/dist/dyson-mqtt-connect.js +145 -0
  107. package/dist/dyson-mqtt-connect.js.map +1 -0
  108. package/dist/dyson-mqtt-filter.d.ts +10 -0
  109. package/dist/dyson-mqtt-filter.d.ts.map +1 -0
  110. package/dist/dyson-mqtt-filter.js +25 -0
  111. package/dist/dyson-mqtt-filter.js.map +1 -0
  112. package/dist/dyson-mqtt-parse.d.ts +16 -0
  113. package/dist/dyson-mqtt-parse.d.ts.map +1 -0
  114. package/dist/dyson-mqtt-parse.js +100 -0
  115. package/dist/dyson-mqtt-parse.js.map +1 -0
  116. package/dist/dyson-mqtt-subscribe.d.ts +28 -0
  117. package/dist/dyson-mqtt-subscribe.d.ts.map +1 -0
  118. package/dist/dyson-mqtt-subscribe.js +85 -0
  119. package/dist/dyson-mqtt-subscribe.js.map +1 -0
  120. package/dist/dyson-mqtt.d.ts +51 -0
  121. package/dist/dyson-mqtt.d.ts.map +1 -0
  122. package/dist/dyson-mqtt.js +138 -0
  123. package/dist/dyson-mqtt.js.map +1 -0
  124. package/dist/dyson-types.d.ts +18 -0
  125. package/dist/dyson-types.d.ts.map +1 -0
  126. package/dist/dyson-types.js +20 -0
  127. package/dist/dyson-types.js.map +1 -0
  128. package/dist/endpoint-360-behavior.d.ts +55 -0
  129. package/dist/endpoint-360-behavior.d.ts.map +1 -0
  130. package/dist/endpoint-360-behavior.js +156 -0
  131. package/dist/endpoint-360-behavior.js.map +1 -0
  132. package/dist/endpoint-360-rvc.d.ts +14 -0
  133. package/dist/endpoint-360-rvc.d.ts.map +1 -0
  134. package/dist/endpoint-360-rvc.js +149 -0
  135. package/dist/endpoint-360-rvc.js.map +1 -0
  136. package/dist/endpoint-360.d.ts +35 -0
  137. package/dist/endpoint-360.d.ts.map +1 -0
  138. package/dist/endpoint-360.js +197 -0
  139. package/dist/endpoint-360.js.map +1 -0
  140. package/dist/endpoint-air-purifier.d.ts +24 -0
  141. package/dist/endpoint-air-purifier.d.ts.map +1 -0
  142. package/dist/endpoint-air-purifier.js +97 -0
  143. package/dist/endpoint-air-purifier.js.map +1 -0
  144. package/dist/endpoint-air-quality.d.ts +11 -0
  145. package/dist/endpoint-air-quality.d.ts.map +1 -0
  146. package/dist/endpoint-air-quality.js +104 -0
  147. package/dist/endpoint-air-quality.js.map +1 -0
  148. package/dist/endpoint-air-thermostat.d.ts +358 -0
  149. package/dist/endpoint-air-thermostat.d.ts.map +1 -0
  150. package/dist/endpoint-air-thermostat.js +67 -0
  151. package/dist/endpoint-air-thermostat.js.map +1 -0
  152. package/dist/endpoint-air.d.ts +125 -0
  153. package/dist/endpoint-air.d.ts.map +1 -0
  154. package/dist/endpoint-air.js +459 -0
  155. package/dist/endpoint-air.js.map +1 -0
  156. package/dist/endpoint-base.d.ts +36 -0
  157. package/dist/endpoint-base.d.ts.map +1 -0
  158. package/dist/endpoint-base.js +137 -0
  159. package/dist/endpoint-base.js.map +1 -0
  160. package/dist/error-360.d.ts +35 -0
  161. package/dist/error-360.d.ts.map +1 -0
  162. package/dist/error-360.js +105 -0
  163. package/dist/error-360.js.map +1 -0
  164. package/dist/index.d.ts +5 -0
  165. package/dist/index.d.ts.map +1 -0
  166. package/dist/index.js +8 -0
  167. package/dist/index.js.map +1 -0
  168. package/dist/logger-filter.d.ts +23 -0
  169. package/dist/logger-filter.d.ts.map +1 -0
  170. package/dist/logger-filter.js +104 -0
  171. package/dist/logger-filter.js.map +1 -0
  172. package/dist/logger-options.d.ts +18 -0
  173. package/dist/logger-options.d.ts.map +1 -0
  174. package/dist/logger-options.js +41 -0
  175. package/dist/logger-options.js.map +1 -0
  176. package/dist/logger-prefix.d.ts +12 -0
  177. package/dist/logger-prefix.d.ts.map +1 -0
  178. package/dist/logger-prefix.js +37 -0
  179. package/dist/logger-prefix.js.map +1 -0
  180. package/dist/periodic.d.ts +31 -0
  181. package/dist/periodic.d.ts.map +1 -0
  182. package/dist/periodic.js +102 -0
  183. package/dist/periodic.js.map +1 -0
  184. package/dist/platform.d.ts +18 -0
  185. package/dist/platform.d.ts.map +1 -0
  186. package/dist/platform.js +138 -0
  187. package/dist/platform.js.map +1 -0
  188. package/dist/settings.d.ts +9 -0
  189. package/dist/settings.d.ts.map +1 -0
  190. package/dist/settings.js +28 -0
  191. package/dist/settings.js.map +1 -0
  192. package/dist/ti/config-types-ti.d.ts +16 -0
  193. package/dist/ti/config-types-ti.d.ts.map +1 -0
  194. package/dist/ti/config-types-ti.js +65 -0
  195. package/dist/ti/config-types-ti.js.map +1 -0
  196. package/dist/ti/config-types.d.ts +37 -0
  197. package/dist/ti/config-types.d.ts.map +1 -0
  198. package/dist/ti/config-types.js +11 -0
  199. package/dist/ti/config-types.js.map +1 -0
  200. package/dist/ti/dyson-360-msg-types-ti.d.ts +22 -0
  201. package/dist/ti/dyson-360-msg-types-ti.d.ts.map +1 -0
  202. package/dist/ti/dyson-360-msg-types-ti.js +134 -0
  203. package/dist/ti/dyson-360-msg-types-ti.js.map +1 -0
  204. package/dist/ti/dyson-360-msg-types.d.ts +55 -0
  205. package/dist/ti/dyson-360-msg-types.d.ts.map +1 -0
  206. package/dist/ti/dyson-360-msg-types.js +13 -0
  207. package/dist/ti/dyson-360-msg-types.js.map +1 -0
  208. package/dist/ti/dyson-360-types-ti.d.ts +17 -0
  209. package/dist/ti/dyson-360-types-ti.d.ts.map +1 -0
  210. package/dist/ti/dyson-360-types-ti.js +94 -0
  211. package/dist/ti/dyson-360-types-ti.js.map +1 -0
  212. package/dist/ti/dyson-360-types.d.ts +40 -0
  213. package/dist/ti/dyson-360-types.d.ts.map +1 -0
  214. package/dist/ti/dyson-360-types.js +11 -0
  215. package/dist/ti/dyson-360-types.js.map +1 -0
  216. package/dist/ti/dyson-air-msg-types-ti.d.ts +19 -0
  217. package/dist/ti/dyson-air-msg-types-ti.d.ts.map +1 -0
  218. package/dist/ti/dyson-air-msg-types-ti.js +115 -0
  219. package/dist/ti/dyson-air-msg-types-ti.js.map +1 -0
  220. package/dist/ti/dyson-air-msg-types.d.ts +46 -0
  221. package/dist/ti/dyson-air-msg-types.d.ts.map +1 -0
  222. package/dist/ti/dyson-air-msg-types.js +15 -0
  223. package/dist/ti/dyson-air-msg-types.js.map +1 -0
  224. package/dist/ti/dyson-air-sensor-types-ti.d.ts +9 -0
  225. package/dist/ti/dyson-air-sensor-types-ti.d.ts.map +1 -0
  226. package/dist/ti/dyson-air-sensor-types-ti.js +68 -0
  227. package/dist/ti/dyson-air-sensor-types-ti.js.map +1 -0
  228. package/dist/ti/dyson-air-sensor-types.d.ts +16 -0
  229. package/dist/ti/dyson-air-sensor-types.d.ts.map +1 -0
  230. package/dist/ti/dyson-air-sensor-types.js +12 -0
  231. package/dist/ti/dyson-air-sensor-types.js.map +1 -0
  232. package/dist/ti/dyson-air-state-types-ti.d.ts +9 -0
  233. package/dist/ti/dyson-air-state-types-ti.d.ts.map +1 -0
  234. package/dist/ti/dyson-air-state-types-ti.js +105 -0
  235. package/dist/ti/dyson-air-state-types-ti.js.map +1 -0
  236. package/dist/ti/dyson-air-state-types.d.ts +16 -0
  237. package/dist/ti/dyson-air-state-types.d.ts.map +1 -0
  238. package/dist/ti/dyson-air-state-types.js +12 -0
  239. package/dist/ti/dyson-air-state-types.js.map +1 -0
  240. package/dist/ti/dyson-air-types-ti.d.ts +59 -0
  241. package/dist/ti/dyson-air-types-ti.d.ts.map +1 -0
  242. package/dist/ti/dyson-air-types-ti.js +385 -0
  243. package/dist/ti/dyson-air-types-ti.js.map +1 -0
  244. package/dist/ti/dyson-air-types.d.ts +166 -0
  245. package/dist/ti/dyson-air-types.d.ts.map +1 -0
  246. package/dist/ti/dyson-air-types.js +11 -0
  247. package/dist/ti/dyson-air-types.js.map +1 -0
  248. package/dist/ti/dyson-types-ti.d.ts +10 -0
  249. package/dist/ti/dyson-types-ti.d.ts.map +1 -0
  250. package/dist/ti/dyson-types-ti.js +29 -0
  251. package/dist/ti/dyson-types-ti.js.map +1 -0
  252. package/dist/ti/dyson-types.d.ts +19 -0
  253. package/dist/ti/dyson-types.d.ts.map +1 -0
  254. package/dist/ti/dyson-types.js +11 -0
  255. package/dist/ti/dyson-types.js.map +1 -0
  256. package/dist/utils.d.ts +24 -0
  257. package/dist/utils.d.ts.map +1 -0
  258. package/dist/utils.js +150 -0
  259. package/dist/utils.js.map +1 -0
  260. package/matterbridge-dyson-robot.schema.json +359 -0
  261. package/package.json +93 -0
@@ -0,0 +1,83 @@
1
+ // Matterbridge plugin for Dyson robot vacuum and air treatment devices
2
+ // Copyright © 2025 Alexander Thoukydides
3
+ import { DysonMqtt } from './dyson-mqtt.js';
4
+ import { checkers as dysonMsgCheckers360 } from './ti/dyson-360-msg-types.js';
5
+ import { tryListener } from './utils.js';
6
+ import { Dyson360CleaningMode, Dyson360CleaningType } from './dyson-360-types.js';
7
+ import { DysonModeReason } from './dyson-types.js';
8
+ // Configuration of a Dyson MQTT client for robot vacuums
9
+ const DYSON_MQTT_CONFIG_360 = {
10
+ topics: {
11
+ command: '@/@/command',
12
+ subscribe: ['@/@/status'],
13
+ other: ['@/initialconnection/credentials',
14
+ '@/initialconnection/status']
15
+ },
16
+ messages: {
17
+ prefix: 'Dyson360Msg',
18
+ checkers: dysonMsgCheckers360
19
+ }
20
+ };
21
+ // Dyson MQTT client for robot vacuums
22
+ export class DysonMqtt360 extends DysonMqtt {
23
+ // Construct a new MQTT client
24
+ constructor(log, config, device) {
25
+ super(log, config, device, DYSON_MQTT_CONFIG_360);
26
+ // Handle MQTT events
27
+ this.on('subscribed', tryListener(this, async () =>
28
+ // Request the current status when (re)connected
29
+ this.publish('REQUEST-CURRENT-STATE'))).on('message', tryListener(this, msg => {
30
+ // Update the robot vacuum state from the received messages
31
+ switch (msg.msg) {
32
+ case 'STATE-CHANGE':
33
+ msg = this.convertStateChange(msg);
34
+ // (fallthrough)
35
+ case 'CURRENT-STATE':
36
+ this.updateState(msg);
37
+ break;
38
+ }
39
+ }));
40
+ }
41
+ // Convert a STATE-CHANGE message to CURRENT-STATE format
42
+ convertStateChange(msg) {
43
+ const { msg: _, newstate: state, oldstate, endOfClean, ...otherFields } = msg;
44
+ return { msg: 'CURRENT-STATE', state, ...otherFields };
45
+ }
46
+ // Update the robot vacuum state from a received message
47
+ updateState(msg) {
48
+ // Copy most fields from the message to the state
49
+ const { msg: _msg, time, ...status } = msg;
50
+ this.status.faults = undefined; // (ensure faults get cleared)
51
+ Object.assign(this.status, status);
52
+ // State is fully initialised after the first message has been received
53
+ if (!this.status.initialised) {
54
+ this.status.initialised = true;
55
+ this.log.info('MQTT client initialisation complete');
56
+ }
57
+ }
58
+ // Publish a robot vacuum command to perform an action
59
+ commandAction(msg) {
60
+ switch (msg) {
61
+ case 'START':
62
+ return this.publish('START', {
63
+ 'mode-reason': DysonModeReason.LocalApp,
64
+ fullCleanType: Dyson360CleaningType.Immediate,
65
+ cleaningMode: this.status.defaultCleaningMode && Dyson360CleaningMode.Global
66
+ });
67
+ case 'PAUSE':
68
+ case 'RESUME':
69
+ case 'ABORT':
70
+ return this.publish(msg, {
71
+ 'mode-reason': DysonModeReason.LocalApp
72
+ });
73
+ }
74
+ }
75
+ // Publish a robot vacuum command to set the default power level
76
+ commandPower(defaultVacuumPowerMode) {
77
+ return this.publish('STATE-SET', {
78
+ 'mode-reason': DysonModeReason.LocalApp,
79
+ data: { defaultVacuumPowerMode }
80
+ });
81
+ }
82
+ }
83
+ //# sourceMappingURL=dyson-mqtt-360.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-360.js","sourceRoot":"","sources":["../src/dyson-mqtt-360.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { DysonMqtt } from './dyson-mqtt.js';
2
+ import { TypeMap as DysonMsgMapAir } from './ti/dyson-air-msg-types.js';
3
+ import { Config, DeviceConfig } from './config-types.js';
4
+ import { AnsiLogger } from 'matterbridge/logger';
5
+ import { DysonAirMsgCurrentFaults, DysonAirMsgCurrentState, DysonAirMsgEnvironmentalCurrentSensorData, DysonAirMsgFaultsChange, DysonAirMsgHello, DysonAirMsgStateChange } from './dyson-air-msg-types.js';
6
+ import { DysonAirModuleError, DysonAirModuleWarning, DysonAirProductError, DysonAirProductWarning, DysonAirSensorValueEnum, DysonAirSleepTimerEnum } from './dyson-air-types.js';
7
+ import { DysonAirCurrentSensorData } from './dyson-air-sensor-types.js';
8
+ import { DysonAirProductState } from './dyson-air-state-types.js';
9
+ import { DysonMsgAny } from './dyson-mqtt-parse.js';
10
+ type ProductStateNumericEnumKeys = 'cflr' | 'fnsp' | 'nmdv';
11
+ declare const PRODUCT_STATE_NUMERIC_KEYS: readonly ["hmax", "hflr", "filf", "osal", "osau", "cdrr", "cltr", "humt", "rect"];
12
+ type ProductStateNumericKeys = typeof PRODUCT_STATE_NUMERIC_KEYS[number];
13
+ type ProductStateVerbatimKeys = Exclude<keyof DysonAirProductState, ProductStateNumericEnumKeys | ProductStateNumericKeys>;
14
+ export type DysonMqttProductState = Pick<DysonAirProductState, ProductStateVerbatimKeys> & {
15
+ [K in ProductStateNumericEnumKeys]?: DysonAirProductState[K] | number;
16
+ } & Partial<Record<ProductStateNumericKeys, number>>;
17
+ interface Faults {
18
+ productErrors: Set<DysonAirProductError>;
19
+ productWarnings: Set<DysonAirProductWarning>;
20
+ moduleErrors: Set<DysonAirModuleError>;
21
+ moduleWarnings: Set<DysonAirModuleWarning>;
22
+ }
23
+ type SensorDataV2 = 'hchr' | 'p25r' | 'p10r' | 'va10';
24
+ export type DysonMqttStatusAirSensor = {
25
+ [K in keyof Omit<DysonAirCurrentSensorData, 'sltm' | SensorDataV2>]: DysonAirSensorValueEnum | number;
26
+ } & {
27
+ sltm?: DysonAirSleepTimerEnum | number;
28
+ };
29
+ export type DysonMqttStatusAir = Pick<DysonAirMsgHello, 'version'> & DysonMqttProductState & DysonMqttStatusAirSensor & Partial<Faults>;
30
+ export declare class DysonMqttAir extends DysonMqtt<DysonMsgMapAir, DysonMqttStatusAir> {
31
+ private initialiseMsgs;
32
+ constructor(log: AnsiLogger, config: Config, device: DeviceConfig);
33
+ updateStateFromMessage(msg: DysonMsgAny<DysonMsgMapAir>): void;
34
+ checkIfInitialised(msg: DysonMsgAny<DysonMsgMapAir>): void;
35
+ updateProductInfo(msg: DysonAirMsgHello): void;
36
+ convertStateChange(msg: DysonAirMsgStateChange): DysonAirMsgCurrentState;
37
+ convertFaultsChange(msg: DysonAirMsgFaultsChange): DysonAirMsgCurrentFaults;
38
+ updateState(msg: DysonAirMsgCurrentState): void;
39
+ updateSensorData(msg: DysonAirMsgEnvironmentalCurrentSensorData): void;
40
+ updateFaults(msg: DysonAirMsgCurrentFaults): void;
41
+ commandStateSet(productState: DysonMqttProductState): Promise<void>;
42
+ parseNumericOrEnumValue<T extends string>(description: string, enumMap: Record<string, T>, value?: string, divisor?: number): number | T | undefined;
43
+ }
44
+ export declare function KtoC(kelvin: number): number;
45
+ export declare function roundedKtoC(kelvin: number): number;
46
+ export declare function roundedCtoK(celsius: number): number;
47
+ export {};
48
+ //# sourceMappingURL=dyson-mqtt-air.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-air.d.ts","sourceRoot":"","sources":["../src/dyson-mqtt-air.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,216 @@
1
+ // Matterbridge plugin for Dyson robot vacuum and air treatment devices
2
+ // Copyright © 2025 Alexander Thoukydides
3
+ import { DysonMqtt } from './dyson-mqtt.js';
4
+ import { checkers as dysonMsgCheckersAir } from './ti/dyson-air-msg-types.js';
5
+ import { formatList, tryListener } from './utils.js';
6
+ import { DysonAirCarbonFilterEnum, DysonAirErrorCodeEnum, DysonAirFanSpeed, DysonAirFaultStatus, DysonAirModuleError, DysonAirModuleWarning, DysonAirProductError, DysonAirProductWarning, DysonAirSensorValueEnum, DysonAirSleepTimerEnum, DysonAirWarningCodeEnum } from './dyson-air-types.js';
7
+ import { DysonModeReason } from './dyson-types.js';
8
+ // Configuration of a Dyson MQTT client for robot vacuums
9
+ const DYSON_MQTT_CONFIG_AIR = {
10
+ topics: {
11
+ command: '@/@/command',
12
+ subscribe: ['@/@/status/connection',
13
+ '@/@/status/current',
14
+ '@/@/status/faults']
15
+ },
16
+ messages: {
17
+ prefix: 'DysonAirMsg',
18
+ checkers: dysonMsgCheckersAir
19
+ }
20
+ };
21
+ const PRODUCT_STATE_NUMERIC_KEYS = ['hmax', 'hflr', 'filf', 'osal', 'osau', 'cdrr', 'cltr', 'humt', 'rect'];
22
+ // Dyson MQTT client for air treatment machines
23
+ export class DysonMqttAir extends DysonMqtt {
24
+ // Messages still required for initialisation
25
+ initialiseMsgs = new Set([
26
+ 'HELLO',
27
+ 'CURRENT-STATE',
28
+ 'ENVIRONMENTAL-CURRENT-SENSOR-DATA'
29
+ ]);
30
+ // Construct a new MQTT client
31
+ constructor(log, config, device) {
32
+ super(log, config, device, DYSON_MQTT_CONFIG_AIR);
33
+ // Handle MQTT events
34
+ this.on('subscribed', tryListener(this, async () => {
35
+ // Request the current status when (re)connected
36
+ await this.publish('REQUEST-CURRENT-STATE');
37
+ await this.publish('REQUEST-PRODUCT-ENVIRONMENT-CURRENT-SENSOR-DATA');
38
+ })).on('message', tryListener(this, msg => {
39
+ // Update the robot vacuum state from the received messages
40
+ this.updateStateFromMessage(msg);
41
+ this.checkIfInitialised(msg);
42
+ }));
43
+ }
44
+ // Update state from a received message
45
+ updateStateFromMessage(msg) {
46
+ switch (msg.msg) {
47
+ case 'HELLO':
48
+ this.updateProductInfo(msg);
49
+ break;
50
+ case 'STATE-CHANGE':
51
+ msg = this.convertStateChange(msg);
52
+ // (fallthrough)
53
+ case 'CURRENT-STATE':
54
+ this.updateState(msg);
55
+ break;
56
+ case 'ENVIRONMENTAL-CURRENT-SENSOR-DATA':
57
+ this.updateSensorData(msg);
58
+ break;
59
+ case 'FAULTS-CHANGE':
60
+ msg = this.convertFaultsChange(msg);
61
+ // (fallthrough)
62
+ case 'CURRENT-FAULTS':
63
+ this.updateFaults(msg);
64
+ }
65
+ }
66
+ // Check whether all required messages have been received
67
+ checkIfInitialised(msg) {
68
+ this.initialiseMsgs.delete(msg.msg);
69
+ if (!this.status.initialised && this.initialiseMsgs.size === 0) {
70
+ this.status.initialised = true;
71
+ this.log.info('MQTT client initialisation complete');
72
+ }
73
+ }
74
+ // Update hardware and software state from a received message
75
+ updateProductInfo(msg) {
76
+ this.status.version = msg.version;
77
+ }
78
+ // Convert a STATE-CHANGE message to CURRENT-STATE format
79
+ convertStateChange(msg) {
80
+ const productState = convertChangesToStatus(msg.productState);
81
+ return { ...msg, msg: 'CURRENT-STATE', productState };
82
+ }
83
+ // Convert a FAULTS-CHANGE message to CURRENT-FAULTS format
84
+ convertFaultsChange(msg) {
85
+ return {
86
+ msg: 'CURRENT-FAULTS',
87
+ time: msg.time,
88
+ productErrors: convertChangesToStatus(msg.productErrors),
89
+ productWarnings: convertChangesToStatus(msg.productWarnings),
90
+ moduleErrors: convertChangesToStatus(msg.moduleErrors),
91
+ moduleWarnings: convertChangesToStatus(msg.moduleWarnings)
92
+ };
93
+ }
94
+ // Update product state from a received message
95
+ updateState(msg) {
96
+ // Check whether the error and warning codes are known
97
+ const checkCode = (description, key, knownValues) => {
98
+ const value = msg.productState[key];
99
+ if (value === undefined || Object.values(knownValues).includes(value))
100
+ return;
101
+ this.log.warn(`Received unknown ${description}: ${value}`);
102
+ };
103
+ checkCode('error code', 'ercd', DysonAirErrorCodeEnum);
104
+ checkCode('warning code', 'wacd', DysonAirWarningCodeEnum);
105
+ // Copy everything initially, but some values will be overwritten
106
+ const { productState } = msg;
107
+ Object.assign(this.status, productState);
108
+ // Parse values that can be either numeric or enum values
109
+ this.status.cflr = this.parseNumericOrEnumValue('cflr', DysonAirCarbonFilterEnum, productState.cflr);
110
+ this.status.fnsp = this.parseNumericOrEnumValue('fnsp', DysonAirFanSpeed, productState.fnsp);
111
+ this.status.nmdv = this.parseNumericOrEnumValue('nmdv', DysonAirFanSpeed, productState.nmdv);
112
+ // Parse values that should always be numeric strings
113
+ for (const key of PRODUCT_STATE_NUMERIC_KEYS) {
114
+ const value = productState[key];
115
+ this.status[key] = value === undefined ? undefined : Number(value);
116
+ }
117
+ // Convert target temperature from Kelvin to Celsius
118
+ if (this.status.hmax)
119
+ this.status.hmax = roundedKtoC(this.status.hmax / 10);
120
+ }
121
+ // Update environmental sensor data from a received message
122
+ updateSensorData(msg) {
123
+ const parse = (field, divisor) => this.parseNumericOrEnumValue(field, DysonAirSensorValueEnum, msg.data[field], divisor);
124
+ // Convert the sensor data to numeric form with appropriate scaling
125
+ this.status.hact = parse('hact');
126
+ this.status.co2r = parse('co2r');
127
+ this.status.pact = parse('pact');
128
+ this.status.hcho = parse('hchr') ?? parse('hcho');
129
+ this.status.noxl = parse('noxl');
130
+ this.status.pm25 = parse('p25r') ?? parse('pm25');
131
+ this.status.pm10 = parse('p10r') ?? parse('pm10');
132
+ this.status.vact = parse('va10') ?? parse('vact', 1 / 11);
133
+ // Convert temperature from Kelvin to Celsius
134
+ const kelvin = parse('tact', 10);
135
+ this.status.tact = typeof kelvin === 'number' ? KtoC(kelvin) : kelvin;
136
+ // Similarly for the sleep timer
137
+ this.status.sltm = this.parseNumericOrEnumValue('sltm', DysonAirSleepTimerEnum, msg.data.sltm);
138
+ }
139
+ // Update environmental sensor data from a received message
140
+ updateFaults(msg) {
141
+ const faultKeysCheckers = [
142
+ ['productErrors', DysonAirProductError],
143
+ ['productWarnings', DysonAirProductWarning],
144
+ ['moduleErrors', DysonAirModuleError],
145
+ ['moduleWarnings', DysonAirModuleWarning]
146
+ ];
147
+ // Convert each fault type to a set of active fault codes
148
+ for (const [key, knownValues] of faultKeysCheckers) {
149
+ // Identify unknown faults and active known faults
150
+ const activeFaults = new Set();
151
+ const unknownFaults = new Set();
152
+ for (const [fault, status] of Object.entries(msg[key])) {
153
+ if (!Object.values(knownValues).includes(fault))
154
+ unknownFaults.add(fault);
155
+ else if (status === DysonAirFaultStatus.Fail)
156
+ activeFaults.add(fault);
157
+ }
158
+ // Log warnings for unknown faults (both active and inactive)
159
+ if (unknownFaults.size) {
160
+ this.log.warn(`Received unknown ${key}: ${formatList([...unknownFaults])}`);
161
+ }
162
+ // Update the status with the set of active faults
163
+ this.status[key] = activeFaults;
164
+ }
165
+ }
166
+ // Publish an air treatment machine command to set the product state
167
+ commandStateSet(productState) {
168
+ // Convert values to the format required in the MQTT message
169
+ const data = {};
170
+ const mapEntry = ([key, value]) => {
171
+ if (value === undefined)
172
+ return;
173
+ let valueString;
174
+ if (typeof value === 'number') {
175
+ // Convert numeric values to four digit strings for the command
176
+ const numericValue = key === 'hmax' ? roundedCtoK(value) * 10 : value;
177
+ valueString = numericValue.toFixed(0).padStart(4, '0');
178
+ }
179
+ else {
180
+ // Enum or general string values are already the correct type
181
+ valueString = value;
182
+ }
183
+ data[key] = valueString;
184
+ };
185
+ const entries = Object.entries(productState);
186
+ entries.forEach(mapEntry);
187
+ // Publish the command
188
+ return this.publish('STATE-SET', {
189
+ 'mode-reason': DysonModeReason.LocalApp,
190
+ data
191
+ });
192
+ }
193
+ // Parse strings that can be numeric or enum values, returning the number or enum value
194
+ parseNumericOrEnumValue(description, enumMap, value, divisor = 1) {
195
+ if (value === undefined || value === '')
196
+ return;
197
+ // Try parsing as a decimal natural number
198
+ if (/^\d+$/.test(value))
199
+ return Number(value) / divisor;
200
+ // Otherwise check if it is a member of the specified enum type
201
+ const expectedValues = Object.values(enumMap);
202
+ if (expectedValues.includes(value))
203
+ return value;
204
+ this.log.warn(`Received unexpected '${description}' value: ${value}`
205
+ + ` (expected ${expectedValues.join(', ')}, or a numeric string)`);
206
+ }
207
+ }
208
+ function convertChangesToStatus(changes) {
209
+ return Object.fromEntries(Object.keys(changes).map((key) => [key, changes[key]?.[1]]));
210
+ }
211
+ // Temperature conversion (accurate version for 'tact')
212
+ export function KtoC(kelvin) { return kelvin - 273.15; }
213
+ // Temperature conversion (rounded version for 'hmax')
214
+ export function roundedKtoC(kelvin) { return kelvin - 273; }
215
+ export function roundedCtoK(celsius) { return celsius + 273; }
216
+ //# sourceMappingURL=dyson-mqtt-air.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-air.js","sourceRoot":"","sources":["../src/dyson-mqtt-air.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { DeviceConfig, DeviceConfigAny, DeviceConfigIoT, DeviceConfigMqtt, DeviceConfigWiFi } from './config-types.js';
2
+ export declare function isConfigWiFi(config: DeviceConfigAny): config is DeviceConfigWiFi;
3
+ export declare function isConfigMqtt(config: DeviceConfigAny): config is DeviceConfigMqtt;
4
+ export declare function isConfigIoT(config: DeviceConfigAny): config is DeviceConfigIoT;
5
+ export declare function getDeviceConfigMqtt(config: DeviceConfigAny): DeviceConfig;
6
+ //# sourceMappingURL=dyson-mqtt-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-config.d.ts","sourceRoot":"","sources":["../src/dyson-mqtt-config.ts"],"names":[],"mappings":"AAIA,OAAO,EACH,YAAY,EACZ,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EACnB,MAAM,mBAAmB,CAAC;AAc3B,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,IAAI,gBAAgB,CAEhF;AACD,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,IAAI,gBAAgB,CAEhF;AACD,wBAAgB,WAAW,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,IAAI,eAAe,CAE9E;AAGD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,YAAY,CAMzE"}
@@ -0,0 +1,47 @@
1
+ // Matterbridge plugin for Dyson robot vacuum and air treatment devices
2
+ // Copyright © 2025 Alexander Thoukydides
3
+ import { createHash } from 'crypto';
4
+ import { assertIsDefined } from './utils.js';
5
+ // Regular expressions to parse Wi-Fi setup SSIDs
6
+ const SSID_360EYE_RE = /^(360EYE-)?(?<sn>[A-Z0-9]{3}-[A-Z]{2}-[A-Z0-9]{8,})/;
7
+ const SSID_OTHER_RE = /^DYSON-(?<sn>[A-Z0-9]{3}-[A-Z]{2}-[A-Z0-9]{8,})-(?<type>[0-9]{3}[A-Z]?)$/;
8
+ // Dyson 360 Eye uses a different Wi-Fi setup SSID format
9
+ const TYPE_360EYE = 'N223';
10
+ // Dyson Pure Hot+Cool Link (HP02) SSIDs don't match the MQTT topic and username
11
+ const TYPE_MAP = new Map([['455A', '455']]);
12
+ // Identify the type of credentials provided
13
+ export function isConfigWiFi(config) {
14
+ return 'wifi_ssid' in config;
15
+ }
16
+ export function isConfigMqtt(config) {
17
+ return 'password' in config;
18
+ }
19
+ export function isConfigIoT(config) {
20
+ return 'endpoint' in config;
21
+ }
22
+ // Convert Wi-Fi setup credentials to local MQTT credentials
23
+ export function getDeviceConfigMqtt(config) {
24
+ if (isConfigMqtt(config) || isConfigIoT(config))
25
+ return config;
26
+ const { wifi_ssid, wifi_password } = config;
27
+ const { root_topic, username } = parseSSID(wifi_ssid);
28
+ const password = hashWifiPassword(wifi_password);
29
+ return { ...config, username, password, root_topic };
30
+ }
31
+ // Extract the MQTT topic and username from a Wi-Fi setup SSID
32
+ function parseSSID(ssid) {
33
+ const match = SSID_360EYE_RE.exec(ssid) ?? SSID_OTHER_RE.exec(ssid);
34
+ if (!match)
35
+ throw new Error(`Unable to parse Product SSID: ${ssid}`);
36
+ const root_topic = match.groups?.type ?? TYPE_360EYE;
37
+ const serialNumber = match.groups?.sn;
38
+ assertIsDefined(serialNumber);
39
+ const username = TYPE_MAP.get(serialNumber) ?? serialNumber;
40
+ return { root_topic, username };
41
+ }
42
+ // Convert a Wi-Fi password to the form required for MQTT
43
+ function hashWifiPassword(password) {
44
+ const sha512 = createHash('sha512').update(password).digest();
45
+ return sha512.toString('base64');
46
+ }
47
+ //# sourceMappingURL=dyson-mqtt-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-config.js","sourceRoot":"","sources":["../src/dyson-mqtt-config.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,yCAAyC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAQpC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,iDAAiD;AACjD,MAAM,cAAc,GAAG,qDAAqD,CAAC;AAC7E,MAAM,aAAa,GAAI,0EAA0E,CAAC;AAElG,yDAAyD;AACzD,MAAM,WAAW,GAAG,MAAM,CAAC;AAE3B,gFAAgF;AAChF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAiB,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AAE5D,4CAA4C;AAC5C,MAAM,UAAU,YAAY,CAAC,MAAuB;IAChD,OAAO,WAAW,IAAI,MAAM,CAAC;AACjC,CAAC;AACD,MAAM,UAAU,YAAY,CAAC,MAAuB;IAChD,OAAO,UAAU,IAAI,MAAM,CAAC;AAChC,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,MAAuB;IAC/C,OAAO,UAAU,IAAI,MAAM,CAAC;AAChC,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,mBAAmB,CAAC,MAAuB;IACvD,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC/D,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC;IAC5C,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,CAAC,CAAC;IACjD,OAAO,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AACzD,CAAC;AAED,8DAA8D;AAC9D,SAAS,SAAS,CAAC,IAAY;IAC3B,MAAM,KAAK,GAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,EAAE,CAAC,CAAC;IACrE,MAAM,UAAU,GAAM,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,WAAW,CAAC;IACxD,MAAM,YAAY,GAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;IACvC,eAAe,CAAC,YAAY,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC;IAC5D,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AACpC,CAAC;AAED,yDAAyD;AACzD,SAAS,gBAAgB,CAAC,QAAgB;IACtC,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;IAC9D,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACrC,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { AnsiLogger } from 'matterbridge/logger';
2
+ import { IClientOptions, MqttClient } from 'mqtt';
3
+ import { Config, DeviceConfig, DeviceConfigIoT, DeviceConfigMqtt } from './config-types.js';
4
+ export declare abstract class DysonMqttConnection<T extends DeviceConfig = DeviceConfig> {
5
+ readonly log: AnsiLogger;
6
+ readonly config: Config;
7
+ readonly deviceConfig: T;
8
+ readonly mqtt: MqttClient;
9
+ terminate: AbortController;
10
+ backoff: number;
11
+ uptimeStart?: number;
12
+ constructor(log: AnsiLogger, config: Config, deviceConfig: T, brokerUrl: string, extraOptions: IClientOptions);
13
+ reconnect(): Promise<void>;
14
+ stop(): Promise<void>;
15
+ }
16
+ export declare class DysonMqttConnectionLocal extends DysonMqttConnection<DeviceConfigMqtt> {
17
+ constructor(log: AnsiLogger, config: Config, deviceConfig: DeviceConfigMqtt);
18
+ }
19
+ export declare class DysonMqttConnectionIoT extends DysonMqttConnection<DeviceConfigIoT> {
20
+ constructor(log: AnsiLogger, config: Config, deviceConfig: DeviceConfigIoT);
21
+ }
22
+ //# sourceMappingURL=dyson-mqtt-connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-connect.d.ts","sourceRoot":"","sources":["../src/dyson-mqtt-connect.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAW,cAAc,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAC3D,OAAO,EACH,MAAM,EACN,YAAY,EACZ,eAAe,EACf,gBAAgB,EACnB,MAAM,mBAAmB,CAAC;AAW3B,8BAAsB,mBAAmB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY;IAcvE,QAAQ,CAAC,GAAG,EAAY,UAAU;IAClC,QAAQ,CAAC,MAAM,EAAS,MAAM;IAC9B,QAAQ,CAAC,YAAY,EAAG,CAAC;IAb7B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAG1B,SAAS,kBAA+B;IAGxC,OAAO,SAAuB;IAC9B,WAAW,CAAC,EAAI,MAAM,CAAC;gBAIV,GAAG,EAAY,UAAU,EACzB,MAAM,EAAS,MAAM,EACrB,YAAY,EAAG,CAAC,EACzB,SAAS,EAAe,MAAM,EAC9B,YAAY,EAAY,cAAc;IAgEpC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAM9B;AAGD,qBAAa,wBAAyB,SAAQ,mBAAmB,CAAC,gBAAgB,CAAC;gBAGnE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,gBAAgB;CAQ9E;AAGD,qBAAa,sBAAuB,SAAQ,mBAAmB,CAAC,eAAe,CAAC;gBAGhE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe;CAa7E"}
@@ -0,0 +1,145 @@
1
+ // Matterbridge plugin for Dyson robot vacuum and air treatment devices
2
+ // Copyright © 2025 Alexander Thoukydides
3
+ import { connect } from 'mqtt';
4
+ import { formatMilliseconds, logError, MS } from './utils.js';
5
+ import { setTimeout } from 'node:timers/promises';
6
+ // Reconnection back-off timings
7
+ const BACKOFF_MIN = 1 * MS; // 1 second minimum backoff
8
+ const BACKOFF_MAX = 60 * MS; // 1 minute maximum backoff
9
+ const BACKOFF_FACTOR = 2; // Double backoff on each failure
10
+ const BACKOFF_RESET_UPTIME = 10 * MS; // >10 second connection to use MIN
11
+ // Manage a Dyson MQTT broker connection
12
+ export class DysonMqttConnection {
13
+ log;
14
+ config;
15
+ deviceConfig;
16
+ // The MQTT client
17
+ mqtt;
18
+ // Abandon reconnection attempts
19
+ terminate = new AbortController();
20
+ // Backoff for reconnection attempts
21
+ backoff = BACKOFF_MIN;
22
+ uptimeStart;
23
+ // Construct a new MQTT client
24
+ constructor(log, config, deviceConfig, brokerUrl, extraOptions) {
25
+ this.log = log;
26
+ this.config = config;
27
+ this.deviceConfig = deviceConfig;
28
+ // MQTT debug logging, if enabled
29
+ let mqttLog;
30
+ if (this.config.debugFeatures.includes('Log MQTT Client')) {
31
+ mqttLog = (...args) => { this.log.debug('MQTT client:', ...args); };
32
+ }
33
+ // Common MQTT options
34
+ const options = {
35
+ log: mqttLog,
36
+ keepalive: 10, // Max 10 seconds between packets
37
+ manualConnect: true, // Disable automatic (re)connection
38
+ reconnectOnConnackError: false, // Disable automatic connection retry
39
+ reconnectPeriod: 0, // Disable automatic reconnection
40
+ resubscribe: true, // Resubscribe to topics on reconnect
41
+ rejectUnauthorized: false, // Allow self-signed certificates
42
+ protocolId: 'MQIsdp', // MQTT version 3.1
43
+ protocolVersion: 3,
44
+ ...extraOptions
45
+ };
46
+ // Create and configure the MQTT client
47
+ this.mqtt = connect(brokerUrl, options);
48
+ this.mqtt.on('connect', () => {
49
+ // Successful connection, so start measuring uptime
50
+ log.info('MQTT client connected to broker');
51
+ if (this.uptimeStart !== undefined)
52
+ this.log.warn("Unexpected MQTT 'connect' event");
53
+ this.uptimeStart = Date.now();
54
+ }).on('error', err => {
55
+ // Connection attempt failed
56
+ log.error(`MQTT client connection error: ${err.message}`);
57
+ // (Will be followed by 'close' event to trigger reconnection)
58
+ }).on('close', () => {
59
+ // Connection closed, so adjust backoff and retry
60
+ if (this.terminate.signal.aborted) {
61
+ this.log.info('MQTT client connection closed; not reconnecting');
62
+ return;
63
+ }
64
+ else if (this.uptimeStart) {
65
+ const uptime = Date.now() - this.uptimeStart;
66
+ this.uptimeStart = undefined;
67
+ const description = `MQTT client closed stream after ${formatMilliseconds(uptime)}`;
68
+ if (uptime < BACKOFF_RESET_UPTIME) {
69
+ // Short connection, so keep the (increased) backoff
70
+ this.log.warn(description);
71
+ }
72
+ else {
73
+ // Stable connection, so reset backoff to its minimum
74
+ this.log.info(`${description}; resetting reconnection backoff to minimum`);
75
+ this.backoff = BACKOFF_MIN;
76
+ }
77
+ }
78
+ else {
79
+ this.log.info('MQTT client closed stream after failed connection attempt');
80
+ }
81
+ // Try to (re)establish the connection again
82
+ void this.reconnect();
83
+ });
84
+ // Attempt the initial connection
85
+ this.log.info('Starting MQTT client...');
86
+ this.mqtt.connect();
87
+ }
88
+ // Attempt a reconnection
89
+ async reconnect() {
90
+ try {
91
+ // Wait for the backoff before reconnecting
92
+ this.log.info(`MQTT client reconnecting in ${formatMilliseconds(this.backoff)}...`);
93
+ const { signal } = this.terminate;
94
+ await setTimeout(this.backoff, undefined, { signal });
95
+ // Attempt the reconnection
96
+ this.log.info('MQTT client attempting reconnection...');
97
+ this.mqtt.reconnect();
98
+ // Increase backoff for the next attempt
99
+ this.backoff = Math.min(this.backoff * BACKOFF_FACTOR, BACKOFF_MAX);
100
+ }
101
+ catch (err) {
102
+ if (!(err instanceof Error && err.name === 'AbortError')) {
103
+ logError(this.log, 'MQTT Reconnect', err);
104
+ }
105
+ // (Don't retry if terminated or otherwise failed synchronously)
106
+ }
107
+ }
108
+ // Stop the MQTT client
109
+ async stop() {
110
+ this.log.info('Stopping MQTT client...');
111
+ this.terminate.abort();
112
+ await this.mqtt.endAsync();
113
+ this.log.info('MQTT client stopped');
114
+ }
115
+ }
116
+ // Manage a local Dyson MQTT broker connection
117
+ export class DysonMqttConnectionLocal extends DysonMqttConnection {
118
+ // Construct a new MQTT client
119
+ constructor(log, config, deviceConfig) {
120
+ const brokerUrl = `mqtt://${deviceConfig.host}:${deviceConfig.port}`;
121
+ const options = {
122
+ username: deviceConfig.username,
123
+ password: deviceConfig.password
124
+ };
125
+ super(log, config, deviceConfig, brokerUrl, options);
126
+ }
127
+ }
128
+ // Manage a Dyson IoT cloud MQTT broker connection
129
+ export class DysonMqttConnectionIoT extends DysonMqttConnection {
130
+ // Construct a new MQTT client
131
+ constructor(log, config, deviceConfig) {
132
+ const brokerUrl = `wss://${deviceConfig.endpoint}/mqtt`;
133
+ const headers = {
134
+ [deviceConfig.token_key]: deviceConfig.token_value,
135
+ 'X-Amz-CustomAuthorizer-Name': deviceConfig.custom_authorizer_name,
136
+ 'X-Amz-CustomAuthorizer-Signature': deviceConfig.token_signature
137
+ };
138
+ const options = {
139
+ clientId: deviceConfig.client_id,
140
+ wsOptions: { headers }
141
+ };
142
+ super(log, config, deviceConfig, brokerUrl, options);
143
+ }
144
+ }
145
+ //# sourceMappingURL=dyson-mqtt-connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-connect.js","sourceRoot":"","sources":["../src/dyson-mqtt-connect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { DysonMsg } from './dyson-types.js';
2
+ import { AnsiLogger } from 'matterbridge/logger';
3
+ export type DysonMqttFiltered = 'duplicate' | 'reordered';
4
+ export declare class DysonMQTTFilter {
5
+ readonly log: AnsiLogger;
6
+ readonly lastMsg: Map<string, DysonMsg>;
7
+ constructor(log: AnsiLogger);
8
+ filter(msg: DysonMsg): DysonMqttFiltered | undefined;
9
+ }
10
+ //# sourceMappingURL=dyson-mqtt-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dyson-mqtt-filter.d.ts","sourceRoot":"","sources":["../src/dyson-mqtt-filter.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGjD,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,WAAW,CAAC;AAG1D,qBAAa,eAAe;IAMZ,QAAQ,CAAC,GAAG,EAAE,UAAU;IAHpC,QAAQ,CAAC,OAAO,wBAA6B;gBAGxB,GAAG,EAAE,UAAU;IAGpC,MAAM,CAAC,GAAG,EAAE,QAAQ,GAAG,iBAAiB,GAAG,SAAS;CAQvD"}