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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +40 -27
  2. package/config.schema.json +66 -12
  3. package/dist/src/accessories/air-conditioning-accessory.js +1 -1
  4. package/dist/src/accessories/air-conditioning-accessory.js.map +1 -1
  5. package/dist/src/accessories/altherma-accessory.js +1 -1
  6. package/dist/src/accessories/altherma-accessory.js.map +1 -1
  7. package/dist/src/accessories/base-accessory.d.ts +5 -4
  8. package/dist/src/accessories/base-accessory.d.ts.map +1 -1
  9. package/dist/src/accessories/base-accessory.js +9 -10
  10. package/dist/src/accessories/base-accessory.js.map +1 -1
  11. package/dist/src/api/daikin-api.d.ts +22 -6
  12. package/dist/src/api/daikin-api.d.ts.map +1 -1
  13. package/dist/src/api/daikin-api.js +49 -23
  14. package/dist/src/api/daikin-api.js.map +1 -1
  15. package/dist/src/api/daikin-controller.d.ts +4 -48
  16. package/dist/src/api/daikin-controller.d.ts.map +1 -1
  17. package/dist/src/api/daikin-controller.js +14 -84
  18. package/dist/src/api/daikin-controller.js.map +1 -1
  19. package/dist/src/api/daikin-mobile-oauth.d.ts +11 -40
  20. package/dist/src/api/daikin-mobile-oauth.d.ts.map +1 -1
  21. package/dist/src/api/daikin-mobile-oauth.js +36 -160
  22. package/dist/src/api/daikin-mobile-oauth.js.map +1 -1
  23. package/dist/src/api/daikin-oauth.d.ts +10 -37
  24. package/dist/src/api/daikin-oauth.d.ts.map +1 -1
  25. package/dist/src/api/daikin-oauth.js +36 -166
  26. package/dist/src/api/daikin-oauth.js.map +1 -1
  27. package/dist/src/api/daikin-schemas.d.ts +20 -22
  28. package/dist/src/api/daikin-schemas.d.ts.map +1 -1
  29. package/dist/src/api/daikin-schemas.js +17 -49
  30. package/dist/src/api/daikin-schemas.js.map +1 -1
  31. package/dist/src/api/daikin-types.d.ts +11 -23
  32. package/dist/src/api/daikin-types.d.ts.map +1 -1
  33. package/dist/src/api/daikin-types.js.map +1 -1
  34. package/dist/src/api/daikin-websocket.d.ts +4 -5
  35. package/dist/src/api/daikin-websocket.d.ts.map +1 -1
  36. package/dist/src/api/daikin-websocket.js +62 -34
  37. package/dist/src/api/daikin-websocket.js.map +1 -1
  38. package/dist/src/api/http-defaults.d.ts +3 -2
  39. package/dist/src/api/http-defaults.d.ts.map +1 -1
  40. package/dist/src/api/http-defaults.js.map +1 -1
  41. package/dist/src/api/http-transport.d.ts +14 -2
  42. package/dist/src/api/http-transport.d.ts.map +1 -1
  43. package/dist/src/api/http-transport.js +32 -9
  44. package/dist/src/api/http-transport.js.map +1 -1
  45. package/dist/src/api/token-client.d.ts +82 -0
  46. package/dist/src/api/token-client.d.ts.map +1 -0
  47. package/dist/src/api/token-client.js +188 -0
  48. package/dist/src/api/token-client.js.map +1 -0
  49. package/dist/src/api/token-storage.d.ts +6 -0
  50. package/dist/src/api/token-storage.d.ts.map +1 -1
  51. package/dist/src/api/token-storage.js +27 -1
  52. package/dist/src/api/token-storage.js.map +1 -1
  53. package/dist/src/config/config-manager.d.ts +69 -125
  54. package/dist/src/config/config-manager.d.ts.map +1 -1
  55. package/dist/src/config/config-manager.js +288 -225
  56. package/dist/src/config/config-manager.js.map +1 -1
  57. package/dist/src/constants/api.constants.d.ts +6 -0
  58. package/dist/src/constants/api.constants.d.ts.map +1 -1
  59. package/dist/src/constants/api.constants.js +7 -1
  60. package/dist/src/constants/api.constants.js.map +1 -1
  61. package/dist/src/constants/auth.constants.d.ts +11 -0
  62. package/dist/src/constants/auth.constants.d.ts.map +1 -1
  63. package/dist/src/constants/auth.constants.js +12 -1
  64. package/dist/src/constants/auth.constants.js.map +1 -1
  65. package/dist/src/constants/device.constants.d.ts +1 -7
  66. package/dist/src/constants/device.constants.d.ts.map +1 -1
  67. package/dist/src/constants/device.constants.js +2 -11
  68. package/dist/src/constants/device.constants.js.map +1 -1
  69. package/dist/src/constants/time.constants.d.ts +0 -2
  70. package/dist/src/constants/time.constants.d.ts.map +1 -1
  71. package/dist/src/constants/time.constants.js +1 -3
  72. package/dist/src/constants/time.constants.js.map +1 -1
  73. package/dist/src/device/accessory-factory.d.ts +0 -5
  74. package/dist/src/device/accessory-factory.d.ts.map +1 -1
  75. package/dist/src/device/accessory-factory.js +0 -7
  76. package/dist/src/device/accessory-factory.js.map +1 -1
  77. package/dist/src/device/capability-detector.d.ts +9 -1
  78. package/dist/src/device/capability-detector.d.ts.map +1 -1
  79. package/dist/src/device/capability-detector.js +32 -28
  80. package/dist/src/device/capability-detector.js.map +1 -1
  81. package/dist/src/device/capability-docs.d.ts.map +1 -1
  82. package/dist/src/device/capability-docs.js +3 -0
  83. package/dist/src/device/capability-docs.js.map +1 -1
  84. package/dist/src/device/data-paths.d.ts +18 -0
  85. package/dist/src/device/data-paths.d.ts.map +1 -0
  86. package/dist/src/device/data-paths.js +20 -0
  87. package/dist/src/device/data-paths.js.map +1 -0
  88. package/dist/src/device/profiles/device-profile.d.ts +0 -5
  89. package/dist/src/device/profiles/device-profile.d.ts.map +1 -1
  90. package/dist/src/device/profiles/device-profile.js +0 -21
  91. package/dist/src/device/profiles/device-profile.js.map +1 -1
  92. package/dist/src/features/base-feature.d.ts +53 -9
  93. package/dist/src/features/base-feature.d.ts.map +1 -1
  94. package/dist/src/features/base-feature.js +93 -40
  95. package/dist/src/features/base-feature.js.map +1 -1
  96. package/dist/src/features/feature-manager.d.ts +27 -15
  97. package/dist/src/features/feature-manager.d.ts.map +1 -1
  98. package/dist/src/features/feature-manager.js +27 -24
  99. package/dist/src/features/feature-manager.js.map +1 -1
  100. package/dist/src/features/index.d.ts +1 -0
  101. package/dist/src/features/index.d.ts.map +1 -1
  102. package/dist/src/features/index.js +1 -0
  103. package/dist/src/features/index.js.map +1 -1
  104. package/dist/src/features/modes/auto-fan-mode.feature.d.ts +16 -10
  105. package/dist/src/features/modes/auto-fan-mode.feature.d.ts.map +1 -1
  106. package/dist/src/features/modes/auto-fan-mode.feature.js +16 -40
  107. package/dist/src/features/modes/auto-fan-mode.feature.js.map +1 -1
  108. package/dist/src/features/modes/dry-operation-mode.feature.d.ts +12 -9
  109. package/dist/src/features/modes/dry-operation-mode.feature.d.ts.map +1 -1
  110. package/dist/src/features/modes/dry-operation-mode.feature.js +11 -30
  111. package/dist/src/features/modes/dry-operation-mode.feature.js.map +1 -1
  112. package/dist/src/features/modes/econo-mode.feature.d.ts +12 -9
  113. package/dist/src/features/modes/econo-mode.feature.d.ts.map +1 -1
  114. package/dist/src/features/modes/econo-mode.feature.js +11 -31
  115. package/dist/src/features/modes/econo-mode.feature.js.map +1 -1
  116. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts +12 -9
  117. package/dist/src/features/modes/fan-only-operation-mode.feature.d.ts.map +1 -1
  118. package/dist/src/features/modes/fan-only-operation-mode.feature.js +11 -30
  119. package/dist/src/features/modes/fan-only-operation-mode.feature.js.map +1 -1
  120. package/dist/src/features/modes/firmware-update.feature.d.ts +7 -6
  121. package/dist/src/features/modes/firmware-update.feature.d.ts.map +1 -1
  122. package/dist/src/features/modes/firmware-update.feature.js +15 -22
  123. package/dist/src/features/modes/firmware-update.feature.js.map +1 -1
  124. package/dist/src/features/modes/holiday-mode.feature.d.ts +2 -1
  125. package/dist/src/features/modes/holiday-mode.feature.d.ts.map +1 -1
  126. package/dist/src/features/modes/holiday-mode.feature.js +5 -15
  127. package/dist/src/features/modes/holiday-mode.feature.js.map +1 -1
  128. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts +15 -12
  129. package/dist/src/features/modes/indoor-silent-mode.feature.d.ts.map +1 -1
  130. package/dist/src/features/modes/indoor-silent-mode.feature.js +15 -39
  131. package/dist/src/features/modes/indoor-silent-mode.feature.js.map +1 -1
  132. package/dist/src/features/modes/oscillation-mode.feature.d.ts +5 -6
  133. package/dist/src/features/modes/oscillation-mode.feature.d.ts.map +1 -1
  134. package/dist/src/features/modes/oscillation-mode.feature.js +14 -36
  135. package/dist/src/features/modes/oscillation-mode.feature.js.map +1 -1
  136. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts +12 -9
  137. package/dist/src/features/modes/outdoor-silent-mode.feature.d.ts.map +1 -1
  138. package/dist/src/features/modes/outdoor-silent-mode.feature.js +11 -31
  139. package/dist/src/features/modes/outdoor-silent-mode.feature.js.map +1 -1
  140. package/dist/src/features/modes/powerful-mode.feature.d.ts +14 -10
  141. package/dist/src/features/modes/powerful-mode.feature.d.ts.map +1 -1
  142. package/dist/src/features/modes/powerful-mode.feature.js +13 -32
  143. package/dist/src/features/modes/powerful-mode.feature.js.map +1 -1
  144. package/dist/src/features/modes/streamer-mode.feature.d.ts +12 -9
  145. package/dist/src/features/modes/streamer-mode.feature.d.ts.map +1 -1
  146. package/dist/src/features/modes/streamer-mode.feature.js +11 -31
  147. package/dist/src/features/modes/streamer-mode.feature.js.map +1 -1
  148. package/dist/src/features/on-off-data-point-feature.d.ts +43 -0
  149. package/dist/src/features/on-off-data-point-feature.d.ts.map +1 -0
  150. package/dist/src/features/on-off-data-point-feature.js +49 -0
  151. package/dist/src/features/on-off-data-point-feature.js.map +1 -0
  152. package/dist/src/platform.d.ts +56 -9
  153. package/dist/src/platform.d.ts.map +1 -1
  154. package/dist/src/platform.js +248 -128
  155. package/dist/src/platform.js.map +1 -1
  156. package/dist/src/services/climate-control/fan-speed.d.ts +45 -0
  157. package/dist/src/services/climate-control/fan-speed.d.ts.map +1 -0
  158. package/dist/src/services/climate-control/fan-speed.js +104 -0
  159. package/dist/src/services/climate-control/fan-speed.js.map +1 -0
  160. package/dist/src/services/climate-control/separate-fan.service.d.ts +45 -0
  161. package/dist/src/services/climate-control/separate-fan.service.d.ts.map +1 -0
  162. package/dist/src/services/climate-control/separate-fan.service.js +82 -0
  163. package/dist/src/services/climate-control/separate-fan.service.js.map +1 -0
  164. package/dist/src/services/climate-control/setpoint-resolver.d.ts +68 -0
  165. package/dist/src/services/climate-control/setpoint-resolver.d.ts.map +1 -0
  166. package/dist/src/services/climate-control/setpoint-resolver.js +170 -0
  167. package/dist/src/services/climate-control/setpoint-resolver.js.map +1 -0
  168. package/dist/src/services/climate-control/swing-controller.d.ts +28 -0
  169. package/dist/src/services/climate-control/swing-controller.d.ts.map +1 -0
  170. package/dist/src/services/climate-control/swing-controller.js +65 -0
  171. package/dist/src/services/climate-control/swing-controller.js.map +1 -0
  172. package/dist/src/services/climate-control.service.d.ts +39 -48
  173. package/dist/src/services/climate-control.service.d.ts.map +1 -1
  174. package/dist/src/services/climate-control.service.js +223 -554
  175. package/dist/src/services/climate-control.service.js.map +1 -1
  176. package/dist/src/services/hot-water-tank.service.d.ts +17 -8
  177. package/dist/src/services/hot-water-tank.service.d.ts.map +1 -1
  178. package/dist/src/services/hot-water-tank.service.js +102 -157
  179. package/dist/src/services/hot-water-tank.service.js.map +1 -1
  180. package/dist/src/types/daikin-enums.d.ts +0 -5
  181. package/dist/src/types/daikin-enums.d.ts.map +1 -1
  182. package/dist/src/types/daikin-enums.js +1 -7
  183. package/dist/src/types/daikin-enums.js.map +1 -1
  184. package/dist/src/types/device-capabilities.d.ts +1 -0
  185. package/dist/src/types/device-capabilities.d.ts.map +1 -1
  186. package/dist/src/utils/errors.d.ts +9 -0
  187. package/dist/src/utils/errors.d.ts.map +1 -0
  188. package/dist/src/utils/errors.js +25 -0
  189. package/dist/src/utils/errors.js.map +1 -0
  190. package/dist/src/utils/hap-write.d.ts +22 -0
  191. package/dist/src/utils/hap-write.d.ts.map +1 -0
  192. package/dist/src/utils/hap-write.js +33 -0
  193. package/dist/src/utils/hap-write.js.map +1 -0
  194. package/dist/src/utils/sleep.d.ts +5 -0
  195. package/dist/src/utils/sleep.d.ts.map +1 -0
  196. package/dist/src/utils/sleep.js +10 -0
  197. package/dist/src/utils/sleep.js.map +1 -0
  198. package/homebridge-ui/README.md +25 -2
  199. package/homebridge-ui/public/index.html +19 -17
  200. package/homebridge-ui/public/script.js +95 -32
  201. package/homebridge-ui/public/styles.css +109 -0
  202. package/homebridge-ui/server.js +687 -453
  203. package/package.json +3 -3
  204. package/dist/src/utils/log-context.d.ts +0 -105
  205. package/dist/src/utils/log-context.d.ts.map +0 -1
  206. package/dist/src/utils/log-context.js +0 -206
  207. package/dist/src/utils/log-context.js.map +0 -1
  208. package/dist/src/utils/update-mapper.d.ts +0 -59
  209. package/dist/src/utils/update-mapper.d.ts.map +0 -1
  210. package/dist/src/utils/update-mapper.js +0 -192
  211. package/dist/src/utils/update-mapper.js.map +0 -1
@@ -3,82 +3,60 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClimateControlService = void 0;
4
4
  const types_1 = require("../types");
5
5
  const features_1 = require("../features");
6
+ const hap_write_1 = require("../utils/hap-write");
7
+ const errors_1 = require("../utils/errors");
6
8
  const constants_1 = require("../constants");
9
+ const fan_speed_1 = require("./climate-control/fan-speed");
10
+ const swing_controller_1 = require("./climate-control/swing-controller");
11
+ const setpoint_resolver_1 = require("./climate-control/setpoint-resolver");
12
+ const separate_fan_service_1 = require("./climate-control/separate-fan.service");
13
+ const DEFAULT_COOLING_TEMPERATURE = 25;
7
14
  class ClimateControlService {
8
15
  platform;
9
16
  accessory;
10
17
  managementPointId;
11
18
  name;
12
19
  service;
13
- fanService;
14
20
  featureManager;
21
+ swing;
22
+ fanSpeed;
23
+ setpoints;
24
+ autoSetpointSync;
25
+ separateFan;
26
+ lastOperationMode;
15
27
  constructor(platform, accessory, managementPointId) {
16
28
  this.platform = platform;
17
29
  this.accessory = accessory;
18
30
  this.managementPointId = managementPointId;
19
31
  this.name = this.accessory.displayName;
20
- this.service = this.accessory.getService(this.platform.Service.HeaterCooler);
32
+ const getDevice = () => this.device;
33
+ this.swing = new swing_controller_1.SwingController(getDevice, managementPointId);
34
+ this.fanSpeed = new fan_speed_1.FanSpeedController(getDevice, managementPointId, () => this.getCurrentOperationMode());
35
+ this.setpoints = new setpoint_resolver_1.SetpointResolver(getDevice, managementPointId, accessory.UUID);
36
+ this.autoSetpointSync = new setpoint_resolver_1.AutoSetpointSync(getDevice, managementPointId, this.setpoints, platform.log, this.name);
37
+ this.separateFan = new separate_fan_service_1.SeparateFanService(platform, accessory, this.name);
38
+ const { Characteristic } = this.platform;
21
39
  this.featureManager = new features_1.FeatureManager(platform, accessory, managementPointId);
22
- this.service = this.service || this.accessory.addService(this.platform.Service.HeaterCooler);
23
- this.service.setCharacteristic(this.platform.Characteristic.Name, this.name);
24
- // Required characteristic
25
- this.service.getCharacteristic(this.platform.Characteristic.Active)
40
+ this.service = this.accessory.getService(this.platform.Service.HeaterCooler)
41
+ || this.accessory.addService(this.platform.Service.HeaterCooler);
42
+ this.service.setCharacteristic(Characteristic.Name, this.name);
43
+ // Required characteristics
44
+ this.service.getCharacteristic(Characteristic.Active)
26
45
  .onSet(this.handleActiveStateSet.bind(this))
27
46
  .onGet(this.handleActiveStateGet.bind(this));
28
- // Required characteristic
29
- this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
47
+ this.service.getCharacteristic(Characteristic.CurrentTemperature)
30
48
  .onGet(this.handleCurrentTemperatureGet.bind(this));
31
- // Required characteristic
32
- this.service.getCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState)
33
- .setProps({
34
- minStep: 1,
35
- minValue: 0,
36
- maxValue: 2,
37
- })
49
+ this.service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
50
+ .setProps({ minStep: 1, minValue: 0, maxValue: 2 })
38
51
  .onGet(this.handleTargetHeaterCoolerStateGet.bind(this))
39
52
  .onSet(this.handleTargetHeaterCoolerStateSet.bind(this));
40
- const roomTemperatureControlForCooling = accessory.context.device.getData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.COOLING}/setpoints/${this.getSetpoint(types_1.DaikinOperationModes.COOLING)}`);
41
- if (roomTemperatureControlForCooling) {
42
- const coolingChar = this.service.getCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature);
43
- // Set value within default HomeKit range first to avoid warning when setProps narrows the range
44
- const coolingValue = typeof roomTemperatureControlForCooling.value === 'number' ? roomTemperatureControlForCooling.value : constants_1.COOLING_TEMP_CLAMP_MAX;
45
- const clampedCoolingValue = Math.max(constants_1.HOMEKIT_TEMP_MIN, Math.min(constants_1.COOLING_TEMP_CLAMP_MAX, coolingValue));
46
- coolingChar.updateValue(clampedCoolingValue);
47
- coolingChar
48
- .setProps({
49
- minStep: roomTemperatureControlForCooling.stepValue,
50
- minValue: roomTemperatureControlForCooling.minValue,
51
- maxValue: roomTemperatureControlForCooling.maxValue,
52
- })
53
- .onGet(this.handleCoolingThresholdTemperatureGet.bind(this))
54
- .onSet(this.handleCoolingThresholdTemperatureSet.bind(this));
55
- }
56
- else {
57
- this.service.removeCharacteristic(this.service.getCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature));
58
- }
59
- const roomTemperatureControlForHeating = accessory.context.device.getData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.HEATING}/setpoints/${this.getSetpoint(types_1.DaikinOperationModes.HEATING)}`);
60
- if (roomTemperatureControlForHeating) {
61
- const heatingChar = this.service.getCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature);
62
- // Set value within default HomeKit range first to avoid warning when setProps narrows the range
63
- const heatingValue = typeof roomTemperatureControlForHeating.value === 'number' ? roomTemperatureControlForHeating.value : constants_1.DEFAULT_ROOM_TEMPERATURE;
64
- const clampedHeatingValue = Math.max(constants_1.HEATING_TEMP_CLAMP_MIN, Math.min(constants_1.HEATING_TEMP_CLAMP_MAX, heatingValue));
65
- heatingChar.updateValue(clampedHeatingValue);
66
- heatingChar
67
- .setProps({
68
- minStep: roomTemperatureControlForHeating.stepValue,
69
- minValue: roomTemperatureControlForHeating.minValue,
70
- maxValue: roomTemperatureControlForHeating.maxValue,
71
- })
72
- .onGet(this.handleHeatingThresholdTemperatureGet.bind(this))
73
- .onSet(this.handleHeatingThresholdTemperatureSet.bind(this));
74
- }
75
- else {
76
- this.service.removeCharacteristic(this.service.getCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature));
77
- }
53
+ this.setupThreshold(Characteristic.CoolingThresholdTemperature, types_1.DaikinOperationModes.COOLING, constants_1.COOLING_TEMP_CLAMP_MAX, constants_1.HOMEKIT_TEMP_MIN, constants_1.COOLING_TEMP_CLAMP_MAX, this.handleCoolingThresholdTemperatureGet, this.handleCoolingThresholdTemperatureSet);
54
+ this.setupThreshold(Characteristic.HeatingThresholdTemperature, types_1.DaikinOperationModes.HEATING, constants_1.DEFAULT_ROOM_TEMPERATURE, constants_1.HEATING_TEMP_CLAMP_MIN, constants_1.HEATING_TEMP_CLAMP_MAX, this.handleHeatingThresholdTemperatureGet, this.handleHeatingThresholdTemperatureSet);
78
55
  this.addOrUpdateCharacteristicRotationSpeed();
79
- if (this.hasSwingModeFeature()) {
56
+ this.lastOperationMode = this.getCurrentOperationMode();
57
+ if (this.swing.isSupported()) {
80
58
  this.platform.log.debug(`[${this.name}] Device has SwingMode, add Characteristic`);
81
- this.service.getCharacteristic(this.platform.Characteristic.SwingMode)
59
+ this.service.getCharacteristic(Characteristic.SwingMode)
82
60
  .onGet(this.handleSwingModeGet.bind(this))
83
61
  .onSet(this.handleSwingModeSet.bind(this));
84
62
  }
@@ -87,82 +65,43 @@ class ClimateControlService {
87
65
  // Set up the optional standalone Fan service (fan speed + oscillation tile)
88
66
  this.setupSeparateFanService();
89
67
  }
90
- /**
91
- * Optional standalone Fan (Fanv2) service.
92
- *
93
- * HomeKit hides the HeaterCooler's RotationSpeed slider and SwingMode toggle
94
- * when the accessory is grouped into a single tile in the Home app — they're
95
- * only reachable by opening the device directly. Exposing a separate Fanv2
96
- * service gives the fan speed slider and oscillation toggle their own tile that
97
- * stays visible even when grouped.
98
- *
99
- * Gated behind the `showSeparateFanControl` config option, and only added when
100
- * the device actually exposes a fixed fan speed and/or a swing mode. The fan's
101
- * Active characteristic mirrors the unit on/off state, and its RotationSpeed /
102
- * SwingMode reuse the same handlers as the HeaterCooler so both services stay
103
- * in sync.
104
- */
105
- setupSeparateFanService() {
106
- if (!this.service) {
107
- return;
108
- }
109
- const subtype = 'separate_fan';
110
- const existing = this.accessory.getServiceById(this.platform.Service.Fanv2, subtype);
111
- const fanControl = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanSpeed/modes/fixed`);
112
- const hasFanSpeed = fanControl.value !== undefined;
113
- const hasSwing = this.hasSwingModeFeature();
114
- const enabled = this.platform.config.showSeparateFanControl === true;
115
- if (!enabled || (!hasFanSpeed && !hasSwing)) {
116
- if (existing) {
117
- this.platform.log.debug(`[${this.name}] Removing separate Fan service`);
118
- this.accessory.removeService(existing);
119
- }
120
- this.fanService = undefined;
121
- return;
122
- }
123
- const fanName = `${this.name} Fan`;
124
- this.platform.log.debug(`[${this.name}] Adding separate Fan service`);
125
- this.fanService = existing || this.accessory.addService(this.platform.Service.Fanv2, fanName, subtype);
126
- this.fanService.setCharacteristic(this.platform.Characteristic.Name, fanName);
127
- this.fanService.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
128
- this.fanService.setCharacteristic(this.platform.Characteristic.ConfiguredName, fanName);
129
- // Active mirrors the unit on/off (same handlers as the HeaterCooler Active).
130
- this.fanService.getCharacteristic(this.platform.Characteristic.Active)
131
- .onGet(this.handleActiveStateGet.bind(this))
132
- .onSet(this.handleActiveStateSet.bind(this));
133
- if (hasFanSpeed) {
134
- this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed)
135
- .setProps({
136
- minStep: percentStep(fanControl.maxValue),
137
- minValue: 0,
138
- maxValue: 100,
139
- })
140
- .onGet(this.handleRotationSpeedGet.bind(this))
141
- .onSet(this.handleRotationSpeedSet.bind(this));
142
- }
143
- else {
144
- this.fanService.removeCharacteristic(this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed));
145
- }
146
- if (hasSwing) {
147
- this.fanService.getCharacteristic(this.platform.Characteristic.SwingMode)
148
- .onGet(this.handleSwingModeGet.bind(this))
149
- .onSet(this.handleSwingModeSet.bind(this));
150
- }
68
+ get device() {
69
+ return this.accessory.context.device;
70
+ }
71
+ getData(dataPoint, path) {
72
+ return this.device.getData(this.managementPointId, dataPoint, path);
73
+ }
74
+ setData(dataPoint, pathOrValue, value) {
75
+ return this.device.setData(this.managementPointId, dataPoint, pathOrValue, value);
76
+ }
77
+ write(characteristic, op) {
78
+ return (0, hap_write_1.withHapWrite)(this.platform, `[${this.name}] ${characteristic}`, op);
151
79
  }
152
80
  /**
153
- * Execute a device write operation with proper HAP error handling.
154
- * Catches errors, logs a warning, and throws HapStatusError(SERVICE_COMMUNICATION_FAILURE)
155
- * so HomeKit shows "No Response" instead of Homebridge logging "plugin threw an error".
81
+ * Threshold characteristics. getData() always returns a wrapper, so these are
82
+ * always present; the props come from the device's setpoint constraints.
156
83
  */
157
- async setDeviceData(characteristic, operation) {
158
- try {
159
- await operation();
160
- this.platform.forceUpdateDevices();
161
- }
162
- catch (e) {
163
- this.platform.log.warn(`[${this.name}] Failed to set ${characteristic}: ${e instanceof Error ? e.message : e}`);
164
- throw new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
165
- }
84
+ setupThreshold(characteristic, operationMode, fallback, clampMin, clampMax, onGet, onSet) {
85
+ const setpoint = this.setpoints.read(operationMode);
86
+ const char = this.service.getCharacteristic(characteristic);
87
+ // Set value within default HomeKit range first to avoid warning when setProps narrows the range
88
+ const value = typeof setpoint.value === 'number' ? setpoint.value : fallback;
89
+ char.updateValue(Math.max(clampMin, Math.min(clampMax, value)));
90
+ char
91
+ .setProps({ minStep: setpoint.stepValue, minValue: setpoint.minValue, maxValue: setpoint.maxValue })
92
+ .onGet(onGet.bind(this))
93
+ .onSet(onSet.bind(this));
94
+ }
95
+ setupSeparateFanService() {
96
+ const fixed = this.fanSpeed.fixed();
97
+ this.separateFan.setup({ hasFanSpeed: fixed.value !== undefined, fanSpeedMax: fixed.maxValue, hasSwing: this.swing.isSupported() }, {
98
+ getActive: this.handleActiveStateGet.bind(this),
99
+ setActive: this.handleActiveStateSet.bind(this),
100
+ getRotationSpeed: this.handleRotationSpeedGet.bind(this),
101
+ setRotationSpeed: this.handleRotationSpeedSet.bind(this),
102
+ getSwing: this.handleSwingModeGet.bind(this),
103
+ setSwing: this.handleSwingModeSet.bind(this),
104
+ });
166
105
  }
167
106
  /**
168
107
  * Push current device state to all HAP characteristics via updateValue().
@@ -171,91 +110,57 @@ class ClimateControlService {
171
110
  * scene commands before they ever reach onSet.
172
111
  */
173
112
  refreshValues() {
174
- if (!this.service) {
175
- return;
176
- }
113
+ const { Characteristic } = this.platform;
177
114
  try {
178
- const onOff = this.accessory.context.device.getData(this.managementPointId, 'onOffMode', undefined).value;
115
+ const isOn = this.getData('onOffMode').value === types_1.DaikinOnOffModes.ON;
179
116
  const operationMode = this.getCurrentOperationMode();
180
- const isOn = onOff === types_1.DaikinOnOffModes.ON;
181
- this.service.getCharacteristic(this.platform.Characteristic.Active)
182
- .updateValue(isOn ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE);
183
- this.fanService?.getCharacteristic(this.platform.Characteristic.Active)
184
- .updateValue(isOn ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE);
185
- let currentState;
117
+ // The fixed fan speed range (or its existence) depends on the operation
118
+ // mode, so re-evaluate the RotationSpeed characteristic only when it changes.
119
+ if (operationMode !== this.lastOperationMode) {
120
+ this.lastOperationMode = operationMode;
121
+ this.addOrUpdateCharacteristicRotationSpeed();
122
+ }
123
+ this.service.getCharacteristic(Characteristic.Active)
124
+ .updateValue(isOn ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE);
125
+ this.separateFan.updateActive(isOn);
126
+ let currentState = Characteristic.CurrentHeaterCoolerState.IDLE;
186
127
  if (!isOn) {
187
- currentState = this.platform.Characteristic.CurrentHeaterCoolerState.INACTIVE;
128
+ currentState = Characteristic.CurrentHeaterCoolerState.INACTIVE;
188
129
  }
189
130
  else if (operationMode === types_1.DaikinOperationModes.COOLING) {
190
- currentState = this.platform.Characteristic.CurrentHeaterCoolerState.COOLING;
131
+ currentState = Characteristic.CurrentHeaterCoolerState.COOLING;
191
132
  }
192
133
  else if (operationMode === types_1.DaikinOperationModes.HEATING) {
193
- currentState = this.platform.Characteristic.CurrentHeaterCoolerState.HEATING;
194
- }
195
- else {
196
- currentState = this.platform.Characteristic.CurrentHeaterCoolerState.IDLE;
197
- }
198
- this.service.getCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState)
199
- .updateValue(currentState);
200
- let targetState;
201
- switch (operationMode) {
202
- case types_1.DaikinOperationModes.COOLING:
203
- targetState = this.platform.Characteristic.TargetHeaterCoolerState.COOL;
204
- break;
205
- case types_1.DaikinOperationModes.HEATING:
206
- targetState = this.platform.Characteristic.TargetHeaterCoolerState.HEAT;
207
- break;
208
- default:
209
- targetState = this.platform.Characteristic.TargetHeaterCoolerState.AUTO;
134
+ currentState = Characteristic.CurrentHeaterCoolerState.HEATING;
210
135
  }
211
- this.service.getCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState)
212
- .updateValue(targetState);
213
- const temperature = this.accessory.context.device.getData(this.managementPointId, 'sensoryData', '/' + this.getCurrentControlMode()).value;
214
- this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
215
- .updateValue(typeof temperature === 'number' && isFinite(temperature) ? temperature : constants_1.DEFAULT_ROOM_TEMPERATURE);
216
- const coolingTemp = this.accessory.context.device.getData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.COOLING}/setpoints/${this.getSetpoint(types_1.DaikinOperationModes.COOLING)}`).value;
136
+ this.service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).updateValue(currentState);
137
+ this.service.getCharacteristic(Characteristic.TargetHeaterCoolerState).updateValue(this.targetHeaterCoolerState(operationMode));
138
+ this.service.getCharacteristic(Characteristic.CurrentTemperature).updateValue(this.currentTemperature());
139
+ const coolingTemp = this.setpoints.read(types_1.DaikinOperationModes.COOLING).value;
217
140
  if (coolingTemp !== undefined) {
218
- this.service.getCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature)
219
- .updateValue(typeof coolingTemp === 'number' && isFinite(coolingTemp) ? coolingTemp : 25);
141
+ this.service.getCharacteristic(Characteristic.CoolingThresholdTemperature)
142
+ .updateValue(finiteOr(coolingTemp, DEFAULT_COOLING_TEMPERATURE));
220
143
  }
221
- const heatingTemp = this.accessory.context.device.getData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.HEATING}/setpoints/${this.getSetpoint(types_1.DaikinOperationModes.HEATING)}`).value;
144
+ const heatingTemp = this.setpoints.read(types_1.DaikinOperationModes.HEATING).value;
222
145
  if (heatingTemp !== undefined) {
223
- this.service.getCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature)
224
- .updateValue(typeof heatingTemp === 'number' && isFinite(heatingTemp) ? heatingTemp : constants_1.DEFAULT_ROOM_TEMPERATURE);
146
+ this.service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
147
+ .updateValue(finiteOr(heatingTemp, constants_1.DEFAULT_ROOM_TEMPERATURE));
225
148
  }
226
- // Only push RotationSpeed to HomeKit when the device is in 'fixed' fan mode.
227
- // When in 'auto' or 'quiet' mode, the stored `fixed` value doesn't represent
228
- // the actual fan speed — it's just a fallback for when the user switches back.
229
- // Pushing it would seed the HomeKit cache with a misleading value, which the
230
- // home hub may then replay (as a "cache verification") when another characteristic
231
- // on this service is changed — accidentally switching the device out of auto/quiet
232
- // mode. The onSet guard in handleRotationSpeedSet catches the replay, but keeping
233
- // the cache accurate from the start prevents the scenario entirely.
234
- const fanSpeedCurrentMode = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanSpeed/currentMode`);
235
- if (fanSpeedCurrentMode.value === types_1.DaikinFanSpeedModes.FIXED) {
236
- const fanSpeedData = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanSpeed/modes/fixed`);
237
- if (fanSpeedData.value !== undefined) {
238
- const percent = deviceSpeedToPercent(fanSpeedData.value, fanSpeedData.maxValue);
239
- this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed)
240
- .updateValue(percent);
241
- if (this.fanService?.testCharacteristic(this.platform.Characteristic.RotationSpeed)) {
242
- this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed).updateValue(percent);
243
- }
244
- }
149
+ // Only push RotationSpeed when the device is in 'fixed' fan mode. In 'auto'
150
+ // or 'quiet' the stored `fixed` value doesn't represent the actual speed;
151
+ // pushing it would seed the HomeKit cache with a value the home hub may
152
+ // replay (as a "cache verification") when another characteristic changes,
153
+ // accidentally switching the device out of auto/quiet. handleRotationSpeedSet
154
+ // guards against that replay too, but an accurate cache prevents it entirely.
155
+ const percent = this.fanSpeed.percentIfFixed(operationMode);
156
+ if (percent !== undefined) {
157
+ this.service.getCharacteristic(Characteristic.RotationSpeed).updateValue(percent);
158
+ this.separateFan.updateRotationSpeed(percent);
245
159
  }
246
- if (this.hasSwingModeFeature()) {
247
- const verticalSwingMode = this.hasSwingModeVerticalFeature()
248
- ? this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanDirection/vertical/currentMode`).value
249
- : null;
250
- const horizontalSwingMode = this.hasSwingModeHorizontalFeature()
251
- ? this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanDirection/horizontal/currentMode`).value
252
- : null;
253
- const swingEnabled = horizontalSwingMode !== types_1.DaikinFanDirectionHorizontalModes.STOP
254
- && verticalSwingMode !== types_1.DaikinFanDirectionVerticalModes.STOP;
255
- this.service.getCharacteristic(this.platform.Characteristic.SwingMode)
256
- .updateValue(swingEnabled ? this.platform.Characteristic.SwingMode.SWING_ENABLED : this.platform.Characteristic.SwingMode.SWING_DISABLED);
257
- this.fanService?.getCharacteristic(this.platform.Characteristic.SwingMode)
258
- .updateValue(swingEnabled ? this.platform.Characteristic.SwingMode.SWING_ENABLED : this.platform.Characteristic.SwingMode.SWING_DISABLED);
160
+ if (this.swing.isSupported()) {
161
+ const swingMode = this.swing.isSwinging() ? Characteristic.SwingMode.SWING_ENABLED : Characteristic.SwingMode.SWING_DISABLED;
162
+ this.service.getCharacteristic(Characteristic.SwingMode).updateValue(swingMode);
163
+ this.separateFan.updateSwing(swingMode);
259
164
  }
260
165
  // Push feature switches (PowerfulMode, EconoMode, etc.) so toggling
261
166
  // these from the Daikin app reaches HomeKit on the next WebSocket update
@@ -263,417 +168,181 @@ class ClimateControlService {
263
168
  this.featureManager.refreshAll();
264
169
  }
265
170
  catch (e) {
266
- this.platform.log.debug(`[${this.name}] refreshValues error: ${e instanceof Error ? e.message : e}`);
171
+ this.platform.log.debug(`[${this.name}] refreshValues error: ${(0, errors_1.toMessage)(e)}`);
267
172
  }
268
173
  }
174
+ /**
175
+ * Add/refresh RotationSpeed for the current operation mode's fixed fan speed,
176
+ * or remove it when that mode has none (getData() returns `{ value: undefined }`
177
+ * for a missing path; setting it would fail at the API).
178
+ *
179
+ * The characteristic stays in percentage space (see fan-speed.ts). setProps
180
+ * runs FIRST to widen any narrow range left over from a prior session, or the
181
+ * updateValue below trips HAP's validateUserInput; minValue=0 (not
182
+ * stepPercent) keeps HAP happy for cached values below one step —
183
+ * percentToDeviceSpeed clamps writes up to the device minValue anyway.
184
+ */
269
185
  addOrUpdateCharacteristicRotationSpeed() {
270
- if (!this.service) {
271
- throw Error('Service not initialized');
272
- }
273
- // getData() returns { value: undefined } when the path is missing — checking the
274
- // wrapper for truthiness always succeeds, so the characteristic was being added
275
- // (with undefined min/max/step) even for devices that don't support a fixed fan
276
- // speed in the current operation mode. Setting it would then fail at the API.
277
- const fanControl = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanSpeed/modes/fixed`);
278
- if (fanControl.value !== undefined) {
279
- // Daikin units expose a small integer fan-speed scale (typically 1-5 or 1-3).
280
- // iOS Home renders RotationSpeed as a 0-100% slider regardless of setProps,
281
- // so we keep the characteristic in percentage space and map both directions:
282
- // device 5/5 → HomeKit 100% (full bar), device 1/5 → 20%, etc.
283
- // minStep = 100/maxValue gives the discrete positions that round-trip cleanly.
284
- //
285
- // Order matters: setProps FIRST to widen any narrow range left over from a
286
- // prior session (e.g. an old build that capped at maxValue=5). Otherwise the
287
- // updateValue below trips HAP's validateUserInput. minValue=0 (not stepPercent)
288
- // keeps HAP happy when the cached value is below stepPercent — the slider's
289
- // 0 position is harmless since percentToDeviceSpeed clamps writes up to the
290
- // device minValue anyway.
291
- const rotationChar = this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed);
292
- const stepPercent = percentStep(fanControl.maxValue);
293
- const percent = deviceSpeedToPercent(fanControl.value, fanControl.maxValue);
294
- rotationChar
295
- .setProps({
296
- minStep: stepPercent,
297
- minValue: 0,
298
- maxValue: 100,
299
- })
300
- .onGet(this.handleRotationSpeedGet.bind(this))
301
- .onSet(this.handleRotationSpeedSet.bind(this));
302
- // Only seed the HomeKit cache with the stored fixed percentage when the
303
- // device is actually in 'fixed' fan mode. During construction this runs
304
- // before refreshValues() — without this guard, the initial seed would
305
- // push a misleading value (e.g. 100% when the device is in auto mode)
306
- // that the home hub may later replay as a "cache verification" write.
307
- const operationMode = this.getCurrentOperationMode();
308
- const currentMode = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanSpeed/currentMode`);
309
- if (currentMode.value === types_1.DaikinFanSpeedModes.FIXED) {
310
- rotationChar.updateValue(percent);
311
- }
186
+ const { RotationSpeed } = this.platform.Characteristic;
187
+ const fanControl = this.fanSpeed.fixed();
188
+ if (fanControl.value === undefined) {
189
+ this.service.removeCharacteristic(this.service.getCharacteristic(RotationSpeed));
190
+ return;
312
191
  }
313
- else {
314
- this.service.removeCharacteristic(this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed));
192
+ const rotationChar = this.service.getCharacteristic(RotationSpeed);
193
+ rotationChar
194
+ .setProps({ minStep: (0, fan_speed_1.percentStep)(fanControl.maxValue), minValue: 0, maxValue: 100 })
195
+ .onGet(this.handleRotationSpeedGet.bind(this))
196
+ .onSet(this.handleRotationSpeedSet.bind(this));
197
+ // Only seed the cache in 'fixed' fan mode — during construction this runs
198
+ // before refreshValues(), and a misleading seed (e.g. 100% while in auto)
199
+ // may later be replayed by the home hub as a "cache verification" write.
200
+ const percent = this.fanSpeed.percentIfFixed();
201
+ if (percent !== undefined) {
202
+ rotationChar.updateValue(percent);
315
203
  }
316
204
  }
317
205
  async handleActiveStateGet() {
318
- const state = this.accessory.context.device.getData(this.managementPointId, 'onOffMode', undefined).value;
319
- this.platform.log.debug(`[${this.name}] GET ActiveState, state: ${state}, last update: ${this.accessory.context.device.getLastUpdated()}`);
206
+ const state = this.getData('onOffMode').value;
207
+ this.platform.log.debug(`[${this.name}] GET ActiveState, state: ${state}, last update: ${this.device.getLastUpdated()}`);
320
208
  return state === types_1.DaikinOnOffModes.ON;
321
209
  }
322
210
  async handleActiveStateSet(value) {
323
211
  // HAP sends Active as 0 (INACTIVE) or 1 (ACTIVE), not a boolean
324
212
  const desired = value === this.platform.Characteristic.Active.ACTIVE;
325
- const current = this.accessory.context.device.getData(this.managementPointId, 'onOffMode', undefined).value;
326
- if ((current === types_1.DaikinOnOffModes.ON) === desired) {
213
+ if ((this.getData('onOffMode').value === types_1.DaikinOnOffModes.ON) === desired) {
327
214
  this.platform.log.debug(`[${this.name}] SET ActiveState skipped — already ${desired ? 'on' : 'off'}`);
328
215
  return;
329
216
  }
330
217
  this.platform.log.debug(`[${this.name}] SET ActiveState, state: ${value}`);
331
- await this.setDeviceData('ActiveState', async () => {
332
- await this.accessory.context.device.setData(this.managementPointId, 'onOffMode', desired ? types_1.DaikinOnOffModes.ON : types_1.DaikinOnOffModes.OFF, undefined);
333
- });
218
+ await this.write('ActiveState', () => this.setData('onOffMode', desired ? types_1.DaikinOnOffModes.ON : types_1.DaikinOnOffModes.OFF, undefined));
334
219
  }
335
220
  async handleCurrentTemperatureGet() {
336
- const temperature = this.accessory.context.device.getData(this.managementPointId, 'sensoryData', '/' + this.getCurrentControlMode()).value;
337
- const lastUpdate = this.accessory.context.device.getLastUpdated();
338
- this.platform.log.debug(`[${this.name}] GET CurrentTemperature, temperature: ${temperature}, last update: ${lastUpdate}`);
339
- // Return a valid temperature value, defaulting to 20 if undefined
340
- return typeof temperature === 'number' && isFinite(temperature) ? temperature : constants_1.DEFAULT_ROOM_TEMPERATURE;
221
+ const temperature = this.currentTemperature();
222
+ this.platform.log.debug(`[${this.name}] GET CurrentTemperature, temperature: ${temperature}, last update: ${this.device.getLastUpdated()}`);
223
+ return temperature;
224
+ }
225
+ currentTemperature() {
226
+ return finiteOr(this.getData('sensoryData', '/' + this.getCurrentControlMode()).value, constants_1.DEFAULT_ROOM_TEMPERATURE);
341
227
  }
342
228
  async handleCoolingThresholdTemperatureGet() {
343
- const setpoint = this.getSetpoint(types_1.DaikinOperationModes.COOLING);
344
- const path = `/operationModes/${types_1.DaikinOperationModes.COOLING}/setpoints/${setpoint}`;
345
- const temperature = this.accessory.context.device.getData(this.managementPointId, 'temperatureControl', path).value;
346
- const lastUpdate = this.accessory.context.device.getLastUpdated();
347
- this.platform.log.debug(`[${this.name}] GET CoolingThresholdTemperature, temperature: ${temperature}, last update: ${lastUpdate}`);
348
- return typeof temperature === 'number' && isFinite(temperature) ? temperature : 25;
229
+ return this.thresholdGet('cooling', DEFAULT_COOLING_TEMPERATURE);
349
230
  }
350
231
  async handleCoolingThresholdTemperatureSet(value) {
232
+ await this.thresholdSet('cooling', value);
233
+ }
234
+ async handleHeatingThresholdTemperatureGet() {
235
+ return this.thresholdGet('heating', constants_1.DEFAULT_ROOM_TEMPERATURE);
236
+ }
237
+ async handleHeatingThresholdTemperatureSet(value) {
238
+ await this.thresholdSet('heating', value);
239
+ }
240
+ thresholdGet(kind, fallback) {
241
+ const temperature = this.setpoints.read(thresholdMode(kind)).value;
242
+ const label = kind === 'cooling' ? 'CoolingThresholdTemperature' : 'HeatingThresholdTemperature';
243
+ this.platform.log.debug(`[${this.name}] GET ${label}, temperature: ${temperature}, last update: ${this.device.getLastUpdated()}`);
244
+ return finiteOr(temperature, fallback);
245
+ }
246
+ /**
247
+ * Write a threshold, then mirror the heating/cooling midpoint to Daikin's
248
+ * single AUTO setpoint — once, after concurrent threshold writes settle
249
+ * (see AutoSetpointSync).
250
+ */
251
+ async thresholdSet(kind, value) {
351
252
  const temperature = Math.round(value * 2) / 2;
352
- this.platform.log.debug(`[${this.name}] SET CoolingThresholdTemperature, temperature to: ${temperature}`);
353
- await this.setDeviceData('CoolingThresholdTemperature', async () => {
354
- await this.accessory.context.device.setData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.COOLING}/setpoints/${this.getSetpoint(types_1.DaikinOperationModes.COOLING)}`, temperature);
355
- await this.syncAutoSetpointIfSupported({ cooling: temperature });
356
- });
253
+ const label = kind === 'cooling' ? 'CoolingThresholdTemperature' : 'HeatingThresholdTemperature';
254
+ const operationMode = thresholdMode(kind);
255
+ if (this.setpoints.read(operationMode).value === temperature) {
256
+ this.platform.log.debug(`[${this.name}] SET ${label} skipped — already ${temperature}`);
257
+ return;
258
+ }
259
+ this.platform.log.debug(`[${this.name}] SET ${label}, temperature to: ${temperature}`);
260
+ await this.write(label, () => this.autoSetpointSync.writeThreshold(kind, temperature, () => this.setData('temperatureControl', this.setpoints.path(operationMode), temperature)));
357
261
  }
358
262
  async handleRotationSpeedGet() {
359
- const fanSpeedData = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanSpeed/modes/fixed`);
360
- const speed = fanSpeedData.value;
361
- const percent = typeof speed === 'number' && isFinite(speed)
362
- ? deviceSpeedToPercent(speed, fanSpeedData.maxValue)
363
- : percentStep(fanSpeedData.maxValue);
364
- this.platform.log.debug(`[${this.name}] GET RotationSpeed, device speed: ${speed} → ${percent}%, ` +
365
- `last update: ${this.accessory.context.device.getLastUpdated()}`);
263
+ const { speed, percent } = this.fanSpeed.read();
264
+ this.platform.log.debug(`[${this.name}] GET RotationSpeed, device speed: ${speed} → ${percent}%, last update: ${this.device.getLastUpdated()}`);
366
265
  return percent;
367
266
  }
368
267
  async handleRotationSpeedSet(value) {
369
- const operationMode = this.getCurrentOperationMode();
370
- // Skip if the current operation mode doesn't support a fixed fan speed.
371
- // Without this check, we'd PATCH a non-existent path and the Daikin API would
372
- // respond with a 422 (visible to the user as "No Response" in HomeKit).
373
- const fixedFanSpeed = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanSpeed/modes/fixed`);
374
- if (fixedFanSpeed.value === undefined) {
375
- this.platform.log.debug(`[${this.name}] SET RotationSpeed skipped — operationMode ${operationMode} does not support fixed fan speed`);
376
- return;
377
- }
378
- const deviceSpeed = percentToDeviceSpeed(value, fixedFanSpeed.minValue, fixedFanSpeed.maxValue);
379
- this.platform.log.debug(`[${this.name}] SET RotationSpeed, ${value}% → device speed ${deviceSpeed}`);
380
- const currentMode = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanSpeed/currentMode`);
381
- // Guard: skip when the device is in auto or quiet fan mode and the incoming
382
- // speed matches the stored fixed value. HomeKit may replay the cached
383
- // RotationSpeed when another characteristic (e.g. temperature setpoint)
384
- // changes on the same service. Without this guard, a simple temperature
385
- // adjustment would accidentally switch the fan from auto/quiet to fixed mode
386
- // at the stored speed (often 5/5).
387
- if (currentMode.value !== undefined &&
388
- currentMode.value !== types_1.DaikinFanSpeedModes.FIXED &&
389
- deviceSpeed === fixedFanSpeed.value) {
390
- this.platform.log.debug(`[${this.name}] SET RotationSpeed skipped — device is in "${currentMode.value}" mode ` +
391
- `and speed ${deviceSpeed} already matches the stored fixed value. ` +
392
- 'This is likely a HomeKit cache replay, not a user fan-speed change.');
268
+ const plan = this.fanSpeed.plan(value);
269
+ if ('skip' in plan) {
270
+ this.platform.log.debug(`[${this.name}] SET RotationSpeed skipped — ${plan.skip}`);
393
271
  return;
394
272
  }
395
- const allowedModes = (currentMode.values ?? []);
396
- await this.setDeviceData('RotationSpeed', async () => {
397
- // Only switch currentMode to 'fixed' when needed (and when supported). Some
398
- // operation modes (e.g. dry on some units) restrict currentMode to 'auto' only.
399
- if (currentMode.value !== types_1.DaikinFanSpeedModes.FIXED && allowedModes.includes(types_1.DaikinFanSpeedModes.FIXED)) {
400
- await this.accessory.context.device.setData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanSpeed/currentMode`, types_1.DaikinFanSpeedModes.FIXED);
273
+ this.platform.log.debug(`[${this.name}] SET RotationSpeed, ${value}% → device speed ${plan.deviceSpeed}`);
274
+ await this.write('RotationSpeed', async () => {
275
+ for (const { path, value: speedValue } of plan.writes) {
276
+ await this.setData('fanControl', path, speedValue);
401
277
  }
402
- await this.accessory.context.device.setData(this.managementPointId, 'fanControl', `/operationModes/${operationMode}/fanSpeed/modes/fixed`, deviceSpeed);
403
278
  });
404
- // Moving the slider flips fanSpeed/currentMode to 'fixed', which means any
405
- // fan-mode switches (Auto fan mode, Indoor silent) are now off. Push their
406
- // state immediately so HomeKit reflects it without waiting for the next poll
407
- // — setData has already updated the in-memory cache optimistically.
279
+ // Moving the slider flips fanSpeed/currentMode to 'fixed', so the fan-mode
280
+ // switches (Auto fan mode, Indoor silent) are now off. Push their state now —
281
+ // setData has already updated the in-memory cache optimistically.
408
282
  this.featureManager.refreshAll();
409
283
  }
410
- async handleHeatingThresholdTemperatureGet() {
411
- const setpoint = this.getSetpoint(types_1.DaikinOperationModes.HEATING);
412
- const path = `/operationModes/${types_1.DaikinOperationModes.HEATING}/setpoints/${setpoint}`;
413
- const temperature = this.accessory.context.device.getData(this.managementPointId, 'temperatureControl', path).value;
414
- const lastUpdate = this.accessory.context.device.getLastUpdated();
415
- this.platform.log.debug(`[${this.name}] GET HeatingThresholdTemperature, temperature: ${temperature}, last update: ${lastUpdate}`);
416
- return typeof temperature === 'number' && isFinite(temperature) ? temperature : constants_1.DEFAULT_ROOM_TEMPERATURE;
417
- }
418
- async handleHeatingThresholdTemperatureSet(value) {
419
- const temperature = Math.round(value * 2) / 2;
420
- this.platform.log.debug(`[${this.name}] SET HeatingThresholdTemperature, temperature to: ${temperature}`);
421
- await this.setDeviceData('HeatingThresholdTemperature', async () => {
422
- await this.accessory.context.device.setData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.HEATING}/setpoints/${this.getSetpoint(types_1.DaikinOperationModes.HEATING)}`, temperature);
423
- await this.syncAutoSetpointIfSupported({ heating: temperature });
424
- });
425
- }
426
- /**
427
- * Daikin's auto operationMode uses a SINGLE setpoint, not a range like
428
- * HomeKit's HeaterCooler (heating threshold + cooling threshold). Without
429
- * this sync the Daikin app keeps showing whatever auto setpoint was there
430
- * when the device was last in auto mode, regardless of what the user picks
431
- * in HomeKit. Mirror the midpoint of the current heating/cooling thresholds
432
- * to /operationModes/auto/setpoints/roomTemperature whenever either
433
- * threshold is set, so the Daikin app and HomeKit stay in sync.
434
- *
435
- * No-op when the device doesn't expose an auto setpoint (e.g. devices
436
- * without an auto operationMode at all).
437
- */
438
- async syncAutoSetpointIfSupported(overrides = {}) {
439
- // Best-effort. Failures here (e.g. getSetpoint throwing for Altherma's
440
- // weatherDependentHeatingFixedCooling + leavingWaterTemperature combo, or
441
- // any PATCH error) must not propagate — the primary heating/cooling write
442
- // already succeeded by this point, and a missing auto-sync just leaves
443
- // the Daikin app showing a slightly stale auto setpoint.
444
- try {
445
- const autoSetpointKey = this.getSetpoint(types_1.DaikinOperationModes.AUTO);
446
- const autoPath = `/operationModes/${types_1.DaikinOperationModes.AUTO}/setpoints/${autoSetpointKey}`;
447
- const autoData = this.accessory.context.device.getData(this.managementPointId, 'temperatureControl', autoPath);
448
- if (autoData.value === undefined) {
449
- return;
450
- }
451
- const heatingKey = this.getSetpoint(types_1.DaikinOperationModes.HEATING);
452
- const coolingKey = this.getSetpoint(types_1.DaikinOperationModes.COOLING);
453
- const heating = overrides.heating ?? this.accessory.context.device.getData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.HEATING}/setpoints/${heatingKey}`).value;
454
- const cooling = overrides.cooling ?? this.accessory.context.device.getData(this.managementPointId, 'temperatureControl', `/operationModes/${types_1.DaikinOperationModes.COOLING}/setpoints/${coolingKey}`).value;
455
- if (typeof heating !== 'number' || typeof cooling !== 'number') {
456
- return;
457
- }
458
- // Midpoint rounded to nearest 0.5° — matches the step used by the
459
- // heating/cooling setters above, and the stepValue Daikin returns.
460
- let midpoint = Math.round(heating + cooling) / 2;
461
- if (typeof autoData.minValue === 'number') {
462
- midpoint = Math.max(autoData.minValue, midpoint);
463
- }
464
- if (typeof autoData.maxValue === 'number') {
465
- midpoint = Math.min(autoData.maxValue, midpoint);
466
- }
467
- if (typeof autoData.value === 'number' && Math.abs(autoData.value - midpoint) < 0.01) {
468
- return;
469
- }
470
- this.platform.log.debug(`[${this.name}] SYNC AutoSetpoint, heating=${heating} cooling=${cooling} → auto=${midpoint}`);
471
- await this.accessory.context.device.setData(this.managementPointId, 'temperatureControl', autoPath, midpoint);
472
- }
473
- catch (e) {
474
- this.platform.log.debug(`[${this.name}] AutoSetpoint sync skipped: ${e instanceof Error ? e.message : e}`);
475
- }
476
- }
477
284
  async handleTargetHeaterCoolerStateGet() {
478
285
  const operationMode = this.getCurrentOperationMode();
479
- const lastUpdate = this.accessory.context.device.getLastUpdated();
480
- this.platform.log.debug(`[${this.name}] GET TargetHeaterCoolerState, operationMode: ${operationMode}, last update: ${lastUpdate}`);
286
+ this.platform.log.debug(`[${this.name}] GET TargetHeaterCoolerState, operationMode: ${operationMode}, last update: ${this.device.getLastUpdated()}`);
287
+ return this.targetHeaterCoolerState(operationMode);
288
+ }
289
+ targetHeaterCoolerState(operationMode) {
290
+ const { TargetHeaterCoolerState } = this.platform.Characteristic;
481
291
  switch (operationMode) {
482
292
  case types_1.DaikinOperationModes.COOLING:
483
- return this.platform.Characteristic.TargetHeaterCoolerState.COOL;
293
+ return TargetHeaterCoolerState.COOL;
484
294
  case types_1.DaikinOperationModes.HEATING:
485
- return this.platform.Characteristic.TargetHeaterCoolerState.HEAT;
486
- case types_1.DaikinOperationModes.DRY:
487
- this.addOrUpdateCharacteristicRotationSpeed();
488
- return this.platform.Characteristic.TargetHeaterCoolerState.AUTO;
295
+ return TargetHeaterCoolerState.HEAT;
489
296
  default:
490
- return this.platform.Characteristic.TargetHeaterCoolerState.AUTO;
297
+ return TargetHeaterCoolerState.AUTO;
491
298
  }
492
299
  }
493
300
  async handleTargetHeaterCoolerStateSet(value) {
494
- const operationMode = value;
301
+ const { TargetHeaterCoolerState } = this.platform.Characteristic;
495
302
  this.platform.log.debug(`[${this.name}] SET TargetHeaterCoolerState, OperationMode to: ${value}`);
496
- let daikinOperationMode = types_1.DaikinOperationModes.COOLING;
497
- switch (operationMode) {
498
- case this.platform.Characteristic.TargetHeaterCoolerState.COOL:
499
- daikinOperationMode = types_1.DaikinOperationModes.COOLING;
500
- break;
501
- case this.platform.Characteristic.TargetHeaterCoolerState.HEAT:
502
- daikinOperationMode = types_1.DaikinOperationModes.HEATING;
503
- break;
504
- case this.platform.Characteristic.TargetHeaterCoolerState.AUTO:
505
- daikinOperationMode = types_1.DaikinOperationModes.AUTO;
506
- break;
303
+ const daikinOperationMode = value === TargetHeaterCoolerState.HEAT ? types_1.DaikinOperationModes.HEATING
304
+ : value === TargetHeaterCoolerState.AUTO ? types_1.DaikinOperationModes.AUTO
305
+ : types_1.DaikinOperationModes.COOLING;
306
+ // Compared against the Daikin mode, not the HomeKit state: AUTO while the
307
+ // unit is in dry/fanOnly (which read back as AUTO) must still switch to auto.
308
+ if (this.getCurrentOperationMode() === daikinOperationMode) {
309
+ this.platform.log.debug(`[${this.name}] SET TargetHeaterCoolerState skipped — already ${daikinOperationMode}`);
310
+ return;
507
311
  }
508
312
  this.platform.log.debug(`[${this.name}] SET TargetHeaterCoolerState, daikinOperationMode to: ${daikinOperationMode}`);
509
- await this.setDeviceData('TargetHeaterCoolerState', async () => {
510
- await this.accessory.context.device.setData(this.managementPointId, 'operationMode', daikinOperationMode, undefined);
511
- // Note: onOffMode is intentionally NOT set here — the Active characteristic
512
- // exclusively controls on/off. iOS always sends Active=1 alongside a mode
513
- // change, so forcing onOffMode=ON here races against a concurrent Active=0
514
- // (e.g. a "turn off" scene) and can leave devices ON.
515
- });
313
+ // Note: onOffMode is intentionally NOT set here — the Active characteristic
314
+ // exclusively controls on/off. iOS always sends Active=1 alongside a mode
315
+ // change, so forcing onOffMode=ON here races against a concurrent Active=0
316
+ // (e.g. a "turn off" scene) and can leave devices ON.
317
+ await this.write('TargetHeaterCoolerState', () => this.setData('operationMode', daikinOperationMode, undefined));
516
318
  }
517
319
  async handleSwingModeSet(value) {
518
- const swingMode = value;
519
- const daikinSwingMode = swingMode === 1 ? types_1.DaikinFanDirectionHorizontalModes.SWING : types_1.DaikinFanDirectionHorizontalModes.STOP;
520
- this.platform.log.debug(`[${this.name}] SET SwingMode, swingmode to: ${swingMode}/${daikinSwingMode}`);
521
- await this.setDeviceData('SwingMode', async () => {
522
- if (this.hasSwingModeHorizontalFeature()) {
523
- await this.accessory.context.device.setData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanDirection/horizontal/currentMode`, daikinSwingMode);
524
- }
525
- if (this.hasSwingModeVerticalFeature()) {
526
- await this.accessory.context.device.setData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanDirection/vertical/currentMode`, daikinSwingMode);
527
- }
528
- });
320
+ const enabled = value === this.platform.Characteristic.SwingMode.SWING_ENABLED;
321
+ if (this.swing.axesToWrite(enabled).length === 0) {
322
+ this.platform.log.debug(`[${this.name}] SET SwingMode skipped — already ${enabled ? 'swinging' : 'stopped'}`);
323
+ return;
324
+ }
325
+ this.platform.log.debug(`[${this.name}] SET SwingMode, swingmode to: ${value}`);
326
+ await this.write('SwingMode', () => this.swing.set(enabled));
529
327
  }
530
328
  async handleSwingModeGet() {
531
- const verticalSwingMode = this.hasSwingModeVerticalFeature() ? this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanDirection/vertical/currentMode`).value : null;
532
- const horizontalSwingMode = this.hasSwingModeHorizontalFeature() ? this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanDirection/horizontal/currentMode`).value : null;
533
- const lastUpdate = this.accessory.context.device.getLastUpdated();
534
- this.platform.log.debug(`[${this.name}] GET SwingMode, verticalSwingMode: ${verticalSwingMode}, last update: ${lastUpdate}`);
535
- this.platform.log.debug(`[${this.name}] GET SwingMode, horizontalSwingMode: ${horizontalSwingMode}, last update: ${lastUpdate}`);
536
- if (horizontalSwingMode === types_1.DaikinFanDirectionHorizontalModes.STOP || verticalSwingMode === types_1.DaikinFanDirectionVerticalModes.STOP) {
537
- return this.platform.Characteristic.SwingMode.SWING_DISABLED;
538
- }
539
- return this.platform.Characteristic.SwingMode.SWING_ENABLED;
329
+ this.platform.log.debug(`[${this.name}] GET SwingMode, ${this.swing.describe()}, last update: ${this.device.getLastUpdated()}`);
330
+ return this.swing.isSwinging()
331
+ ? this.platform.Characteristic.SwingMode.SWING_ENABLED
332
+ : this.platform.Characteristic.SwingMode.SWING_DISABLED;
540
333
  }
541
334
  getCurrentOperationMode() {
542
- return this.accessory.context.device.getData(this.managementPointId, 'operationMode', undefined).value;
335
+ return this.getData('operationMode').value;
543
336
  }
544
337
  getCurrentControlMode() {
545
- const controlMode = this.accessory.context.device.getData(this.managementPointId, 'controlMode', undefined);
546
- // Only Altherma devices have a controlMode, others have a fixed controlMode of ROOM_TEMPERATURE AFAIK
547
- if (!controlMode.value) {
548
- return types_1.DaikinControlModes.ROOM_TEMPERATURE;
549
- }
550
- return controlMode.value;
551
- }
552
- getSetpointMode() {
553
- const setpointMode = this.accessory.context.device.getData(this.managementPointId, 'setpointMode', undefined);
554
- if (!setpointMode.value) {
555
- return null;
556
- }
557
- return setpointMode.value;
558
- }
559
- getSetpoint(operationMode) {
560
- // depending on the settings of the device the temperatureControl can be set in different ways "DaikinTemperatureControlSetpoints"
561
- // Docs: https://developer.cloud.daikineurope.com/docs/b0dffcaa-7b51-428a-bdff-a7c8a64195c0/supported_features
562
- // Looks like the setpointMode is the most important one to determine the setpoint,
563
- // then the controleMode and in case of weatherDependentHeatingFixedCooling also the operation mode
564
- // If the setpointMode is not available (in case on non-Althermas), we can use the controlMode to determine the setpoint
565
- const setpointMode = this.getSetpointMode();
566
- const controlMode = this.getCurrentControlMode();
567
- if (setpointMode) {
568
- switch (setpointMode) {
569
- case types_1.DaikinSetpointModes.FIXED:
570
- switch (controlMode) {
571
- case types_1.DaikinControlModes.LEAVING_WATER_TEMPERATURE:
572
- return types_1.DaikinTemperatureControlSetpoints.LEAVING_WATER_TEMPERATURE;
573
- default:
574
- return types_1.DaikinTemperatureControlSetpoints.ROOM_TEMPERATURE;
575
- }
576
- case types_1.DaikinSetpointModes.WEATHER_DEPENDENT:
577
- switch (controlMode) {
578
- case types_1.DaikinControlModes.LEAVING_WATER_TEMPERATURE:
579
- return types_1.DaikinTemperatureControlSetpoints.LEAVING_WATER_OFFSET;
580
- default:
581
- return types_1.DaikinTemperatureControlSetpoints.ROOM_TEMPERATURE;
582
- }
583
- case types_1.DaikinSetpointModes.WEATHER_DEPENDENT_HEATING_FIXED_COOLING:
584
- switch (controlMode) {
585
- case types_1.DaikinControlModes.ROOM_TEMPERATURE:
586
- return types_1.DaikinTemperatureControlSetpoints.ROOM_TEMPERATURE;
587
- case types_1.DaikinControlModes.LEAVING_WATER_TEMPERATURE:
588
- switch (operationMode) {
589
- case types_1.DaikinOperationModes.HEATING:
590
- return types_1.DaikinTemperatureControlSetpoints.LEAVING_WATER_OFFSET;
591
- case types_1.DaikinOperationModes.COOLING:
592
- return types_1.DaikinTemperatureControlSetpoints.LEAVING_WATER_TEMPERATURE;
593
- }
594
- }
595
- }
596
- throw new Error(`Could not determine the TemperatureControlSetpoint for operationMode: ${operationMode}, `
597
- + `setpointMode: ${setpointMode}, controlMode: ${controlMode}, deviceId: ${this.accessory.UUID}`);
598
- }
599
- switch (controlMode) {
600
- case types_1.DaikinControlModes.LEAVING_WATER_TEMPERATURE:
601
- return types_1.DaikinTemperatureControlSetpoints.LEAVING_WATER_OFFSET;
602
- default:
603
- return types_1.DaikinTemperatureControlSetpoints.ROOM_TEMPERATURE;
604
- }
605
- }
606
- hasSwingModeVerticalFeature() {
607
- // getData() returns { value: undefined } when the path is missing — checking the
608
- // wrapper for truthiness always succeeds. We need to confirm the value itself is set.
609
- const verticalSwing = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanDirection/vertical/currentMode`);
610
- return verticalSwing.value !== undefined;
611
- }
612
- hasSwingModeHorizontalFeature() {
613
- const horizontalSwing = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanDirection/horizontal/currentMode`);
614
- return horizontalSwing.value !== undefined;
615
- }
616
- hasSwingModeFeature() {
617
- return this.hasSwingModeVerticalFeature() || this.hasSwingModeHorizontalFeature();
618
- }
619
- hasPowerfulModeFeature() {
620
- const powerfulMode = this.accessory.context.device.getData(this.managementPointId, 'powerfulMode', undefined);
621
- this.platform.log.debug(`[${this.name}] hasPowerfulModeFeature, powerfulMode: ${Boolean(powerfulMode)}`);
622
- return Boolean(powerfulMode);
623
- }
624
- hasEconoModeFeature() {
625
- const econoMode = this.accessory.context.device.getData(this.managementPointId, 'econoMode', undefined);
626
- this.platform.log.debug(`[${this.name}] hasEconoModeFeature, econoMode: ${Boolean(econoMode)}`);
627
- return Boolean(econoMode);
628
- }
629
- hasStreamerModeFeature() {
630
- const streamerMode = this.accessory.context.device.getData(this.managementPointId, 'streamerMode', undefined);
631
- this.platform.log.debug(`[${this.name}] hasStreamerModeFeature, streamerMode: ${Boolean(streamerMode)}`);
632
- return Boolean(streamerMode);
633
- }
634
- hasOutdoorSilentModeFeature() {
635
- const OutdoorSilentMode = this.accessory.context.device.getData(this.managementPointId, 'outdoorSilentMode', undefined);
636
- this.platform.log.debug(`[${this.name}] hasOutdoorSilentModeFeature, outdoorSilentMode: ${Boolean(OutdoorSilentMode)}`);
637
- return Boolean(OutdoorSilentMode);
638
- }
639
- hasIndoorSilentModeFeature() {
640
- const currentModeFanControl = this.accessory.context.device.getData(this.managementPointId, 'fanControl', `/operationModes/${this.getCurrentOperationMode()}/fanSpeed/currentMode`);
641
- if (!currentModeFanControl) {
642
- return false;
643
- }
644
- const fanSpeedValues = currentModeFanControl.values || [];
645
- this.platform.log.debug(`[${this.name}] hasIndoorSilentModeFeature, indoorSilentMode: ${fanSpeedValues.includes(types_1.DaikinFanSpeedModes.QUIET)}`);
646
- return fanSpeedValues.includes(types_1.DaikinFanSpeedModes.QUIET);
647
- }
648
- hasOperationMode(operationMode) {
649
- const operationModeValues = this.accessory.context.device.getData(this.managementPointId, 'operationMode', undefined).values || [];
650
- this.platform.log.debug(`[${this.name}] has ${operationMode}: ${operationModeValues.includes(operationMode)}`);
651
- return operationModeValues.includes(operationMode);
652
- }
653
- hasDryOperationModeFeature() {
654
- return this.hasOperationMode(types_1.DaikinOperationModes.DRY);
655
- }
656
- hasFanOnlyOperationModeFeature() {
657
- return this.hasOperationMode(types_1.DaikinOperationModes.FAN_ONLY);
338
+ return this.setpoints.getControlMode();
658
339
  }
659
340
  }
660
341
  exports.ClimateControlService = ClimateControlService;
661
- // Daikin's fan speed scale (e.g. 1..5) → HomeKit's 0..100% percentage slider.
662
- // Pulled to module scope so refreshValues / addOrUpdate / get / set use the
663
- // same math and the same fallback for missing maxValue.
664
- const DEFAULT_FAN_MAX = 5;
665
- function percentStep(maxValue) {
666
- return 100 / (maxValue && maxValue > 0 ? maxValue : DEFAULT_FAN_MAX);
667
- }
668
- function deviceSpeedToPercent(speed, maxValue) {
669
- const max = maxValue && maxValue > 0 ? maxValue : DEFAULT_FAN_MAX;
670
- const percent = Math.round((speed / max) * 100);
671
- return Math.max(0, Math.min(100, percent));
342
+ function thresholdMode(kind) {
343
+ return kind === 'cooling' ? types_1.DaikinOperationModes.COOLING : types_1.DaikinOperationModes.HEATING;
672
344
  }
673
- function percentToDeviceSpeed(percent, minValue, maxValue) {
674
- const min = minValue && minValue > 0 ? minValue : 1;
675
- const max = maxValue && maxValue > 0 ? maxValue : DEFAULT_FAN_MAX;
676
- const raw = Math.round((percent / 100) * max);
677
- return Math.max(min, Math.min(max, raw));
345
+ function finiteOr(value, fallback) {
346
+ return typeof value === 'number' && isFinite(value) ? value : fallback;
678
347
  }
679
348
  //# sourceMappingURL=climate-control.service.js.map