@mp-consulting/homebridge-lg-thinq 1.0.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 (241) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/CHANGELOG.md +98 -0
  3. package/LICENSE +176 -0
  4. package/README.md +114 -0
  5. package/config.schema.json +399 -0
  6. package/dist/__tests__/baseDevice.spec.d.ts +1 -0
  7. package/dist/__tests__/baseDevice.spec.js +96 -0
  8. package/dist/__tests__/baseDevice.spec.js.map +1 -0
  9. package/dist/__tests__/deviceControl.coercion.spec.d.ts +1 -0
  10. package/dist/__tests__/deviceControl.coercion.spec.js +53 -0
  11. package/dist/__tests__/deviceControl.coercion.spec.js.map +1 -0
  12. package/dist/__tests__/helper.spec.d.ts +1 -0
  13. package/dist/__tests__/helper.spec.js +74 -0
  14. package/dist/__tests__/helper.spec.js.map +1 -0
  15. package/dist/baseDevice.d.ts +40 -0
  16. package/dist/baseDevice.js +85 -0
  17. package/dist/baseDevice.js.map +1 -0
  18. package/dist/baseDevice.spec.d.ts +1 -0
  19. package/dist/baseDevice.spec.js +107 -0
  20. package/dist/baseDevice.spec.js.map +1 -0
  21. package/dist/characteristics/TotalConsumption.d.ts +2 -0
  22. package/dist/characteristics/TotalConsumption.js +17 -0
  23. package/dist/characteristics/TotalConsumption.js.map +1 -0
  24. package/dist/characteristics/index.d.ts +2 -0
  25. package/dist/characteristics/index.js +7 -0
  26. package/dist/characteristics/index.js.map +1 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +89 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/devices/AeroTower.d.ts +24 -0
  31. package/dist/devices/AeroTower.js +113 -0
  32. package/dist/devices/AeroTower.js.map +1 -0
  33. package/dist/devices/AirConditioner.d.ts +425 -0
  34. package/dist/devices/AirConditioner.js +1253 -0
  35. package/dist/devices/AirConditioner.js.map +1 -0
  36. package/dist/devices/AirPurifier.d.ts +50 -0
  37. package/dist/devices/AirPurifier.js +281 -0
  38. package/dist/devices/AirPurifier.js.map +1 -0
  39. package/dist/devices/Dehumidifier.d.ts +28 -0
  40. package/dist/devices/Dehumidifier.js +175 -0
  41. package/dist/devices/Dehumidifier.js.map +1 -0
  42. package/dist/devices/Dishwasher.d.ts +64 -0
  43. package/dist/devices/Dishwasher.js +740 -0
  44. package/dist/devices/Dishwasher.js.map +1 -0
  45. package/dist/devices/Microwave.d.ts +128 -0
  46. package/dist/devices/Microwave.js +1939 -0
  47. package/dist/devices/Microwave.js.map +1 -0
  48. package/dist/devices/Oven.d.ts +148 -0
  49. package/dist/devices/Oven.js +1850 -0
  50. package/dist/devices/Oven.js.map +1 -0
  51. package/dist/devices/RangeHood.d.ts +16 -0
  52. package/dist/devices/RangeHood.js +99 -0
  53. package/dist/devices/RangeHood.js.map +1 -0
  54. package/dist/devices/Refrigerator.d.ts +50 -0
  55. package/dist/devices/Refrigerator.js +325 -0
  56. package/dist/devices/Refrigerator.js.map +1 -0
  57. package/dist/devices/Styler.d.ts +27 -0
  58. package/dist/devices/Styler.js +76 -0
  59. package/dist/devices/Styler.js.map +1 -0
  60. package/dist/devices/WasherDryer.d.ts +39 -0
  61. package/dist/devices/WasherDryer.js +170 -0
  62. package/dist/devices/WasherDryer.js.map +1 -0
  63. package/dist/devices/WasherDryer2.d.ts +9 -0
  64. package/dist/devices/WasherDryer2.js +16 -0
  65. package/dist/devices/WasherDryer2.js.map +1 -0
  66. package/dist/errors/AuthenticationError.d.ts +2 -0
  67. package/dist/errors/AuthenticationError.js +3 -0
  68. package/dist/errors/AuthenticationError.js.map +1 -0
  69. package/dist/errors/ManualProcessNeeded.d.ts +3 -0
  70. package/dist/errors/ManualProcessNeeded.js +4 -0
  71. package/dist/errors/ManualProcessNeeded.js.map +1 -0
  72. package/dist/errors/MonitorError.d.ts +2 -0
  73. package/dist/errors/MonitorError.js +3 -0
  74. package/dist/errors/MonitorError.js.map +1 -0
  75. package/dist/errors/NotConnectedError.d.ts +3 -0
  76. package/dist/errors/NotConnectedError.js +4 -0
  77. package/dist/errors/NotConnectedError.js.map +1 -0
  78. package/dist/errors/TokenError.d.ts +2 -0
  79. package/dist/errors/TokenError.js +3 -0
  80. package/dist/errors/TokenError.js.map +1 -0
  81. package/dist/errors/TokenExpiredError.d.ts +3 -0
  82. package/dist/errors/TokenExpiredError.js +4 -0
  83. package/dist/errors/TokenExpiredError.js.map +1 -0
  84. package/dist/errors/index.d.ts +6 -0
  85. package/dist/errors/index.js +7 -0
  86. package/dist/errors/index.js.map +1 -0
  87. package/dist/helper.d.ts +24 -0
  88. package/dist/helper.js +66 -0
  89. package/dist/helper.js.map +1 -0
  90. package/dist/helper.spec.d.ts +1 -0
  91. package/dist/helper.spec.js +74 -0
  92. package/dist/helper.spec.js.map +1 -0
  93. package/dist/index.d.ts +6 -0
  94. package/dist/index.js +9 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/lib/API.d.ts +141 -0
  97. package/dist/lib/API.js +362 -0
  98. package/dist/lib/API.js.map +1 -0
  99. package/dist/lib/API.spec.d.ts +1 -0
  100. package/dist/lib/API.spec.js +55 -0
  101. package/dist/lib/API.spec.js.map +1 -0
  102. package/dist/lib/Auth.d.ts +99 -0
  103. package/dist/lib/Auth.js +348 -0
  104. package/dist/lib/Auth.js.map +1 -0
  105. package/dist/lib/Auth.spec.d.ts +1 -0
  106. package/dist/lib/Auth.spec.js +111 -0
  107. package/dist/lib/Auth.spec.js.map +1 -0
  108. package/dist/lib/Device.d.ts +88 -0
  109. package/dist/lib/Device.js +95 -0
  110. package/dist/lib/Device.js.map +1 -0
  111. package/dist/lib/Device.spec.d.ts +1 -0
  112. package/dist/lib/Device.spec.js +53 -0
  113. package/dist/lib/Device.spec.js.map +1 -0
  114. package/dist/lib/DeviceModel.d.ts +164 -0
  115. package/dist/lib/DeviceModel.js +321 -0
  116. package/dist/lib/DeviceModel.js.map +1 -0
  117. package/dist/lib/DeviceModel.spec.d.ts +1 -0
  118. package/dist/lib/DeviceModel.spec.js +90 -0
  119. package/dist/lib/DeviceModel.spec.js.map +1 -0
  120. package/dist/lib/Gateway.d.ts +18 -0
  121. package/dist/lib/Gateway.js +25 -0
  122. package/dist/lib/Gateway.js.map +1 -0
  123. package/dist/lib/Gateway.spec.d.ts +1 -0
  124. package/dist/lib/Gateway.spec.js +35 -0
  125. package/dist/lib/Gateway.spec.js.map +1 -0
  126. package/dist/lib/Persist.d.ts +101 -0
  127. package/dist/lib/Persist.js +245 -0
  128. package/dist/lib/Persist.js.map +1 -0
  129. package/dist/lib/Persist.spec.d.ts +1 -0
  130. package/dist/lib/Persist.spec.js +90 -0
  131. package/dist/lib/Persist.spec.js.map +1 -0
  132. package/dist/lib/Session.d.ts +80 -0
  133. package/dist/lib/Session.js +100 -0
  134. package/dist/lib/Session.js.map +1 -0
  135. package/dist/lib/Session.spec.d.ts +1 -0
  136. package/dist/lib/Session.spec.js +43 -0
  137. package/dist/lib/Session.spec.js.map +1 -0
  138. package/dist/lib/ThinQ.d.ts +28 -0
  139. package/dist/lib/ThinQ.js +373 -0
  140. package/dist/lib/ThinQ.js.map +1 -0
  141. package/dist/lib/__tests__/API.spec.d.ts +1 -0
  142. package/dist/lib/__tests__/API.spec.js +55 -0
  143. package/dist/lib/__tests__/API.spec.js.map +1 -0
  144. package/dist/lib/__tests__/Auth.spec.d.ts +1 -0
  145. package/dist/lib/__tests__/Auth.spec.js +110 -0
  146. package/dist/lib/__tests__/Auth.spec.js.map +1 -0
  147. package/dist/lib/__tests__/Device.spec.d.ts +1 -0
  148. package/dist/lib/__tests__/Device.spec.js +53 -0
  149. package/dist/lib/__tests__/Device.spec.js.map +1 -0
  150. package/dist/lib/__tests__/DeviceModel.spec.d.ts +1 -0
  151. package/dist/lib/__tests__/DeviceModel.spec.js +90 -0
  152. package/dist/lib/__tests__/DeviceModel.spec.js.map +1 -0
  153. package/dist/lib/__tests__/Gateway.spec.d.ts +1 -0
  154. package/dist/lib/__tests__/Gateway.spec.js +35 -0
  155. package/dist/lib/__tests__/Gateway.spec.js.map +1 -0
  156. package/dist/lib/__tests__/Persist.spec.d.ts +1 -0
  157. package/dist/lib/__tests__/Persist.spec.js +90 -0
  158. package/dist/lib/__tests__/Persist.spec.js.map +1 -0
  159. package/dist/lib/__tests__/Session.spec.d.ts +1 -0
  160. package/dist/lib/__tests__/Session.spec.js +43 -0
  161. package/dist/lib/__tests__/Session.spec.js.map +1 -0
  162. package/dist/lib/constants.d.ts +95 -0
  163. package/dist/lib/constants.js +106 -0
  164. package/dist/lib/constants.js.map +1 -0
  165. package/dist/lib/request.d.ts +2 -0
  166. package/dist/lib/request.js +66 -0
  167. package/dist/lib/request.js.map +1 -0
  168. package/dist/platform.d.ts +41 -0
  169. package/dist/platform.js +229 -0
  170. package/dist/platform.js.map +1 -0
  171. package/dist/settings.d.ts +8 -0
  172. package/dist/settings.js +9 -0
  173. package/dist/settings.js.map +1 -0
  174. package/dist/status/BaseStatus.d.ts +48 -0
  175. package/dist/status/BaseStatus.js +59 -0
  176. package/dist/status/BaseStatus.js.map +1 -0
  177. package/dist/types/snapshots.d.ts +142 -0
  178. package/dist/types/snapshots.js +6 -0
  179. package/dist/types/snapshots.js.map +1 -0
  180. package/dist/utils/__tests__/normalize.spec.d.ts +1 -0
  181. package/dist/utils/__tests__/normalize.spec.js +95 -0
  182. package/dist/utils/__tests__/normalize.spec.js.map +1 -0
  183. package/dist/utils/normalize.d.ts +22 -0
  184. package/dist/utils/normalize.js +51 -0
  185. package/dist/utils/normalize.js.map +1 -0
  186. package/dist/v1/__tests__/prepareControlData.spec.d.ts +1 -0
  187. package/dist/v1/__tests__/prepareControlData.spec.js +48 -0
  188. package/dist/v1/__tests__/prepareControlData.spec.js.map +1 -0
  189. package/dist/v1/devices/AC.d.ts +13 -0
  190. package/dist/v1/devices/AC.js +112 -0
  191. package/dist/v1/devices/AC.js.map +1 -0
  192. package/dist/v1/devices/AirPurifier.d.ts +15 -0
  193. package/dist/v1/devices/AirPurifier.js +57 -0
  194. package/dist/v1/devices/AirPurifier.js.map +1 -0
  195. package/dist/v1/devices/RangeHood.d.ts +6 -0
  196. package/dist/v1/devices/RangeHood.js +12 -0
  197. package/dist/v1/devices/RangeHood.js.map +1 -0
  198. package/dist/v1/devices/Refrigerator.d.ts +17 -0
  199. package/dist/v1/devices/Refrigerator.js +69 -0
  200. package/dist/v1/devices/Refrigerator.js.map +1 -0
  201. package/dist/v1/devices/Washer.d.ts +10 -0
  202. package/dist/v1/devices/Washer.js +23 -0
  203. package/dist/v1/devices/Washer.js.map +1 -0
  204. package/dist/v1/devices/index.d.ts +6 -0
  205. package/dist/v1/devices/index.js +7 -0
  206. package/dist/v1/devices/index.js.map +1 -0
  207. package/dist/v1/helper.d.ts +14 -0
  208. package/dist/v1/helper.js +111 -0
  209. package/dist/v1/helper.js.map +1 -0
  210. package/dist/v1/transforms/AirPurifierState.d.ts +2 -0
  211. package/dist/v1/transforms/AirPurifierState.js +9 -0
  212. package/dist/v1/transforms/AirPurifierState.js.map +1 -0
  213. package/dist/v1/transforms/AirState.d.ts +9 -0
  214. package/dist/v1/transforms/AirState.js +55 -0
  215. package/dist/v1/transforms/AirState.js.map +1 -0
  216. package/dist/v1/transforms/HoodState.d.ts +17 -0
  217. package/dist/v1/transforms/HoodState.js +20 -0
  218. package/dist/v1/transforms/HoodState.js.map +1 -0
  219. package/dist/v1/transforms/RefState.d.ts +6 -0
  220. package/dist/v1/transforms/RefState.js +29 -0
  221. package/dist/v1/transforms/RefState.js.map +1 -0
  222. package/dist/v1/transforms/WasherDryer.d.ts +49 -0
  223. package/dist/v1/transforms/WasherDryer.js +56 -0
  224. package/dist/v1/transforms/WasherDryer.js.map +1 -0
  225. package/docs/authorization.md +40 -0
  226. package/docs/device-configuration.md +68 -0
  227. package/homebridge-ui/public/index.html +120 -0
  228. package/homebridge-ui/public/js/app.js +300 -0
  229. package/homebridge-ui/public/js/countries.js +233 -0
  230. package/homebridge-ui/public/styles.css +185 -0
  231. package/homebridge-ui/server.js +103 -0
  232. package/jest.config.ts +39 -0
  233. package/package.json +83 -0
  234. package/sample/README.md +10 -0
  235. package/sample/airconditioner-model.json +3080 -0
  236. package/sample/airconditioner-snapshot.json +49 -0
  237. package/sample/airconditioner.json +157 -0
  238. package/sample/dishwasher-model.json +869 -0
  239. package/sample/dishwasher.json +125 -0
  240. package/sample/washer_dryer-model.json +1294 -0
  241. package/sample/washer_dryer.json +126 -0
@@ -0,0 +1,1253 @@
1
+ import { BaseDevice } from '../baseDevice.js';
2
+ import { ValueType } from '../lib/DeviceModel.js';
3
+ import { cToF, fToC, normalizeBoolean, normalizeNumber, safeParseInt } from '../helper.js';
4
+ import { AC_MODEL_FEATURES, ONE_MINUTE_MS, HUNDRED_MS } from '../lib/constants.js';
5
+ export var ACModelType;
6
+ (function (ACModelType) {
7
+ ACModelType["AWHP"] = "AWHP";
8
+ ACModelType["RAC"] = "RAC";
9
+ })(ACModelType || (ACModelType = {}));
10
+ export const FAN_SPEED_AUTO = 8;
11
+ export var FanSpeed;
12
+ (function (FanSpeed) {
13
+ FanSpeed[FanSpeed["LOW"] = 2] = "LOW";
14
+ FanSpeed[FanSpeed["LOW_MEDIUM"] = 3] = "LOW_MEDIUM";
15
+ FanSpeed[FanSpeed["MEDIUM"] = 4] = "MEDIUM";
16
+ FanSpeed[FanSpeed["MEDIUM_HIGH"] = 5] = "MEDIUM_HIGH";
17
+ FanSpeed[FanSpeed["HIGH"] = 6] = "HIGH";
18
+ })(FanSpeed || (FanSpeed = {}));
19
+ export var OpMode;
20
+ (function (OpMode) {
21
+ OpMode[OpMode["AUTO"] = 6] = "AUTO";
22
+ OpMode[OpMode["COOL"] = 0] = "COOL";
23
+ OpMode[OpMode["HEAT"] = 4] = "HEAT";
24
+ OpMode[OpMode["FAN"] = 2] = "FAN";
25
+ OpMode[OpMode["DRY"] = 1] = "DRY";
26
+ OpMode[OpMode["AIR_CLEAN"] = 5] = "AIR_CLEAN";
27
+ })(OpMode || (OpMode = {}));
28
+ /**
29
+ * Represents an LG ThinQ Air Conditioner device.
30
+ * This class extends the `baseDevice` class and provides functionality to control and monitor
31
+ * various features of an air conditioner, such as temperature, fan speed, swing mode, and more.
32
+ */
33
+ export default class AirConditioner extends BaseDevice {
34
+ platform;
35
+ accessory;
36
+ service;
37
+ serviceAirQuality;
38
+ serviceSensor;
39
+ serviceHumiditySensor;
40
+ serviceLight;
41
+ serviceFanV2;
42
+ // more feature
43
+ serviceJetMode; // jet mode
44
+ serviceQuietMode;
45
+ serviceEnergySaveMode;
46
+ serviceAirClean;
47
+ /** @deprecated Use AC_MODEL_FEATURES from lib/constants.js instead */
48
+ jetModeModels = AC_MODEL_FEATURES.jetMode;
49
+ /** @deprecated Use AC_MODEL_FEATURES from lib/constants.js instead */
50
+ quietModeModels = AC_MODEL_FEATURES.quietMode;
51
+ /** @deprecated Use AC_MODEL_FEATURES from lib/constants.js instead */
52
+ energySaveModeModels = AC_MODEL_FEATURES.energySaveMode;
53
+ airCleanModels = ['RAC_056905'];
54
+ currentTargetState = 2; // default target: COOL
55
+ serviceLabelButtons;
56
+ monitorInterval;
57
+ constructor(platform, accessory, logger) {
58
+ super(platform, accessory, logger);
59
+ this.platform = platform;
60
+ this.accessory = accessory;
61
+ const device = this.accessory.context.device;
62
+ const { Service: { TemperatureSensor, HumiditySensor, Switch, Lightbulb, HeaterCooler, }, } = this.platform;
63
+ this.service = this.getOrCreateService(HeaterCooler, device.name);
64
+ this.createHeaterCoolerService();
65
+ this.service.addOptionalCharacteristic(this.platform.customCharacteristics.TotalConsumption);
66
+ if (this.config.ac_air_quality && this.Status.airQuality) {
67
+ this.createAirQualityService();
68
+ }
69
+ else if (this.serviceAirQuality) {
70
+ accessory.removeService(this.serviceAirQuality);
71
+ }
72
+ this.serviceSensor = this.ensureService(TemperatureSensor, 'Temperature Sensor', this.config.ac_temperature_sensor);
73
+ if (this.serviceSensor) {
74
+ this.serviceSensor.updateCharacteristic(platform.Characteristic.StatusActive, false);
75
+ this.serviceSensor.addLinkedService(this.service);
76
+ }
77
+ this.serviceHumiditySensor = this.ensureService(HumiditySensor, 'Humidity Sensor', this.config.ac_humidity_sensor);
78
+ if (this.serviceHumiditySensor) {
79
+ this.serviceHumiditySensor.updateCharacteristic(platform.Characteristic.StatusActive, false);
80
+ this.serviceHumiditySensor.addLinkedService(this.service);
81
+ }
82
+ this.serviceLight = this.ensureService(Lightbulb, 'Light', this.config.ac_led_control);
83
+ if (this.serviceLight) {
84
+ this.serviceLight.getCharacteristic(platform.Characteristic.On)
85
+ .onSet(this.setLight.bind(this))
86
+ .updateValue(false); // off as default
87
+ this.serviceLight.addLinkedService(this.service);
88
+ }
89
+ if (this.config.ac_fan_control) {
90
+ this.createFanService();
91
+ }
92
+ else if (this.serviceFanV2) {
93
+ accessory.removeService(this.serviceFanV2);
94
+ }
95
+ // more feature
96
+ const enableJetMode = this.config.ac_jet_control && this.isJetModeEnabled(device.model);
97
+ this.serviceJetMode = this.ensureService(Switch, 'Jet Mode', enableJetMode, 'Jet Mode');
98
+ if (this.serviceJetMode) {
99
+ this.serviceJetMode.addOptionalCharacteristic(platform.Characteristic.ConfiguredName);
100
+ this.serviceJetMode.setCharacteristic(platform.Characteristic.ConfiguredName, device.name + ' Jet Mode');
101
+ this.serviceJetMode.getCharacteristic(platform.Characteristic.On)
102
+ .onSet(this.setJetModeActive.bind(this));
103
+ }
104
+ this.serviceQuietMode = this.ensureService(Switch, 'Quiet mode', this.quietModeModels.includes(device.model), 'Quiet mode');
105
+ if (this.serviceQuietMode) {
106
+ this.serviceQuietMode.updateCharacteristic(platform.Characteristic.Name, 'Quiet mode');
107
+ this.serviceQuietMode.getCharacteristic(platform.Characteristic.On)
108
+ .onSet(this.setQuietModeActive.bind(this));
109
+ }
110
+ const enableEnergySave = this.energySaveModeModels.includes(device.model) && this.config.ac_energy_save;
111
+ this.serviceEnergySaveMode = this.ensureService(Switch, 'Energy save', enableEnergySave, 'Energy save');
112
+ if (this.serviceEnergySaveMode) {
113
+ this.serviceEnergySaveMode.addOptionalCharacteristic(platform.Characteristic.ConfiguredName);
114
+ this.serviceEnergySaveMode.setCharacteristic(platform.Characteristic.ConfiguredName, device.name + ' Energy save');
115
+ this.serviceEnergySaveMode.getCharacteristic(platform.Characteristic.On)
116
+ .onSet(this.setEnergySaveActive.bind(this));
117
+ }
118
+ const enableAirClean = this.airCleanModels.includes(device.model) && this.config.ac_air_clean;
119
+ this.serviceAirClean = this.ensureService(Switch, 'Air Purify', enableAirClean, 'Air Purify');
120
+ if (this.serviceAirClean) {
121
+ this.serviceAirClean.addOptionalCharacteristic(platform.Characteristic.ConfiguredName);
122
+ this.serviceAirClean.setCharacteristic(platform.Characteristic.ConfiguredName, device.name + ' Air Purify');
123
+ this.serviceAirClean.getCharacteristic(platform.Characteristic.On)
124
+ .onSet(this.setAirCleanActive.bind(this));
125
+ }
126
+ this.setupButton(device);
127
+ // send request every minute to update temperature
128
+ // https://github.com/mp-consulting/homebridge-lg-thinq/issues/177
129
+ // Skip for models that don't support the monitor timeout command
130
+ const supportsMonitorTimeout = !AC_MODEL_FEATURES.noMonitorTimeout.some(m => device.model.includes(m));
131
+ if (supportsMonitorTimeout) {
132
+ this.monitorInterval = setInterval(async () => {
133
+ if (device.online) {
134
+ try {
135
+ await this.platform.ThinQ?.deviceControl(device.id, {
136
+ dataKey: 'airState.mon.timeout',
137
+ dataValue: '70',
138
+ }, 'Set', 'allEventEnable', 'control');
139
+ }
140
+ catch (error) {
141
+ this.logger.debug('Error sending monitor timeout command:', error);
142
+ }
143
+ }
144
+ }, ONE_MINUTE_MS);
145
+ }
146
+ }
147
+ destroy() {
148
+ if (this.monitorInterval) {
149
+ clearInterval(this.monitorInterval);
150
+ this.monitorInterval = undefined;
151
+ }
152
+ }
153
+ createFanService() {
154
+ const { Service: { Fanv2, }, Characteristic, } = this.platform;
155
+ const device = this.accessory.context.device;
156
+ // fan controller
157
+ this.serviceFanV2 = this.getOrCreateService(Fanv2, device.name + ' Fan');
158
+ this.serviceFanV2.addLinkedService(this.service);
159
+ this.serviceFanV2.getCharacteristic(Characteristic.Active)
160
+ .onGet(() => {
161
+ return this.Status.isPowerOn ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE;
162
+ })
163
+ .onSet((value) => {
164
+ const isOn = value;
165
+ if ((this.Status.isPowerOn && isOn) || (!this.Status.isPowerOn && !isOn)) {
166
+ return;
167
+ }
168
+ // do not allow change status via home app, revert to prev status in 0.1s
169
+ setTimeout(() => {
170
+ this.serviceFanV2?.updateCharacteristic(Characteristic.Active, this.Status.isPowerOn ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE);
171
+ this.serviceFanV2?.updateCharacteristic(Characteristic.RotationSpeed, this.Status.windStrength);
172
+ }, HUNDRED_MS);
173
+ })
174
+ .updateValue(Characteristic.Active.INACTIVE);
175
+ this.serviceFanV2.addOptionalCharacteristic(Characteristic.ConfiguredName);
176
+ this.serviceFanV2.setCharacteristic(Characteristic.ConfiguredName, device.name + ' Fan');
177
+ this.serviceFanV2.getCharacteristic(Characteristic.CurrentFanState)
178
+ .onGet(() => {
179
+ return this.Status.isPowerOn ? Characteristic.CurrentFanState.BLOWING_AIR : Characteristic.CurrentFanState.INACTIVE;
180
+ })
181
+ .setProps({
182
+ validValues: [Characteristic.CurrentFanState.INACTIVE, Characteristic.CurrentFanState.BLOWING_AIR],
183
+ })
184
+ .updateValue(Characteristic.CurrentFanState.INACTIVE);
185
+ this.serviceFanV2.getCharacteristic(Characteristic.TargetFanState)
186
+ .onSet(this.setFanState.bind(this));
187
+ this.serviceFanV2.getCharacteristic(Characteristic.RotationSpeed)
188
+ .setProps({
189
+ minValue: 0,
190
+ maxValue: Object.keys(FanSpeed).length / 2,
191
+ minStep: 0.1,
192
+ })
193
+ .onSet(this.setFanSpeed.bind(this));
194
+ this.serviceFanV2.getCharacteristic(Characteristic.SwingMode)
195
+ .onSet(this.setSwingMode.bind(this))
196
+ .updateValue(this.Status.isSwingOn ? Characteristic.SwingMode.SWING_ENABLED : Characteristic.SwingMode.SWING_DISABLED);
197
+ }
198
+ createAirQualityService() {
199
+ const { Service: { AirQualitySensor, }, } = this.platform;
200
+ this.serviceAirQuality = this.getOrCreateService(AirQualitySensor, 'Air Quality');
201
+ }
202
+ createHeaterCoolerService() {
203
+ const device = this.accessory.context.device;
204
+ const { Characteristic } = this.platform;
205
+ this.service.setCharacteristic(Characteristic.Name, device.name);
206
+ this.service.getCharacteristic(Characteristic.Active)
207
+ .onSet(this.setActive.bind(this));
208
+ this.service.getCharacteristic(Characteristic.CurrentHeaterCoolerState);
209
+ if (this.config.ac_mode === 'BOTH') {
210
+ this.service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
211
+ .setProps({
212
+ validValues: [
213
+ Characteristic.TargetHeaterCoolerState.AUTO,
214
+ Characteristic.TargetHeaterCoolerState.COOL,
215
+ Characteristic.TargetHeaterCoolerState.HEAT,
216
+ ],
217
+ })
218
+ .updateValue(Characteristic.TargetHeaterCoolerState.COOL);
219
+ }
220
+ else if (this.config.ac_mode === 'COOLING') {
221
+ this.service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
222
+ .setProps({
223
+ validValues: [
224
+ Characteristic.TargetHeaterCoolerState.COOL,
225
+ ],
226
+ })
227
+ .updateValue(Characteristic.TargetHeaterCoolerState.COOL);
228
+ }
229
+ else if (this.config.ac_mode === 'HEATING') {
230
+ this.service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
231
+ .setProps({
232
+ validValues: [
233
+ Characteristic.TargetHeaterCoolerState.HEAT,
234
+ ],
235
+ })
236
+ .updateValue(Characteristic.TargetHeaterCoolerState.HEAT);
237
+ }
238
+ this.service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
239
+ .onSet(this.setTargetState.bind(this));
240
+ const status = this.Status;
241
+ if (status.currentTemperature) {
242
+ this.service.updateCharacteristic(Characteristic.CurrentTemperature, status.currentTemperature);
243
+ }
244
+ this.service.getCharacteristic(Characteristic.CurrentTemperature);
245
+ const targetHeatTemperature = status.getTemperatureRange(status.getTemperatureRangeForHeating());
246
+ if (targetHeatTemperature) {
247
+ this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
248
+ .setProps({
249
+ minValue: status.convertTemperatureCelsiusFromLGToHomekit(targetHeatTemperature.min),
250
+ maxValue: status.convertTemperatureCelsiusFromLGToHomekit(targetHeatTemperature.max),
251
+ minStep: targetHeatTemperature.step || 0.01,
252
+ });
253
+ }
254
+ const targetCoolTemperature = status.getTemperatureRange(status.getTemperatureRangeForCooling());
255
+ if (targetCoolTemperature) {
256
+ this.service.getCharacteristic(Characteristic.CoolingThresholdTemperature)
257
+ .setProps({
258
+ minValue: status.convertTemperatureCelsiusFromLGToHomekit(targetCoolTemperature.min),
259
+ maxValue: status.convertTemperatureCelsiusFromLGToHomekit(targetCoolTemperature.max),
260
+ minStep: targetCoolTemperature.step || 0.01,
261
+ });
262
+ }
263
+ this.service.getCharacteristic(Characteristic.CoolingThresholdTemperature)
264
+ .onSet(this.setTargetTemperature.bind(this));
265
+ this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
266
+ .onSet(this.setTargetTemperature.bind(this));
267
+ if (!this.config.ac_fan_control) {
268
+ this.service.getCharacteristic(Characteristic.RotationSpeed)
269
+ .setProps({
270
+ minValue: 0,
271
+ maxValue: Object.keys(FanSpeed).length / 2,
272
+ minStep: 0.1,
273
+ })
274
+ .onSet(this.setFanSpeed.bind(this));
275
+ }
276
+ this.service.getCharacteristic(Characteristic.SwingMode)
277
+ .onSet(this.setSwingMode.bind(this));
278
+ }
279
+ get config() {
280
+ return {
281
+ ac_swing_mode: 'BOTH',
282
+ ac_air_quality: false,
283
+ ac_mode: 'BOTH',
284
+ ac_temperature_sensor: false,
285
+ ac_humidity_sensor: false,
286
+ ac_led_control: false,
287
+ ac_fan_control: false,
288
+ ac_jet_control: false,
289
+ ac_temperature_unit: 'C',
290
+ ac_buttons: [],
291
+ ac_air_clean: true,
292
+ ac_energy_save: true,
293
+ ...super.config,
294
+ };
295
+ }
296
+ get Status() {
297
+ return new ACStatus(this.accessory.context.device.snapshot, this.accessory.context.device, this.config, this.logger);
298
+ }
299
+ /**
300
+ * Sets the energy-saving mode for the air conditioner.
301
+ *
302
+ * @param value - A boolean indicating whether to enable or disable energy-saving mode.
303
+ */
304
+ async setEnergySaveActive(value) {
305
+ const device = this.accessory.context.device;
306
+ const enabled = normalizeBoolean(value);
307
+ const status = this.Status;
308
+ if (!(status.isPowerOn && status.opMode === OpMode.COOL)) {
309
+ this.logger.debug(`Energy save mode is not supported in the current state. Power: ${status.isPowerOn}, Mode: ${status.opMode}`);
310
+ return;
311
+ }
312
+ try {
313
+ await this.platform.ThinQ?.deviceControl(device.id, {
314
+ dataKey: 'airState.powerSave.basic',
315
+ dataValue: enabled ? 1 : 0,
316
+ });
317
+ this.accessory.context.device.data.snapshot['airState.powerSave.basic'] = enabled ? 1 : 0;
318
+ this.updateAccessoryenergySaveModeModelsCharacteristic();
319
+ }
320
+ catch (error) {
321
+ this.logger.error('Error setting energy save mode:', error);
322
+ }
323
+ }
324
+ /**
325
+ * Sets the air purification mode for the air conditioner.
326
+ *
327
+ * @param value - A boolean indicating whether to enable or disable air purification mode.
328
+ */
329
+ async setAirCleanActive(value) {
330
+ const device = this.accessory.context.device;
331
+ const status = this.Status;
332
+ const enabled = normalizeBoolean(value);
333
+ if (!(status.isPowerOn && status.opMode === OpMode.COOL)) {
334
+ this.logger.debug(`Air clean mode is not supported in the current state. Power: ${status.isPowerOn}, Mode: ${status.opMode}`);
335
+ return;
336
+ }
337
+ try {
338
+ await this.platform.ThinQ?.deviceControl(device.id, {
339
+ dataKey: 'airState.wMode.airClean',
340
+ dataValue: enabled ? 1 : 0,
341
+ });
342
+ this.accessory.context.device.data.snapshot['airState.wMode.airClean'] = enabled ? 1 : 0;
343
+ this.updateAccessoryairCleanModelsCharacteristic();
344
+ }
345
+ catch (error) {
346
+ this.logger.error('Error setting air clean mode:', error);
347
+ }
348
+ }
349
+ /**
350
+ * Sets the quiet mode for the air conditioner.
351
+ *
352
+ * @param value - A boolean indicating whether to enable or disable quiet mode.
353
+ */
354
+ async setQuietModeActive(value) {
355
+ const device = this.accessory.context.device;
356
+ const enabled = normalizeBoolean(value);
357
+ const status = this.Status;
358
+ if (!(status.isPowerOn && status.opMode === OpMode.COOL)) {
359
+ this.logger.debug(`Quiet mode is not supported in the current state. Power: ${status.isPowerOn}, Mode: ${status.opMode}`);
360
+ return;
361
+ }
362
+ try {
363
+ await this.platform.ThinQ?.deviceControl(device.id, {
364
+ dataKey: 'airState.miscFuncState.silentAWHP',
365
+ dataValue: enabled ? 1 : 0,
366
+ });
367
+ this.accessory.context.device.data.snapshot['airState.miscFuncState.silentAWHP'] = enabled ? 1 : 0;
368
+ this.updateAccessoryquietModeModelsCharacteristic();
369
+ }
370
+ catch (error) {
371
+ this.logger.error('Error setting quiet mode:', error);
372
+ }
373
+ }
374
+ /**
375
+ * Sets the jet mode active state for the air conditioner device.
376
+ *
377
+ * @param value - The desired state of jet mode, where `true` activates jet mode and `false` deactivates it.
378
+ * @returns The resulting state of jet mode after the operation.
379
+ *
380
+ * @remarks
381
+ * - Jet mode can only be activated if the device is powered on and the operation mode (`opMode`) is set to 0.
382
+ * - If the operation fails, an error is logged, and the method returns the opposite state of the requested value.
383
+ * - If jet mode is not supported in the current state, the method logs a debug message and returns `INACTIVE`.
384
+ *
385
+ * @throws Logs an error if there is an issue with the device control operation.
386
+ */
387
+ async setJetModeActive(value) {
388
+ const device = this.accessory.context.device;
389
+ const enabled = normalizeBoolean(value);
390
+ const status = this.Status;
391
+ if (!(status.isPowerOn && status.opMode === OpMode.COOL)) {
392
+ this.logger.debug(`Jet mode is not supported in the current state. Power: ${status.isPowerOn}, Mode: ${status.opMode}`);
393
+ return;
394
+ }
395
+ try {
396
+ await this.platform.ThinQ?.deviceControl(device.id, {
397
+ dataKey: 'airState.wMode.jet',
398
+ dataValue: enabled ? 1 : 0,
399
+ });
400
+ this.accessory.context.device.data.snapshot['airState.wMode.jet'] = enabled ? 1 : 0;
401
+ this.updateAccessoryJetModeCharacteristic();
402
+ }
403
+ catch (error) {
404
+ this.logger.error('Error setting jet mode:', error);
405
+ }
406
+ }
407
+ /**
408
+ * Sets the fan state of the air conditioner to either AUTO or MANUAL mode.
409
+ *
410
+ * @param value - The desired fan state, represented as a `CharacteristicValue`.
411
+ * It can be either `TargetFanState.AUTO` or `TargetFanState.MANUAL`.
412
+ * @returns The updated fan state, which will match the input value if the operation succeeds,
413
+ * or the opposite state if the operation fails or the power is off.
414
+ *
415
+ * @remarks
416
+ * - If the air conditioner is powered off, the method logs a debug message and returns
417
+ * the opposite of the requested fan state.
418
+ * - If the air conditioner is powered on, it attempts to update the fan state via the
419
+ * ThinQ API. On success, the fan state is updated in the device's context. On failure,
420
+ * an error is logged, and the opposite fan state is returned.
421
+ * - The AUTO mode corresponds to a wind strength value of 8, while MANUAL mode corresponds
422
+ * to a high fan speed.
423
+ *
424
+ * @throws This method does not throw errors directly but logs them if the ThinQ API call fails.
425
+ */
426
+ async setFanState(value) {
427
+ const status = this.Status;
428
+ if (!status.isPowerOn) {
429
+ this.logger.debug('Power is off, cannot set fan state');
430
+ return;
431
+ }
432
+ const device = this.accessory.context.device;
433
+ const { TargetFanState } = this.platform.Characteristic;
434
+ try {
435
+ const vNum = normalizeNumber(value);
436
+ const isAuto = (vNum !== null) ? (vNum === TargetFanState.AUTO) : normalizeBoolean(value);
437
+ const windStrength = isAuto ? FAN_SPEED_AUTO : FanSpeed.HIGH;
438
+ await this.platform.ThinQ?.deviceControl(device.id, {
439
+ dataKey: 'airState.windStrength',
440
+ dataValue: windStrength,
441
+ });
442
+ this.accessory.context.device.data.snapshot['airState.windStrength'] = windStrength;
443
+ this.updateAccessoryFanStateCharacteristics();
444
+ this.updateAccessoryFanV2Characteristic();
445
+ }
446
+ catch (error) {
447
+ this.logger.error('Error setting fan state:', error);
448
+ }
449
+ }
450
+ /**
451
+ * Updates the accessory characteristics based on the current device state.
452
+ *
453
+ * @param device - The device object containing the current state.
454
+ */
455
+ updateAccessoryCharacteristic(device) {
456
+ super.updateAccessoryCharacteristic(device);
457
+ this.updateAccessoryActiveCharacteristic();
458
+ this.updateAccessoryCurrentTemperatureCharacteristic();
459
+ this.updateAccessoryStateCharacteristics();
460
+ this.updateAccessoryTemperatureCharacteristics();
461
+ this.updateAccessoryFanStateCharacteristics();
462
+ this.updateAccessoryTotalConsumptionCharacteristic();
463
+ this.updateAccessoryAirQualityCharacteristic();
464
+ this.updateAccessoryTemperatureSensorCharacteristic();
465
+ this.updateAccessoryHumiditySensorCharacteristic();
466
+ this.updateAccessoryFanV2Characteristic();
467
+ this.updateAccessoryLedControlCharacteristic();
468
+ this.updateAccessoryJetModeCharacteristic();
469
+ this.updateAccessoryquietModeModelsCharacteristic();
470
+ this.updateAccessoryenergySaveModeModelsCharacteristic();
471
+ this.updateAccessoryairCleanModelsCharacteristic();
472
+ }
473
+ /**
474
+ * Updates the "Active" characteristic of the accessory's service to reflect the current power status.
475
+ *
476
+ * This method checks the power status of the device (`isPowerOn`) and updates the "Active" characteristic
477
+ * accordingly. If the device is powered on, the characteristic is set to `ACTIVE`, otherwise it is set to `INACTIVE`.
478
+ */
479
+ updateAccessoryActiveCharacteristic() {
480
+ this.service.updateCharacteristic(this.platform.Characteristic.Active, this.Status.isPowerOn ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE);
481
+ }
482
+ /**
483
+ * Updates the `CurrentTemperature` characteristic of the accessory's service
484
+ * with the current temperature value from the device's status.
485
+ *
486
+ * This method ensures that the Homebridge platform reflects the most recent
487
+ * temperature reading from the air conditioner.
488
+ */
489
+ updateAccessoryCurrentTemperatureCharacteristic() {
490
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.Status.currentTemperature);
491
+ }
492
+ /**
493
+ * Updates the state characteristics of the accessory based on the current status of the air conditioner.
494
+ *
495
+ * This method synchronizes the accessory's characteristics with the current operational state of the air conditioner,
496
+ * including power status, operating mode, and temperature settings. It updates the `CurrentHeaterCoolerState` and
497
+ * `TargetHeaterCoolerState` characteristics accordingly.
498
+ *
499
+ * Behavior:
500
+ * - If the air conditioner is powered off, the state is set to `INACTIVE`.
501
+ * - If the operating mode is `COOL`, the state is set to `COOLING` and the target state to `COOL`.
502
+ * - If the operating mode is `HEAT`, the state is set to `HEATING` and the target state to `HEAT`.
503
+ * - If the operating mode is `AUTO` or undefined (`-1`), the state is determined based on the current and target temperatures:
504
+ * - If the current temperature is below the target temperature, the state is set to `HEATING` and the target state to `HEAT`.
505
+ * - Otherwise, the state is set to `COOLING` and the target state to `COOL`.
506
+ * - For other modes, no specific behavior is defined.
507
+ *
508
+ * @remarks
509
+ * This method assumes that the `Status` object contains the necessary properties (`isPowerOn`, `opMode`, `currentTemperature`,
510
+ * and `targetTemperature`) and that the `service` object provides the `updateCharacteristic` method.
511
+ */
512
+ updateAccessoryStateCharacteristics() {
513
+ if (!this.Status.isPowerOn) {
514
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.platform.Characteristic.CurrentHeaterCoolerState.INACTIVE);
515
+ }
516
+ else if (this.Status.opMode === OpMode.COOL) {
517
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.platform.Characteristic.CurrentHeaterCoolerState.COOLING);
518
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState, this.platform.Characteristic.TargetHeaterCoolerState.COOL);
519
+ }
520
+ else if (this.Status.opMode === OpMode.HEAT) {
521
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.platform.Characteristic.CurrentHeaterCoolerState.HEATING);
522
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState, this.platform.Characteristic.TargetHeaterCoolerState.HEAT);
523
+ }
524
+ else if ([OpMode.AUTO, -1].includes(this.Status.opMode)) {
525
+ // auto mode, detect based on current & target temperature
526
+ if (this.Status.currentTemperature < this.Status.targetTemperature) {
527
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.platform.Characteristic.CurrentHeaterCoolerState.HEATING);
528
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState, this.platform.Characteristic.TargetHeaterCoolerState.HEAT);
529
+ }
530
+ else {
531
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.platform.Characteristic.CurrentHeaterCoolerState.COOLING);
532
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState, this.platform.Characteristic.TargetHeaterCoolerState.COOL);
533
+ }
534
+ }
535
+ else {
536
+ // another mode
537
+ }
538
+ }
539
+ /**
540
+ * Updates the accessory's temperature characteristics based on the current state
541
+ * of the heater or cooler. Depending on whether the device is in heating or cooling
542
+ * mode, it updates the corresponding threshold temperature characteristic.
543
+ *
544
+ * - If the current state is `HEATING`, the `HeatingThresholdTemperature` characteristic
545
+ * is updated with the target temperature.
546
+ * - If the current state is `COOLING`, the `CoolingThresholdTemperature` characteristic
547
+ * is updated with the target temperature, and a debug log is generated.
548
+ *
549
+ * @remarks
550
+ * This method relies on the `Status.targetTemperature` property to determine the
551
+ * target temperature and the `CurrentHeaterCoolerState` characteristic to determine
552
+ * the current operating mode of the device.
553
+ */
554
+ updateAccessoryTemperatureCharacteristics() {
555
+ this.logger.warn(`updateAccessoryTemperatureCharacteristics: ${this.accessory.context.device.snapshot['airState.tempState.target']}`);
556
+ this.logger.warn(`updateAccessoryTemperatureCharacteristics: ${this.Status.targetTemperature}`);
557
+ const temperature = this.Status.targetTemperature;
558
+ const currentState = this.service.getCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState).value;
559
+ if (currentState === this.platform.Characteristic.CurrentHeaterCoolerState.HEATING) {
560
+ this.service.updateCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature, temperature);
561
+ }
562
+ if (currentState === this.platform.Characteristic.CurrentHeaterCoolerState.COOLING) {
563
+ this.logger.debug('Setting cooling target temperature = ', temperature);
564
+ this.service.updateCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature, temperature);
565
+ }
566
+ }
567
+ /**
568
+ * Updates the fan state characteristics of the accessory.
569
+ *
570
+ * This method updates the `RotationSpeed` and `SwingMode` characteristics
571
+ * of the accessory's service based on the current status of the device.
572
+ *
573
+ * - `RotationSpeed` is updated using the `windStrength` value from the device status.
574
+ * - `SwingMode` is updated based on whether the swing mode is enabled or disabled.
575
+ *
576
+ * @remarks
577
+ * The `SwingMode` characteristic is set to `SWING_ENABLED` if the swing mode is on,
578
+ * otherwise it is set to `SWING_DISABLED`.
579
+ */
580
+ updateAccessoryFanStateCharacteristics() {
581
+ const windStrength = this.Status.windStrength;
582
+ const isSwingOn = this.Status.isSwingOn;
583
+ this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, windStrength);
584
+ // eslint-disable-next-line max-len
585
+ this.service.updateCharacteristic(this.platform.Characteristic.SwingMode, isSwingOn ? this.platform.Characteristic.SwingMode.SWING_ENABLED : this.platform.Characteristic.SwingMode.SWING_DISABLED);
586
+ }
587
+ /**
588
+ * Updates the Total Consumption characteristic of the accessory with the current consumption value.
589
+ * This method retrieves the current consumption from the device's status and updates the
590
+ * corresponding custom characteristic in the Homebridge service.
591
+ *
592
+ * @remarks
593
+ * Ensure that the `TotalConsumption` custom characteristic is properly defined in the platform
594
+ * and that the `Status.currentConsumption` value is up-to-date before calling this method.
595
+ */
596
+ updateAccessoryTotalConsumptionCharacteristic() {
597
+ this.service.updateCharacteristic(this.platform.customCharacteristics.TotalConsumption, this.Status.currentConsumption);
598
+ }
599
+ /**
600
+ * Updates the air quality characteristics of the accessory based on the current air quality status.
601
+ * This method checks if the air quality feature is enabled and updates the corresponding characteristics
602
+ * in the Homebridge service with the current air quality readings.
603
+ *
604
+ * @remarks
605
+ * The method updates the `AirQuality`, `PM2_5Density`, and `PM10Density` characteristics if the air quality
606
+ * data is available and the air quality feature is enabled.
607
+ */
608
+ updateAccessoryAirQualityCharacteristic() {
609
+ // air quality
610
+ if (this.config.ac_air_quality && this.serviceAirQuality && this.Status.airQuality && this.Status.airQuality.isOn) {
611
+ this.serviceAirQuality.updateCharacteristic(this.platform.Characteristic.AirQuality, this.Status.airQuality.overall);
612
+ if (this.Status.airQuality.PM2) {
613
+ this.serviceAirQuality.updateCharacteristic(this.platform.Characteristic.PM2_5Density, this.Status.airQuality.PM2);
614
+ }
615
+ if (this.Status.airQuality.PM10) {
616
+ this.serviceAirQuality.updateCharacteristic(this.platform.Characteristic.PM10Density, this.Status.airQuality.PM10);
617
+ }
618
+ }
619
+ }
620
+ /**
621
+ * Updates the temperature sensor characteristics of the accessory.
622
+ *
623
+ * This method checks if the air conditioner temperature sensor is enabled in the configuration
624
+ * and if the temperature sensor service (`serviceSensor`) is available. If both conditions are met,
625
+ * it updates the following characteristics:
626
+ *
627
+ * - `CurrentTemperature`: Reflects the current temperature reported by the air conditioner.
628
+ * - `StatusActive`: Indicates whether the air conditioner is powered on.
629
+ *
630
+ * @remarks
631
+ * Ensure that the `config.ac_temperature_sensor` is properly set and that the `serviceSensor` is initialized
632
+ * before calling this method to avoid runtime errors.
633
+ */
634
+ updateAccessoryTemperatureSensorCharacteristic() {
635
+ if (this.config.ac_temperature_sensor && this.serviceSensor) {
636
+ this.serviceSensor.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.Status.currentTemperature);
637
+ this.serviceSensor.updateCharacteristic(this.platform.Characteristic.StatusActive, this.Status.isPowerOn);
638
+ }
639
+ }
640
+ /**
641
+ * Updates the characteristics of the humidity sensor accessory.
642
+ *
643
+ * This method updates the `CurrentRelativeHumidity` and `StatusActive` characteristics
644
+ * of the humidity sensor service if the humidity sensor is enabled in the configuration
645
+ * (`ac_humidity_sensor`) and the `serviceHumiditySensor` is defined.
646
+ *
647
+ * - `CurrentRelativeHumidity` is updated with the current relative humidity value from the device status.
648
+ * - `StatusActive` is updated based on whether the air conditioner is powered on.
649
+ */
650
+ updateAccessoryHumiditySensorCharacteristic() {
651
+ // humidity sensor
652
+ if (this.config.ac_humidity_sensor && this.serviceHumiditySensor) {
653
+ this.serviceHumiditySensor.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.Status.currentRelativeHumidity);
654
+ this.serviceHumiditySensor.updateCharacteristic(this.platform.Characteristic.StatusActive, this.Status.isPowerOn);
655
+ }
656
+ }
657
+ /**
658
+ * Updates the characteristics of the Fan V2 service for the accessory.
659
+ *
660
+ * This method synchronizes the accessory's Fan V2 service characteristics with the current
661
+ * status of the air conditioner, including power state, swing mode, wind strength, and
662
+ * whether the fan is in auto or manual mode.
663
+ *
664
+ * The following characteristics are updated:
665
+ * - `Active`: Indicates whether the fan is active or inactive based on the power state.
666
+ * - `TargetFanState`: Sets the fan state to AUTO or MANUAL depending on the wind strength mode.
667
+ * - `RotationSpeed`: Updates the fan's rotation speed if in manual mode.
668
+ * - `SwingMode`: Indicates whether the swing mode is enabled or disabled.
669
+ *
670
+ * This method only performs updates if the `ac_fan_control` configuration is enabled and
671
+ * the `serviceFanV2` is defined.
672
+ */
673
+ updateAccessoryFanV2Characteristic() {
674
+ const status = this.Status;
675
+ const isPowerOn = status.isPowerOn;
676
+ const isSwingOn = status.isSwingOn;
677
+ const windStrength = status.windStrength;
678
+ // handle fan service
679
+ if (this.config.ac_fan_control && this.serviceFanV2) {
680
+ this.serviceFanV2.updateCharacteristic(this.platform.Characteristic.Active, isPowerOn ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE);
681
+ if (status.isWindStrengthAuto) {
682
+ this.serviceFanV2.updateCharacteristic(this.platform.Characteristic.TargetFanState, this.platform.Characteristic.TargetFanState.AUTO);
683
+ }
684
+ else {
685
+ this.serviceFanV2.updateCharacteristic(this.platform.Characteristic.TargetFanState, this.platform.Characteristic.TargetFanState.MANUAL);
686
+ this.serviceFanV2.updateCharacteristic(this.platform.Characteristic.RotationSpeed, windStrength);
687
+ }
688
+ // eslint-disable-next-line max-len
689
+ this.serviceFanV2.updateCharacteristic(this.platform.Characteristic.SwingMode, isSwingOn ? this.platform.Characteristic.SwingMode.SWING_ENABLED : this.platform.Characteristic.SwingMode.SWING_DISABLED);
690
+ }
691
+ }
692
+ /**
693
+ * Updates the LED control characteristic of the accessory.
694
+ *
695
+ * This method checks the current status of the accessory's light and updates
696
+ * the corresponding characteristic in the Homebridge service if the LED control
697
+ * configuration is enabled and the serviceLight is defined.
698
+ *
699
+ * @remarks
700
+ * - The `isLightOn` status is retrieved from the accessory's current status.
701
+ * - The `ac_led_control` configuration determines whether the LED control feature is enabled.
702
+ * - The `serviceLight` represents the Homebridge service responsible for the light characteristic.
703
+ */
704
+ updateAccessoryLedControlCharacteristic() {
705
+ const isLightOn = this.Status.isLightOn;
706
+ if (this.config.ac_led_control && this.serviceLight) {
707
+ this.serviceLight.updateCharacteristic(this.platform.Characteristic.On, isLightOn);
708
+ }
709
+ }
710
+ updateAccessoryJetModeCharacteristic() {
711
+ // more feature
712
+ const model = this.accessory.context.device.model;
713
+ if (this.isJetModeEnabled(model) && this.serviceJetMode) {
714
+ this.serviceJetMode.updateCharacteristic(this.platform.Characteristic.On, !!this.accessory.context.device.snapshot['airState.wMode.jet']);
715
+ }
716
+ }
717
+ updateAccessoryquietModeModelsCharacteristic() {
718
+ const device = this.accessory.context.device;
719
+ const model = device.model;
720
+ if (this.quietModeModels.includes(model) && this.serviceQuietMode) {
721
+ this.serviceQuietMode.updateCharacteristic(this.platform.Characteristic.On, !!device.snapshot['airState.miscFuncState.silentAWHP']);
722
+ }
723
+ }
724
+ updateAccessoryenergySaveModeModelsCharacteristic() {
725
+ const device = this.accessory.context.device;
726
+ const model = device.model;
727
+ if (this.energySaveModeModels.includes(model) && this.config.ac_energy_save) {
728
+ this.serviceEnergySaveMode?.updateCharacteristic(this.platform.Characteristic.On, !!device.snapshot['airState.powerSave.basic']);
729
+ }
730
+ }
731
+ updateAccessoryairCleanModelsCharacteristic() {
732
+ const device = this.accessory.context.device;
733
+ const model = device.model;
734
+ if (this.airCleanModels.includes(model) && this.config.ac_air_clean) {
735
+ this.serviceAirClean?.updateCharacteristic(this.platform.Characteristic.On, !!device.snapshot['airState.wMode.airClean']);
736
+ }
737
+ }
738
+ async setLight(value) {
739
+ const status = this.Status;
740
+ if (!status.isPowerOn) {
741
+ this.logger.debug('Power is off, cannot set light state');
742
+ return;
743
+ }
744
+ try {
745
+ const device = this.accessory.context.device;
746
+ await this.platform.ThinQ?.deviceControl(device.id, {
747
+ dataKey: 'airState.lightingState.displayControl',
748
+ dataValue: value ? 1 : 0,
749
+ });
750
+ this.accessory.context.device.data.snapshot['airState.lightingState.displayControl'] = value ? 1 : 0;
751
+ this.updateAccessoryLedControlCharacteristic();
752
+ }
753
+ catch (error) {
754
+ this.logger.error('Error setting light state:', error);
755
+ }
756
+ }
757
+ /**
758
+ * Sets the target state of the air conditioner based on the provided HomeKit characteristic value.
759
+ * Maps the HomeKit target states to the corresponding LG operation modes and updates the device state.
760
+ *
761
+ * @param value - The target state value from HomeKit, represented as a `CharacteristicValue`.
762
+ * Possible values include AUTO, HEAT, and COOL.
763
+ * @returns The updated target state value if successful, or `null` if an error occurs.
764
+ *
765
+ * @throws Logs an error if the operation mode cannot be updated on the device.
766
+ */
767
+ async setTargetState(value) {
768
+ this.logger.debug('Set target AC mode = ', value);
769
+ const vNum = normalizeNumber(value);
770
+ if (vNum === null) {
771
+ return;
772
+ }
773
+ this.currentTargetState = vNum;
774
+ const { Characteristic: { TargetHeaterCoolerState, }, } = this.platform;
775
+ // Map HomeKit states to LG opModes
776
+ let opMode;
777
+ switch (value) {
778
+ case TargetHeaterCoolerState.AUTO:
779
+ opMode = OpMode.AUTO; // LG’s AUTO mode = 6
780
+ break;
781
+ case TargetHeaterCoolerState.HEAT:
782
+ opMode = OpMode.HEAT; // LG’s HEAT mode = 4
783
+ break;
784
+ case TargetHeaterCoolerState.COOL:
785
+ opMode = OpMode.COOL; // LG’s COOL mode = 0
786
+ break;
787
+ default:
788
+ opMode = this.Status.opMode; // Keep current mode
789
+ }
790
+ if (opMode === this.Status.opMode) {
791
+ return;
792
+ }
793
+ try {
794
+ await this.setOpMode(this.accessory.context.device.id, opMode);
795
+ }
796
+ catch (error) {
797
+ this.logger.error('Error setting target state:', error);
798
+ }
799
+ }
800
+ async setActive(value) {
801
+ const device = this.accessory.context.device;
802
+ const isOn = normalizeBoolean(value);
803
+ const isOnNumeric = isOn ? 1 : 0;
804
+ this.logger.debug('Set power on = ', isOnNumeric, ' current status = ', this.Status.isPowerOn);
805
+ if ((this.Status.isPowerOn && isOnNumeric === 1) || (!this.Status.isPowerOn && isOnNumeric === 0)) {
806
+ this.logger.debug('Power state already matches incoming value; skipping deviceControl.');
807
+ return;
808
+ }
809
+ try {
810
+ const success = await this.platform.ThinQ?.deviceControl(device.id, {
811
+ dataKey: 'airState.operation',
812
+ dataValue: isOnNumeric,
813
+ }, 'Operation');
814
+ if (success) {
815
+ this.accessory.context.device.data.snapshot['airState.operation'] = isOnNumeric;
816
+ this.updateAccessoryActiveCharacteristic();
817
+ }
818
+ }
819
+ catch (error) {
820
+ this.logger.error('Error setting active state:', error);
821
+ }
822
+ }
823
+ /**
824
+ * Sets the target temperature for the air conditioner.
825
+ *
826
+ * @param value - The desired target temperature as a `CharacteristicValue`.
827
+ *
828
+ * @returns The target temperature if successfully set, or `null` if an error occurs or the operation is invalid.
829
+ *
830
+ * @remarks
831
+ * - If the air conditioner is powered off, the method logs an error and returns `null`.
832
+ * - If the provided value is not a number, the method logs an error and returns `null`.
833
+ * - The method checks whether the target temperature is within the valid range for the current mode
834
+ * (cooling or heating). If the value is out of range, it logs an error and returns `null`.
835
+ * - If the target temperature is the same as the current temperature, no action is taken, and the method logs a debug message.
836
+ * - The temperature value is converted from HomeKit format to LG format before being sent to the device.
837
+ * - If the operation fails, an error is logged, and the method returns `null`.
838
+ *
839
+ * @throws This method does not throw exceptions but logs errors instead.
840
+ */
841
+ async setTargetTemperature(value) {
842
+ const status = this.Status;
843
+ if (!status.isPowerOn) {
844
+ this.logger.error('Power is off, cannot set target temperature');
845
+ return;
846
+ }
847
+ const device = this.accessory.context.device;
848
+ const vNum = normalizeNumber(value);
849
+ if (vNum === null) {
850
+ this.logger.error('Invalid temperature value: ', value);
851
+ return;
852
+ }
853
+ // Calculate LG temperature with the status helper
854
+ const temperatureLG = status.convertTemperatureCelsiusFromHomekitToLG(vNum);
855
+ if (typeof temperatureLG !== 'number' || isNaN(temperatureLG)) {
856
+ this.logger.error('Converted temperature is not a valid number:', temperatureLG);
857
+ return;
858
+ }
859
+ if (temperatureLG === status.targetTemperature) {
860
+ this.logger.debug('Target temperature is identical to current setting; skipping.');
861
+ return;
862
+ }
863
+ try {
864
+ await this.platform.ThinQ?.deviceControl(device.id, {
865
+ dataKey: 'airState.tempState.target',
866
+ dataValue: temperatureLG,
867
+ });
868
+ this.accessory.context.device.data.snapshot['airState.tempState.target'] = temperatureLG;
869
+ this.updateAccessoryTemperatureCharacteristics();
870
+ return;
871
+ }
872
+ catch (error) {
873
+ this.logger.error('Error setting target temperature:', error);
874
+ }
875
+ }
876
+ /**
877
+ * Sets the fan speed of the air conditioner.
878
+ *
879
+ * @param value - The desired fan speed value, which is expected to be a number.
880
+ * The value is rounded and constrained to a minimum of 1.
881
+ * @returns The provided fan speed value if the operation is successful, or `null` if the power is off
882
+ * or an error occurs during the operation.
883
+ *
884
+ * @remarks
885
+ * - If the air conditioner is not powered on (`this.Status.isPowerOn` is `false`), the method exits early and returns `null`.
886
+ * - The fan speed value is mapped to a corresponding wind strength value using the `FanSpeed` enumeration.
887
+ * - The method sends a control command to the ThinQ platform to update the fan speed.
888
+ * - If the operation is successful, the updated wind strength value is stored in the device's snapshot.
889
+ * - Any errors encountered during the operation are logged, and the method returns `null`.
890
+ *
891
+ * @throws This method does not throw exceptions directly but logs errors internally if the operation fails.
892
+ */
893
+ async setFanSpeed(value) {
894
+ if (!this.Status.isPowerOn) {
895
+ return;
896
+ }
897
+ const vNum = normalizeNumber(value);
898
+ if (vNum === null) {
899
+ return;
900
+ }
901
+ const speedValue = Math.max(1, Math.round(vNum));
902
+ this.logger.debug('Set fan speed = ', speedValue);
903
+ const device = this.accessory.context.device;
904
+ const windStrength = safeParseInt(Object.keys(FanSpeed)[speedValue - 1], FanSpeed.HIGH);
905
+ try {
906
+ await this.platform.ThinQ?.deviceControl(device.id, {
907
+ dataKey: 'airState.windStrength',
908
+ dataValue: windStrength,
909
+ });
910
+ this.accessory.context.device.data.snapshot['airState.windStrength'] = windStrength;
911
+ }
912
+ catch (error) {
913
+ this.logger.error('Error setting fan speed:', error);
914
+ }
915
+ }
916
+ async setSwingMode(value) {
917
+ if (!this.Status.isPowerOn) {
918
+ this.logger.debug('Power is off, cannot set swing mode');
919
+ return;
920
+ }
921
+ const swingValue = !!value ? '100' : '0';
922
+ const device = this.accessory.context.device;
923
+ try {
924
+ if (this.config.ac_swing_mode === 'BOTH') {
925
+ await this.platform.ThinQ?.deviceControl(device.id, {
926
+ dataKey: null,
927
+ dataValue: null,
928
+ dataSetList: {
929
+ 'airState.wDir.vStep': swingValue,
930
+ 'airState.wDir.hStep': swingValue,
931
+ },
932
+ dataGetList: null,
933
+ }, 'Set', 'favoriteCtrl');
934
+ this.accessory.context.device.data.snapshot['airState.wDir.vStep'] = swingValue;
935
+ this.accessory.context.device.data.snapshot['airState.wDir.hStep'] = swingValue;
936
+ }
937
+ else if (this.config.ac_swing_mode === 'VERTICAL') {
938
+ await this.platform.ThinQ?.deviceControl(device.id, {
939
+ dataKey: 'airState.wDir.vStep',
940
+ dataValue: swingValue,
941
+ });
942
+ this.accessory.context.device.data.snapshot['airState.wDir.vStep'] = swingValue;
943
+ }
944
+ else if (this.config.ac_swing_mode === 'HORIZONTAL') {
945
+ await this.platform.ThinQ?.deviceControl(device.id, {
946
+ dataKey: 'airState.wDir.hStep',
947
+ dataValue: swingValue,
948
+ });
949
+ this.accessory.context.device.data.snapshot['airState.wDir.hStep'] = swingValue;
950
+ }
951
+ this.updateAccessoryFanStateCharacteristics();
952
+ this.updateAccessoryFanV2Characteristic();
953
+ }
954
+ catch (error) {
955
+ this.logger.error('Error setting swing mode:', error);
956
+ }
957
+ }
958
+ async setOpMode(deviceId, opMode) {
959
+ try {
960
+ const result = await this.platform.ThinQ?.deviceControl(deviceId, {
961
+ dataKey: 'airState.opMode',
962
+ dataValue: opMode,
963
+ });
964
+ return !!result;
965
+ }
966
+ catch (error) {
967
+ this.logger.error('Error setting operation mode:', error);
968
+ return false;
969
+ }
970
+ }
971
+ isJetModeEnabled(model) {
972
+ return this.jetModeModels.includes(model); // cool mode only
973
+ }
974
+ setupButton(device) {
975
+ if (!this.config.ac_buttons.length) {
976
+ return;
977
+ }
978
+ this.serviceLabelButtons = this.accessory.getService('Buttons')
979
+ || this.accessory.addService(this.platform.Service.ServiceLabel, 'Buttons', 'Buttons');
980
+ // remove all buttons before
981
+ for (let i = 0; i < this.serviceLabelButtons.linkedServices.length; i++) {
982
+ this.accessory.removeService(this.serviceLabelButtons.linkedServices[i]);
983
+ }
984
+ for (let i = 0; i < this.config.ac_buttons.length; i++) {
985
+ this.setupButtonOpmode(device, this.config.ac_buttons[i].name, safeParseInt(this.config.ac_buttons[i].op_mode));
986
+ }
987
+ }
988
+ setupButtonOpmode(device, name, opMode) {
989
+ const { Service: { Switch, }, Characteristic, } = this.platform;
990
+ if (!this.serviceLabelButtons) {
991
+ this.logger.error('ServiceLabelButtons not found cant setup button');
992
+ return;
993
+ }
994
+ const serviceButton = this.accessory.getService(name) || this.accessory.addService(Switch, name, name);
995
+ serviceButton.addOptionalCharacteristic(Characteristic.ConfiguredName);
996
+ serviceButton.setCharacteristic(Characteristic.ConfiguredName, name);
997
+ serviceButton.getCharacteristic(this.platform.Characteristic.On)
998
+ .onGet(() => {
999
+ return this.Status.opMode === opMode;
1000
+ })
1001
+ .onSet((value) => {
1002
+ this.handleButtonOpmode(value, opMode);
1003
+ });
1004
+ this.serviceLabelButtons.addLinkedService(serviceButton);
1005
+ }
1006
+ /**
1007
+ * Handles the operation mode button press for the air conditioner.
1008
+ *
1009
+ * @param value - The characteristic value indicating the button state (true for pressed, false for released).
1010
+ * @param opMode - The operation mode to set when the button is pressed.
1011
+ *
1012
+ * When the button is pressed (`value` is true) and the current operation mode (`this.Status.opMode`)
1013
+ * is different from the provided `opMode`, the method updates the operation mode to the provided `opMode`.
1014
+ *
1015
+ * When the button is released (`value` is false), the method resets the operation mode to `OpMode.COOL`,
1016
+ * updates the accessory state characteristics, and restores the target state to the current target state.
1017
+ *
1018
+ * @returns A promise that resolves when the operation mode and related states are successfully updated.
1019
+ */
1020
+ async handleButtonOpmode(value, opMode) {
1021
+ if (value) {
1022
+ if (this.Status.opMode !== opMode) {
1023
+ await this.setOpMode(this.accessory.context.device.id, opMode);
1024
+ this.accessory.context.device.data.snapshot['airState.opMode'] = opMode;
1025
+ }
1026
+ }
1027
+ else {
1028
+ await this.setOpMode(this.accessory.context.device.id, OpMode.COOL);
1029
+ this.accessory.context.device.data.snapshot['airState.opMode'] = OpMode.COOL;
1030
+ await this.setTargetState(this.currentTargetState);
1031
+ }
1032
+ }
1033
+ }
1034
+ export class ACStatus {
1035
+ data;
1036
+ device;
1037
+ config;
1038
+ logger;
1039
+ constructor(data, device, config, logger) {
1040
+ this.data = data;
1041
+ this.device = device;
1042
+ this.config = config;
1043
+ this.logger = logger;
1044
+ }
1045
+ /**
1046
+ * detect fahrenheit unit device by country code
1047
+ * list: us
1048
+ */
1049
+ get isFahrenheitUnit() {
1050
+ return (this.config.ac_temperature_unit || '').toLowerCase() === 'f';
1051
+ }
1052
+ /**
1053
+ * Converts temperature from Homekit to LG format.
1054
+ * @param temperatureInCelsius The temperature in Celsius to convert.
1055
+ * @returns The converted temperature in LG format.
1056
+ */
1057
+ convertTemperatureCelsiusFromHomekitToLG(temperatureInCelsius) {
1058
+ const tempNum = Number(temperatureInCelsius);
1059
+ if (!this.isFahrenheitUnit) {
1060
+ return tempNum;
1061
+ }
1062
+ const temperatureInFahrenheit = Math.round(cToF(tempNum));
1063
+ try {
1064
+ const mapped = this.device.deviceModel.lookupMonitorValue && this.device.deviceModel.lookupMonitorValue('TempFahToCel', String(temperatureInFahrenheit));
1065
+ if (typeof mapped !== 'undefined' && mapped !== null) {
1066
+ const n = Number(mapped);
1067
+ if (!isNaN(n)) {
1068
+ return n;
1069
+ }
1070
+ }
1071
+ }
1072
+ catch (e) {
1073
+ this.logger.warn('Temperature mapping lookup failed, falling back to direct conversion.', e);
1074
+ }
1075
+ return temperatureInFahrenheit;
1076
+ }
1077
+ /**
1078
+ * algorithm conversion LG vs Homekit is different
1079
+ * so we need to handle it before submit to homekit
1080
+ */
1081
+ convertTemperatureCelsiusFromLGToHomekit(temperature) {
1082
+ const tempNum = Number(temperature);
1083
+ if (!this.isFahrenheitUnit) {
1084
+ return tempNum;
1085
+ }
1086
+ try {
1087
+ const mapped = this.device.deviceModel.lookupMonitorValue && this.device.deviceModel.lookupMonitorValue('TempCelToFah', String(tempNum));
1088
+ if (typeof mapped !== 'undefined' && mapped !== null) {
1089
+ const n = Number(mapped);
1090
+ if (!isNaN(n)) {
1091
+ return Math.round(fToC(n) * 100) / 100;
1092
+ }
1093
+ }
1094
+ }
1095
+ catch (e) {
1096
+ this.logger.warn('Temperature mapping lookup failed, falling back to direct conversion.', e);
1097
+ }
1098
+ const c = Math.round(fToC(tempNum) * 100) / 100;
1099
+ return c;
1100
+ }
1101
+ get opMode() {
1102
+ return this.data['airState.opMode'];
1103
+ }
1104
+ get isPowerOn() {
1105
+ return !!this.data['airState.operation'];
1106
+ }
1107
+ get currentRelativeHumidity() {
1108
+ const humidity = safeParseInt(this.data['airState.humidity.current']);
1109
+ if (humidity > 100) {
1110
+ return humidity / 10;
1111
+ }
1112
+ return humidity;
1113
+ }
1114
+ get currentTemperature() {
1115
+ return this.convertTemperatureCelsiusFromLGToHomekit(this.data['airState.tempState.current']);
1116
+ }
1117
+ get targetTemperature() {
1118
+ return this.convertTemperatureCelsiusFromLGToHomekit(this.data['airState.tempState.target']);
1119
+ }
1120
+ get airQuality() {
1121
+ // air quality not available
1122
+ if (!('airState.quality.overall' in this.data) && !('airState.quality.PM2' in this.data) && !('airState.quality.PM10' in this.data)) {
1123
+ return null;
1124
+ }
1125
+ return {
1126
+ isOn: this.isPowerOn || this.data['airState.quality.sensorMon'],
1127
+ overall: safeParseInt(this.data['airState.quality.overall']),
1128
+ PM2: safeParseInt(this.data['airState.quality.PM2']),
1129
+ PM10: safeParseInt(this.data['airState.quality.PM10']),
1130
+ };
1131
+ }
1132
+ // Should return 0 - 100 int
1133
+ get windStrength() {
1134
+ const raw = this.data && this.data['airState.windStrength'];
1135
+ const num = Number(raw);
1136
+ if (!isNaN(num)) {
1137
+ if (num === FAN_SPEED_AUTO) {
1138
+ return Math.round(Object.keys(FanSpeed).length / 2);
1139
+ }
1140
+ const min = 2;
1141
+ const max = 6;
1142
+ if (num >= min && num <= max) {
1143
+ return Math.round(((num - min) / (max - min)) * 100) || 1;
1144
+ }
1145
+ }
1146
+ return Math.round(Object.keys(FanSpeed).length / 2);
1147
+ }
1148
+ get isWindStrengthAuto() {
1149
+ const raw = this.data && this.data['airState.windStrength'];
1150
+ return Number(raw) === FAN_SPEED_AUTO;
1151
+ }
1152
+ get isSwingOn() {
1153
+ const vStep = Math.floor((this.data['airState.wDir.vStep'] || 0) / 100), hStep = Math.floor((this.data['airState.wDir.hStep'] || 0) / 100);
1154
+ return !!(vStep + hStep);
1155
+ }
1156
+ get isLightOn() {
1157
+ return !!this.data['airState.lightingState.displayControl'];
1158
+ }
1159
+ get currentConsumption() {
1160
+ const consumption = this.data['airState.energy.onCurrent'];
1161
+ if (isNaN(consumption)) {
1162
+ return 0;
1163
+ }
1164
+ return consumption / 100;
1165
+ }
1166
+ get type() {
1167
+ return this.device.deviceModel.data.Info.modelType || ACModelType.RAC;
1168
+ }
1169
+ /**
1170
+ * Retrieves the temperature range based on the provided minimum and maximum range values.
1171
+ *
1172
+ * @param [minRange, maxRange] - A tuple containing the minimum and maximum range values as `EnumValue` objects.
1173
+ * @returns A `RangeValue` object representing the temperature range, including its type, minimum, maximum, and step values.
1174
+ *
1175
+ * The method first attempts to calculate the temperature range using the provided `minRange` and `maxRange` values.
1176
+ * If these values are not sufficient to determine a valid range, it falls back to retrieving the range from the device model's
1177
+ * `airState.tempState.limitMin` or `airState.tempState.target` properties.
1178
+ */
1179
+ getTemperatureRange([minRange, maxRange]) {
1180
+ let temperature = {
1181
+ type: ValueType.Range,
1182
+ min: 0,
1183
+ max: 0,
1184
+ step: 0.01,
1185
+ };
1186
+ if (minRange && maxRange) {
1187
+ const minRangeOptions = Object.values(minRange.options).filter((v) => typeof v === 'number');
1188
+ const maxRangeOptions = Object.values(maxRange.options).filter((v) => typeof v === 'number');
1189
+ if (minRangeOptions.length > 1) {
1190
+ temperature.min = Math.min(...minRangeOptions.filter(v => v !== 0));
1191
+ }
1192
+ if (maxRangeOptions.length > 1) {
1193
+ temperature.max = Math.max(...maxRangeOptions.filter(v => v !== 0));
1194
+ }
1195
+ }
1196
+ if (!temperature || !temperature.min || !temperature.max) {
1197
+ temperature = this.device.deviceModel.value('airState.tempState.limitMin');
1198
+ }
1199
+ if (!temperature || !temperature.min || !temperature.max) {
1200
+ temperature = this.device.deviceModel.value('airState.tempState.target');
1201
+ }
1202
+ return temperature;
1203
+ }
1204
+ /**
1205
+ * Retrieves the temperature range for heating based on the air conditioner's model type.
1206
+ *
1207
+ * For AWHP models, the range is determined using water temperature heating limits.
1208
+ * For other models, the range is determined using general heating limits.
1209
+ *
1210
+ * @returns A tuple containing two `EnumValue` objects:
1211
+ * - The first element represents the minimum heating temperature.
1212
+ * - The second element represents the maximum heating temperature.
1213
+ */
1214
+ getTemperatureRangeForHeating() {
1215
+ let heatLowLimitKey, heatHighLimitKey;
1216
+ if (this.type === ACModelType.AWHP) {
1217
+ heatLowLimitKey = 'support.airState.tempState.waterTempHeatMin';
1218
+ heatHighLimitKey = 'support.airState.tempState.waterTempHeatMax';
1219
+ }
1220
+ else {
1221
+ heatLowLimitKey = 'support.heatLowLimit';
1222
+ heatHighLimitKey = 'support.heatHighLimit';
1223
+ }
1224
+ const tempHeatMinRange = this.device.deviceModel.value(heatLowLimitKey);
1225
+ const tempHeatMaxRange = this.device.deviceModel.value(heatHighLimitKey);
1226
+ return [tempHeatMinRange, tempHeatMaxRange];
1227
+ }
1228
+ /**
1229
+ * Retrieves the temperature range for cooling based on the air conditioner's model type.
1230
+ *
1231
+ * For AWHP models, the range is determined using water temperature cooling limits.
1232
+ * For other models, the range is determined using general cooling limits.
1233
+ *
1234
+ * @returns A tuple containing two `EnumValue` objects:
1235
+ * - The first element represents the minimum cooling temperature.
1236
+ * - The second element represents the maximum cooling temperature.
1237
+ */
1238
+ getTemperatureRangeForCooling() {
1239
+ let coolLowLimitKey, coolHighLimitKey;
1240
+ if (this.type === ACModelType.AWHP) {
1241
+ coolLowLimitKey = 'support.airState.tempState.waterTempCoolMin';
1242
+ coolHighLimitKey = 'support.airState.tempState.waterTempCoolMax';
1243
+ }
1244
+ else {
1245
+ coolLowLimitKey = 'support.coolLowLimit';
1246
+ coolHighLimitKey = 'support.coolHighLimit';
1247
+ }
1248
+ const tempCoolMinRange = this.device.deviceModel.value(coolLowLimitKey);
1249
+ const tempCoolMaxRange = this.device.deviceModel.value(coolHighLimitKey);
1250
+ return [tempCoolMinRange, tempCoolMaxRange];
1251
+ }
1252
+ }
1253
+ //# sourceMappingURL=AirConditioner.js.map