matterbridge-melcloud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +239 -0
- package/LICENSE +202 -0
- package/README.md +163 -0
- package/dist/melcloudApi.js +170 -0
- package/dist/module.js +264 -0
- package/matterbridge-melcloud.config.json +11 -0
- package/matterbridge-melcloud.schema.json +73 -0
- package/npm-shrinkwrap.json +377 -0
- package/package.json +77 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
const MELCLOUD_ENDPOINT = 'https://app.melcloud.com/Mitsubishi.Wifi.Client';
|
|
3
|
+
export var MelCloudMode;
|
|
4
|
+
(function (MelCloudMode) {
|
|
5
|
+
MelCloudMode[MelCloudMode["HEATING"] = 1] = "HEATING";
|
|
6
|
+
MelCloudMode[MelCloudMode["DRYING"] = 2] = "DRYING";
|
|
7
|
+
MelCloudMode[MelCloudMode["COOLING"] = 3] = "COOLING";
|
|
8
|
+
MelCloudMode[MelCloudMode["FAN"] = 7] = "FAN";
|
|
9
|
+
MelCloudMode[MelCloudMode["AUTO"] = 8] = "AUTO";
|
|
10
|
+
})(MelCloudMode || (MelCloudMode = {}));
|
|
11
|
+
export var MatterThermostatMode;
|
|
12
|
+
(function (MatterThermostatMode) {
|
|
13
|
+
MatterThermostatMode[MatterThermostatMode["OFF"] = 0] = "OFF";
|
|
14
|
+
MatterThermostatMode[MatterThermostatMode["AUTO"] = 1] = "AUTO";
|
|
15
|
+
MatterThermostatMode[MatterThermostatMode["COOL"] = 3] = "COOL";
|
|
16
|
+
MatterThermostatMode[MatterThermostatMode["HEAT"] = 4] = "HEAT";
|
|
17
|
+
})(MatterThermostatMode || (MatterThermostatMode = {}));
|
|
18
|
+
export class MelCloudApi {
|
|
19
|
+
client;
|
|
20
|
+
contextKey = null;
|
|
21
|
+
username;
|
|
22
|
+
password;
|
|
23
|
+
constructor(username, password) {
|
|
24
|
+
this.username = username;
|
|
25
|
+
this.password = password;
|
|
26
|
+
this.client = axios.create({
|
|
27
|
+
baseURL: MELCLOUD_ENDPOINT,
|
|
28
|
+
headers: {
|
|
29
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:73.0)',
|
|
30
|
+
Accept: 'application/json, text/javascript, */*; q=0.01',
|
|
31
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async connect() {
|
|
37
|
+
try {
|
|
38
|
+
const response = await this.client.post('/Login/ClientLogin', {
|
|
39
|
+
Email: this.username,
|
|
40
|
+
Password: this.password,
|
|
41
|
+
Language: 0,
|
|
42
|
+
AppVersion: '1.19.1.1',
|
|
43
|
+
Persist: true,
|
|
44
|
+
CaptchaResponse: null,
|
|
45
|
+
});
|
|
46
|
+
if (!response.data.ErrorId) {
|
|
47
|
+
this.contextKey = response.data.LoginData.ContextKey;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
isConnected() {
|
|
57
|
+
return this.contextKey !== null;
|
|
58
|
+
}
|
|
59
|
+
getAuthHeaders() {
|
|
60
|
+
return {
|
|
61
|
+
'X-MitsContextKey': this.contextKey,
|
|
62
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
63
|
+
Cookie: 'policyaccepted=true',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async loadDevices() {
|
|
67
|
+
if (!this.contextKey) {
|
|
68
|
+
throw new Error('Not connected to MelCloud');
|
|
69
|
+
}
|
|
70
|
+
const response = await this.client.get('/User/ListDevices', {
|
|
71
|
+
headers: this.getAuthHeaders(),
|
|
72
|
+
});
|
|
73
|
+
const devices = [];
|
|
74
|
+
for (const house of response.data) {
|
|
75
|
+
if (house.Structure?.Devices) {
|
|
76
|
+
devices.push(...house.Structure.Devices);
|
|
77
|
+
}
|
|
78
|
+
if (house.Structure?.Areas) {
|
|
79
|
+
for (const area of house.Structure.Areas) {
|
|
80
|
+
if (area.Devices) {
|
|
81
|
+
devices.push(...area.Devices);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (house.Structure?.Floors) {
|
|
86
|
+
for (const floor of house.Structure.Floors) {
|
|
87
|
+
if (floor.Devices) {
|
|
88
|
+
devices.push(...floor.Devices);
|
|
89
|
+
}
|
|
90
|
+
if (floor.Areas) {
|
|
91
|
+
for (const area of floor.Areas) {
|
|
92
|
+
if (area.Devices) {
|
|
93
|
+
devices.push(...area.Devices);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return devices;
|
|
101
|
+
}
|
|
102
|
+
async getDeviceState(deviceId, buildingId) {
|
|
103
|
+
if (!this.contextKey) {
|
|
104
|
+
throw new Error('Not connected to MelCloud');
|
|
105
|
+
}
|
|
106
|
+
const response = await this.client.get('/Device/Get', {
|
|
107
|
+
params: { id: deviceId, buildingID: buildingId },
|
|
108
|
+
headers: this.getAuthHeaders(),
|
|
109
|
+
});
|
|
110
|
+
return response.data;
|
|
111
|
+
}
|
|
112
|
+
async setDeviceState(deviceId, buildingId, updates) {
|
|
113
|
+
if (!this.contextKey) {
|
|
114
|
+
throw new Error('Not connected to MelCloud');
|
|
115
|
+
}
|
|
116
|
+
const currentState = await this.getDeviceState(deviceId, buildingId);
|
|
117
|
+
const newState = {
|
|
118
|
+
...currentState,
|
|
119
|
+
...updates,
|
|
120
|
+
HasPendingCommand: true,
|
|
121
|
+
};
|
|
122
|
+
await this.client.post('/Device/SetAta', newState, {
|
|
123
|
+
headers: this.getAuthHeaders(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async setPower(deviceId, buildingId, power) {
|
|
127
|
+
await this.setDeviceState(deviceId, buildingId, {
|
|
128
|
+
EffectiveFlags: 1,
|
|
129
|
+
Power: power,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async setTemperature(deviceId, buildingId, temperature) {
|
|
133
|
+
await this.setDeviceState(deviceId, buildingId, {
|
|
134
|
+
EffectiveFlags: 4,
|
|
135
|
+
SetTemperature: temperature,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async setOperationMode(deviceId, buildingId, mode) {
|
|
139
|
+
await this.setDeviceState(deviceId, buildingId, {
|
|
140
|
+
EffectiveFlags: 6,
|
|
141
|
+
OperationMode: mode,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
static matterToMelCloudMode(matterMode) {
|
|
145
|
+
switch (matterMode) {
|
|
146
|
+
case MatterThermostatMode.HEAT:
|
|
147
|
+
return MelCloudMode.HEATING;
|
|
148
|
+
case MatterThermostatMode.COOL:
|
|
149
|
+
return MelCloudMode.COOLING;
|
|
150
|
+
case MatterThermostatMode.AUTO:
|
|
151
|
+
return MelCloudMode.AUTO;
|
|
152
|
+
default:
|
|
153
|
+
return MelCloudMode.AUTO;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
static melCloudToMatterMode(melCloudMode) {
|
|
157
|
+
switch (melCloudMode) {
|
|
158
|
+
case MelCloudMode.HEATING:
|
|
159
|
+
return MatterThermostatMode.HEAT;
|
|
160
|
+
case MelCloudMode.COOLING:
|
|
161
|
+
case MelCloudMode.DRYING:
|
|
162
|
+
return MatterThermostatMode.COOL;
|
|
163
|
+
case MelCloudMode.AUTO:
|
|
164
|
+
case MelCloudMode.FAN:
|
|
165
|
+
return MatterThermostatMode.AUTO;
|
|
166
|
+
default:
|
|
167
|
+
return MatterThermostatMode.AUTO;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { MatterbridgeDynamicPlatform, MatterbridgeEndpoint, thermostatDevice } from 'matterbridge';
|
|
2
|
+
import { MelCloudApi, MelCloudMode } from './melcloudApi.js';
|
|
3
|
+
const SystemMode = {
|
|
4
|
+
Off: 0,
|
|
5
|
+
Auto: 1,
|
|
6
|
+
Cool: 3,
|
|
7
|
+
Heat: 4,
|
|
8
|
+
EmergencyHeat: 5,
|
|
9
|
+
Precooling: 6,
|
|
10
|
+
FanOnly: 7,
|
|
11
|
+
Dry: 8,
|
|
12
|
+
Sleep: 9,
|
|
13
|
+
};
|
|
14
|
+
const THERMOSTAT_CLUSTER = 'Thermostat';
|
|
15
|
+
const ONOFF_CLUSTER = 'OnOff';
|
|
16
|
+
export default function initializePlugin(matterbridge, log, config) {
|
|
17
|
+
return new MelCloudPlatform(matterbridge, log, config);
|
|
18
|
+
}
|
|
19
|
+
export class MelCloudPlatform extends MatterbridgeDynamicPlatform {
|
|
20
|
+
melCloudApi = null;
|
|
21
|
+
deviceMap = new Map();
|
|
22
|
+
pollingTimer = null;
|
|
23
|
+
config;
|
|
24
|
+
constructor(matterbridge, log, config) {
|
|
25
|
+
super(matterbridge, log, config);
|
|
26
|
+
this.config = config;
|
|
27
|
+
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.4.0')) {
|
|
28
|
+
throw new Error(`This plugin requires Matterbridge version >= "3.4.0". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`);
|
|
29
|
+
}
|
|
30
|
+
this.log.info('Initializing MelCloud Platform...');
|
|
31
|
+
}
|
|
32
|
+
async onStart(reason) {
|
|
33
|
+
this.log.info(`onStart called with reason: ${reason ?? 'none'}`);
|
|
34
|
+
await this.ready;
|
|
35
|
+
await this.clearSelect();
|
|
36
|
+
if (!this.config.username || !this.config.password) {
|
|
37
|
+
this.log.error('MelCloud username and password are required. Please configure them in the plugin settings.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.melCloudApi = new MelCloudApi(this.config.username, this.config.password);
|
|
41
|
+
const connected = await this.melCloudApi.connect();
|
|
42
|
+
if (!connected) {
|
|
43
|
+
this.log.error('Failed to connect to MelCloud. Please check your credentials.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.log.info('Successfully connected to MelCloud');
|
|
47
|
+
await this.discoverDevices();
|
|
48
|
+
const interval = this.config.pollingInterval ?? 30000;
|
|
49
|
+
this.pollingTimer = setInterval(() => this.pollDevices(), interval);
|
|
50
|
+
}
|
|
51
|
+
async onConfigure() {
|
|
52
|
+
await super.onConfigure();
|
|
53
|
+
this.log.info('onConfigure called');
|
|
54
|
+
await this.pollDevices();
|
|
55
|
+
}
|
|
56
|
+
async onChangeLoggerLevel(logLevel) {
|
|
57
|
+
this.log.info(`onChangeLoggerLevel called with: ${logLevel}`);
|
|
58
|
+
}
|
|
59
|
+
async onShutdown(reason) {
|
|
60
|
+
await super.onShutdown(reason);
|
|
61
|
+
this.log.info(`onShutdown called with reason: ${reason ?? 'none'}`);
|
|
62
|
+
if (this.pollingTimer) {
|
|
63
|
+
clearInterval(this.pollingTimer);
|
|
64
|
+
this.pollingTimer = null;
|
|
65
|
+
}
|
|
66
|
+
if (this.config.unregisterOnShutdown === true) {
|
|
67
|
+
await this.unregisterAllDevices();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async discoverDevices() {
|
|
71
|
+
if (!this.melCloudApi) {
|
|
72
|
+
this.log.error('MelCloud API not initialized');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.log.info('Discovering MelCloud devices...');
|
|
76
|
+
try {
|
|
77
|
+
const devices = await this.melCloudApi.loadDevices();
|
|
78
|
+
this.log.info(`Found ${devices.length} MelCloud device(s)`);
|
|
79
|
+
for (const device of devices) {
|
|
80
|
+
if (device.Device?.DeviceType !== 0) {
|
|
81
|
+
this.log.info(`Skipping device ${device.DeviceName} (unsupported type: ${device.Device?.DeviceType})`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
await this.registerMelCloudDevice(device);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.log.error(`Failed to discover devices: ${error}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async registerMelCloudDevice(device) {
|
|
92
|
+
const deviceId = `melcloud-${device.DeviceID}`;
|
|
93
|
+
const serialNumber = `MC${device.DeviceID}`;
|
|
94
|
+
this.log.info(`Registering device: ${device.DeviceName} (ID: ${device.DeviceID})`);
|
|
95
|
+
let initialState;
|
|
96
|
+
try {
|
|
97
|
+
initialState = await this.melCloudApi.getDeviceState(device.DeviceID, device.BuildingID);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
this.log.error(`Failed to get initial state for ${device.DeviceName}: ${error}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const minTemp = device.MinTemperature ?? 16;
|
|
104
|
+
const maxTemp = device.MaxTemperature ?? 31;
|
|
105
|
+
const currentTemp = initialState.RoomTemperature ?? 20;
|
|
106
|
+
const targetTemp = initialState.SetTemperature ?? 21;
|
|
107
|
+
const systemMode = initialState.Power
|
|
108
|
+
? this.melCloudModeToMatterSystemMode(initialState.OperationMode)
|
|
109
|
+
: SystemMode.Off;
|
|
110
|
+
const thermostatEndpoint = new MatterbridgeEndpoint(thermostatDevice, { id: deviceId })
|
|
111
|
+
.createDefaultBridgedDeviceBasicInformationClusterServer(device.DeviceName, serialNumber, this.matterbridge.aggregatorVendorId, 'Mitsubishi Electric', 'MelCloud AC', device.DeviceID, '1.0.0')
|
|
112
|
+
.createDefaultPowerSourceWiredClusterServer()
|
|
113
|
+
.createDefaultOnOffClusterServer(initialState.Power)
|
|
114
|
+
.createDefaultThermostatClusterServer(currentTemp, targetTemp, targetTemp, 25, minTemp, maxTemp, minTemp, maxTemp);
|
|
115
|
+
thermostatEndpoint.addCommandHandler('setpointRaiseLower', async (data) => {
|
|
116
|
+
this.log.info(`setpointRaiseLower command: mode=${data.request.mode}, amount=${data.request.amount}`);
|
|
117
|
+
await this.handleSetpointRaiseLower(deviceId, data.request.mode, data.request.amount);
|
|
118
|
+
});
|
|
119
|
+
this.deviceMap.set(deviceId, {
|
|
120
|
+
deviceId: device.DeviceID,
|
|
121
|
+
buildingId: device.BuildingID,
|
|
122
|
+
endpoint: thermostatEndpoint,
|
|
123
|
+
minTemp,
|
|
124
|
+
maxTemp,
|
|
125
|
+
});
|
|
126
|
+
this.setSelectDevice(serialNumber, device.DeviceName);
|
|
127
|
+
const selected = this.validateDevice([device.DeviceName, serialNumber]);
|
|
128
|
+
if (selected) {
|
|
129
|
+
await this.registerDevice(thermostatEndpoint);
|
|
130
|
+
thermostatEndpoint.subscribeAttribute(THERMOSTAT_CLUSTER, 'systemMode', async (newValue) => {
|
|
131
|
+
await this.handleSystemModeChange(deviceId, newValue);
|
|
132
|
+
}, this.log);
|
|
133
|
+
thermostatEndpoint.subscribeAttribute(THERMOSTAT_CLUSTER, 'occupiedHeatingSetpoint', async (newValue) => {
|
|
134
|
+
await this.handleSetpointChange(deviceId, newValue);
|
|
135
|
+
}, this.log);
|
|
136
|
+
thermostatEndpoint.subscribeAttribute(THERMOSTAT_CLUSTER, 'occupiedCoolingSetpoint', async (newValue) => {
|
|
137
|
+
await this.handleSetpointChange(deviceId, newValue);
|
|
138
|
+
}, this.log);
|
|
139
|
+
thermostatEndpoint.subscribeAttribute(ONOFF_CLUSTER, 'onOff', async (newValue) => {
|
|
140
|
+
await this.handleOnOffChange(deviceId, newValue);
|
|
141
|
+
}, this.log);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
melCloudModeToMatterSystemMode(melCloudMode) {
|
|
145
|
+
switch (melCloudMode) {
|
|
146
|
+
case MelCloudMode.HEATING:
|
|
147
|
+
return SystemMode.Heat;
|
|
148
|
+
case MelCloudMode.COOLING:
|
|
149
|
+
return SystemMode.Cool;
|
|
150
|
+
case MelCloudMode.DRYING:
|
|
151
|
+
return SystemMode.Dry;
|
|
152
|
+
case MelCloudMode.AUTO:
|
|
153
|
+
return SystemMode.Auto;
|
|
154
|
+
case MelCloudMode.FAN:
|
|
155
|
+
return SystemMode.FanOnly;
|
|
156
|
+
default:
|
|
157
|
+
return SystemMode.Auto;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
matterSystemModeToMelCloud(systemMode) {
|
|
161
|
+
switch (systemMode) {
|
|
162
|
+
case SystemMode.Off:
|
|
163
|
+
return { power: false };
|
|
164
|
+
case SystemMode.Heat:
|
|
165
|
+
return { power: true, mode: MelCloudMode.HEATING };
|
|
166
|
+
case SystemMode.Cool:
|
|
167
|
+
return { power: true, mode: MelCloudMode.COOLING };
|
|
168
|
+
case SystemMode.Auto:
|
|
169
|
+
return { power: true, mode: MelCloudMode.AUTO };
|
|
170
|
+
case SystemMode.FanOnly:
|
|
171
|
+
return { power: true, mode: MelCloudMode.FAN };
|
|
172
|
+
case SystemMode.Dry:
|
|
173
|
+
return { power: true, mode: MelCloudMode.DRYING };
|
|
174
|
+
default:
|
|
175
|
+
return { power: true, mode: MelCloudMode.AUTO };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async handleOnOffChange(deviceId, onOff) {
|
|
179
|
+
const deviceInfo = this.deviceMap.get(deviceId);
|
|
180
|
+
if (!deviceInfo || !this.melCloudApi)
|
|
181
|
+
return;
|
|
182
|
+
this.log.info(`OnOff change for ${deviceId}: ${onOff}`);
|
|
183
|
+
try {
|
|
184
|
+
await this.melCloudApi.setPower(deviceInfo.deviceId, deviceInfo.buildingId, onOff);
|
|
185
|
+
if (!onOff) {
|
|
186
|
+
await deviceInfo.endpoint.setAttribute(THERMOSTAT_CLUSTER, 'systemMode', SystemMode.Off, this.log);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.log.error(`Failed to set power: ${error}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async handleSystemModeChange(deviceId, newMode) {
|
|
194
|
+
const deviceInfo = this.deviceMap.get(deviceId);
|
|
195
|
+
if (!deviceInfo || !this.melCloudApi)
|
|
196
|
+
return;
|
|
197
|
+
this.log.info(`System mode change for ${deviceId}: ${newMode}`);
|
|
198
|
+
const { power, mode } = this.matterSystemModeToMelCloud(newMode);
|
|
199
|
+
try {
|
|
200
|
+
await this.melCloudApi.setPower(deviceInfo.deviceId, deviceInfo.buildingId, power);
|
|
201
|
+
await deviceInfo.endpoint.setAttribute(ONOFF_CLUSTER, 'onOff', power, this.log);
|
|
202
|
+
if (power && mode !== undefined) {
|
|
203
|
+
await this.melCloudApi.setOperationMode(deviceInfo.deviceId, deviceInfo.buildingId, mode);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
this.log.error(`Failed to set system mode: ${error}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async handleSetpointChange(deviceId, newSetpoint) {
|
|
211
|
+
const deviceInfo = this.deviceMap.get(deviceId);
|
|
212
|
+
if (!deviceInfo || !this.melCloudApi)
|
|
213
|
+
return;
|
|
214
|
+
const temperature = newSetpoint / 100;
|
|
215
|
+
const clampedTemp = Math.max(deviceInfo.minTemp, Math.min(deviceInfo.maxTemp, temperature));
|
|
216
|
+
this.log.info(`Setpoint change for ${deviceId}: ${clampedTemp}°C`);
|
|
217
|
+
try {
|
|
218
|
+
await this.melCloudApi.setTemperature(deviceInfo.deviceId, deviceInfo.buildingId, clampedTemp);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
this.log.error(`Failed to set temperature: ${error}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async handleSetpointRaiseLower(deviceId, mode, amount) {
|
|
225
|
+
const deviceInfo = this.deviceMap.get(deviceId);
|
|
226
|
+
if (!deviceInfo || !this.melCloudApi)
|
|
227
|
+
return;
|
|
228
|
+
try {
|
|
229
|
+
const state = await this.melCloudApi.getDeviceState(deviceInfo.deviceId, deviceInfo.buildingId);
|
|
230
|
+
const currentSetpoint = state.SetTemperature;
|
|
231
|
+
const delta = amount / 10;
|
|
232
|
+
const newSetpoint = Math.max(deviceInfo.minTemp, Math.min(deviceInfo.maxTemp, currentSetpoint + delta));
|
|
233
|
+
await this.melCloudApi.setTemperature(deviceInfo.deviceId, deviceInfo.buildingId, newSetpoint);
|
|
234
|
+
await deviceInfo.endpoint.setAttribute(THERMOSTAT_CLUSTER, 'occupiedHeatingSetpoint', newSetpoint * 100, this.log);
|
|
235
|
+
await deviceInfo.endpoint.setAttribute(THERMOSTAT_CLUSTER, 'occupiedCoolingSetpoint', newSetpoint * 100, this.log);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
this.log.error(`Failed to raise/lower setpoint: ${error}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async pollDevices() {
|
|
242
|
+
if (!this.melCloudApi)
|
|
243
|
+
return;
|
|
244
|
+
for (const [deviceId, deviceInfo] of this.deviceMap) {
|
|
245
|
+
try {
|
|
246
|
+
const state = await this.melCloudApi.getDeviceState(deviceInfo.deviceId, deviceInfo.buildingId);
|
|
247
|
+
const localTemp = (state.RoomTemperature ?? 20) * 100;
|
|
248
|
+
await deviceInfo.endpoint.setAttribute(THERMOSTAT_CLUSTER, 'localTemperature', localTemp, this.log);
|
|
249
|
+
const setpoint = (state.SetTemperature ?? 21) * 100;
|
|
250
|
+
await deviceInfo.endpoint.setAttribute(THERMOSTAT_CLUSTER, 'occupiedHeatingSetpoint', setpoint, this.log);
|
|
251
|
+
await deviceInfo.endpoint.setAttribute(THERMOSTAT_CLUSTER, 'occupiedCoolingSetpoint', setpoint, this.log);
|
|
252
|
+
const polledSystemMode = state.Power
|
|
253
|
+
? this.melCloudModeToMatterSystemMode(state.OperationMode)
|
|
254
|
+
: SystemMode.Off;
|
|
255
|
+
await deviceInfo.endpoint.setAttribute(THERMOSTAT_CLUSTER, 'systemMode', polledSystemMode, this.log);
|
|
256
|
+
await deviceInfo.endpoint.setAttribute(ONOFF_CLUSTER, 'onOff', state.Power, this.log);
|
|
257
|
+
this.log.debug?.(`Polled ${deviceId}: ${state.RoomTemperature}°C, setpoint: ${state.SetTemperature}°C, power: ${state.Power}, mode: ${polledSystemMode}`);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
this.log.error(`Failed to poll device ${deviceId}: ${error}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Matterbridge MelCloud Plugin",
|
|
3
|
+
"description": "matterbridge-melcloud - Mitsubishi Air Conditioning via MelCloud",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": {
|
|
7
|
+
"title": "Plugin Name",
|
|
8
|
+
"description": "Plugin name",
|
|
9
|
+
"type": "string",
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"ui:widget": "hidden"
|
|
12
|
+
},
|
|
13
|
+
"type": {
|
|
14
|
+
"title": "Plugin Type",
|
|
15
|
+
"description": "Plugin type",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"readOnly": true,
|
|
18
|
+
"ui:widget": "hidden"
|
|
19
|
+
},
|
|
20
|
+
"username": {
|
|
21
|
+
"title": "MelCloud Username",
|
|
22
|
+
"description": "Your MelCloud account email address",
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
"password": {
|
|
26
|
+
"title": "MelCloud Password",
|
|
27
|
+
"description": "Your MelCloud account password",
|
|
28
|
+
"type": "string",
|
|
29
|
+
"ui:widget": "password"
|
|
30
|
+
},
|
|
31
|
+
"pollingInterval": {
|
|
32
|
+
"title": "Polling Interval (ms)",
|
|
33
|
+
"description": "How often to poll MelCloud for device state updates (in milliseconds). Default: 30000 (30 seconds)",
|
|
34
|
+
"type": "number",
|
|
35
|
+
"default": 30000,
|
|
36
|
+
"minimum": 10000,
|
|
37
|
+
"maximum": 300000
|
|
38
|
+
},
|
|
39
|
+
"whiteList": {
|
|
40
|
+
"title": "White List",
|
|
41
|
+
"description": "Only the devices in the list will be exposed. If the list is empty, all the devices will be exposed.",
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"uniqueItems": true,
|
|
47
|
+
"selectFrom": "name"
|
|
48
|
+
},
|
|
49
|
+
"blackList": {
|
|
50
|
+
"title": "Black List",
|
|
51
|
+
"description": "The devices in the list will not be exposed. If the list is empty, no devices will be excluded.",
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"uniqueItems": true,
|
|
57
|
+
"selectFrom": "name"
|
|
58
|
+
},
|
|
59
|
+
"debug": {
|
|
60
|
+
"title": "Enable Debug",
|
|
61
|
+
"description": "Enable debug mode for the plugin. Only for debugging purposes, not recommended for production use.",
|
|
62
|
+
"type": "boolean",
|
|
63
|
+
"default": false
|
|
64
|
+
},
|
|
65
|
+
"unregisterOnShutdown": {
|
|
66
|
+
"title": "Unregister On Shutdown",
|
|
67
|
+
"description": "Unregister all devices on shutdown. Only for debugging purposes, not recommended for production use.",
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"default": false
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"required": ["username", "password"]
|
|
73
|
+
}
|