homebridge-melcloud-control 4.13.2 → 4.14.0-beta.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.
package/CHANGELOG.md CHANGED
@@ -25,6 +25,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
25
25
  - For plugin < v4.6.0 use Homebridge UI <= v5.5.0
26
26
  - For plugin >= v4.6.0 use Homebridge UI >= v5.13.0
27
27
 
28
+ ## [4.14.0-beta.1] - (17.10.2026)
29
+
30
+ ### Changes
31
+
32
+ - fixed: MELCloud Home, the outdoor temperature of the report was an old value of the previous day, because the report window ended at midnight at the beginning of the current day. The report is now requested from the current day
33
+
34
+ ## [4.14.0-beta.0] - (17.10.2026)
35
+
36
+ ### Changes
37
+
38
+ - added: MELCloud Home, outdoor temperature sensor for Air Conditioner (ATA) from the trend summary report, because the status of the unit does not contain the outdoor temperature
39
+ - the sensor is created when `Outdoor Temperature Sensor` is enabled and the unit has an outdoor reading in the report, the report is read every 30 minutes
40
+ - the report is updated only while the unit is running, so the sensor keeps the last known value when the unit is off
41
+ - units without an outdoor reading, for example multi split systems, stay without the sensor
42
+
28
43
  ## [4.13.2] - (17.10.2026)
29
44
 
30
45
  ### Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.13.2",
4
+ "version": "4.14.0-beta.1",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -12,6 +12,8 @@ class MelCloudAta extends EventEmitter {
12
12
  this.restFulEnabled = account.restFul?.enable;
13
13
  this.mqttEnabled = account.mqtt?.enable;
14
14
  this.deviceId = device.id;
15
+ this.temperatureOutdoorSensor = device.temperatureOutdoorSensor || false;
16
+ this.melCloudClass = melCloudClass;
15
17
  this.defaultTempsFile = defaultTempsFile;
16
18
 
17
19
  this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
@@ -22,6 +24,8 @@ class MelCloudAta extends EventEmitter {
22
24
  //set default values
23
25
  this.deviceData = {};
24
26
  this.client = melCloudClass.client;
27
+ this.outdoorReading = null;
28
+ this.outdoorCheckedAt = 0;
25
29
 
26
30
  //handle melcloud events
27
31
  melCloudClass.on('client', (client) => {
@@ -120,8 +124,33 @@ class MelCloudAta extends EventEmitter {
120
124
  });
121
125
  }
122
126
 
127
+ // MELCloud Home does not send the outdoor temperature in the status of the unit, read it from the trend summary report
128
+ // every 30 minutes, the report is updated only while the unit is running
129
+ async checkOutdoorTemperature(deviceData) {
130
+ if (this.accountTypeMelcloud || !this.temperatureOutdoorSensor || (deviceData.Device.HasOutdoorTemperature && !this.outdoorReading)) return;
131
+
132
+ const now = Date.now();
133
+ if (now - this.outdoorCheckedAt >= 1_800_000) {
134
+ this.outdoorCheckedAt = now;
135
+ try {
136
+ this.outdoorReading = await this.melCloudClass.getOutdoorTemperature(this.deviceId) ?? this.outdoorReading;
137
+ } catch (error) {
138
+ if (this.logWarn) this.emit('warn', `Unit ${this.deviceId}, ${error.message}`);
139
+ }
140
+ }
141
+
142
+ if (!this.outdoorReading) return;
143
+ const { value, timestamp } = this.outdoorReading;
144
+ if (this.logDebug && timestamp && now - timestamp > 10_800_000) this.emit('debug', `Unit ${this.deviceId}, outdoor temperature ${value} is older than 3 hours, last measurement ${new Date(timestamp).toLocaleString()}`);
145
+
146
+ deviceData.Device.HasOutdoorTemperature = true;
147
+ deviceData.Device.OutdoorTemperature = value;
148
+ }
149
+
123
150
  async updateState(type, deviceData) {
124
151
  try {
152
+ await this.checkOutdoorTemperature(deviceData);
153
+
125
154
  if (!this.accountTypeMelcloud) {
126
155
  if (type === 'ws') {
127
156
  deviceData.Device.OperationMode = AirConditioner.OperationModeMapEnumToEnumWs[deviceData.Device.OperationMode] ?? deviceData.Device.OperationMode;
@@ -726,6 +726,40 @@ class MelCloudHome extends EventEmitter {
726
726
  }
727
727
  }
728
728
 
729
+ // Latest outdoor temperature of a unit from the trend summary report, the status of the unit does not contain it.
730
+ // The last point of every dataset is stamped with the generation time of the report and only repeats the last known
731
+ // value, so the time of the real measurement is the point before it. Returns null when the unit has no outdoor reading.
732
+ async getOutdoorTemperature(unitId) {
733
+ try {
734
+ // the report is a window of 7 days that ends at midnight after `from` and `to` is ignored, so `from` must be today
735
+ const day = (offset) => {
736
+ const date = new Date(Date.now() + offset * 86400000);
737
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
738
+ };
739
+ const params = { unitId, period: 'Daily', from: `${day(0)}T00:00:00.0000000`, to: `${day(1)}T00:00:00.0000000` };
740
+ const resp = await this.client.get(ApiUrls.Home.Get.ReportTrendSummary, { params });
741
+ if (this.logDebug) this.emit('debug', `Trend summary unit ${unitId}: ${JSON.stringify(resp.data)}`);
742
+
743
+ const reports = Array.isArray(resp.data) ? resp.data : [];
744
+ const datasets = reports.flatMap(report => report.datasets ?? []);
745
+ const outdoor = datasets.find(dataset => dataset.id === 'outside_temperature');
746
+ const points = (outdoor?.data ?? []).filter(point => this.functions.isValidValue(point?.y));
747
+
748
+ if (points.length > 0) {
749
+ const last = points.at(-1);
750
+ const otherStamps = datasets.filter(dataset => dataset !== outdoor).map(dataset => dataset.data?.at(-1)?.x);
751
+ const carried = points.length > 1 && otherStamps.includes(last.x);
752
+ return { value: last.y, timestamp: Date.parse((carried ? points.at(-2) : last).x) || null };
753
+ }
754
+
755
+ // no point in the requested range, use the last value before it
756
+ const trigger = reports.flatMap(report => report.previousTriggers ?? []).find(item => item.measure === 'outside_temperature' && this.functions.isValidValue(item.value));
757
+ return trigger ? { value: trigger.value, timestamp: Date.parse(trigger.trigger) || null } : null;
758
+ } catch (error) {
759
+ throw new Error(`Get outdoor temperature error: ${error.message}`);
760
+ }
761
+ }
762
+
729
763
  // ── Devices ───────────────────────────────────────────────────────────────
730
764
 
731
765
  // Wraps checkDevicesList with a single retry on timeout or network error.