homebridge-ac-freedom 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/config.schema.json +131 -0
- package/index.js +15 -0
- package/package.json +31 -0
- package/src/ac-accessory.js +481 -0
- package/src/broadlink-api.js +340 -0
- package/src/cloud-api.js +288 -0
- package/src/platform.js +140 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "AcFreedom",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": false,
|
|
5
|
+
"headerDisplay": "AUX Air Conditioner control via Broadlink (local) or AUX Cloud API.",
|
|
6
|
+
"schema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"title": "Platform Name",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"default": "AC Freedom",
|
|
13
|
+
"required": true
|
|
14
|
+
},
|
|
15
|
+
"devices": {
|
|
16
|
+
"title": "Devices",
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"name": {
|
|
22
|
+
"title": "Device Name",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"default": "AC Freedom",
|
|
25
|
+
"required": true
|
|
26
|
+
},
|
|
27
|
+
"connection": {
|
|
28
|
+
"title": "Connection Mode",
|
|
29
|
+
"type": "string",
|
|
30
|
+
"default": "cloud",
|
|
31
|
+
"oneOf": [
|
|
32
|
+
{ "title": "Cloud (AUX Cloud API)", "enum": ["cloud"] },
|
|
33
|
+
{ "title": "Local (Broadlink UDP)", "enum": ["local"] }
|
|
34
|
+
],
|
|
35
|
+
"required": true
|
|
36
|
+
},
|
|
37
|
+
"cloud": {
|
|
38
|
+
"title": "Cloud Settings",
|
|
39
|
+
"type": "object",
|
|
40
|
+
"condition": { "functionBody": "return model.devices[arrayIndices[0]].connection === 'cloud';" },
|
|
41
|
+
"properties": {
|
|
42
|
+
"email": {
|
|
43
|
+
"title": "Email",
|
|
44
|
+
"type": "string",
|
|
45
|
+
"required": true
|
|
46
|
+
},
|
|
47
|
+
"password": {
|
|
48
|
+
"title": "Password",
|
|
49
|
+
"type": "string",
|
|
50
|
+
"required": true
|
|
51
|
+
},
|
|
52
|
+
"region": {
|
|
53
|
+
"title": "Region",
|
|
54
|
+
"type": "string",
|
|
55
|
+
"default": "eu",
|
|
56
|
+
"oneOf": [
|
|
57
|
+
{ "title": "Europe", "enum": ["eu"] },
|
|
58
|
+
{ "title": "USA", "enum": ["usa"] },
|
|
59
|
+
{ "title": "China", "enum": ["cn"] },
|
|
60
|
+
{ "title": "Russia", "enum": ["rus"] }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"deviceId": {
|
|
64
|
+
"title": "Device ID (endpointId, leave empty for auto-detect)",
|
|
65
|
+
"type": "string"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"local": {
|
|
70
|
+
"title": "Local Settings",
|
|
71
|
+
"type": "object",
|
|
72
|
+
"condition": { "functionBody": "return model.devices[arrayIndices[0]].connection === 'local';" },
|
|
73
|
+
"properties": {
|
|
74
|
+
"ip": {
|
|
75
|
+
"title": "IP Address",
|
|
76
|
+
"type": "string",
|
|
77
|
+
"required": true
|
|
78
|
+
},
|
|
79
|
+
"mac": {
|
|
80
|
+
"title": "MAC Address (AA:BB:CC:DD:EE:FF)",
|
|
81
|
+
"type": "string",
|
|
82
|
+
"required": true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"hvacModes": {
|
|
87
|
+
"title": "Enabled HVAC Modes",
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"auto": { "title": "Auto", "type": "boolean", "default": true },
|
|
91
|
+
"cool": { "title": "Cool", "type": "boolean", "default": true },
|
|
92
|
+
"heat": { "title": "Heat", "type": "boolean", "default": true }
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"presets": {
|
|
96
|
+
"title": "Enabled Preset Modes (shown as switches in climate card)",
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"sleep": { "title": "Sleep Mode", "type": "boolean", "default": true },
|
|
100
|
+
"health": { "title": "Health / Ionizer", "type": "boolean", "default": true },
|
|
101
|
+
"eco": { "title": "Eco / Mildew Prevention", "type": "boolean", "default": true },
|
|
102
|
+
"clean": { "title": "Self Clean", "type": "boolean", "default": true }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"showDisplay": {
|
|
106
|
+
"title": "Show Display Switch",
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
"default": true
|
|
109
|
+
},
|
|
110
|
+
"pollInterval": {
|
|
111
|
+
"title": "Poll Interval (seconds)",
|
|
112
|
+
"type": "integer",
|
|
113
|
+
"default": 30,
|
|
114
|
+
"minimum": 5,
|
|
115
|
+
"maximum": 300
|
|
116
|
+
},
|
|
117
|
+
"tempStep": {
|
|
118
|
+
"title": "Temperature Step",
|
|
119
|
+
"type": "number",
|
|
120
|
+
"default": 0.5,
|
|
121
|
+
"oneOf": [
|
|
122
|
+
{ "title": "0.5 °C", "enum": [0.5] },
|
|
123
|
+
{ "title": "1 °C", "enum": [1] }
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* homebridge-ac-freedom
|
|
3
|
+
*
|
|
4
|
+
* Homebridge plugin for AUX air conditioners via Broadlink (local) and AUX Cloud API.
|
|
5
|
+
* Exposes HeaterCooler with linked Switch services for presets inside the climate card.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { AcFreedomPlatform } = require('./src/platform');
|
|
9
|
+
|
|
10
|
+
const PLUGIN_NAME = 'homebridge-ac-freedom';
|
|
11
|
+
const PLATFORM_NAME = 'AcFreedom';
|
|
12
|
+
|
|
13
|
+
module.exports = (api) => {
|
|
14
|
+
api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, AcFreedomPlatform);
|
|
15
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-ac-freedom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Homebridge plugin for AUX air conditioners via Broadlink (local UDP) and AUX Cloud API",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"lint": "eslint src/"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"homebridge-plugin",
|
|
11
|
+
"homebridge",
|
|
12
|
+
"aux",
|
|
13
|
+
"broadlink",
|
|
14
|
+
"air-conditioner",
|
|
15
|
+
"hvac",
|
|
16
|
+
"climate"
|
|
17
|
+
],
|
|
18
|
+
"author": "azadaydinli",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/azadaydinli/homebridge-ac-freedom.git"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"homebridge": ">=1.6.0",
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {},
|
|
29
|
+
"devDependencies": {},
|
|
30
|
+
"homepage": "https://github.com/azadaydinli/homebridge-ac-freedom"
|
|
31
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AcFreedomAccessory
|
|
3
|
+
*
|
|
4
|
+
* Exposes a HeaterCooler service with linked Switch services for preset modes.
|
|
5
|
+
* Presets (sleep, health, eco, clean) and display appear as toggles inside
|
|
6
|
+
* the climate card in HomeKit.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
// Cloud API param keys
|
|
12
|
+
const CLOUD = {
|
|
13
|
+
POWER: 'pwr',
|
|
14
|
+
MODE: 'ac_mode',
|
|
15
|
+
TEMP_TARGET: 'temp',
|
|
16
|
+
TEMP_AMBIENT: 'envtemp',
|
|
17
|
+
FAN_SPEED: 'ac_mark',
|
|
18
|
+
SWING_V: 'ac_vdir',
|
|
19
|
+
SWING_H: 'ac_hdir',
|
|
20
|
+
SLEEP: 'ac_slp',
|
|
21
|
+
HEALTH: 'ac_health',
|
|
22
|
+
ECO: 'mldprf',
|
|
23
|
+
CLEAN: 'ac_clean',
|
|
24
|
+
DISPLAY: 'scrdisp',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Cloud mode values: 0=COOL, 1=HEAT, 2=DRY, 3=FAN, 4=AUTO
|
|
28
|
+
const CLOUD_MODE = { AUTO: 4, COOL: 0, HEAT: 1, DRY: 2, FAN: 3 };
|
|
29
|
+
|
|
30
|
+
// Fan speed values
|
|
31
|
+
const FAN_SPEED = { AUTO: 0, LOW: 1, MEDIUM: 2, HIGH: 3, TURBO: 4, MUTE: 5 };
|
|
32
|
+
|
|
33
|
+
class AcFreedomAccessory {
|
|
34
|
+
constructor(platform, accessory, config, deviceApi) {
|
|
35
|
+
this.platform = platform;
|
|
36
|
+
this.accessory = accessory;
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.deviceApi = deviceApi;
|
|
39
|
+
this.log = platform.log;
|
|
40
|
+
|
|
41
|
+
this.Service = platform.api.hap.Service;
|
|
42
|
+
this.Characteristic = platform.api.hap.Characteristic;
|
|
43
|
+
|
|
44
|
+
// Current state cache
|
|
45
|
+
this.state = {
|
|
46
|
+
power: false,
|
|
47
|
+
mode: CLOUD_MODE.AUTO,
|
|
48
|
+
targetTemp: 24,
|
|
49
|
+
currentTemp: 24,
|
|
50
|
+
fanSpeed: FAN_SPEED.AUTO,
|
|
51
|
+
swingV: false,
|
|
52
|
+
swingH: false,
|
|
53
|
+
sleep: false,
|
|
54
|
+
health: false,
|
|
55
|
+
eco: false,
|
|
56
|
+
clean: false,
|
|
57
|
+
display: true,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.presetConfigs = {
|
|
61
|
+
sleep: { label: 'Sleep Mode', cloudKey: CLOUD.SLEEP, localAttr: 'sleep' },
|
|
62
|
+
health: { label: 'Health', cloudKey: CLOUD.HEALTH, localAttr: 'health' },
|
|
63
|
+
eco: { label: 'Eco', cloudKey: CLOUD.ECO, localAttr: 'mildew' },
|
|
64
|
+
clean: { label: 'Clean', cloudKey: CLOUD.CLEAN, localAttr: 'clean' },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this.setupAccessoryInfo();
|
|
68
|
+
this.setupHeaterCooler();
|
|
69
|
+
this.setupPresetSwitches();
|
|
70
|
+
this.setupDisplaySwitch();
|
|
71
|
+
|
|
72
|
+
// Start polling
|
|
73
|
+
const interval = (config.pollInterval || 30) * 1000;
|
|
74
|
+
this.pollTimer = setInterval(() => this.pollState(), interval);
|
|
75
|
+
this.pollState();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Accessory Information ──────────────────────────────────────
|
|
79
|
+
setupAccessoryInfo() {
|
|
80
|
+
const infoService = this.accessory.getService(this.Service.AccessoryInformation)
|
|
81
|
+
|| this.accessory.addService(this.Service.AccessoryInformation);
|
|
82
|
+
|
|
83
|
+
infoService
|
|
84
|
+
.setCharacteristic(this.Characteristic.Manufacturer, 'AUX')
|
|
85
|
+
.setCharacteristic(this.Characteristic.Model, 'AC Freedom')
|
|
86
|
+
.setCharacteristic(this.Characteristic.SerialNumber, this.config.name || 'AC-001');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── HeaterCooler Service ───────────────────────────────────────
|
|
90
|
+
setupHeaterCooler() {
|
|
91
|
+
this.heaterCooler = this.accessory.getService(this.Service.HeaterCooler)
|
|
92
|
+
|| this.accessory.addService(this.Service.HeaterCooler, this.config.name);
|
|
93
|
+
|
|
94
|
+
const C = this.Characteristic;
|
|
95
|
+
const hvac = this.config.hvacModes || { auto: true, cool: true, heat: true };
|
|
96
|
+
|
|
97
|
+
// Active (on/off)
|
|
98
|
+
this.heaterCooler.getCharacteristic(C.Active)
|
|
99
|
+
.onGet(() => this.state.power ? C.Active.ACTIVE : C.Active.INACTIVE)
|
|
100
|
+
.onSet(async (value) => {
|
|
101
|
+
this.state.power = value === C.Active.ACTIVE;
|
|
102
|
+
await this.sendPower(this.state.power);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Current state
|
|
106
|
+
this.heaterCooler.getCharacteristic(C.CurrentHeaterCoolerState)
|
|
107
|
+
.onGet(() => {
|
|
108
|
+
if (!this.state.power) return C.CurrentHeaterCoolerState.INACTIVE;
|
|
109
|
+
switch (this.state.mode) {
|
|
110
|
+
case CLOUD_MODE.HEAT: return C.CurrentHeaterCoolerState.HEATING;
|
|
111
|
+
case CLOUD_MODE.COOL: return C.CurrentHeaterCoolerState.COOLING;
|
|
112
|
+
default: return C.CurrentHeaterCoolerState.IDLE;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Target state (auto/heat/cool)
|
|
117
|
+
const validStates = [];
|
|
118
|
+
if (hvac.auto) validStates.push(C.TargetHeaterCoolerState.AUTO);
|
|
119
|
+
if (hvac.heat) validStates.push(C.TargetHeaterCoolerState.HEAT);
|
|
120
|
+
if (hvac.cool) validStates.push(C.TargetHeaterCoolerState.COOL);
|
|
121
|
+
if (validStates.length === 0) validStates.push(C.TargetHeaterCoolerState.AUTO);
|
|
122
|
+
|
|
123
|
+
this.heaterCooler.getCharacteristic(C.TargetHeaterCoolerState)
|
|
124
|
+
.setProps({ validValues: validStates })
|
|
125
|
+
.onGet(() => {
|
|
126
|
+
switch (this.state.mode) {
|
|
127
|
+
case CLOUD_MODE.HEAT: return C.TargetHeaterCoolerState.HEAT;
|
|
128
|
+
case CLOUD_MODE.COOL: return C.TargetHeaterCoolerState.COOL;
|
|
129
|
+
default: return C.TargetHeaterCoolerState.AUTO;
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.onSet(async (value) => {
|
|
133
|
+
let mode;
|
|
134
|
+
switch (value) {
|
|
135
|
+
case C.TargetHeaterCoolerState.HEAT: mode = CLOUD_MODE.HEAT; break;
|
|
136
|
+
case C.TargetHeaterCoolerState.COOL: mode = CLOUD_MODE.COOL; break;
|
|
137
|
+
default: mode = CLOUD_MODE.AUTO; break;
|
|
138
|
+
}
|
|
139
|
+
this.state.mode = mode;
|
|
140
|
+
await this.sendMode(mode);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Current temperature
|
|
144
|
+
this.heaterCooler.getCharacteristic(C.CurrentTemperature)
|
|
145
|
+
.setProps({ minValue: -20, maxValue: 60 })
|
|
146
|
+
.onGet(() => this.state.currentTemp);
|
|
147
|
+
|
|
148
|
+
// Cooling threshold temperature
|
|
149
|
+
const step = this.config.tempStep || 0.5;
|
|
150
|
+
this.heaterCooler.getCharacteristic(C.CoolingThresholdTemperature)
|
|
151
|
+
.setProps({ minValue: 16, maxValue: 32, minStep: step })
|
|
152
|
+
.onGet(() => this.state.targetTemp)
|
|
153
|
+
.onSet(async (value) => {
|
|
154
|
+
this.state.targetTemp = value;
|
|
155
|
+
await this.sendTemperature(value);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Heating threshold temperature
|
|
159
|
+
this.heaterCooler.getCharacteristic(C.HeatingThresholdTemperature)
|
|
160
|
+
.setProps({ minValue: 16, maxValue: 32, minStep: step })
|
|
161
|
+
.onGet(() => this.state.targetTemp)
|
|
162
|
+
.onSet(async (value) => {
|
|
163
|
+
this.state.targetTemp = value;
|
|
164
|
+
await this.sendTemperature(value);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Rotation speed (fan: 0=auto, 20=mute, 40=low, 60=med, 80=high, 100=turbo)
|
|
168
|
+
this.heaterCooler.getCharacteristic(C.RotationSpeed)
|
|
169
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: 20 })
|
|
170
|
+
.onGet(() => this.fanSpeedToPercent(this.state.fanSpeed))
|
|
171
|
+
.onSet(async (value) => {
|
|
172
|
+
this.state.fanSpeed = this.percentToFanSpeed(value);
|
|
173
|
+
await this.sendFanSpeed(this.state.fanSpeed);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Swing mode
|
|
177
|
+
this.heaterCooler.getCharacteristic(C.SwingMode)
|
|
178
|
+
.onGet(() => (this.state.swingV || this.state.swingH)
|
|
179
|
+
? C.SwingMode.SWING_ENABLED
|
|
180
|
+
: C.SwingMode.SWING_DISABLED)
|
|
181
|
+
.onSet(async (value) => {
|
|
182
|
+
const on = value === C.SwingMode.SWING_ENABLED;
|
|
183
|
+
this.state.swingV = on;
|
|
184
|
+
this.state.swingH = on;
|
|
185
|
+
await this.sendSwing(on, on);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Preset Switches (linked to HeaterCooler) ──────────────────
|
|
190
|
+
setupPresetSwitches() {
|
|
191
|
+
const presets = this.config.presets || { sleep: true, health: true, eco: true, clean: true };
|
|
192
|
+
|
|
193
|
+
this.presetSwitches = {};
|
|
194
|
+
|
|
195
|
+
for (const [key, cfg] of Object.entries(this.presetConfigs)) {
|
|
196
|
+
if (!presets[key]) {
|
|
197
|
+
// Remove if previously existed
|
|
198
|
+
const existing = this.accessory.getServiceById(this.Service.Switch, key);
|
|
199
|
+
if (existing) this.accessory.removeService(existing);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const switchService = this.accessory.getServiceById(this.Service.Switch, key)
|
|
204
|
+
|| this.accessory.addService(this.Service.Switch, cfg.label, key);
|
|
205
|
+
|
|
206
|
+
switchService.setCharacteristic(this.Characteristic.Name, cfg.label);
|
|
207
|
+
|
|
208
|
+
switchService.getCharacteristic(this.Characteristic.On)
|
|
209
|
+
.onGet(() => this.state[key])
|
|
210
|
+
.onSet(async (value) => {
|
|
211
|
+
// Turn off other presets when activating one
|
|
212
|
+
if (value) {
|
|
213
|
+
for (const otherKey of Object.keys(this.presetConfigs)) {
|
|
214
|
+
if (otherKey !== key) this.state[otherKey] = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.state[key] = value;
|
|
218
|
+
await this.sendPreset(key, value);
|
|
219
|
+
// Update other switches UI
|
|
220
|
+
this.refreshPresetSwitches(key);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Link to HeaterCooler so it appears inside the climate card
|
|
224
|
+
this.heaterCooler.addLinkedService(switchService);
|
|
225
|
+
this.presetSwitches[key] = switchService;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
refreshPresetSwitches(exceptKey) {
|
|
230
|
+
for (const [key, svc] of Object.entries(this.presetSwitches)) {
|
|
231
|
+
if (key !== exceptKey) {
|
|
232
|
+
svc.updateCharacteristic(this.Characteristic.On, this.state[key]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Display Switch (linked to HeaterCooler) ────────────────────
|
|
238
|
+
setupDisplaySwitch() {
|
|
239
|
+
if (this.config.showDisplay === false) {
|
|
240
|
+
const existing = this.accessory.getServiceById(this.Service.Switch, 'display');
|
|
241
|
+
if (existing) this.accessory.removeService(existing);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.displaySwitch = this.accessory.getServiceById(this.Service.Switch, 'display')
|
|
246
|
+
|| this.accessory.addService(this.Service.Switch, 'Display', 'display');
|
|
247
|
+
|
|
248
|
+
this.displaySwitch.setCharacteristic(this.Characteristic.Name, 'Display');
|
|
249
|
+
|
|
250
|
+
this.displaySwitch.getCharacteristic(this.Characteristic.On)
|
|
251
|
+
.onGet(() => this.state.display)
|
|
252
|
+
.onSet(async (value) => {
|
|
253
|
+
this.state.display = value;
|
|
254
|
+
await this.sendDisplay(value);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
this.heaterCooler.addLinkedService(this.displaySwitch);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Fan speed mapping ──────────────────────────────────────────
|
|
261
|
+
fanSpeedToPercent(speed) {
|
|
262
|
+
const map = { 0: 0, 5: 20, 1: 40, 2: 60, 3: 80, 4: 100 };
|
|
263
|
+
return map[speed] ?? 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
percentToFanSpeed(pct) {
|
|
267
|
+
if (pct <= 0) return FAN_SPEED.AUTO;
|
|
268
|
+
if (pct <= 20) return FAN_SPEED.MUTE;
|
|
269
|
+
if (pct <= 40) return FAN_SPEED.LOW;
|
|
270
|
+
if (pct <= 60) return FAN_SPEED.MEDIUM;
|
|
271
|
+
if (pct <= 80) return FAN_SPEED.HIGH;
|
|
272
|
+
return FAN_SPEED.TURBO;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Poll state ─────────────────────────────────────────────────
|
|
276
|
+
async pollState() {
|
|
277
|
+
try {
|
|
278
|
+
if (this.deviceApi.type === 'cloud') {
|
|
279
|
+
await this.pollCloud();
|
|
280
|
+
} else {
|
|
281
|
+
await this.pollLocal();
|
|
282
|
+
}
|
|
283
|
+
this.updateCharacteristics();
|
|
284
|
+
} catch (err) {
|
|
285
|
+
this.log.warn('Poll failed: %s', err.message);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async pollCloud() {
|
|
290
|
+
const { api, device } = this.deviceApi;
|
|
291
|
+
try {
|
|
292
|
+
const params = await api.getDeviceParams(device);
|
|
293
|
+
if (!params) return;
|
|
294
|
+
|
|
295
|
+
this.state.power = !!params[CLOUD.POWER];
|
|
296
|
+
this.state.mode = params[CLOUD.MODE] ?? CLOUD_MODE.AUTO;
|
|
297
|
+
this.state.targetTemp = (params[CLOUD.TEMP_TARGET] ?? 240) / 10;
|
|
298
|
+
this.state.currentTemp = (params[CLOUD.TEMP_AMBIENT] ?? 240) / 10;
|
|
299
|
+
this.state.fanSpeed = params[CLOUD.FAN_SPEED] ?? FAN_SPEED.AUTO;
|
|
300
|
+
this.state.swingV = !!params[CLOUD.SWING_V];
|
|
301
|
+
this.state.swingH = !!params[CLOUD.SWING_H];
|
|
302
|
+
this.state.sleep = !!params[CLOUD.SLEEP];
|
|
303
|
+
this.state.health = !!params[CLOUD.HEALTH];
|
|
304
|
+
this.state.eco = !!params[CLOUD.ECO];
|
|
305
|
+
this.state.clean = !!params[CLOUD.CLEAN];
|
|
306
|
+
this.state.display = !!params[CLOUD.DISPLAY];
|
|
307
|
+
} catch (err) {
|
|
308
|
+
// Re-login on token expiry
|
|
309
|
+
if (err.message && err.message.includes('token')) {
|
|
310
|
+
const cloud = this.config.cloud;
|
|
311
|
+
await api.login(cloud.email, cloud.password);
|
|
312
|
+
}
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async pollLocal() {
|
|
318
|
+
const { api } = this.deviceApi;
|
|
319
|
+
const ok = await api.update();
|
|
320
|
+
if (!ok) return;
|
|
321
|
+
|
|
322
|
+
const s = api.state;
|
|
323
|
+
this.state.power = !!s.power;
|
|
324
|
+
this.state.targetTemp = s.temperature;
|
|
325
|
+
this.state.currentTemp = s.ambientTemp;
|
|
326
|
+
this.state.fanSpeed = s.fanSpeed;
|
|
327
|
+
this.state.swingV = s.verticalFixation === 7;
|
|
328
|
+
this.state.swingH = s.horizontalFixation === 7;
|
|
329
|
+
this.state.sleep = !!s.sleep;
|
|
330
|
+
this.state.health = !!s.health;
|
|
331
|
+
this.state.eco = !!s.mildew;
|
|
332
|
+
this.state.clean = !!s.clean;
|
|
333
|
+
this.state.display = !!s.display;
|
|
334
|
+
|
|
335
|
+
// Map local mode to cloud mode values
|
|
336
|
+
const modeMap = { 1: CLOUD_MODE.COOL, 2: CLOUD_MODE.DRY, 4: CLOUD_MODE.HEAT, 6: CLOUD_MODE.FAN, 8: CLOUD_MODE.AUTO };
|
|
337
|
+
this.state.mode = modeMap[s.mode] ?? CLOUD_MODE.AUTO;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
updateCharacteristics() {
|
|
341
|
+
const C = this.Characteristic;
|
|
342
|
+
|
|
343
|
+
this.heaterCooler.updateCharacteristic(C.Active,
|
|
344
|
+
this.state.power ? C.Active.ACTIVE : C.Active.INACTIVE);
|
|
345
|
+
|
|
346
|
+
if (!this.state.power) {
|
|
347
|
+
this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
|
|
348
|
+
C.CurrentHeaterCoolerState.INACTIVE);
|
|
349
|
+
} else {
|
|
350
|
+
switch (this.state.mode) {
|
|
351
|
+
case CLOUD_MODE.HEAT:
|
|
352
|
+
this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
|
|
353
|
+
C.CurrentHeaterCoolerState.HEATING);
|
|
354
|
+
break;
|
|
355
|
+
case CLOUD_MODE.COOL:
|
|
356
|
+
this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
|
|
357
|
+
C.CurrentHeaterCoolerState.COOLING);
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
this.heaterCooler.updateCharacteristic(C.CurrentHeaterCoolerState,
|
|
361
|
+
C.CurrentHeaterCoolerState.IDLE);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this.heaterCooler.updateCharacteristic(C.CurrentTemperature, this.state.currentTemp);
|
|
366
|
+
this.heaterCooler.updateCharacteristic(C.CoolingThresholdTemperature, this.state.targetTemp);
|
|
367
|
+
this.heaterCooler.updateCharacteristic(C.HeatingThresholdTemperature, this.state.targetTemp);
|
|
368
|
+
this.heaterCooler.updateCharacteristic(C.RotationSpeed, this.fanSpeedToPercent(this.state.fanSpeed));
|
|
369
|
+
this.heaterCooler.updateCharacteristic(C.SwingMode,
|
|
370
|
+
(this.state.swingV || this.state.swingH) ? C.SwingMode.SWING_ENABLED : C.SwingMode.SWING_DISABLED);
|
|
371
|
+
|
|
372
|
+
// Update preset switches
|
|
373
|
+
for (const [key, svc] of Object.entries(this.presetSwitches || {})) {
|
|
374
|
+
svc.updateCharacteristic(C.On, this.state[key]);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Update display switch
|
|
378
|
+
if (this.displaySwitch) {
|
|
379
|
+
this.displaySwitch.updateCharacteristic(C.On, this.state.display);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Send commands ──────────────────────────────────────────────
|
|
384
|
+
async sendPower(on) {
|
|
385
|
+
if (this.deviceApi.type === 'cloud') {
|
|
386
|
+
await this.cloudSet({ [CLOUD.POWER]: on ? 1 : 0 });
|
|
387
|
+
} else {
|
|
388
|
+
this.deviceApi.api.state.power = on ? 1 : 0;
|
|
389
|
+
await this.deviceApi.api.setState();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async sendMode(mode) {
|
|
394
|
+
if (this.deviceApi.type === 'cloud') {
|
|
395
|
+
await this.cloudSet({ [CLOUD.POWER]: 1, [CLOUD.MODE]: mode });
|
|
396
|
+
} else {
|
|
397
|
+
// Map cloud mode to local mode
|
|
398
|
+
const localModeMap = {
|
|
399
|
+
[CLOUD_MODE.AUTO]: 8, [CLOUD_MODE.COOL]: 1,
|
|
400
|
+
[CLOUD_MODE.HEAT]: 4, [CLOUD_MODE.DRY]: 2, [CLOUD_MODE.FAN]: 6,
|
|
401
|
+
};
|
|
402
|
+
const api = this.deviceApi.api;
|
|
403
|
+
api.state.power = 1;
|
|
404
|
+
api.state.mode = localModeMap[mode] ?? 8;
|
|
405
|
+
await api.setState();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async sendTemperature(temp) {
|
|
410
|
+
if (this.deviceApi.type === 'cloud') {
|
|
411
|
+
await this.cloudSet({ [CLOUD.TEMP_TARGET]: Math.round(temp * 10) });
|
|
412
|
+
} else {
|
|
413
|
+
this.deviceApi.api.state.temperature = temp;
|
|
414
|
+
await this.deviceApi.api.setState();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async sendFanSpeed(speed) {
|
|
419
|
+
if (this.deviceApi.type === 'cloud') {
|
|
420
|
+
await this.cloudSet({ [CLOUD.FAN_SPEED]: speed });
|
|
421
|
+
} else {
|
|
422
|
+
const api = this.deviceApi.api;
|
|
423
|
+
api.state.fanSpeed = speed;
|
|
424
|
+
api.state.turbo = speed === FAN_SPEED.TURBO ? 1 : 0;
|
|
425
|
+
api.state.mute = speed === FAN_SPEED.MUTE ? 1 : 0;
|
|
426
|
+
await api.setState();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async sendSwing(v, h) {
|
|
431
|
+
if (this.deviceApi.type === 'cloud') {
|
|
432
|
+
await this.cloudSet({ [CLOUD.SWING_V]: v ? 1 : 0, [CLOUD.SWING_H]: h ? 1 : 0 });
|
|
433
|
+
} else {
|
|
434
|
+
const api = this.deviceApi.api;
|
|
435
|
+
api.state.verticalFixation = v ? 7 : 0;
|
|
436
|
+
api.state.horizontalFixation = h ? 7 : 0;
|
|
437
|
+
await api.setState();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async sendPreset(key, on) {
|
|
442
|
+
const cfg = this.presetConfigs[key];
|
|
443
|
+
if (!cfg) return;
|
|
444
|
+
|
|
445
|
+
if (this.deviceApi.type === 'cloud') {
|
|
446
|
+
// Turn off all presets, then set the target one
|
|
447
|
+
const update = {
|
|
448
|
+
[CLOUD.SLEEP]: 0,
|
|
449
|
+
[CLOUD.HEALTH]: 0,
|
|
450
|
+
[CLOUD.ECO]: 0,
|
|
451
|
+
[CLOUD.CLEAN]: 0,
|
|
452
|
+
};
|
|
453
|
+
if (on) update[cfg.cloudKey] = 1;
|
|
454
|
+
await this.cloudSet(update);
|
|
455
|
+
} else {
|
|
456
|
+
const api = this.deviceApi.api;
|
|
457
|
+
api.state.sleep = 0;
|
|
458
|
+
api.state.health = 0;
|
|
459
|
+
api.state.mildew = 0;
|
|
460
|
+
api.state.clean = 0;
|
|
461
|
+
if (on) api.state[cfg.localAttr] = 1;
|
|
462
|
+
await api.setState();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async sendDisplay(on) {
|
|
467
|
+
if (this.deviceApi.type === 'cloud') {
|
|
468
|
+
await this.cloudSet({ [CLOUD.DISPLAY]: on ? 1 : 0 });
|
|
469
|
+
} else {
|
|
470
|
+
this.deviceApi.api.state.display = on ? 1 : 0;
|
|
471
|
+
await this.deviceApi.api.setState();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async cloudSet(params) {
|
|
476
|
+
const { api, device } = this.deviceApi;
|
|
477
|
+
await api.setDeviceParams(device, params);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
module.exports = { AcFreedomAccessory };
|