iobroker.gree-hvac
Version:
Adapter for Gree and C&H conditioners
596 lines (595 loc) • 23.8 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var utils = __toESM(require("@iobroker/adapter-core"));
var import_device_manager = require("./lib/device_manager");
var import_properties_map = __toESM(require("./lib/properties_map"));
var import_device_state = require("./lib/device-state");
var import_adapter_utils = __toESM(require("./lib/adapter-utils"));
const MinPollInterval = 1e3;
const MaxPollInterval = 6e4;
const CheckDevicesTimeoutMs = 1e3;
const CheckDevicesTimeout = "CheckDevices";
const MinCelsiusTemperature = 16;
const MaxCelsiusTemperature = 30;
const MinFahrenheitTemperature = 60;
const MaxFahrenheitTemperature = 86;
class GreeHvac extends utils.Adapter {
deviceManager;
timeouts = {};
activeDevices = [];
constructor(options) {
super({
...options,
name: "gree-hvac"
});
try {
this.on("ready", this.onReady.bind(this));
this.on("stateChange", this.onStateChange.bind(this));
this.on("message", this.onMessage.bind(this));
this.on("unload", this.onUnload.bind(this));
} catch (error) {
this.log.error(`Error in constructor: ${error}`);
this.sendError(error, "Error in constructor");
}
}
async onReady() {
var _a, _b;
try {
this.log.info(`Device list: ${JSON.stringify(this.config.devicelist)}`);
this.log.info(`Poll interval: ${this.config.pollInterval}`);
if (!this.config.devicelist || this.config.devicelist.length === 0 || !import_adapter_utils.default.validateIPList(this.config.devicelist)) {
this.log.error(`Invalid device list: ${JSON.stringify(this.config.devicelist)}`);
void ((_a = this.stop) == null ? void 0 : _a.call(this));
return;
}
if (this.config.pollInterval < MinPollInterval || isNaN(this.config.pollInterval) || this.config.pollInterval > MaxPollInterval) {
this.log.error(`Invalid poll interval: ${this.config.pollInterval}`);
void ((_b = this.stop) == null ? void 0 : _b.call(this));
return;
}
await this.setStateAsync("info.connection", { val: false, ack: true });
const adapterObjects = await this.getAdapterObjectsAsync();
for (const key in adapterObjects) {
if (Object.prototype.hasOwnProperty.call(adapterObjects, key) && key.endsWith(".alive") === true) {
await this.setStateAsync(key, { val: false, ack: true });
}
}
const devicesArray = this.config.devicelist.map((item) => item.deviceIp);
const devices = devicesArray.join(";");
this.subscribeStates("*");
this.deviceManager = new import_device_manager.DeviceManager(devices, this.log, this.config.requestTimeoutMs);
this.deviceManager.on("device_bound", async (deviceId, device) => {
try {
await this.processDevice(deviceId, device);
await this.pollDevices(deviceId, true);
} catch (error) {
this.log.error(`Error in device_bound event for device ${deviceId}: ${error}`);
}
});
this.checkDevices();
} catch (error) {
this.log.error(`Error in onReady: ${error}`);
this.sendError(error, "Error in onReady");
}
}
async pollDevices(deviceId, isFirst) {
if (isFirst) {
try {
await this.getDeviceStatus(deviceId);
} catch {
}
}
this.timeouts[deviceId] = this.setTimeout(async () => {
try {
await this.getDeviceStatus(deviceId);
this.clearTimeout(this.timeouts[deviceId]);
} catch {
}
await this.pollDevices(deviceId, false);
}, this.config.pollInterval);
}
checkDevices() {
this.timeouts[CheckDevicesTimeout] = this.setTimeout(async () => {
const allActive = this.activeDevices.length > 0 && this.activeDevices.every((device) => device.isActive === true);
await this.setStateAsync("info.connection", { val: allActive, ack: true });
this.clearTimeout(this.timeouts[CheckDevicesTimeout]);
this.checkDevices();
}, CheckDevicesTimeoutMs);
}
async getDeviceStatus(deviceId) {
const deviceItem = this.activeDevices.find((device) => device.id === deviceId);
try {
const deviceStatus = await this.deviceManager.getDeviceStatus(deviceId);
if (deviceItem && deviceItem.isActive === false) {
this.log.info(`Device ${deviceId} is responding again`);
deviceItem.isActive = true;
}
if (deviceItem) {
deviceItem.lastSeen = /* @__PURE__ */ new Date();
}
void this.processDeviceStatus(deviceId, deviceStatus);
} catch (error) {
await this.setStateAsync(`${deviceId}.alive`, { val: false, ack: true });
if (deviceItem && deviceItem.isActive === true) {
this.log.error(`Error in getDeviceStatus for device ${deviceId}: ${error}`);
deviceItem.isActive = false;
}
}
}
async processDeviceStatus(deviceId, deviceStatus) {
try {
deviceId = this.nameToId(deviceId);
await this.setStateAsync(`${deviceId}.alive`, { val: true, ack: true });
for (const key in deviceStatus) {
if (Object.prototype.hasOwnProperty.call(deviceStatus, key)) {
let value = deviceStatus[key];
const mapItem = import_properties_map.default.find((item) => item.hvacName === key);
if (!mapItem) {
this.log.warn(`Property ${key} not found in the map`);
continue;
}
value = import_adapter_utils.default.mapValue(value, mapItem);
value = import_adapter_utils.default.convertValue(deviceStatus, value, mapItem);
await this.setStateAsync(`${deviceId}.${mapItem.name}`, {
val: value,
ack: true
});
}
}
} catch (error) {
this.log.error(`Error in processDeviceStatus for device ${deviceId}: ${error}`);
this.sendError(error, `Error in processDeviceStatus for device ${deviceId}`);
}
}
async processDevice(deviceId, device) {
try {
this.activeDevices.push(new import_device_state.DeviceState(deviceId));
deviceId = this.nameToId(deviceId);
this.log.info(`Device ${deviceId} bound`);
await this.setObjectNotExistsAsync(deviceId, {
type: "device",
common: {
name: deviceId
},
native: {}
});
await this.setObjectNotExistsAsync(`${deviceId}.deviceInfo`, {
type: "state",
common: {
name: "Device Info",
type: "string",
role: "info",
read: true,
write: false
},
native: {}
});
await this.setStateAsync(`${deviceId}.deviceInfo`, { val: JSON.stringify(device), ack: true });
await this.setObjectNotExistsAsync(`${deviceId}.alive`, {
type: "state",
common: {
name: "Is alive",
type: "boolean",
read: true,
write: false,
role: "indicator.state"
},
native: {}
});
await this.setStateAsync(`${deviceId}.alive`, { val: true, ack: true });
for (const property of import_properties_map.default) {
try {
const propertyObjectName = `${deviceId}.${property.name}`;
if (await this.objectExists(propertyObjectName) === true) {
const propertyObject = await this.getObjectAsync(propertyObjectName);
if (propertyObject && import_adapter_utils.default.areObjectsTheSame(propertyObject, JSON.parse(property.definition)) === false) {
await this.delObjectAsync(propertyObjectName);
await this.setObjectNotExistsAsync(propertyObjectName, JSON.parse(property.definition));
}
} else {
await this.setObjectNotExistsAsync(propertyObjectName, JSON.parse(property.definition));
}
} catch (error) {
this.log.error(`Error in processDevice for device ${deviceId}: ${error}`);
this.log.error(`Property ${property.name}, definition '${property.definition}'`);
this.sendError(error, `Property ${property.name}, definition '${property.definition}'`);
}
}
} catch (error) {
this.log.error(`Error in processDevice for device ${deviceId}: ${error}`);
this.sendError(error, `Error in processDevice for device ${deviceId}`);
}
}
nameToId(pName) {
return (pName || "").replace(this.FORBIDDEN_CHARS, "_");
}
onUnload(callback) {
try {
for (const deviceId in this.timeouts) {
this.clearTimeout(this.timeouts[deviceId]);
}
if (this.deviceManager) {
this.deviceManager.close();
}
callback();
} catch (error) {
this.log.error(`Error in onUnload: ${error}`);
this.sendError(error, "Error in onUnload");
callback();
}
}
async onStateChange(id, state) {
try {
if (state && state.ack === false) {
const deviceInfo = this.getDeviceInfo(id);
if (!deviceInfo) {
return;
}
const { deviceId, devicePath, property } = deviceInfo;
if (property === "temperature-unit") {
await this.onTemperatureUnitChange(devicePath, state.val);
}
const mapItem = import_properties_map.default.find((item) => item.name === property);
if (mapItem) {
const payload = await this.createPayload(devicePath);
const cmdResult = await this.deviceManager.setDeviceState(deviceId, payload);
if (cmdResult) {
await this.processDeviceStatus(deviceId, cmdResult);
}
}
}
} catch (error) {
this.log.error(`Error in onStateChange for state ${id}: ${error}`);
this.sendError(error, `Error in onStateChange for state ${id}`);
}
}
async onTemperatureUnitChange(devicePath, temperatureUnit) {
var _a, _b, _c, _d;
const temperature = Number((_b = (_a = await this.getStateAsync(`${devicePath}.target-temperature`)) == null ? void 0 : _a.val) != null ? _b : 0);
const roomTemperature = Number((_d = (_c = await this.getStateAsync(`${devicePath}.room-temperature`)) == null ? void 0 : _c.val) != null ? _d : 0);
if (temperatureUnit === 0) {
let celsius = import_adapter_utils.default.fahrenheitToCelsius(temperature);
await this.setStateAsync(`${devicePath}.target-temperature`, celsius, true);
celsius = import_adapter_utils.default.fahrenheitToCelsius(roomTemperature);
await this.setStateAsync(`${devicePath}.room-temperature`, celsius, true);
} else {
let fahrenheit = import_adapter_utils.default.celsiusToFahrenheit(temperature);
await this.setStateAsync(`${devicePath}.target-temperature`, fahrenheit, true);
fahrenheit = import_adapter_utils.default.celsiusToFahrenheit(roomTemperature);
await this.setStateAsync(`${devicePath}.room-temperature`, fahrenheit, true);
}
}
async createPayload(deviceId) {
var _a;
try {
let payload = {};
for (const property of import_properties_map.default.filter((e) => !e.isReadOnly())) {
if (await this.objectExists(`${deviceId}.${property.name}`) === true) {
const state = await this.getStateAsync(`${deviceId}.${property.name}`);
if (state && state.val !== null) {
const definition = JSON.parse(property.definition);
if ((_a = definition.native) == null ? void 0 : _a.valuesMap) {
const valuesMap = definition.native.valuesMap;
const valueMap = valuesMap.find((item) => item.value === state.val);
if (valueMap) {
payload[property.hvacName] = valueMap.targetValue;
} else {
payload[property.hvacName] = state.val;
}
} else {
payload[property.hvacName] = state.val;
}
}
}
}
for (const property of import_properties_map.default.filter((e) => e.toConverter !== null)) {
if (property.toConverter) {
payload = property.toConverter(payload);
}
}
return payload;
} catch (error) {
this.log.error(`Error in createPayload: ${error}`);
this.sendError(error, "Error in createPayload");
return {};
}
}
getDeviceInfo(id) {
try {
const parts = id.split(".");
const deviceId = parts[2];
const devicePath = parts.slice(0, -1).join(".");
const property = parts[parts.length - 1];
return { deviceId, devicePath, property };
} catch (error) {
this.log.error(`Error in getDeviceInfo: ${error}`);
this.sendError(error, `Error in getDeviceInfo, id: ${id}`);
return null;
}
}
async onMessage(obj) {
this.log.debug(`Received message ${JSON.stringify(obj)}`);
if (typeof obj === "object" && obj.message) {
switch (obj.command) {
case "getDevices":
await this.processGetDevicesCommand(obj);
break;
case "remoteCommand":
await this.processRemoteCommand(obj);
break;
case "renameDevice":
await this.processRenameDevice(obj);
break;
default: {
this.log.warn(`Unknown command ${obj.command}`);
const result = { error: `Unknown command ${obj.command}` };
if (obj.callback) {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
break;
}
}
}
}
async processRenameDevice(obj) {
const result = {};
try {
const msg = obj.message;
const deviceId = msg.deviceId;
const deviceName = msg.name;
const deviceObject = await this.getObjectAsync(deviceId);
if (!deviceObject) {
this.log.warn(`Device ${deviceId} not found`);
return;
}
await this.extendObjectAsync(deviceId, { common: { name: deviceName } });
this.log.info(`Device ${deviceObject.common.name} renamed to ${deviceName}`);
result.result = { deviceId, name: deviceName };
} catch (error) {
result.error = error.message;
}
if (obj.callback) {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
}
async processRemoteCommand(obj) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
const result = {};
try {
const msg = obj.message;
const command = msg.command;
const deviceId = msg.deviceId;
let state;
const powerState = (_a = await this.getStateAsync(`${deviceId}.power`)) == null ? void 0 : _a.val;
const isAlive = (_b = await this.getStateAsync(`${deviceId}.alive`)) == null ? void 0 : _b.val;
const temperatureUnit = (_d = (_c = await this.getStateAsync(`${deviceId}.temperature-unit`)) == null ? void 0 : _c.val) != null ? _d : 0;
let minTemperature = MinCelsiusTemperature;
let maxTemperature = MaxCelsiusTemperature;
if (temperatureUnit === 1) {
minTemperature = MinFahrenheitTemperature;
maxTemperature = MaxFahrenheitTemperature;
}
if (isAlive === false) {
throw new Error("Device is not responding");
}
let newState;
switch (command) {
case "on-off-btn":
newState = powerState === 1 ? 0 : 1;
await this.setStateAsync(`${deviceId}.power`, newState);
break;
case "temperature-up-btn":
if (powerState === 0) {
throw new Error("Device power is off");
}
state = (_f = (_e = await this.getStateAsync(`${deviceId}.target-temperature`)) == null ? void 0 : _e.val) != null ? _f : 0;
newState = Number(state) + 1;
if (newState > maxTemperature) {
newState = maxTemperature;
}
await this.setStateAsync(`${deviceId}.target-temperature`, newState);
break;
case "temperature-down-btn":
if (powerState === 0) {
throw new Error("Device power is off");
}
state = (_h = (_g = await this.getStateAsync(`${deviceId}.target-temperature`)) == null ? void 0 : _g.val) != null ? _h : 0;
newState = Number(state) - 1;
if (newState < minTemperature) {
newState = minTemperature;
}
await this.setStateAsync(`${deviceId}.target-temperature`, newState);
break;
case "mode-btn":
if (powerState === 0) {
throw new Error("Device power is off");
}
state = (_j = (_i = await this.getStateAsync(`${deviceId}.mode`)) == null ? void 0 : _i.val) != null ? _j : 0;
newState = Number(state) + 1;
if (newState > 4) {
newState = 0;
}
await this.setStateAsync(`${deviceId}.mode`, newState);
break;
case "fan-btn":
if (powerState === 0) {
throw new Error("Device power is off");
}
state = (_l = (_k = await this.getStateAsync(`${deviceId}.fan-speed`)) == null ? void 0 : _k.val) != null ? _l : 0;
newState = Number(state) + 1;
if (newState > 4) {
newState = 0;
}
await this.setStateAsync(`${deviceId}.fan-speed`, newState);
break;
case "turbo-btn":
if (powerState === 0) {
throw new Error("Device power is off");
}
state = (_n = (_m = await this.getStateAsync(`${deviceId}.turbo`)) == null ? void 0 : _m.val) != null ? _n : 0;
newState = Number(state) + 1;
if (newState > 1) {
newState = 0;
}
await this.setStateAsync(`${deviceId}.turbo`, newState);
break;
case "display-btn":
state = (_p = (_o = await this.getStateAsync(`${deviceId}.display-state`)) == null ? void 0 : _o.val) != null ? _p : 0;
newState = Number(state) + 1;
if (newState > 1) {
newState = 0;
}
await this.setStateAsync(`${deviceId}.display-state`, newState);
break;
case "temperature-unit-btn":
newState = temperatureUnit === 1 ? 0 : 1;
await this.setStateAsync(`${deviceId}.temperature-unit`, newState);
break;
}
result.result = "Ok";
} catch (error) {
result.error = error.message;
}
if (obj.callback) {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
}
async processGetDevicesCommand(obj) {
const result = {};
try {
const allObjects = await this.getAdapterObjectsAsync();
const deviceObjects = Object.keys(allObjects).map((key) => ({ id: key, value: allObjects[key] })).filter((item) => item.id.split(".").length === 3 && item.value.type === "device").map((item) => ({
id: item.id,
name: item.value.common.name
}));
let devices = await this.collectDeviceInfo(deviceObjects);
devices = devices.sort((a, b) => {
var _a, _b, _c, _d;
const nameA = typeof a.name === "string" ? a.name : (_b = (_a = a.name) == null ? void 0 : _a.en) != null ? _b : "";
const nameB = typeof b.name === "string" ? b.name : (_d = (_c = b.name) == null ? void 0 : _c.en) != null ? _d : "";
return nameA > nameB ? 1 : nameB > nameA ? -1 : 0;
});
result.result = devices;
} catch (error) {
result.error = error.message;
}
if (obj.callback) {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
}
async collectDeviceInfo(deviceObjects) {
var _a, _b, _c, _d, _e;
const devicesInfo = [];
for (let i = 0; i < deviceObjects.length; i++) {
const deviceItem = deviceObjects[i];
const deviceInfoState = ((_b = (_a = await this.getStateAsync(`${deviceItem.id}.deviceInfo`)) == null ? void 0 : _a.val) != null ? _b : "{}").toString();
const deviceObject = JSON.parse(deviceInfoState);
const deviceInfo = {
id: deviceObject.mac,
ip: deviceObject.address,
name: deviceItem.name,
alive: null
};
for (let j = 0; j < import_properties_map.default.length; j++) {
try {
const property = import_properties_map.default[j];
const state = (_c = await this.getStateAsync(`${deviceItem.id}.${property.name}`)) == null ? void 0 : _c.val;
deviceInfo[property.name] = state;
} catch {
}
}
const aliveState = (_e = (_d = await this.getStateAsync(`${deviceItem.id}.alive`)) == null ? void 0 : _d.val) != null ? _e : false;
deviceInfo.alive = aliveState;
devicesInfo.push(deviceInfo);
}
return devicesInfo;
}
isNetworkError(error) {
const networkErrorCodes = /* @__PURE__ */ new Set([
"ECONNREFUSED",
"ECONNRESET",
"ETIMEDOUT",
"ENETUNREACH",
"EHOSTUNREACH",
"ENETDOWN",
"ENOTFOUND",
"ENONET",
"ECONNABORTED",
"EADDRNOTAVAIL",
"EADDRINUSE"
]);
if (error instanceof Error) {
const code = error.code;
if (code && networkErrorCodes.has(code)) {
return true;
}
const msg = error.message.toLowerCase();
if (msg.includes("timed out") || msg.includes("socket") || msg.includes("network")) {
return true;
}
}
return false;
}
sendError(error, message) {
var _a, _b, _c, _d, _e;
if (this.isNetworkError(error)) {
return;
}
try {
const adapter = this;
if (adapter.supportsFeature && adapter.supportsFeature("PLUGINS")) {
const sentryInstance = (_a = adapter.getPluginInstance) == null ? void 0 : _a.call(adapter, "sentry");
if (sentryInstance) {
const Sentry = (_b = sentryInstance.getSentryObject) == null ? void 0 : _b.call(sentryInstance);
if (Sentry) {
if (message) {
(_c = Sentry.configureScope) == null ? void 0 : _c.call(Sentry, (scope) => {
var _a2, _b2;
(_b2 = scope.addBreadcrumb) == null ? void 0 : _b2.call(scope, {
type: "error",
category: "error message",
level: (_a2 = Sentry.Severity) == null ? void 0 : _a2.Error,
message
});
});
}
if (typeof error === "string") {
(_d = Sentry.captureException) == null ? void 0 : _d.call(Sentry, new Error(error));
} else {
(_e = Sentry.captureException) == null ? void 0 : _e.call(Sentry, error);
}
}
}
}
} catch (err) {
console.error("Error in sendError:", err);
}
}
}
if (require.main !== module) {
module.exports = (options) => new GreeHvac(options);
} else {
new GreeHvac();
}
//# sourceMappingURL=main.js.map