homebridge-tuya-pool-heater 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/config.schema.json +247 -0
- package/dist/heaterCoolerAccessory.d.ts +31 -0
- package/dist/heaterCoolerAccessory.js +223 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/platform.d.ts +26 -0
- package/dist/platform.js +161 -0
- package/dist/settings.d.ts +81 -0
- package/dist/settings.js +66 -0
- package/dist/settings.test.d.ts +1 -0
- package/dist/settings.test.js +152 -0
- package/dist/thermostatAccessory.d.ts +27 -0
- package/dist/thermostatAccessory.js +238 -0
- package/dist/tuyaApi.d.ts +19 -0
- package/dist/tuyaApi.js +233 -0
- package/dist/tuyaApi.test.d.ts +1 -0
- package/dist/tuyaApi.test.js +294 -0
- package/package.json +59 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HeaterCoolerAccessory = void 0;
|
|
4
|
+
const settings_1 = require("./settings");
|
|
5
|
+
class HeaterCoolerAccessory {
|
|
6
|
+
service;
|
|
7
|
+
platform;
|
|
8
|
+
accessory;
|
|
9
|
+
deviceConfig;
|
|
10
|
+
// Cached state
|
|
11
|
+
active = false;
|
|
12
|
+
currentTemperature = 20;
|
|
13
|
+
heatingThresholdTemperature = 28;
|
|
14
|
+
coolingThresholdTemperature = 20;
|
|
15
|
+
currentHeaterCoolerState = 0; // INACTIVE
|
|
16
|
+
targetHeaterCoolerState = 0; // AUTO
|
|
17
|
+
// Debounce polling after commands
|
|
18
|
+
lastCommandTime = 0;
|
|
19
|
+
commandDebounceMs = 10000; // Ignore polls for 10 seconds after command
|
|
20
|
+
constructor(platform, accessory, deviceConfig) {
|
|
21
|
+
this.platform = platform;
|
|
22
|
+
this.accessory = accessory;
|
|
23
|
+
this.deviceConfig = deviceConfig;
|
|
24
|
+
// Set accessory information
|
|
25
|
+
this.accessory.getService(this.platform.Service.AccessoryInformation)
|
|
26
|
+
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Tuya')
|
|
27
|
+
.setCharacteristic(this.platform.Characteristic.Model, 'Pool Heat Pump')
|
|
28
|
+
.setCharacteristic(this.platform.Characteristic.SerialNumber, deviceConfig.id);
|
|
29
|
+
// Get or create HeaterCooler service
|
|
30
|
+
this.service = this.accessory.getService(this.platform.Service.HeaterCooler)
|
|
31
|
+
|| this.accessory.addService(this.platform.Service.HeaterCooler);
|
|
32
|
+
this.service.setCharacteristic(this.platform.Characteristic.Name, deviceConfig.name);
|
|
33
|
+
// Configure Active
|
|
34
|
+
this.service.getCharacteristic(this.platform.Characteristic.Active)
|
|
35
|
+
.onGet(this.getActive.bind(this))
|
|
36
|
+
.onSet(this.setActive.bind(this));
|
|
37
|
+
// Configure CurrentTemperature
|
|
38
|
+
this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
|
|
39
|
+
.setProps({
|
|
40
|
+
minValue: -10,
|
|
41
|
+
maxValue: 60,
|
|
42
|
+
minStep: 0.1,
|
|
43
|
+
})
|
|
44
|
+
.onGet(this.getCurrentTemperature.bind(this));
|
|
45
|
+
// Configure CurrentHeaterCoolerState (read-only)
|
|
46
|
+
this.service.getCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState)
|
|
47
|
+
.onGet(this.getCurrentHeaterCoolerState.bind(this));
|
|
48
|
+
// Configure TargetHeaterCoolerState
|
|
49
|
+
this.service.getCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState)
|
|
50
|
+
.onGet(this.getTargetHeaterCoolerState.bind(this))
|
|
51
|
+
.onSet(this.setTargetHeaterCoolerState.bind(this));
|
|
52
|
+
// Configure HeatingThresholdTemperature
|
|
53
|
+
this.service.getCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature)
|
|
54
|
+
.setProps({
|
|
55
|
+
minValue: 5,
|
|
56
|
+
maxValue: 55,
|
|
57
|
+
minStep: 1,
|
|
58
|
+
})
|
|
59
|
+
.onGet(this.getHeatingThresholdTemperature.bind(this))
|
|
60
|
+
.onSet(this.setHeatingThresholdTemperature.bind(this));
|
|
61
|
+
// Configure CoolingThresholdTemperature
|
|
62
|
+
this.service.getCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature)
|
|
63
|
+
.setProps({
|
|
64
|
+
minValue: 5,
|
|
65
|
+
maxValue: 35,
|
|
66
|
+
minStep: 1,
|
|
67
|
+
})
|
|
68
|
+
.onGet(this.getCoolingThresholdTemperature.bind(this))
|
|
69
|
+
.onSet(this.setCoolingThresholdTemperature.bind(this));
|
|
70
|
+
// Register for status updates
|
|
71
|
+
this.platform.registerStatusCallback(this.deviceConfig.id, (status) => {
|
|
72
|
+
this.updateState(status);
|
|
73
|
+
});
|
|
74
|
+
// Initial status fetch
|
|
75
|
+
this.fetchInitialStatus();
|
|
76
|
+
}
|
|
77
|
+
async fetchInitialStatus() {
|
|
78
|
+
try {
|
|
79
|
+
const status = await this.platform.tuyaApi.getDeviceStatus(this.deviceConfig.id);
|
|
80
|
+
this.updateState(status);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.platform.log.error('Failed to fetch initial status:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
updateState(status) {
|
|
87
|
+
// Ignore polled updates briefly after sending a command to avoid race conditions
|
|
88
|
+
if (Date.now() - this.lastCommandTime < this.commandDebounceMs) {
|
|
89
|
+
this.platform.log.debug('Ignoring polled state update (recent command sent)');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const dp of status) {
|
|
93
|
+
switch (dp.code) {
|
|
94
|
+
case settings_1.DP_CODES.SWITCH:
|
|
95
|
+
this.active = dp.value;
|
|
96
|
+
this.service.updateCharacteristic(this.platform.Characteristic.Active, this.active
|
|
97
|
+
? this.platform.Characteristic.Active.ACTIVE
|
|
98
|
+
: this.platform.Characteristic.Active.INACTIVE);
|
|
99
|
+
if (!this.active) {
|
|
100
|
+
this.currentHeaterCoolerState = this.platform.Characteristic.CurrentHeaterCoolerState.INACTIVE;
|
|
101
|
+
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.currentHeaterCoolerState);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case settings_1.DP_CODES.TEMP_CURRENT:
|
|
105
|
+
this.currentTemperature = (0, settings_1.tuyaTempToCelsius)(dp.value);
|
|
106
|
+
this.service.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.currentTemperature);
|
|
107
|
+
break;
|
|
108
|
+
case settings_1.DP_CODES.SET_HEATING_TEMP:
|
|
109
|
+
this.heatingThresholdTemperature = (0, settings_1.tuyaTempToCelsius)(dp.value);
|
|
110
|
+
this.service.updateCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature, this.heatingThresholdTemperature);
|
|
111
|
+
break;
|
|
112
|
+
case settings_1.DP_CODES.SET_COOLING_TEMP:
|
|
113
|
+
this.coolingThresholdTemperature = (0, settings_1.tuyaTempToCelsius)(dp.value);
|
|
114
|
+
this.service.updateCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature, this.coolingThresholdTemperature);
|
|
115
|
+
break;
|
|
116
|
+
case settings_1.DP_CODES.MODE:
|
|
117
|
+
this.updateModeFromTuya(dp.value);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
updateModeFromTuya(mode) {
|
|
123
|
+
if (!this.active) {
|
|
124
|
+
this.currentHeaterCoolerState = this.platform.Characteristic.CurrentHeaterCoolerState.INACTIVE;
|
|
125
|
+
}
|
|
126
|
+
else if ((0, settings_1.isHeatingMode)(mode)) {
|
|
127
|
+
this.currentHeaterCoolerState = this.platform.Characteristic.CurrentHeaterCoolerState.HEATING;
|
|
128
|
+
this.targetHeaterCoolerState = this.platform.Characteristic.TargetHeaterCoolerState.HEAT;
|
|
129
|
+
}
|
|
130
|
+
else if ((0, settings_1.isCoolingMode)(mode)) {
|
|
131
|
+
this.currentHeaterCoolerState = this.platform.Characteristic.CurrentHeaterCoolerState.COOLING;
|
|
132
|
+
this.targetHeaterCoolerState = this.platform.Characteristic.TargetHeaterCoolerState.COOL;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Auto mode - determine current state based on current vs target temp
|
|
136
|
+
if (this.currentTemperature < this.heatingThresholdTemperature) {
|
|
137
|
+
this.currentHeaterCoolerState = this.platform.Characteristic.CurrentHeaterCoolerState.HEATING;
|
|
138
|
+
}
|
|
139
|
+
else if (this.currentTemperature > this.coolingThresholdTemperature) {
|
|
140
|
+
this.currentHeaterCoolerState = this.platform.Characteristic.CurrentHeaterCoolerState.COOLING;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.currentHeaterCoolerState = this.platform.Characteristic.CurrentHeaterCoolerState.IDLE;
|
|
144
|
+
}
|
|
145
|
+
this.targetHeaterCoolerState = this.platform.Characteristic.TargetHeaterCoolerState.AUTO;
|
|
146
|
+
}
|
|
147
|
+
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.currentHeaterCoolerState);
|
|
148
|
+
this.service.updateCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState, this.targetHeaterCoolerState);
|
|
149
|
+
}
|
|
150
|
+
// Handlers
|
|
151
|
+
async getActive() {
|
|
152
|
+
return this.active
|
|
153
|
+
? this.platform.Characteristic.Active.ACTIVE
|
|
154
|
+
: this.platform.Characteristic.Active.INACTIVE;
|
|
155
|
+
}
|
|
156
|
+
async setActive(value) {
|
|
157
|
+
const active = value === this.platform.Characteristic.Active.ACTIVE;
|
|
158
|
+
this.platform.log.info(`Setting active to ${active}`);
|
|
159
|
+
this.lastCommandTime = Date.now();
|
|
160
|
+
const success = await this.platform.tuyaApi.sendCommand(this.deviceConfig.id, settings_1.DP_CODES.SWITCH, active);
|
|
161
|
+
if (success) {
|
|
162
|
+
this.active = active;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async getCurrentTemperature() {
|
|
166
|
+
return this.currentTemperature;
|
|
167
|
+
}
|
|
168
|
+
async getCurrentHeaterCoolerState() {
|
|
169
|
+
return this.currentHeaterCoolerState;
|
|
170
|
+
}
|
|
171
|
+
async getTargetHeaterCoolerState() {
|
|
172
|
+
return this.targetHeaterCoolerState;
|
|
173
|
+
}
|
|
174
|
+
async setTargetHeaterCoolerState(value) {
|
|
175
|
+
const state = value;
|
|
176
|
+
this.platform.log.info(`Setting target heater/cooler state to ${state}`);
|
|
177
|
+
this.lastCommandTime = Date.now();
|
|
178
|
+
let mode;
|
|
179
|
+
switch (state) {
|
|
180
|
+
case this.platform.Characteristic.TargetHeaterCoolerState.HEAT:
|
|
181
|
+
mode = settings_1.TUYA_MODES.HEATING_SMART;
|
|
182
|
+
break;
|
|
183
|
+
case this.platform.Characteristic.TargetHeaterCoolerState.COOL:
|
|
184
|
+
mode = settings_1.TUYA_MODES.COOLING_SMART;
|
|
185
|
+
break;
|
|
186
|
+
case this.platform.Characteristic.TargetHeaterCoolerState.AUTO:
|
|
187
|
+
default:
|
|
188
|
+
mode = settings_1.TUYA_MODES.AUTO;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const success = await this.platform.tuyaApi.sendCommand(this.deviceConfig.id, settings_1.DP_CODES.MODE, mode);
|
|
192
|
+
if (success) {
|
|
193
|
+
this.targetHeaterCoolerState = state;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async getHeatingThresholdTemperature() {
|
|
197
|
+
return this.heatingThresholdTemperature;
|
|
198
|
+
}
|
|
199
|
+
async setHeatingThresholdTemperature(value) {
|
|
200
|
+
const temp = value;
|
|
201
|
+
this.platform.log.info(`Setting heating threshold temperature to ${temp}°C`);
|
|
202
|
+
this.lastCommandTime = Date.now();
|
|
203
|
+
const tuyaTemp = (0, settings_1.celsiusToTuyaTemp)(temp);
|
|
204
|
+
const success = await this.platform.tuyaApi.sendCommand(this.deviceConfig.id, settings_1.DP_CODES.SET_HEATING_TEMP, tuyaTemp);
|
|
205
|
+
if (success) {
|
|
206
|
+
this.heatingThresholdTemperature = temp;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async getCoolingThresholdTemperature() {
|
|
210
|
+
return this.coolingThresholdTemperature;
|
|
211
|
+
}
|
|
212
|
+
async setCoolingThresholdTemperature(value) {
|
|
213
|
+
const temp = value;
|
|
214
|
+
this.platform.log.info(`Setting cooling threshold temperature to ${temp}°C`);
|
|
215
|
+
this.lastCommandTime = Date.now();
|
|
216
|
+
const tuyaTemp = (0, settings_1.celsiusToTuyaTemp)(temp);
|
|
217
|
+
const success = await this.platform.tuyaApi.sendCommand(this.deviceConfig.id, settings_1.DP_CODES.SET_COOLING_TEMP, tuyaTemp);
|
|
218
|
+
if (success) {
|
|
219
|
+
this.coolingThresholdTemperature = temp;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
exports.HeaterCoolerAccessory = HeaterCoolerAccessory;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const settings_1 = require("./settings");
|
|
4
|
+
const platform_1 = require("./platform");
|
|
5
|
+
exports.default = (api) => {
|
|
6
|
+
api.registerPlatform(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, platform_1.TuyaPoolHeatPumpPlatform);
|
|
7
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, Service, Characteristic } from 'homebridge';
|
|
2
|
+
import { TuyaPoolHeatPumpConfig, TuyaDeviceStatus } from './settings';
|
|
3
|
+
import { TuyaApi } from './tuyaApi';
|
|
4
|
+
type StatusCallback = (status: TuyaDeviceStatus[]) => void;
|
|
5
|
+
export declare class TuyaPoolHeatPumpPlatform implements DynamicPlatformPlugin {
|
|
6
|
+
readonly Service: typeof Service;
|
|
7
|
+
readonly Characteristic: typeof Characteristic;
|
|
8
|
+
readonly accessories: PlatformAccessory[];
|
|
9
|
+
readonly api: API;
|
|
10
|
+
readonly log: Logger;
|
|
11
|
+
readonly config: TuyaPoolHeatPumpConfig;
|
|
12
|
+
tuyaApi: TuyaApi;
|
|
13
|
+
private readonly pollInterval;
|
|
14
|
+
private pollTimer;
|
|
15
|
+
private readonly statusCallbacks;
|
|
16
|
+
constructor(log: Logger, config: TuyaPoolHeatPumpConfig, api: API);
|
|
17
|
+
configureAccessory(accessory: PlatformAccessory): void;
|
|
18
|
+
discoverDevices(): Promise<void>;
|
|
19
|
+
private authenticateWithRetry;
|
|
20
|
+
private scheduleReconnect;
|
|
21
|
+
private registerDevice;
|
|
22
|
+
private startPolling;
|
|
23
|
+
private pollDeviceStatus;
|
|
24
|
+
registerStatusCallback(deviceId: string, callback: StatusCallback): void;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TuyaPoolHeatPumpPlatform = void 0;
|
|
4
|
+
const settings_1 = require("./settings");
|
|
5
|
+
const tuyaApi_1 = require("./tuyaApi");
|
|
6
|
+
const thermostatAccessory_1 = require("./thermostatAccessory");
|
|
7
|
+
const heaterCoolerAccessory_1 = require("./heaterCoolerAccessory");
|
|
8
|
+
class TuyaPoolHeatPumpPlatform {
|
|
9
|
+
Service;
|
|
10
|
+
Characteristic;
|
|
11
|
+
accessories = [];
|
|
12
|
+
api;
|
|
13
|
+
log;
|
|
14
|
+
config;
|
|
15
|
+
tuyaApi;
|
|
16
|
+
pollInterval;
|
|
17
|
+
pollTimer = null;
|
|
18
|
+
statusCallbacks = new Map();
|
|
19
|
+
constructor(log, config, api) {
|
|
20
|
+
this.log = log;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.api = api;
|
|
23
|
+
this.Service = api.hap.Service;
|
|
24
|
+
this.Characteristic = api.hap.Characteristic;
|
|
25
|
+
this.pollInterval = config.options?.pollInterval || settings_1.DEFAULT_POLL_INTERVAL;
|
|
26
|
+
this.log.debug('Finished initializing platform:', config.name);
|
|
27
|
+
api.on('didFinishLaunching', () => {
|
|
28
|
+
this.log.debug('Executed didFinishLaunching callback');
|
|
29
|
+
this.discoverDevices();
|
|
30
|
+
});
|
|
31
|
+
api.on('shutdown', () => {
|
|
32
|
+
this.log.debug('Homebridge is shutting down');
|
|
33
|
+
if (this.pollTimer) {
|
|
34
|
+
clearInterval(this.pollTimer);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
configureAccessory(accessory) {
|
|
39
|
+
this.log.info('Loading accessory from cache:', accessory.displayName);
|
|
40
|
+
this.accessories.push(accessory);
|
|
41
|
+
}
|
|
42
|
+
async discoverDevices() {
|
|
43
|
+
if (!this.config.options) {
|
|
44
|
+
this.log.error('No options configured. Please configure the plugin.');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!this.config.devices || this.config.devices.length === 0) {
|
|
48
|
+
this.log.warn('No devices configured. Please add devices in the config.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Initialize Tuya API
|
|
52
|
+
this.tuyaApi = new tuyaApi_1.TuyaApi(this.config.options, this.log);
|
|
53
|
+
const authenticated = await this.authenticateWithRetry(3, 10000);
|
|
54
|
+
if (!authenticated) {
|
|
55
|
+
this.log.error('Failed to authenticate after retries. Will keep trying in background.');
|
|
56
|
+
this.scheduleReconnect();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Register devices
|
|
60
|
+
for (const deviceConfig of this.config.devices) {
|
|
61
|
+
this.registerDevice(deviceConfig);
|
|
62
|
+
}
|
|
63
|
+
// Start polling for status updates
|
|
64
|
+
this.startPolling();
|
|
65
|
+
}
|
|
66
|
+
async authenticateWithRetry(maxRetries, delayMs) {
|
|
67
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
await this.tuyaApi.authenticate();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.log.warn(`Authentication attempt ${attempt}/${maxRetries} failed:`, error);
|
|
74
|
+
if (attempt < maxRetries) {
|
|
75
|
+
this.log.info(`Retrying in ${delayMs / 1000} seconds...`);
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
scheduleReconnect() {
|
|
83
|
+
const reconnectDelay = 60000; // 1 minute
|
|
84
|
+
this.log.info(`Scheduling reconnection attempt in ${reconnectDelay / 1000} seconds...`);
|
|
85
|
+
setTimeout(async () => {
|
|
86
|
+
this.log.info('Attempting to reconnect to Tuya API...');
|
|
87
|
+
const authenticated = await this.authenticateWithRetry(3, 10000);
|
|
88
|
+
if (authenticated) {
|
|
89
|
+
this.log.info('Reconnection successful!');
|
|
90
|
+
// Register devices if not already done
|
|
91
|
+
for (const deviceConfig of this.config.devices) {
|
|
92
|
+
this.registerDevice(deviceConfig);
|
|
93
|
+
}
|
|
94
|
+
this.startPolling();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.scheduleReconnect();
|
|
98
|
+
}
|
|
99
|
+
}, reconnectDelay);
|
|
100
|
+
}
|
|
101
|
+
registerDevice(deviceConfig) {
|
|
102
|
+
const uuid = this.api.hap.uuid.generate(deviceConfig.id);
|
|
103
|
+
const existingAccessory = this.accessories.find(acc => acc.UUID === uuid);
|
|
104
|
+
if (existingAccessory) {
|
|
105
|
+
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
|
|
106
|
+
existingAccessory.context.device = deviceConfig;
|
|
107
|
+
if (deviceConfig.accessoryType === 'thermostat') {
|
|
108
|
+
new thermostatAccessory_1.ThermostatAccessory(this, existingAccessory, deviceConfig);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
new heaterCoolerAccessory_1.HeaterCoolerAccessory(this, existingAccessory, deviceConfig);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.log.info('Adding new accessory:', deviceConfig.name);
|
|
116
|
+
const accessory = new this.api.platformAccessory(deviceConfig.name, uuid);
|
|
117
|
+
accessory.context.device = deviceConfig;
|
|
118
|
+
if (deviceConfig.accessoryType === 'thermostat') {
|
|
119
|
+
new thermostatAccessory_1.ThermostatAccessory(this, accessory, deviceConfig);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
new heaterCoolerAccessory_1.HeaterCoolerAccessory(this, accessory, deviceConfig);
|
|
123
|
+
}
|
|
124
|
+
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
startPolling() {
|
|
128
|
+
this.log.info(`Starting status polling every ${this.pollInterval / 1000} seconds`);
|
|
129
|
+
this.pollTimer = setInterval(async () => {
|
|
130
|
+
for (const accessory of this.accessories) {
|
|
131
|
+
const deviceConfig = accessory.context.device;
|
|
132
|
+
if (deviceConfig) {
|
|
133
|
+
try {
|
|
134
|
+
await this.pollDeviceStatus(deviceConfig.id);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
this.log.error(`Failed to poll device ${deviceConfig.id}:`, error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}, this.pollInterval);
|
|
142
|
+
}
|
|
143
|
+
async pollDeviceStatus(deviceId) {
|
|
144
|
+
try {
|
|
145
|
+
const status = await this.tuyaApi.getDeviceStatus(deviceId);
|
|
146
|
+
this.log.debug(`Device ${deviceId} status:`, JSON.stringify(status));
|
|
147
|
+
// Call registered callback for this device
|
|
148
|
+
const callback = this.statusCallbacks.get(deviceId);
|
|
149
|
+
if (callback) {
|
|
150
|
+
callback(status);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
this.log.error(`Failed to get status for device ${deviceId}:`, error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
registerStatusCallback(deviceId, callback) {
|
|
158
|
+
this.statusCallbacks.set(deviceId, callback);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.TuyaPoolHeatPumpPlatform = TuyaPoolHeatPumpPlatform;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { PlatformConfig } from 'homebridge';
|
|
2
|
+
export declare const PLATFORM_NAME = "TuyaPoolHeatPump";
|
|
3
|
+
export declare const PLUGIN_NAME = "homebridge-tuya-pool-heater";
|
|
4
|
+
export declare const TUYA_ENDPOINTS: Record<string, string>;
|
|
5
|
+
export declare const DEFAULT_POLL_INTERVAL = 30000;
|
|
6
|
+
export declare const TEMP_SCALE = 10;
|
|
7
|
+
export declare const TEMP_RANGES: {
|
|
8
|
+
readonly heating: {
|
|
9
|
+
readonly min: 5;
|
|
10
|
+
readonly max: 55;
|
|
11
|
+
};
|
|
12
|
+
readonly cooling: {
|
|
13
|
+
readonly min: 5;
|
|
14
|
+
readonly max: 35;
|
|
15
|
+
};
|
|
16
|
+
readonly auto: {
|
|
17
|
+
readonly min: 5;
|
|
18
|
+
readonly max: 40;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare const DP_CODES: {
|
|
22
|
+
readonly SWITCH: "switch";
|
|
23
|
+
readonly MODE: "mode";
|
|
24
|
+
readonly TEMP_CURRENT: "temp_current";
|
|
25
|
+
readonly SET_HEATING_TEMP: "set_heating_temp";
|
|
26
|
+
readonly SET_COOLING_TEMP: "set_cold_temp";
|
|
27
|
+
readonly SET_AUTO_TEMP: "set_auto_temp";
|
|
28
|
+
};
|
|
29
|
+
export declare const TUYA_MODES: {
|
|
30
|
+
readonly AUTO: "Auto";
|
|
31
|
+
readonly HEATING_SMART: "Heating_Smart";
|
|
32
|
+
readonly HEATING_POWERFUL: "Heating_Powerful";
|
|
33
|
+
readonly HEATING_SILENT: "Heating_Silent";
|
|
34
|
+
readonly COOLING_SMART: "Cooling_Smart";
|
|
35
|
+
readonly COOLING_POWERFUL: "Cooling_Powerful";
|
|
36
|
+
readonly COOLING_SILENT: "Cooling_Silent";
|
|
37
|
+
};
|
|
38
|
+
export type TuyaMode = typeof TUYA_MODES[keyof typeof TUYA_MODES];
|
|
39
|
+
export type AccessoryType = 'thermostat' | 'heatercooler';
|
|
40
|
+
export interface TempRangeConfig {
|
|
41
|
+
min: number;
|
|
42
|
+
max: number;
|
|
43
|
+
}
|
|
44
|
+
export interface DeviceConfig {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
accessoryType: AccessoryType;
|
|
48
|
+
heatingRange?: TempRangeConfig;
|
|
49
|
+
coolingRange?: TempRangeConfig;
|
|
50
|
+
autoRange?: TempRangeConfig;
|
|
51
|
+
}
|
|
52
|
+
export interface PluginOptions {
|
|
53
|
+
accessId: string;
|
|
54
|
+
accessKey: string;
|
|
55
|
+
endpoint: string;
|
|
56
|
+
username: string;
|
|
57
|
+
password: string;
|
|
58
|
+
countryCode: number;
|
|
59
|
+
pollInterval?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface TuyaPoolHeatPumpConfig extends PlatformConfig {
|
|
62
|
+
options: PluginOptions;
|
|
63
|
+
devices: DeviceConfig[];
|
|
64
|
+
}
|
|
65
|
+
export interface TuyaDeviceStatus {
|
|
66
|
+
code: string;
|
|
67
|
+
value: boolean | number | string;
|
|
68
|
+
}
|
|
69
|
+
export interface HeatPumpState {
|
|
70
|
+
active: boolean;
|
|
71
|
+
mode: TuyaMode;
|
|
72
|
+
currentTemperature: number;
|
|
73
|
+
targetHeatingTemperature: number;
|
|
74
|
+
targetCoolingTemperature: number;
|
|
75
|
+
targetAutoTemperature: number;
|
|
76
|
+
}
|
|
77
|
+
export declare function isHeatingMode(mode: string): boolean;
|
|
78
|
+
export declare function isCoolingMode(mode: string): boolean;
|
|
79
|
+
export declare function isAutoMode(mode: string): boolean;
|
|
80
|
+
export declare function tuyaTempToCelsius(value: number): number;
|
|
81
|
+
export declare function celsiusToTuyaTemp(value: number): number;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TUYA_MODES = exports.DP_CODES = exports.TEMP_RANGES = exports.TEMP_SCALE = exports.DEFAULT_POLL_INTERVAL = exports.TUYA_ENDPOINTS = exports.PLUGIN_NAME = exports.PLATFORM_NAME = void 0;
|
|
4
|
+
exports.isHeatingMode = isHeatingMode;
|
|
5
|
+
exports.isCoolingMode = isCoolingMode;
|
|
6
|
+
exports.isAutoMode = isAutoMode;
|
|
7
|
+
exports.tuyaTempToCelsius = tuyaTempToCelsius;
|
|
8
|
+
exports.celsiusToTuyaTemp = celsiusToTuyaTemp;
|
|
9
|
+
exports.PLATFORM_NAME = 'TuyaPoolHeatPump';
|
|
10
|
+
exports.PLUGIN_NAME = 'homebridge-tuya-pool-heater';
|
|
11
|
+
// Tuya API Endpoints by region
|
|
12
|
+
exports.TUYA_ENDPOINTS = {
|
|
13
|
+
us: 'https://openapi.tuyaus.com',
|
|
14
|
+
eu: 'https://openapi.tuyaeu.com',
|
|
15
|
+
cn: 'https://openapi.tuyacn.com',
|
|
16
|
+
in: 'https://openapi.tuyain.com',
|
|
17
|
+
};
|
|
18
|
+
// Default polling interval in milliseconds
|
|
19
|
+
exports.DEFAULT_POLL_INTERVAL = 30000;
|
|
20
|
+
// Temperature scaling factor (Tuya stores temps as value * 10)
|
|
21
|
+
exports.TEMP_SCALE = 10;
|
|
22
|
+
// Temperature ranges per mode (in Celsius)
|
|
23
|
+
exports.TEMP_RANGES = {
|
|
24
|
+
heating: { min: 5, max: 55 },
|
|
25
|
+
cooling: { min: 5, max: 35 },
|
|
26
|
+
auto: { min: 5, max: 40 },
|
|
27
|
+
};
|
|
28
|
+
// Tuya DP codes for pool heat pumps
|
|
29
|
+
exports.DP_CODES = {
|
|
30
|
+
SWITCH: 'switch',
|
|
31
|
+
MODE: 'mode',
|
|
32
|
+
TEMP_CURRENT: 'temp_current',
|
|
33
|
+
SET_HEATING_TEMP: 'set_heating_temp',
|
|
34
|
+
SET_COOLING_TEMP: 'set_cold_temp',
|
|
35
|
+
SET_AUTO_TEMP: 'set_auto_temp',
|
|
36
|
+
};
|
|
37
|
+
// Tuya mode values
|
|
38
|
+
exports.TUYA_MODES = {
|
|
39
|
+
AUTO: 'Auto',
|
|
40
|
+
HEATING_SMART: 'Heating_Smart',
|
|
41
|
+
HEATING_POWERFUL: 'Heating_Powerful',
|
|
42
|
+
HEATING_SILENT: 'Heating_Silent',
|
|
43
|
+
COOLING_SMART: 'Cooling_Smart',
|
|
44
|
+
COOLING_POWERFUL: 'Cooling_Powerful',
|
|
45
|
+
COOLING_SILENT: 'Cooling_Silent',
|
|
46
|
+
};
|
|
47
|
+
// Helper to check if mode is heating
|
|
48
|
+
function isHeatingMode(mode) {
|
|
49
|
+
return mode.toLowerCase().includes('heating');
|
|
50
|
+
}
|
|
51
|
+
// Helper to check if mode is cooling
|
|
52
|
+
function isCoolingMode(mode) {
|
|
53
|
+
return mode.toLowerCase().includes('cooling');
|
|
54
|
+
}
|
|
55
|
+
// Helper to check if mode is auto
|
|
56
|
+
function isAutoMode(mode) {
|
|
57
|
+
return mode.toLowerCase() === 'auto';
|
|
58
|
+
}
|
|
59
|
+
// Convert Tuya temperature to Celsius
|
|
60
|
+
function tuyaTempToCelsius(value) {
|
|
61
|
+
return value / exports.TEMP_SCALE;
|
|
62
|
+
}
|
|
63
|
+
// Convert Celsius to Tuya temperature
|
|
64
|
+
function celsiusToTuyaTemp(value) {
|
|
65
|
+
return Math.round(value * exports.TEMP_SCALE);
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|