homebridge-mitsubishi-comfort 1.0.0 → 1.3.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.
- package/README.md +110 -7
- package/config.schema.json +57 -5
- package/dist/accessory.d.ts +8 -4
- package/dist/accessory.js +92 -44
- package/dist/kumo-api.d.ts +26 -6
- package/dist/kumo-api.js +246 -37
- package/dist/platform.d.ts +12 -0
- package/dist/platform.js +172 -1
- package/dist/settings.d.ts +6 -1
- package/dist/settings.js +2 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
# Homebridge
|
|
1
|
+
# Homebridge Mitsubishi Comfort
|
|
2
2
|
|
|
3
3
|
A Homebridge plugin for Mitsubishi heat pumps using the Kumo Cloud v3 API.
|
|
4
4
|
|
|
5
|
+
## ⚠️ Disclaimer
|
|
6
|
+
|
|
7
|
+
This plugin is not affiliated with, endorsed by, or associated with Mitsubishi Electric in any way. It is an independent, unofficial plugin developed by the community for personal use.
|
|
8
|
+
|
|
9
|
+
**Use at your own risk.** The author assumes no liability for any damage, data loss, or issues that may arise from using this plugin. By using this plugin, you acknowledge that you do so entirely at your own discretion and risk.
|
|
10
|
+
|
|
5
11
|
## Features
|
|
6
12
|
|
|
13
|
+
- **Intelligent streaming-first architecture** with automatic fallback
|
|
14
|
+
- **95% reduction in API calls** when streaming is healthy (optimal mode)
|
|
15
|
+
- **Real-time streaming updates** via Socket.IO for instant status changes
|
|
16
|
+
- **Adaptive polling** that activates only when streaming fails
|
|
7
17
|
- Full HomeKit thermostat integration
|
|
8
18
|
- Support for Heat, Cool, Auto, and Off modes
|
|
9
19
|
- Temperature control
|
|
10
20
|
- Current temperature and humidity display
|
|
11
21
|
- Automatic token refresh
|
|
12
|
-
- Status polling every 30 seconds
|
|
13
22
|
- Multi-site and multi-zone support
|
|
23
|
+
- Device exclusion/hiding support
|
|
24
|
+
- Comprehensive logging for streaming/polling state transitions
|
|
14
25
|
|
|
15
26
|
## Installation
|
|
16
27
|
|
|
@@ -60,10 +71,68 @@ Add the following to your Homebridge `config.json`:
|
|
|
60
71
|
| `name` | string | No | Platform name (default: "Kumo") |
|
|
61
72
|
| `username` | string | Yes | Your Kumo Cloud email address |
|
|
62
73
|
| `password` | string | Yes | Your Kumo Cloud password |
|
|
63
|
-
| `pollInterval` | number | No |
|
|
64
|
-
| `
|
|
74
|
+
| `pollInterval` | number | No | Polling interval when streaming is healthy in seconds (default: 30, minimum: 5) |
|
|
75
|
+
| `disablePolling` | boolean | No | **Recommended:** Disable polling when streaming is healthy (auto-enables if streaming fails, default: false) |
|
|
76
|
+
| `degradedPollInterval` | number | No | Fast polling interval when streaming is unhealthy in seconds (default: 10, minimum: 5, maximum: 60) |
|
|
77
|
+
| `streamingHealthCheckInterval` | number | No | How often to check if streaming is healthy in seconds (default: 30, minimum: 10, maximum: 300) |
|
|
78
|
+
| `streamingStaleThreshold` | number | No | Consider streaming stale if no updates received for this long in seconds (default: 60, minimum: 30, maximum: 600) |
|
|
79
|
+
| `excludeDevices` | string[] | No | Array of device serial numbers to hide from HomeKit |
|
|
65
80
|
| `debug` | boolean | No | Enable debug logging (default: false) |
|
|
66
81
|
|
|
82
|
+
### Recommended Configuration for Optimal Efficiency
|
|
83
|
+
|
|
84
|
+
For best performance and minimal network traffic, enable streaming-only mode:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"platforms": [
|
|
89
|
+
{
|
|
90
|
+
"platform": "KumoV3",
|
|
91
|
+
"name": "Kumo",
|
|
92
|
+
"username": "your-email@example.com",
|
|
93
|
+
"password": "your-password",
|
|
94
|
+
"disablePolling": true
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This configuration:
|
|
101
|
+
- Uses streaming for all device updates when healthy (0 polling queries)
|
|
102
|
+
- Automatically activates 10-second polling if streaming disconnects
|
|
103
|
+
- Reduces API calls by ~95% (from ~257/hour to ~12/hour)
|
|
104
|
+
- Only makes token refresh queries every 15 minutes during normal operation
|
|
105
|
+
|
|
106
|
+
### Debug Mode
|
|
107
|
+
|
|
108
|
+
When `debug: true` is enabled, the plugin will log detailed information including:
|
|
109
|
+
|
|
110
|
+
- API requests and responses with timing information
|
|
111
|
+
- Raw JSON data from zone/device API responses showing all available fields
|
|
112
|
+
- Real-time streaming updates with complete device state
|
|
113
|
+
- Authentication and token refresh events
|
|
114
|
+
- WebSocket connection status
|
|
115
|
+
|
|
116
|
+
**Note:** Debug mode may log sensitive information and should only be enabled for troubleshooting. The plugin will display a warning when debug mode is active.
|
|
117
|
+
|
|
118
|
+
### Known Limitations
|
|
119
|
+
|
|
120
|
+
- **Outdoor Temperature**: The Kumo Cloud API does not expose outdoor temperature data from the outdoor units. While outdoor units have temperature sensors (used for defrost cycles), this data is only available through direct CN105 serial connections, not through the cloud API.
|
|
121
|
+
|
|
122
|
+
- **Temperature Display Differences**: Mitsubishi and Apple use different Fahrenheit-to-Celsius conversion tables, which can cause temperature setpoints to display differently in each app:
|
|
123
|
+
|
|
124
|
+
**When you set 70°F in Apple Home:**
|
|
125
|
+
- HomeKit converts using standard math: 70°F → 21.111°C
|
|
126
|
+
- Your unit is set to exactly 21.111°C (70.0°F)
|
|
127
|
+
- Mitsubishi Comfort app may display this as ~69°F due to their custom conversion table
|
|
128
|
+
|
|
129
|
+
**When you set 70°F in Mitsubishi Comfort app:**
|
|
130
|
+
- Mitsubishi converts using their custom table: 70°F → 21.5°C (0.5°C increments)
|
|
131
|
+
- Your unit is set to 21.5°C (which equals 70.7°F in standard conversion)
|
|
132
|
+
- Apple Home displays this as 71°F (because 21.5°C = 70.7°F)
|
|
133
|
+
|
|
134
|
+
**Both apps are technically correct** - they just use different conversion standards. The actual Celsius value sent to your unit is accurate in both cases. For consistency, pick one app for temperature control rather than mixing both.
|
|
135
|
+
|
|
67
136
|
## Development
|
|
68
137
|
|
|
69
138
|
### Build
|
|
@@ -85,8 +154,22 @@ This will compile TypeScript, link the plugin, and restart on changes.
|
|
|
85
154
|
1. **Authentication**: The plugin logs in to the Kumo Cloud v3 API using your credentials
|
|
86
155
|
2. **Token Management**: Access tokens are automatically refreshed every 15 minutes
|
|
87
156
|
3. **Discovery**: All sites and zones are discovered and registered as HomeKit thermostats
|
|
88
|
-
4. **
|
|
89
|
-
5. **
|
|
157
|
+
4. **Real-time Streaming**: Establishes Socket.IO connection for instant device updates
|
|
158
|
+
5. **Intelligent Fallback**:
|
|
159
|
+
- **Normal Mode** (streaming healthy): Updates via streaming only, minimal API calls
|
|
160
|
+
- **Degraded Mode** (streaming failed): Automatic fallback to fast polling (10s intervals)
|
|
161
|
+
- **Health Monitoring**: Continuous checking of streaming connection status
|
|
162
|
+
- **Automatic Recovery**: Returns to streaming-only mode when connection restored
|
|
163
|
+
6. **Control**: Changes made in HomeKit are sent to the Kumo Cloud API
|
|
164
|
+
|
|
165
|
+
### Update Strategy
|
|
166
|
+
|
|
167
|
+
The plugin uses a smart streaming-first approach with automatic fallback:
|
|
168
|
+
|
|
169
|
+
- **When streaming is healthy**: All device updates arrive via Socket.IO in real-time. If `disablePolling: true` is set, no polling occurs (optimal mode).
|
|
170
|
+
- **When streaming disconnects**: Plugin automatically switches to degraded mode with fast polling (default: 10s intervals) to ensure devices remain responsive.
|
|
171
|
+
- **When streaming reconnects**: Plugin automatically returns to normal mode, halting polling if `disablePolling: true`.
|
|
172
|
+
- **Race condition prevention**: Timestamp-based filtering ensures newer updates always take precedence, regardless of source.
|
|
90
173
|
|
|
91
174
|
## Supported Characteristics
|
|
92
175
|
|
|
@@ -99,12 +182,18 @@ This will compile TypeScript, link the plugin, and restart on changes.
|
|
|
99
182
|
|
|
100
183
|
## API Endpoints Used
|
|
101
184
|
|
|
185
|
+
### REST API
|
|
102
186
|
- `POST /v3/login` - Authentication
|
|
103
187
|
- `GET /v3/sites` - Get all sites
|
|
104
188
|
- `GET /v3/sites/{siteId}/zones` - Get zones for a site
|
|
105
189
|
- `GET /v3/devices/{deviceSerial}/status` - Get device status
|
|
106
190
|
- `POST /v3/devices/send-command` - Send commands to device
|
|
107
191
|
|
|
192
|
+
### Socket.IO Streaming
|
|
193
|
+
- `wss://socket-prod.kumocloud.com` - Real-time device updates via Socket.IO
|
|
194
|
+
- Emits `subscribe` event with device serial to receive updates
|
|
195
|
+
- Receives `device_update` events with full device state
|
|
196
|
+
|
|
108
197
|
## Security
|
|
109
198
|
|
|
110
199
|
### Best Practices
|
|
@@ -143,7 +232,21 @@ This will compile TypeScript, link the plugin, and restart on changes.
|
|
|
143
232
|
|
|
144
233
|
## License
|
|
145
234
|
|
|
146
|
-
|
|
235
|
+
Apache License 2.0
|
|
236
|
+
|
|
237
|
+
Copyright 2024
|
|
238
|
+
|
|
239
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
240
|
+
you may not use this file except in compliance with the License.
|
|
241
|
+
You may obtain a copy of the License at
|
|
242
|
+
|
|
243
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
244
|
+
|
|
245
|
+
Unless required by applicable law or agreed to in writing, software
|
|
246
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
247
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
248
|
+
See the License for the specific language governing permissions and
|
|
249
|
+
limitations under the License.
|
|
147
250
|
|
|
148
251
|
## Credits
|
|
149
252
|
|
package/config.schema.json
CHANGED
|
@@ -31,7 +31,13 @@
|
|
|
31
31
|
"type": "integer",
|
|
32
32
|
"default": 30,
|
|
33
33
|
"minimum": 5,
|
|
34
|
-
"description": "
|
|
34
|
+
"description": "Polling interval when streaming is healthy (minimum 5 seconds, default 30)"
|
|
35
|
+
},
|
|
36
|
+
"disablePolling": {
|
|
37
|
+
"title": "Disable Polling",
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"default": false,
|
|
40
|
+
"description": "Disable polling when streaming is healthy (will auto-enable if streaming fails)"
|
|
35
41
|
},
|
|
36
42
|
"debug": {
|
|
37
43
|
"title": "Debug Mode",
|
|
@@ -40,13 +46,40 @@
|
|
|
40
46
|
"description": "Enable verbose debug logging (requires running Homebridge with -D flag)"
|
|
41
47
|
},
|
|
42
48
|
"excludeDevices": {
|
|
43
|
-
"title": "
|
|
49
|
+
"title": "Hide Device IDs",
|
|
44
50
|
"type": "array",
|
|
51
|
+
"uniqueItems": true,
|
|
45
52
|
"items": {
|
|
46
53
|
"type": "string",
|
|
47
|
-
"title": "Device Serial Number"
|
|
54
|
+
"title": "Device Serial Number",
|
|
55
|
+
"placeholder": "e.g., 9X34P008S100095F",
|
|
56
|
+
"description": "Enter device serial number to hide"
|
|
48
57
|
},
|
|
49
|
-
"description": "
|
|
58
|
+
"description": "Device serial numbers to hide from HomeKit (found in logs during device discovery)"
|
|
59
|
+
},
|
|
60
|
+
"streamingHealthCheckInterval": {
|
|
61
|
+
"title": "Streaming Health Check Interval (seconds)",
|
|
62
|
+
"type": "integer",
|
|
63
|
+
"default": 30,
|
|
64
|
+
"minimum": 10,
|
|
65
|
+
"maximum": 300,
|
|
66
|
+
"description": "How often to check if streaming is healthy (default: 30 seconds)"
|
|
67
|
+
},
|
|
68
|
+
"streamingStaleThreshold": {
|
|
69
|
+
"title": "Streaming Stale Threshold (seconds)",
|
|
70
|
+
"type": "integer",
|
|
71
|
+
"default": 60,
|
|
72
|
+
"minimum": 30,
|
|
73
|
+
"maximum": 600,
|
|
74
|
+
"description": "Consider streaming stale if no updates received for this long (default: 60 seconds)"
|
|
75
|
+
},
|
|
76
|
+
"degradedPollInterval": {
|
|
77
|
+
"title": "Degraded Mode Poll Interval (seconds)",
|
|
78
|
+
"type": "integer",
|
|
79
|
+
"default": 10,
|
|
80
|
+
"minimum": 5,
|
|
81
|
+
"maximum": 60,
|
|
82
|
+
"description": "Fast polling interval when streaming is unhealthy (default: 10 seconds)"
|
|
50
83
|
}
|
|
51
84
|
}
|
|
52
85
|
},
|
|
@@ -62,7 +95,26 @@
|
|
|
62
95
|
"title": "Advanced Settings",
|
|
63
96
|
"expandable": true,
|
|
64
97
|
"expanded": false,
|
|
65
|
-
"items": [
|
|
98
|
+
"items": [
|
|
99
|
+
"pollInterval",
|
|
100
|
+
"degradedPollInterval",
|
|
101
|
+
"disablePolling",
|
|
102
|
+
"streamingHealthCheckInterval",
|
|
103
|
+
"streamingStaleThreshold",
|
|
104
|
+
{
|
|
105
|
+
"key": "excludeDevices",
|
|
106
|
+
"type": "array",
|
|
107
|
+
"title": "Hide Device IDs",
|
|
108
|
+
"buttonText": "Add Device Serial",
|
|
109
|
+
"items": [
|
|
110
|
+
{
|
|
111
|
+
"type": "string",
|
|
112
|
+
"placeholder": "e.g., 9X34P008S100095F"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"debug"
|
|
117
|
+
]
|
|
66
118
|
}
|
|
67
119
|
]
|
|
68
120
|
}
|
package/dist/accessory.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PlatformAccessory, CharacteristicValue } from 'homebridge';
|
|
2
2
|
import { KumoV3Platform } from './platform';
|
|
3
3
|
import { KumoAPI } from './kumo-api';
|
|
4
|
+
import { Zone } from './settings';
|
|
4
5
|
export declare class KumoThermostatAccessory {
|
|
5
6
|
private readonly platform;
|
|
6
7
|
private readonly accessory;
|
|
@@ -12,9 +13,14 @@ export declare class KumoThermostatAccessory {
|
|
|
12
13
|
private currentStatus;
|
|
13
14
|
private pollIntervalMs;
|
|
14
15
|
private hasHumiditySensor;
|
|
16
|
+
private lastUpdateTimestamp;
|
|
17
|
+
private lastUpdateSource;
|
|
15
18
|
constructor(platform: KumoV3Platform, accessory: PlatformAccessory, kumoAPI: KumoAPI, pollIntervalSeconds?: number);
|
|
16
|
-
private
|
|
17
|
-
|
|
19
|
+
private handleStreamingUpdate;
|
|
20
|
+
getSiteId(): string;
|
|
21
|
+
getDeviceSerial(): string;
|
|
22
|
+
updateFromZone(zone: Zone): void;
|
|
23
|
+
private processZoneUpdate;
|
|
18
24
|
private mapToCurrentHeatingCoolingState;
|
|
19
25
|
private mapToTargetHeatingCoolingState;
|
|
20
26
|
private getTargetTempFromStatus;
|
|
@@ -24,8 +30,6 @@ export declare class KumoThermostatAccessory {
|
|
|
24
30
|
getCurrentTemperature(): Promise<CharacteristicValue>;
|
|
25
31
|
getTargetTemperature(): Promise<CharacteristicValue>;
|
|
26
32
|
setTargetTemperature(value: CharacteristicValue): Promise<void>;
|
|
27
|
-
getTemperatureDisplayUnits(): Promise<CharacteristicValue>;
|
|
28
|
-
setTemperatureDisplayUnits(value: CharacteristicValue): Promise<void>;
|
|
29
33
|
getCurrentRelativeHumidity(): Promise<CharacteristicValue>;
|
|
30
34
|
destroy(): void;
|
|
31
35
|
}
|
package/dist/accessory.js
CHANGED
|
@@ -10,6 +10,8 @@ class KumoThermostatAccessory {
|
|
|
10
10
|
this.pollTimer = null;
|
|
11
11
|
this.currentStatus = null;
|
|
12
12
|
this.hasHumiditySensor = false;
|
|
13
|
+
this.lastUpdateTimestamp = 0;
|
|
14
|
+
this.lastUpdateSource = 'none';
|
|
13
15
|
this.deviceSerial = this.accessory.context.device.deviceSerial;
|
|
14
16
|
this.siteId = this.accessory.context.device.siteId;
|
|
15
17
|
this.pollIntervalMs = (pollIntervalSeconds || settings_1.POLL_INTERVAL / 1000) * 1000;
|
|
@@ -30,29 +32,65 @@ class KumoThermostatAccessory {
|
|
|
30
32
|
this.service.getCharacteristic(this.platform.Characteristic.TargetTemperature)
|
|
31
33
|
.onGet(this.getTargetTemperature.bind(this))
|
|
32
34
|
.onSet(this.setTargetTemperature.bind(this));
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
.onSet(this.setTemperatureDisplayUnits.bind(this));
|
|
36
|
-
this.startPolling();
|
|
35
|
+
this.kumoAPI.subscribeToDevice(this.deviceSerial, this.handleStreamingUpdate.bind(this));
|
|
36
|
+
this.platform.log.debug(`Registered streaming callback for ${this.deviceSerial}`);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
38
|
+
handleStreamingUpdate(deviceSerial, data) {
|
|
39
|
+
if (data.roomTemp === undefined || data.roomTemp === null) {
|
|
40
|
+
this.platform.log.debug(`Streaming update for ${deviceSerial} missing essential data, skipping`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const updateTimestamp = Date.now();
|
|
44
|
+
this.platform.log.debug(`Streaming update received for ${deviceSerial}: temp=${data.roomTemp}, mode=${data.operationMode}, power=${data.power}`);
|
|
45
|
+
const zoneUpdate = {
|
|
46
|
+
adapter: {
|
|
47
|
+
id: data.id || '',
|
|
48
|
+
deviceSerial: deviceSerial,
|
|
49
|
+
roomTemp: data.roomTemp,
|
|
50
|
+
spHeat: data.spHeat,
|
|
51
|
+
spCool: data.spCool,
|
|
52
|
+
spAuto: data.spAuto || null,
|
|
53
|
+
humidity: data.humidity || null,
|
|
54
|
+
power: data.power,
|
|
55
|
+
operationMode: data.operationMode,
|
|
56
|
+
previousOperationMode: data.operationMode,
|
|
57
|
+
fanSpeed: data.fanSpeed || 'auto',
|
|
58
|
+
airDirection: data.airDirection || 'auto',
|
|
59
|
+
connected: true,
|
|
60
|
+
isSimulator: false,
|
|
61
|
+
hasSensor: data.humidity !== null && data.humidity !== undefined,
|
|
62
|
+
hasMhk2: false,
|
|
63
|
+
scheduleOwner: 'adapter',
|
|
64
|
+
scheduleHoldEndTime: 0,
|
|
65
|
+
rssi: data.rssi,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
this.processZoneUpdate(zoneUpdate, 'streaming', updateTimestamp);
|
|
69
|
+
}
|
|
70
|
+
getSiteId() {
|
|
71
|
+
return this.siteId;
|
|
72
|
+
}
|
|
73
|
+
getDeviceSerial() {
|
|
74
|
+
return this.deviceSerial;
|
|
43
75
|
}
|
|
44
|
-
|
|
76
|
+
updateFromZone(zone) {
|
|
77
|
+
const updateTimestamp = Date.now();
|
|
78
|
+
this.processZoneUpdate(zone, 'polling', updateTimestamp);
|
|
79
|
+
}
|
|
80
|
+
processZoneUpdate(zone, source, timestamp) {
|
|
45
81
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
82
|
+
if (timestamp < this.lastUpdateTimestamp) {
|
|
83
|
+
this.platform.log.debug(`[${this.deviceSerial}] Ignoring ${source} update: ` +
|
|
84
|
+
`${this.lastUpdateTimestamp - timestamp}ms older than last ${this.lastUpdateSource} update`);
|
|
49
85
|
return;
|
|
50
86
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
this.lastUpdateTimestamp = timestamp;
|
|
88
|
+
const previousSource = this.lastUpdateSource;
|
|
89
|
+
this.lastUpdateSource = source;
|
|
90
|
+
if (previousSource !== source && previousSource !== 'none') {
|
|
91
|
+
this.platform.log.debug(`[${this.deviceSerial}] Update source changed: ${previousSource} → ${source}`);
|
|
55
92
|
}
|
|
93
|
+
this.platform.log.debug(`Processing ${source} update for ${this.deviceSerial}`);
|
|
56
94
|
if (zone.adapter.roomTemp === undefined || zone.adapter.roomTemp === null) {
|
|
57
95
|
this.platform.log.error(`Device ${this.deviceSerial} has invalid roomTemp: ${zone.adapter.roomTemp}`);
|
|
58
96
|
this.platform.log.debug('Zone adapter data:', JSON.stringify(zone.adapter));
|
|
@@ -87,7 +125,7 @@ class KumoThermostatAccessory {
|
|
|
87
125
|
spAuto: zone.adapter.spAuto,
|
|
88
126
|
};
|
|
89
127
|
this.currentStatus = status;
|
|
90
|
-
this.platform.log.debug(
|
|
128
|
+
this.platform.log.debug(`${this.accessory.displayName}: ${status.roomTemp}°C (target: ${this.getTargetTempFromStatus(status)}°C, mode: ${status.operationMode})`);
|
|
91
129
|
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, this.mapToCurrentHeatingCoolingState(status));
|
|
92
130
|
this.service.updateCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, this.mapToTargetHeatingCoolingState(status));
|
|
93
131
|
if (status.roomTemp !== undefined && status.roomTemp !== null && !isNaN(status.roomTemp)) {
|
|
@@ -95,6 +133,8 @@ class KumoThermostatAccessory {
|
|
|
95
133
|
}
|
|
96
134
|
const targetTemp = this.getTargetTempFromStatus(status);
|
|
97
135
|
if (targetTemp !== undefined && targetTemp !== null && !isNaN(targetTemp)) {
|
|
136
|
+
const targetTempF = (targetTemp * 9 / 5) + 32;
|
|
137
|
+
this.platform.log.debug(`[TEMP UPDATE] ${this.accessory.displayName}: API returned target ${targetTemp.toFixed(3)}°C (${targetTempF.toFixed(1)}°F) [mode: ${status.operationMode}]`);
|
|
98
138
|
this.service.updateCharacteristic(this.platform.Characteristic.TargetTemperature, targetTemp);
|
|
99
139
|
}
|
|
100
140
|
if (this.hasHumiditySensor && status.humidity !== null) {
|
|
@@ -210,7 +250,10 @@ class KumoThermostatAccessory {
|
|
|
210
250
|
operationMode,
|
|
211
251
|
});
|
|
212
252
|
if (success) {
|
|
213
|
-
|
|
253
|
+
if (this.currentStatus) {
|
|
254
|
+
this.currentStatus.operationMode = operationMode;
|
|
255
|
+
this.currentStatus.power = operationMode === 'off' ? 0 : 1;
|
|
256
|
+
}
|
|
214
257
|
}
|
|
215
258
|
else {
|
|
216
259
|
this.platform.log.error(`Failed to set target heating cooling state for ${this.accessory.displayName}`);
|
|
@@ -223,16 +266,18 @@ class KumoThermostatAccessory {
|
|
|
223
266
|
this.currentStatus = status;
|
|
224
267
|
}
|
|
225
268
|
else {
|
|
226
|
-
this.platform.log.
|
|
269
|
+
this.platform.log.debug('No status available yet for getCurrentTemperature, returning default');
|
|
227
270
|
return 20;
|
|
228
271
|
}
|
|
229
272
|
}
|
|
230
273
|
const temp = this.currentStatus.roomTemp;
|
|
231
274
|
if (temp === undefined || temp === null || isNaN(temp)) {
|
|
232
|
-
this.
|
|
275
|
+
if (this.lastUpdateSource !== 'none') {
|
|
276
|
+
this.platform.log.warn(`Invalid roomTemp value for ${this.accessory.displayName}:`, temp);
|
|
277
|
+
}
|
|
233
278
|
return 20;
|
|
234
279
|
}
|
|
235
|
-
this.platform.log.debug(
|
|
280
|
+
this.platform.log.debug(`HomeKit get current temp for ${this.accessory.displayName}: ${temp}°C`);
|
|
236
281
|
return temp;
|
|
237
282
|
}
|
|
238
283
|
async getTargetTemperature() {
|
|
@@ -242,55 +287,60 @@ class KumoThermostatAccessory {
|
|
|
242
287
|
this.currentStatus = status;
|
|
243
288
|
}
|
|
244
289
|
else {
|
|
245
|
-
this.platform.log.
|
|
290
|
+
this.platform.log.debug('No status available yet for getTargetTemperature, returning default');
|
|
246
291
|
return 20;
|
|
247
292
|
}
|
|
248
293
|
}
|
|
249
294
|
const temp = this.getTargetTempFromStatus(this.currentStatus);
|
|
250
295
|
if (temp === undefined || temp === null || isNaN(temp)) {
|
|
251
|
-
this.
|
|
296
|
+
if (this.lastUpdateSource !== 'none') {
|
|
297
|
+
this.platform.log.warn(`Invalid target temperature value for ${this.accessory.displayName}:`, temp);
|
|
298
|
+
}
|
|
252
299
|
return 20;
|
|
253
300
|
}
|
|
254
|
-
this.platform.log.debug(
|
|
301
|
+
this.platform.log.debug(`HomeKit get target temp for ${this.accessory.displayName}: ${temp}°C`);
|
|
255
302
|
return temp;
|
|
256
303
|
}
|
|
257
304
|
async setTargetTemperature(value) {
|
|
258
305
|
const temp = value;
|
|
259
|
-
|
|
306
|
+
const tempF = (temp * 9 / 5) + 32;
|
|
307
|
+
this.platform.log.info(`[TEMP CHANGE] ${this.accessory.displayName}: HomeKit sent ${temp.toFixed(3)}°C (${tempF.toFixed(1)}°F)`);
|
|
260
308
|
if (!this.currentStatus) {
|
|
261
309
|
this.platform.log.error('Cannot set temperature - no current status');
|
|
262
310
|
return;
|
|
263
311
|
}
|
|
264
|
-
const roundedTemp = Math.round(temp * 2) / 2;
|
|
265
|
-
this.platform.log.debug(`Rounded temperature from ${temp} to ${roundedTemp}`);
|
|
266
312
|
const commands = {};
|
|
267
313
|
if (this.currentStatus.operationMode === 'heat') {
|
|
268
|
-
commands.spHeat =
|
|
314
|
+
commands.spHeat = temp;
|
|
269
315
|
}
|
|
270
316
|
else if (this.currentStatus.operationMode === 'cool') {
|
|
271
|
-
commands.spCool =
|
|
317
|
+
commands.spCool = temp;
|
|
272
318
|
}
|
|
273
319
|
else if (this.currentStatus.operationMode === 'auto') {
|
|
274
|
-
commands.spHeat =
|
|
275
|
-
commands.spCool =
|
|
320
|
+
commands.spHeat = temp;
|
|
321
|
+
commands.spCool = temp;
|
|
276
322
|
}
|
|
277
323
|
else {
|
|
278
|
-
commands.spHeat =
|
|
324
|
+
commands.spHeat = temp;
|
|
279
325
|
}
|
|
326
|
+
this.platform.log.info(`[TEMP CHANGE] ${this.accessory.displayName}: Sending to API: ${JSON.stringify(commands)}°C`);
|
|
280
327
|
const success = await this.kumoAPI.sendCommand(this.deviceSerial, commands);
|
|
281
328
|
if (success) {
|
|
282
|
-
|
|
329
|
+
this.platform.log.info(`[TEMP CHANGE] ${this.accessory.displayName}: Command accepted by API`);
|
|
330
|
+
if (this.currentStatus) {
|
|
331
|
+
if (commands.spHeat !== undefined) {
|
|
332
|
+
this.currentStatus.spHeat = commands.spHeat;
|
|
333
|
+
}
|
|
334
|
+
if (commands.spCool !== undefined) {
|
|
335
|
+
this.currentStatus.spCool = commands.spCool;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.service.updateCharacteristic(this.platform.Characteristic.TargetTemperature, temp);
|
|
283
339
|
}
|
|
284
340
|
else {
|
|
285
341
|
this.platform.log.error(`Failed to set target temperature for ${this.accessory.displayName}: ${JSON.stringify(commands)}`);
|
|
286
342
|
}
|
|
287
343
|
}
|
|
288
|
-
async getTemperatureDisplayUnits() {
|
|
289
|
-
return this.platform.Characteristic.TemperatureDisplayUnits.CELSIUS;
|
|
290
|
-
}
|
|
291
|
-
async setTemperatureDisplayUnits(value) {
|
|
292
|
-
this.platform.log.debug('Set TemperatureDisplayUnits:', value);
|
|
293
|
-
}
|
|
294
344
|
async getCurrentRelativeHumidity() {
|
|
295
345
|
var _a;
|
|
296
346
|
if (!this.currentStatus) {
|
|
@@ -304,10 +354,8 @@ class KumoThermostatAccessory {
|
|
|
304
354
|
return humidity;
|
|
305
355
|
}
|
|
306
356
|
destroy() {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
this.pollTimer = null;
|
|
310
|
-
}
|
|
357
|
+
this.kumoAPI.unsubscribeFromDevice(this.deviceSerial);
|
|
358
|
+
this.platform.log.debug(`Unsubscribed from streaming updates for ${this.deviceSerial}`);
|
|
311
359
|
}
|
|
312
360
|
}
|
|
313
361
|
exports.KumoThermostatAccessory = KumoThermostatAccessory;
|
package/dist/kumo-api.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Logger } from 'homebridge';
|
|
2
2
|
import { Site, Zone, DeviceStatus, Commands } from './settings';
|
|
3
|
+
export type DeviceUpdateCallback = (deviceSerial: string, status: Partial<DeviceStatus>) => void;
|
|
3
4
|
export declare class KumoAPI {
|
|
4
5
|
private readonly username;
|
|
5
6
|
private readonly password;
|
|
@@ -8,9 +9,20 @@ export declare class KumoAPI {
|
|
|
8
9
|
private refreshToken;
|
|
9
10
|
private tokenExpiresAt;
|
|
10
11
|
private refreshTimer;
|
|
11
|
-
private siteEtags;
|
|
12
12
|
private debugMode;
|
|
13
|
-
|
|
13
|
+
private refreshInProgress;
|
|
14
|
+
private socket;
|
|
15
|
+
private streamingEnabled;
|
|
16
|
+
private deviceUpdateCallbacks;
|
|
17
|
+
private reconnectAttempts;
|
|
18
|
+
private maxReconnectAttempts;
|
|
19
|
+
private lastStreamingUpdate;
|
|
20
|
+
private streamingHealthCallbacks;
|
|
21
|
+
private healthCheckTimer;
|
|
22
|
+
private streamingHealthCheckInterval;
|
|
23
|
+
private streamingStaleThreshold;
|
|
24
|
+
private isStreamingHealthy;
|
|
25
|
+
constructor(username: string, password: string, log: Logger, debug?: boolean, enableStreaming?: boolean);
|
|
14
26
|
private maskToken;
|
|
15
27
|
login(): Promise<boolean>;
|
|
16
28
|
private scheduleTokenRefresh;
|
|
@@ -20,11 +32,19 @@ export declare class KumoAPI {
|
|
|
20
32
|
private makeAuthenticatedRequest;
|
|
21
33
|
getSites(): Promise<Site[]>;
|
|
22
34
|
getZones(siteId: string): Promise<Zone[]>;
|
|
23
|
-
getZonesWithETag(siteId: string): Promise<{
|
|
24
|
-
zones: Zone[];
|
|
25
|
-
notModified: boolean;
|
|
26
|
-
}>;
|
|
27
35
|
getDeviceStatus(deviceSerial: string): Promise<DeviceStatus | null>;
|
|
28
36
|
sendCommand(deviceSerial: string, commands: Commands): Promise<boolean>;
|
|
37
|
+
startStreaming(deviceSerials: string[]): Promise<boolean>;
|
|
38
|
+
subscribeToDevice(deviceSerial: string, callback: DeviceUpdateCallback): void;
|
|
39
|
+
unsubscribeFromDevice(deviceSerial: string): void;
|
|
40
|
+
isStreamingConnected(): boolean;
|
|
41
|
+
setStreamingHealthConfig(checkInterval: number, staleThreshold: number): void;
|
|
42
|
+
onStreamingHealthChange(callback: (isHealthy: boolean) => void): void;
|
|
43
|
+
getStreamingHealth(): boolean;
|
|
44
|
+
private updateStreamingTimestamp;
|
|
45
|
+
private checkStreamingHealth;
|
|
46
|
+
private notifyHealthChange;
|
|
47
|
+
private startHealthChecks;
|
|
48
|
+
private stopHealthChecks;
|
|
29
49
|
destroy(): void;
|
|
30
50
|
}
|
package/dist/kumo-api.js
CHANGED
|
@@ -5,9 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.KumoAPI = void 0;
|
|
7
7
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
8
9
|
const settings_1 = require("./settings");
|
|
9
10
|
class KumoAPI {
|
|
10
|
-
constructor(username, password, log, debug = false) {
|
|
11
|
+
constructor(username, password, log, debug = false, enableStreaming = true) {
|
|
11
12
|
this.username = username;
|
|
12
13
|
this.password = password;
|
|
13
14
|
this.log = log;
|
|
@@ -15,13 +16,28 @@ class KumoAPI {
|
|
|
15
16
|
this.refreshToken = null;
|
|
16
17
|
this.tokenExpiresAt = 0;
|
|
17
18
|
this.refreshTimer = null;
|
|
18
|
-
this.siteEtags = new Map();
|
|
19
19
|
this.debugMode = false;
|
|
20
|
+
this.refreshInProgress = null;
|
|
21
|
+
this.socket = null;
|
|
22
|
+
this.streamingEnabled = true;
|
|
23
|
+
this.deviceUpdateCallbacks = new Map();
|
|
24
|
+
this.reconnectAttempts = 0;
|
|
25
|
+
this.maxReconnectAttempts = 5;
|
|
26
|
+
this.lastStreamingUpdate = new Map();
|
|
27
|
+
this.streamingHealthCallbacks = new Set();
|
|
28
|
+
this.healthCheckTimer = null;
|
|
29
|
+
this.streamingHealthCheckInterval = 30000;
|
|
30
|
+
this.streamingStaleThreshold = 60000;
|
|
31
|
+
this.isStreamingHealthy = false;
|
|
20
32
|
this.debugMode = debug;
|
|
33
|
+
this.streamingEnabled = enableStreaming;
|
|
21
34
|
if (this.debugMode) {
|
|
22
35
|
this.log.info('Debug mode enabled');
|
|
23
36
|
this.log.warn('Debug mode may log sensitive information - use only for troubleshooting');
|
|
24
37
|
}
|
|
38
|
+
if (this.streamingEnabled) {
|
|
39
|
+
this.log.info('Streaming mode enabled - real-time updates will be used');
|
|
40
|
+
}
|
|
25
41
|
}
|
|
26
42
|
maskToken(token) {
|
|
27
43
|
if (!token) {
|
|
@@ -99,17 +115,21 @@ class KumoAPI {
|
|
|
99
115
|
headers: {
|
|
100
116
|
'Content-Type': 'application/json',
|
|
101
117
|
'Accept': 'application/json',
|
|
102
|
-
'Authorization': `Bearer ${this.refreshToken}`,
|
|
103
118
|
'X-App-Version': settings_1.APP_VERSION,
|
|
104
119
|
},
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
refresh: this.refreshToken,
|
|
122
|
+
}),
|
|
105
123
|
});
|
|
106
124
|
if (!response.ok) {
|
|
107
|
-
|
|
125
|
+
const errorText = await response.text();
|
|
126
|
+
this.log.warn(`Token refresh failed (${response.status}): ${errorText}`);
|
|
127
|
+
this.log.warn('Attempting full login');
|
|
108
128
|
return await this.login();
|
|
109
129
|
}
|
|
110
130
|
const data = await response.json();
|
|
111
|
-
this.accessToken = data.
|
|
112
|
-
this.refreshToken = data.
|
|
131
|
+
this.accessToken = data.access;
|
|
132
|
+
this.refreshToken = data.refresh;
|
|
113
133
|
this.tokenExpiresAt = Date.now() + settings_1.TOKEN_REFRESH_INTERVAL;
|
|
114
134
|
this.log.debug('Access token refreshed successfully');
|
|
115
135
|
this.scheduleTokenRefresh();
|
|
@@ -127,10 +147,22 @@ class KumoAPI {
|
|
|
127
147
|
}
|
|
128
148
|
async ensureAuthenticated() {
|
|
129
149
|
if (!this.accessToken || Date.now() >= this.tokenExpiresAt - (5 * 60 * 1000)) {
|
|
130
|
-
if (
|
|
131
|
-
|
|
150
|
+
if (this.refreshInProgress) {
|
|
151
|
+
this.log.debug('Waiting for existing token refresh to complete');
|
|
152
|
+
return await this.refreshInProgress;
|
|
132
153
|
}
|
|
133
|
-
|
|
154
|
+
this.refreshInProgress = (async () => {
|
|
155
|
+
try {
|
|
156
|
+
if (!this.refreshToken) {
|
|
157
|
+
return await this.login();
|
|
158
|
+
}
|
|
159
|
+
return await this.refreshAccessToken();
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
this.refreshInProgress = null;
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
return await this.refreshInProgress;
|
|
134
166
|
}
|
|
135
167
|
return true;
|
|
136
168
|
}
|
|
@@ -156,7 +188,16 @@ class KumoAPI {
|
|
|
156
188
|
if (body) {
|
|
157
189
|
options.body = JSON.stringify(body);
|
|
158
190
|
}
|
|
159
|
-
const
|
|
191
|
+
const url = `${settings_1.API_BASE_URL}${endpoint}`;
|
|
192
|
+
if (this.debugMode) {
|
|
193
|
+
this.log.info(`→ API Request: ${method} ${endpoint}`);
|
|
194
|
+
if (body) {
|
|
195
|
+
this.log.info(` Body: ${JSON.stringify(body)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const startTime = Date.now();
|
|
199
|
+
const response = await (0, node_fetch_1.default)(url, options);
|
|
200
|
+
const duration = Date.now() - startTime;
|
|
160
201
|
if (response.status === 401) {
|
|
161
202
|
this.log.debug('Received 401, refreshing token and retrying');
|
|
162
203
|
const refreshed = await this.refreshAccessToken();
|
|
@@ -173,9 +214,23 @@ class KumoAPI {
|
|
|
173
214
|
}
|
|
174
215
|
if (!response.ok) {
|
|
175
216
|
this.log.error(`Request failed with status: ${response.status}`);
|
|
217
|
+
if (this.debugMode) {
|
|
218
|
+
const errorText = await response.text();
|
|
219
|
+
this.log.info(` Error response: ${errorText}`);
|
|
220
|
+
}
|
|
176
221
|
return null;
|
|
177
222
|
}
|
|
178
|
-
|
|
223
|
+
const data = await response.json();
|
|
224
|
+
if (this.debugMode) {
|
|
225
|
+
this.log.info(`← API Response: ${response.status} (${duration}ms)`);
|
|
226
|
+
if (Array.isArray(data)) {
|
|
227
|
+
this.log.info(` Returned ${data.length} item(s)`);
|
|
228
|
+
}
|
|
229
|
+
else if (data && typeof data === 'object') {
|
|
230
|
+
this.log.info(` Keys: ${Object.keys(data).join(', ')}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return data;
|
|
179
234
|
}
|
|
180
235
|
catch (error) {
|
|
181
236
|
if (error instanceof Error) {
|
|
@@ -196,58 +251,63 @@ class KumoAPI {
|
|
|
196
251
|
return sites || [];
|
|
197
252
|
}
|
|
198
253
|
async getZones(siteId) {
|
|
199
|
-
this.log.debug(`Fetching zones for site: ${siteId}`);
|
|
200
|
-
const zones = await this.makeAuthenticatedRequest(`/sites/${siteId}/zones`);
|
|
201
|
-
return zones || [];
|
|
202
|
-
}
|
|
203
|
-
async getZonesWithETag(siteId) {
|
|
204
|
-
const etag = this.siteEtags.get(siteId);
|
|
205
254
|
const authenticated = await this.ensureAuthenticated();
|
|
206
255
|
if (!authenticated) {
|
|
207
256
|
this.log.error('Failed to authenticate');
|
|
208
|
-
return
|
|
257
|
+
return [];
|
|
209
258
|
}
|
|
210
259
|
try {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
const response = await (0, node_fetch_1.default)(`${settings_1.API_BASE_URL}/sites/${siteId}/zones`, { headers });
|
|
216
|
-
if (response.status === 304) {
|
|
217
|
-
this.log.debug(`Zones for site ${siteId}: Not Modified (304)`);
|
|
218
|
-
return { zones: [], notModified: true };
|
|
260
|
+
const endpoint = `/sites/${siteId}/zones`;
|
|
261
|
+
if (this.debugMode) {
|
|
262
|
+
this.log.info(`→ API Request: GET ${endpoint}`);
|
|
219
263
|
}
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
const response = await (0, node_fetch_1.default)(`${settings_1.API_BASE_URL}${endpoint}`, {
|
|
266
|
+
headers: this.getAuthHeaders(),
|
|
267
|
+
});
|
|
268
|
+
const duration = Date.now() - startTime;
|
|
220
269
|
if (!response.ok) {
|
|
221
270
|
const errorBody = await response.text();
|
|
222
271
|
this.log.error(`Failed to fetch zones for site ${siteId}: ${response.status} - ${errorBody}`);
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
const newEtag = response.headers.get('etag');
|
|
226
|
-
if (newEtag) {
|
|
227
|
-
this.siteEtags.set(siteId, newEtag);
|
|
272
|
+
return [];
|
|
228
273
|
}
|
|
229
274
|
const zones = await response.json();
|
|
230
275
|
if (this.debugMode) {
|
|
231
|
-
this.log.
|
|
276
|
+
this.log.info(`← API Response: 200 (${duration}ms)`);
|
|
277
|
+
this.log.info(` Fetched ${zones.length} zone(s) for site ${siteId}`);
|
|
278
|
+
zones.forEach(zone => {
|
|
279
|
+
this.log.info(` RAW Zone JSON for ${zone.name}:`);
|
|
280
|
+
this.log.info(JSON.stringify(zone, null, 2));
|
|
281
|
+
});
|
|
232
282
|
zones.forEach(zone => {
|
|
233
|
-
|
|
283
|
+
const a = zone.adapter;
|
|
284
|
+
this.log.info(` ${zone.name} [${a.deviceSerial}]`);
|
|
285
|
+
this.log.info(` Temperature: ${a.roomTemp}°C (current) → Heat: ${a.spHeat}°C, Cool: ${a.spCool}°C, Auto: ${a.spAuto}°C`);
|
|
286
|
+
this.log.info(` Status: ${a.operationMode} mode, power=${a.power}, connected=${a.connected}`);
|
|
287
|
+
this.log.info(` Fan: ${a.fanSpeed}, Direction: ${a.airDirection}, Humidity: ${a.humidity !== null ? a.humidity + '%' : 'N/A'}`);
|
|
288
|
+
this.log.info(` Signal: ${a.rssi !== undefined ? a.rssi + ' dBm' : 'N/A'}`);
|
|
234
289
|
});
|
|
235
290
|
}
|
|
236
|
-
return
|
|
291
|
+
return zones;
|
|
237
292
|
}
|
|
238
293
|
catch (error) {
|
|
239
294
|
if (error instanceof Error) {
|
|
240
|
-
this.log.error('Error fetching zones
|
|
295
|
+
this.log.error('Error fetching zones:', error.message);
|
|
241
296
|
}
|
|
242
297
|
else {
|
|
243
298
|
this.log.error('Error fetching zones: Unknown error occurred');
|
|
244
299
|
}
|
|
245
|
-
return
|
|
300
|
+
return [];
|
|
246
301
|
}
|
|
247
302
|
}
|
|
248
303
|
async getDeviceStatus(deviceSerial) {
|
|
249
304
|
this.log.debug(`Fetching status for device: ${deviceSerial}`);
|
|
250
|
-
|
|
305
|
+
const status = await this.makeAuthenticatedRequest(`/devices/${deviceSerial}/status`);
|
|
306
|
+
if (this.debugMode && status) {
|
|
307
|
+
this.log.info(` RAW Device Status JSON for ${deviceSerial}:`);
|
|
308
|
+
this.log.info(JSON.stringify(status, null, 2));
|
|
309
|
+
}
|
|
310
|
+
return status;
|
|
251
311
|
}
|
|
252
312
|
async sendCommand(deviceSerial, commands) {
|
|
253
313
|
this.log.debug(`Sending command to device ${deviceSerial}:`, JSON.stringify(commands));
|
|
@@ -274,11 +334,160 @@ class KumoAPI {
|
|
|
274
334
|
this.log.debug(`Command sent successfully to device ${deviceSerial}`);
|
|
275
335
|
return true;
|
|
276
336
|
}
|
|
337
|
+
async startStreaming(deviceSerials) {
|
|
338
|
+
var _a;
|
|
339
|
+
if (!this.streamingEnabled) {
|
|
340
|
+
this.log.debug('Streaming is disabled, skipping connection');
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) {
|
|
344
|
+
this.log.debug('Streaming already connected');
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
if (!this.accessToken) {
|
|
348
|
+
this.log.error('Cannot start streaming: not authenticated');
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
this.log.info('Starting streaming connection...');
|
|
353
|
+
this.socket = (0, socket_io_client_1.io)(settings_1.SOCKET_BASE_URL, {
|
|
354
|
+
transports: ['polling', 'websocket'],
|
|
355
|
+
extraHeaders: {
|
|
356
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
357
|
+
'Accept': '*/*',
|
|
358
|
+
'User-Agent': 'kumocloud/1122',
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
this.socket.on('connect', () => {
|
|
362
|
+
var _a, _b;
|
|
363
|
+
this.log.info(`✓ Streaming connected (ID: ${(_a = this.socket) === null || _a === void 0 ? void 0 : _a.id})`);
|
|
364
|
+
this.reconnectAttempts = 0;
|
|
365
|
+
for (const deviceSerial of deviceSerials) {
|
|
366
|
+
this.log.debug(`Subscribing to device: ${deviceSerial}`);
|
|
367
|
+
(_b = this.socket) === null || _b === void 0 ? void 0 : _b.emit('subscribe', deviceSerial);
|
|
368
|
+
}
|
|
369
|
+
for (const deviceSerial of deviceSerials) {
|
|
370
|
+
this.lastStreamingUpdate.set(deviceSerial, Date.now());
|
|
371
|
+
}
|
|
372
|
+
this.isStreamingHealthy = true;
|
|
373
|
+
this.notifyHealthChange(false, true);
|
|
374
|
+
this.startHealthChecks();
|
|
375
|
+
this.log.info('✓ Streaming connection established');
|
|
376
|
+
this.log.info(`Monitoring ${deviceSerials.length} device(s) for real-time updates`);
|
|
377
|
+
});
|
|
378
|
+
this.socket.on('device_update', (data) => {
|
|
379
|
+
const deviceSerial = data.deviceSerial;
|
|
380
|
+
if (!deviceSerial) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.updateStreamingTimestamp(deviceSerial);
|
|
384
|
+
if (this.debugMode) {
|
|
385
|
+
this.log.debug(`Stream update for ${deviceSerial}: temp=${data.roomTemp}°C, mode=${data.operationMode}, power=${data.power}`);
|
|
386
|
+
this.log.info(` RAW Streaming Update JSON for ${deviceSerial}:`);
|
|
387
|
+
this.log.info(JSON.stringify(data, null, 2));
|
|
388
|
+
}
|
|
389
|
+
const callback = this.deviceUpdateCallbacks.get(deviceSerial);
|
|
390
|
+
if (callback) {
|
|
391
|
+
callback(deviceSerial, data);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
this.socket.on('disconnect', (reason) => {
|
|
395
|
+
this.log.warn(`✗ Streaming disconnected: ${reason}`);
|
|
396
|
+
const wasHealthy = this.isStreamingHealthy;
|
|
397
|
+
this.isStreamingHealthy = false;
|
|
398
|
+
this.notifyHealthChange(wasHealthy, false);
|
|
399
|
+
this.stopHealthChecks();
|
|
400
|
+
if (reason !== 'io client disconnect' && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
401
|
+
this.reconnectAttempts++;
|
|
402
|
+
this.log.info(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
403
|
+
}
|
|
404
|
+
else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
405
|
+
this.log.error('Max reconnect attempts reached - polling will handle updates');
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
this.socket.on('connect_error', (error) => {
|
|
409
|
+
this.log.error(`Streaming connection error: ${error.message}`);
|
|
410
|
+
});
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
if (error instanceof Error) {
|
|
415
|
+
this.log.error('Failed to start streaming:', error.message);
|
|
416
|
+
}
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
subscribeToDevice(deviceSerial, callback) {
|
|
421
|
+
var _a;
|
|
422
|
+
this.deviceUpdateCallbacks.set(deviceSerial, callback);
|
|
423
|
+
if ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) {
|
|
424
|
+
this.log.debug(`Subscribing to device: ${deviceSerial}`);
|
|
425
|
+
this.socket.emit('subscribe', deviceSerial);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
unsubscribeFromDevice(deviceSerial) {
|
|
429
|
+
this.deviceUpdateCallbacks.delete(deviceSerial);
|
|
430
|
+
}
|
|
431
|
+
isStreamingConnected() {
|
|
432
|
+
var _a;
|
|
433
|
+
return ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || false;
|
|
434
|
+
}
|
|
435
|
+
setStreamingHealthConfig(checkInterval, staleThreshold) {
|
|
436
|
+
this.streamingHealthCheckInterval = checkInterval * 1000;
|
|
437
|
+
this.streamingStaleThreshold = staleThreshold * 1000;
|
|
438
|
+
this.log.debug(`Streaming health config: check every ${checkInterval}s, stale after ${staleThreshold}s`);
|
|
439
|
+
}
|
|
440
|
+
onStreamingHealthChange(callback) {
|
|
441
|
+
this.streamingHealthCallbacks.add(callback);
|
|
442
|
+
}
|
|
443
|
+
getStreamingHealth() {
|
|
444
|
+
return this.isStreamingHealthy;
|
|
445
|
+
}
|
|
446
|
+
updateStreamingTimestamp(deviceSerial) {
|
|
447
|
+
this.lastStreamingUpdate.set(deviceSerial, Date.now());
|
|
448
|
+
}
|
|
449
|
+
checkStreamingHealth() {
|
|
450
|
+
const wasHealthy = this.isStreamingHealthy;
|
|
451
|
+
this.isStreamingHealthy = this.isStreamingConnected();
|
|
452
|
+
this.notifyHealthChange(wasHealthy, this.isStreamingHealthy);
|
|
453
|
+
}
|
|
454
|
+
notifyHealthChange(wasHealthy, isHealthy) {
|
|
455
|
+
if (wasHealthy !== isHealthy) {
|
|
456
|
+
this.log.info(`Streaming health changed: ${wasHealthy ? 'healthy' : 'unhealthy'} → ${isHealthy ? 'healthy' : 'unhealthy'}`);
|
|
457
|
+
for (const callback of this.streamingHealthCallbacks) {
|
|
458
|
+
callback(isHealthy);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
startHealthChecks() {
|
|
463
|
+
if (this.healthCheckTimer) {
|
|
464
|
+
clearInterval(this.healthCheckTimer);
|
|
465
|
+
}
|
|
466
|
+
this.healthCheckTimer = setInterval(() => {
|
|
467
|
+
this.checkStreamingHealth();
|
|
468
|
+
}, this.streamingHealthCheckInterval);
|
|
469
|
+
this.log.debug('Started streaming health checks');
|
|
470
|
+
}
|
|
471
|
+
stopHealthChecks() {
|
|
472
|
+
if (this.healthCheckTimer) {
|
|
473
|
+
clearInterval(this.healthCheckTimer);
|
|
474
|
+
this.healthCheckTimer = null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
277
477
|
destroy() {
|
|
278
478
|
if (this.refreshTimer) {
|
|
279
479
|
clearTimeout(this.refreshTimer);
|
|
280
480
|
this.refreshTimer = null;
|
|
281
481
|
}
|
|
482
|
+
this.stopHealthChecks();
|
|
483
|
+
this.streamingHealthCallbacks.clear();
|
|
484
|
+
this.lastStreamingUpdate.clear();
|
|
485
|
+
this.log.debug('Streaming health monitoring stopped');
|
|
486
|
+
if (this.socket) {
|
|
487
|
+
this.log.debug('Disconnecting streaming connection');
|
|
488
|
+
this.socket.disconnect();
|
|
489
|
+
this.socket = null;
|
|
490
|
+
}
|
|
282
491
|
}
|
|
283
492
|
}
|
|
284
493
|
exports.KumoAPI = KumoAPI;
|
package/dist/platform.d.ts
CHANGED
|
@@ -9,8 +9,20 @@ export declare class KumoV3Platform implements DynamicPlatformPlugin {
|
|
|
9
9
|
private readonly accessoryHandlers;
|
|
10
10
|
private readonly kumoAPI;
|
|
11
11
|
private readonly kumoConfig;
|
|
12
|
+
private readonly sitePollers;
|
|
13
|
+
private readonly siteAccessories;
|
|
14
|
+
private readonly degradedPollInterval;
|
|
15
|
+
private isStreamingHealthy;
|
|
16
|
+
private isDegradedMode;
|
|
12
17
|
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
13
18
|
private cleanup;
|
|
14
19
|
configureAccessory(accessory: PlatformAccessory): void;
|
|
15
20
|
discoverDevices(): Promise<void>;
|
|
21
|
+
private startSitePoller;
|
|
22
|
+
private pollSite;
|
|
23
|
+
private handleStreamingHealthChange;
|
|
24
|
+
private enterDegradedMode;
|
|
25
|
+
private exitDegradedMode;
|
|
26
|
+
private restartAllPollers;
|
|
27
|
+
private stopAllPollers;
|
|
16
28
|
}
|
package/dist/platform.js
CHANGED
|
@@ -13,6 +13,10 @@ class KumoV3Platform {
|
|
|
13
13
|
this.Characteristic = this.api.hap.Characteristic;
|
|
14
14
|
this.accessories = [];
|
|
15
15
|
this.accessoryHandlers = [];
|
|
16
|
+
this.sitePollers = new Map();
|
|
17
|
+
this.siteAccessories = new Map();
|
|
18
|
+
this.isStreamingHealthy = false;
|
|
19
|
+
this.isDegradedMode = false;
|
|
16
20
|
this.kumoConfig = config;
|
|
17
21
|
this.log.debug('Initializing platform:', this.config.name);
|
|
18
22
|
const kumoConfig = this.kumoConfig;
|
|
@@ -34,7 +38,15 @@ class KumoV3Platform {
|
|
|
34
38
|
throw new Error('Invalid poll interval');
|
|
35
39
|
}
|
|
36
40
|
}
|
|
41
|
+
this.degradedPollInterval = (kumoConfig.degradedPollInterval || 10) * 1000;
|
|
42
|
+
this.log.debug(`Degraded polling interval: ${this.degradedPollInterval / 1000}s`);
|
|
37
43
|
this.kumoAPI = new kumo_api_1.KumoAPI(kumoConfig.username, kumoConfig.password, this.log, kumoConfig.debug || false);
|
|
44
|
+
const healthCheckInterval = kumoConfig.streamingHealthCheckInterval || 30;
|
|
45
|
+
const staleThreshold = kumoConfig.streamingStaleThreshold || 60;
|
|
46
|
+
this.kumoAPI.setStreamingHealthConfig(healthCheckInterval, staleThreshold);
|
|
47
|
+
this.kumoAPI.onStreamingHealthChange((isHealthy) => {
|
|
48
|
+
this.handleStreamingHealthChange(isHealthy);
|
|
49
|
+
});
|
|
38
50
|
this.api.on('didFinishLaunching', () => {
|
|
39
51
|
log.debug('Executed didFinishLaunching callback');
|
|
40
52
|
this.discoverDevices();
|
|
@@ -45,6 +57,11 @@ class KumoV3Platform {
|
|
|
45
57
|
});
|
|
46
58
|
}
|
|
47
59
|
cleanup() {
|
|
60
|
+
for (const [siteId, timer] of this.sitePollers) {
|
|
61
|
+
clearInterval(timer);
|
|
62
|
+
this.log.debug(`Stopped site poller for ${siteId}`);
|
|
63
|
+
}
|
|
64
|
+
this.sitePollers.clear();
|
|
48
65
|
for (const handler of this.accessoryHandlers) {
|
|
49
66
|
handler.destroy();
|
|
50
67
|
}
|
|
@@ -82,7 +99,7 @@ class KumoV3Platform {
|
|
|
82
99
|
const deviceSerial = zone.adapter.deviceSerial;
|
|
83
100
|
const displayName = zone.name;
|
|
84
101
|
if ((_a = this.kumoConfig.excludeDevices) === null || _a === void 0 ? void 0 : _a.includes(deviceSerial)) {
|
|
85
|
-
this.log.info(`
|
|
102
|
+
this.log.info(`Hiding device from HomeKit: ${displayName} (${deviceSerial})`);
|
|
86
103
|
continue;
|
|
87
104
|
}
|
|
88
105
|
const uuid = this.api.hap.uuid.generate(deviceSerial);
|
|
@@ -128,10 +145,164 @@ class KumoV3Platform {
|
|
|
128
145
|
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, staleAccessories);
|
|
129
146
|
}
|
|
130
147
|
this.log.info('Device discovery completed');
|
|
148
|
+
const allDeviceSerials = discoveredDevices.map(d => d.deviceSerial);
|
|
149
|
+
if (allDeviceSerials.length > 0) {
|
|
150
|
+
this.log.info('Starting streaming for real-time updates...');
|
|
151
|
+
const streamingStarted = await this.kumoAPI.startStreaming(allDeviceSerials);
|
|
152
|
+
if (streamingStarted) {
|
|
153
|
+
this.log.info('✓ Streaming enabled - devices will update in real-time');
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.log.warn('Streaming failed to start - falling back to polling');
|
|
157
|
+
}
|
|
158
|
+
const healthCheckInterval = this.kumoConfig.streamingHealthCheckInterval || 30;
|
|
159
|
+
const staleThreshold = this.kumoConfig.streamingStaleThreshold || 60;
|
|
160
|
+
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
161
|
+
this.log.info('Mitsubishi Comfort Plugin Configuration');
|
|
162
|
+
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
163
|
+
this.log.info(`Streaming: ${streamingStarted ? 'ENABLED' : 'DISABLED'}`);
|
|
164
|
+
this.log.info(`Polling mode: ${this.kumoConfig.disablePolling ? 'On-demand only' : 'Enabled'}`);
|
|
165
|
+
this.log.info(`Normal poll interval: ${(this.kumoConfig.pollInterval || 30)}s`);
|
|
166
|
+
this.log.info(`Degraded poll interval: ${this.degradedPollInterval / 1000}s`);
|
|
167
|
+
this.log.info(`Health check interval: ${healthCheckInterval}s`);
|
|
168
|
+
this.log.info(`Stale threshold: ${staleThreshold}s`);
|
|
169
|
+
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
170
|
+
if (streamingStarted) {
|
|
171
|
+
if (this.kumoConfig.disablePolling) {
|
|
172
|
+
this.log.info('Strategy: Streaming primary, polling fallback only');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
this.log.info('Strategy: Streaming primary, polling supplemental');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!this.kumoConfig.disablePolling) {
|
|
180
|
+
const uniqueSites = new Set(discoveredDevices.map(d => { var _a; return (_a = this.accessories.find(a => a.UUID === d.uuid)) === null || _a === void 0 ? void 0 : _a.context.device.siteId; }).filter(Boolean));
|
|
181
|
+
this.log.info(`Initializing pollers for ${uniqueSites.size} site(s)`);
|
|
182
|
+
for (const siteId of uniqueSites) {
|
|
183
|
+
this.startSitePoller(siteId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
this.log.info('Polling disabled - will activate only if streaming fails');
|
|
188
|
+
}
|
|
131
189
|
}
|
|
132
190
|
catch (error) {
|
|
133
191
|
this.log.error('Error during device discovery:', error);
|
|
134
192
|
}
|
|
135
193
|
}
|
|
194
|
+
startSitePoller(siteId) {
|
|
195
|
+
if (this.sitePollers.has(siteId)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (this.isStreamingHealthy && this.kumoConfig.disablePolling) {
|
|
199
|
+
this.log.info(`Skipping poller for site ${siteId} (streaming healthy, polling disabled)`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const interval = this.isDegradedMode ? this.degradedPollInterval : (this.kumoConfig.pollInterval || 30) * 1000;
|
|
203
|
+
const intervalSec = interval / 1000;
|
|
204
|
+
const mode = this.isDegradedMode ? 'DEGRADED' : 'NORMAL';
|
|
205
|
+
this.log.info(`Starting ${mode} poller for site ${siteId}: ${intervalSec}s intervals`);
|
|
206
|
+
const accessories = this.accessoryHandlers.filter(handler => handler.getSiteId() === siteId);
|
|
207
|
+
this.siteAccessories.set(siteId, accessories);
|
|
208
|
+
this.pollSite(siteId);
|
|
209
|
+
const timer = setInterval(() => {
|
|
210
|
+
this.pollSite(siteId);
|
|
211
|
+
}, interval);
|
|
212
|
+
this.sitePollers.set(siteId, timer);
|
|
213
|
+
}
|
|
214
|
+
async pollSite(siteId) {
|
|
215
|
+
try {
|
|
216
|
+
const mode = this.isDegradedMode ? 'DEGRADED' : 'NORMAL';
|
|
217
|
+
const health = this.isStreamingHealthy ? 'healthy' : 'unhealthy';
|
|
218
|
+
this.log.debug(`[${mode}] Polling site ${siteId} (streaming: ${health})`);
|
|
219
|
+
const zones = await this.kumoAPI.getZones(siteId);
|
|
220
|
+
const accessories = this.siteAccessories.get(siteId) || [];
|
|
221
|
+
for (const handler of accessories) {
|
|
222
|
+
const zone = zones.find(z => z.adapter.deviceSerial === handler.getDeviceSerial());
|
|
223
|
+
if (zone) {
|
|
224
|
+
handler.updateFromZone(zone);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.log.warn(`Zone not found for device: ${handler.getDeviceSerial()}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
this.log.error(`Error polling site ${siteId}:`, error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
handleStreamingHealthChange(isHealthy) {
|
|
236
|
+
const wasHealthy = this.isStreamingHealthy;
|
|
237
|
+
this.isStreamingHealthy = isHealthy;
|
|
238
|
+
if (wasHealthy && !isHealthy) {
|
|
239
|
+
this.log.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
240
|
+
this.log.warn('⚠ STREAMING INTERRUPTED');
|
|
241
|
+
this.log.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
242
|
+
this.enterDegradedMode();
|
|
243
|
+
}
|
|
244
|
+
if (!wasHealthy && isHealthy) {
|
|
245
|
+
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
246
|
+
this.log.info('✓ STREAMING RESUMED');
|
|
247
|
+
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
248
|
+
this.exitDegradedMode();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
enterDegradedMode() {
|
|
252
|
+
if (this.isDegradedMode) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.isDegradedMode = true;
|
|
256
|
+
const intervalSec = this.degradedPollInterval / 1000;
|
|
257
|
+
this.log.warn(`→ Switching to DEGRADED MODE`);
|
|
258
|
+
this.log.warn(`→ Polling activated: ${intervalSec}s intervals`);
|
|
259
|
+
this.log.warn(`→ Updates will continue via API polling`);
|
|
260
|
+
if (this.kumoConfig.disablePolling) {
|
|
261
|
+
this.log.warn('→ Overriding disablePolling setting for fallback');
|
|
262
|
+
}
|
|
263
|
+
this.restartAllPollers(this.degradedPollInterval);
|
|
264
|
+
}
|
|
265
|
+
exitDegradedMode() {
|
|
266
|
+
if (!this.isDegradedMode) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.isDegradedMode = false;
|
|
270
|
+
if (this.kumoConfig.disablePolling) {
|
|
271
|
+
this.log.info('→ Returning to NORMAL MODE');
|
|
272
|
+
this.log.info('→ Polling halted (streaming active)');
|
|
273
|
+
this.log.info('→ Updates resume via real-time streaming');
|
|
274
|
+
this.stopAllPollers();
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const normalInterval = (this.kumoConfig.pollInterval || 30) * 1000;
|
|
278
|
+
const normalSec = normalInterval / 1000;
|
|
279
|
+
this.log.info('→ Returning to NORMAL MODE');
|
|
280
|
+
this.log.info(`→ Polling reduced to ${normalSec}s intervals`);
|
|
281
|
+
this.log.info('→ Primary updates via streaming');
|
|
282
|
+
this.restartAllPollers(normalInterval);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
restartAllPollers(intervalMs) {
|
|
286
|
+
const intervalSec = intervalMs / 1000;
|
|
287
|
+
for (const [siteId, timer] of this.sitePollers) {
|
|
288
|
+
clearInterval(timer);
|
|
289
|
+
this.pollSite(siteId);
|
|
290
|
+
const newTimer = setInterval(() => {
|
|
291
|
+
this.pollSite(siteId);
|
|
292
|
+
}, intervalMs);
|
|
293
|
+
this.sitePollers.set(siteId, newTimer);
|
|
294
|
+
this.log.debug(`Poller restarted for site ${siteId}: ${intervalSec}s interval`);
|
|
295
|
+
}
|
|
296
|
+
const siteCount = this.sitePollers.size;
|
|
297
|
+
this.log.info(`✓ ${siteCount} site poller(s) active at ${intervalSec}s intervals`);
|
|
298
|
+
}
|
|
299
|
+
stopAllPollers() {
|
|
300
|
+
for (const [siteId, timer] of this.sitePollers) {
|
|
301
|
+
clearInterval(timer);
|
|
302
|
+
this.log.debug(`Poller stopped for site ${siteId}`);
|
|
303
|
+
}
|
|
304
|
+
this.sitePollers.clear();
|
|
305
|
+
this.log.info('✓ All polling halted');
|
|
306
|
+
}
|
|
136
307
|
}
|
|
137
308
|
exports.KumoV3Platform = KumoV3Platform;
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const PLATFORM_NAME = "KumoV3";
|
|
2
2
|
export declare const PLUGIN_NAME = "homebridge-mitsubishi-comfort";
|
|
3
3
|
export declare const API_BASE_URL = "https://app-prod.kumocloud.com/v3";
|
|
4
|
+
export declare const SOCKET_BASE_URL = "https://socket-prod.kumocloud.com";
|
|
4
5
|
export declare const TOKEN_REFRESH_INTERVAL: number;
|
|
5
6
|
export declare const POLL_INTERVAL: number;
|
|
6
7
|
export declare const APP_VERSION = "3.2.3";
|
|
@@ -10,8 +11,12 @@ export interface KumoConfig {
|
|
|
10
11
|
username: string;
|
|
11
12
|
password: string;
|
|
12
13
|
pollInterval?: number;
|
|
14
|
+
disablePolling?: boolean;
|
|
13
15
|
debug?: boolean;
|
|
14
16
|
excludeDevices?: string[];
|
|
17
|
+
streamingHealthCheckInterval?: number;
|
|
18
|
+
streamingStaleThreshold?: number;
|
|
19
|
+
degradedPollInterval?: number;
|
|
15
20
|
}
|
|
16
21
|
export interface LoginResponse {
|
|
17
22
|
id: string;
|
|
@@ -71,7 +76,7 @@ export interface DeviceStatus {
|
|
|
71
76
|
export interface Commands {
|
|
72
77
|
spHeat?: number;
|
|
73
78
|
spCool?: number;
|
|
74
|
-
operationMode?: 'off' | 'heat' | 'cool' | 'auto';
|
|
79
|
+
operationMode?: 'off' | 'heat' | 'cool' | 'auto' | 'vent' | 'dry';
|
|
75
80
|
fanSpeed?: 'auto' | 'low' | 'medium' | 'high';
|
|
76
81
|
}
|
|
77
82
|
export interface SendCommandRequest {
|
package/dist/settings.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.APP_VERSION = exports.POLL_INTERVAL = exports.TOKEN_REFRESH_INTERVAL = exports.API_BASE_URL = exports.PLUGIN_NAME = exports.PLATFORM_NAME = void 0;
|
|
3
|
+
exports.APP_VERSION = exports.POLL_INTERVAL = exports.TOKEN_REFRESH_INTERVAL = exports.SOCKET_BASE_URL = exports.API_BASE_URL = exports.PLUGIN_NAME = exports.PLATFORM_NAME = void 0;
|
|
4
4
|
exports.PLATFORM_NAME = 'KumoV3';
|
|
5
5
|
exports.PLUGIN_NAME = 'homebridge-mitsubishi-comfort';
|
|
6
6
|
exports.API_BASE_URL = 'https://app-prod.kumocloud.com/v3';
|
|
7
|
+
exports.SOCKET_BASE_URL = 'https://socket-prod.kumocloud.com';
|
|
7
8
|
exports.TOKEN_REFRESH_INTERVAL = 15 * 60 * 1000;
|
|
8
9
|
exports.POLL_INTERVAL = 30 * 1000;
|
|
9
10
|
exports.APP_VERSION = '3.2.3';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-mitsubishi-comfort",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Homebridge plugin for Mitsubishi heat pumps using Kumo Cloud v3 API",
|
|
5
5
|
"author": "burtherman",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/burtherman/homebridge-mitsubishi-comfort#readme",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"node-fetch": "^3.2.0"
|
|
36
|
+
"node-fetch": "^3.2.0",
|
|
37
|
+
"socket.io-client": "^4.8.1"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/node": "^15.12.3",
|