iobroker.ico-cloud
Version:
ICO Poolsensor allow to monitor the state of the water in your pool and recommends actions to take.
574 lines (573 loc) • 20.3 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_api = require("./lib/api");
function encryptDecrypt(key, value) {
if (!value || !key) {
return value;
}
let result = "";
for (let i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
class IcoCloud extends utils.Adapter {
api;
pollInterval = 0;
devices = [];
pollTimeout = null;
unloaded = false;
redirectURI = "";
oauthStateCode = "";
sleeps = [];
constructor(options = {}) {
super({
...options,
name: "ico-cloud"
});
this.on("ready", this.onReady.bind(this));
this.on("message", this.onMessage.bind(this));
this.on("unload", this.onUnload.bind(this));
}
async sleep(ms) {
return new Promise((resolve) => {
this.sleeps.push(
setTimeout(() => {
!this.unloaded && resolve();
}, ms)
);
});
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
const instanceObject = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);
if (instanceObject) {
let updateConfig = false;
if (instanceObject.common.mode !== "schedule") {
instanceObject.common.mode = "schedule";
updateConfig = true;
}
if (instanceObject.common.schedule === void 0 || instanceObject.common.schedule === "59 * * * *") {
this.log.info("Default schedule found and adjusted to spread calls better over the full hour.");
instanceObject.common.schedule = `${Math.floor(Math.random() * 60)} ${Math.floor(Math.random() * 60)} * * * *`;
updateConfig = true;
}
if (updateConfig) {
this.log.debug(`Updating configuration, new schedule: ${instanceObject.common.schedule}`);
await this.setForeignObjectAsync(instanceObject._id, instanceObject);
}
}
const delay = Math.floor(Math.random() * 3e4);
this.log.debug(`Delay execution by ${delay}ms to better spread API calls`);
await this.sleep(delay);
if (this.config.refreshToken) {
this.api = new import_api.Api({
accessToken: this.config.accessToken,
refreshToken: this.config.refreshToken,
log: this.log,
storeNewTokens: this.storeNewTokens.bind(this)
});
this.log.debug("updating devices.");
try {
await this.updateDevices();
} catch (e) {
this.log.info("Could not update devices -> will try to update measurements with known devices anyway.");
this.log.debug(`Error: ${e}`);
}
this.log.debug("updating values.");
await this.poll();
this.log.debug("All done. Exit.");
this.terminate();
} else {
this.log.info(
"Not authorized, yet. Please see configuration. Letting adapter run to process oauth2 callback."
);
}
}
async updateDevices() {
var _a;
const devices = await this.getDevicesAsync();
let poolArray;
let deleteAllowed = true;
try {
poolArray = await this.api.getPools();
} catch (e) {
this.log.warn(
`Could not update pool list: ${e}. Trying to update know pools instead. If this happens a lot, try to login again.`
);
poolArray = [];
for (const device of devices) {
if (device.native.id) {
poolArray.push({ id: device.native.id });
} else {
this.log.warn(
`Pool ${(_a = device == null ? void 0 : device.common) == null ? void 0 : _a.name} is missing device id. Will not be able to update.`
);
deleteAllowed = false;
}
}
}
for (const pool of poolArray) {
if (pool.id) {
try {
const icoDevice = await this.api.getDevice(pool.id);
let found = false;
for (const device of devices) {
const uuid = device._id.split(".").pop();
if (uuid === icoDevice.uuid) {
found = true;
let needsUpdate = false;
if (device.native.poolId !== pool.id) {
needsUpdate = true;
device.native.poolId = pool.id;
}
if (device.native.swVersion !== icoDevice.sw_version) {
needsUpdate = true;
device.native.swVersion = icoDevice.sw_version;
}
if (needsUpdate) {
await this.setObjectAsync(device._id, device);
}
this.devices.push({
poolId: pool.id,
swVersion: icoDevice.sw_version,
uuid: icoDevice.uuid,
hasObjects: {}
});
const index = devices.indexOf(device);
if (index >= 0) {
devices.splice(index, 1);
}
break;
}
}
if (!found) {
const id = `${this.namespace}.${icoDevice.uuid}`;
const deviceObj = {
type: "device",
common: {
name: pool.name
},
native: {
poolId: pool.id,
swVersion: icoDevice.sw_version
}
};
this.devices.push({
poolId: deviceObj.native.poolId,
swVersion: deviceObj.native.swVersion,
hasObjects: {},
uuid: icoDevice.uuid
});
await this.setObjectAsync(id, deviceObj);
}
} catch (e) {
this.log.error(
`Could not update pool ${pool.id}: ${e}. If network error, retry later. Otherwise, please try to login again.`
);
deleteAllowed = false;
}
}
}
if (deleteAllowed) {
for (const device of devices) {
this.log.debug(`Deleting device ${device._id}`);
await this.delObjectAsync(device._id.split(".").pop(), { recursive: true });
}
} else {
for (const deviceObj of devices) {
this.devices.push({
poolId: deviceObj.native.poolId,
swVersion: deviceObj.native.swVersion,
hasObjects: {},
uuid: deviceObj._id.split(".").pop() || ""
});
}
}
}
async createObjectForMeasurement(device, type) {
let role = "state";
let unit = void 0;
switch (type) {
case "temperature": {
role = "value.temperature";
unit = "\xB0C";
break;
}
case "ph": {
role = "value";
break;
}
case "orp": {
role = "value";
unit = "mV";
break;
}
case "salt": {
role = "value";
unit = "mg/L";
break;
}
case "tds": {
role = "value";
unit = "ppm";
break;
}
case "battery": {
role = "value.battery";
unit = "%";
await this.setObjectNotExistsAsync(`${device.uuid}.lowBat`, {
type: "state",
common: {
name: "Low battery warning",
role: "indicator.lowbat",
type: "boolean",
read: true,
write: false
},
native: {}
});
break;
}
case "rssi": {
role = "value.rssi";
unit = "%";
await this.setObjectNotExistsAsync(`${device.uuid}.offline`, {
type: "state",
common: {
name: "Low wifi signal",
role: "indicator.maintenance.unreach",
type: "boolean",
read: true,
write: false
},
native: {}
});
break;
}
}
const id = `${device.uuid}.${type}`;
const stateObj = {
type: "state",
common: {
name: type,
type: "number",
role,
read: true,
write: false,
unit
},
native: {}
};
device.hasObjects[type] = true;
await this.setObjectNotExistsAsync(id, stateObj);
}
async updateMeasurementsOfDevice(device) {
try {
const measures = await this.api.getLastMeasures(device.poolId);
const promises = [];
for (const measure of measures) {
if (measure.is_valid) {
if (!device.hasObjects[measure.data_type]) {
await this.createObjectForMeasurement(device, measure.data_type);
}
const currState = await this.getStateAsync(`${device.uuid}.${measure.data_type}`);
if (!currState || currState.ts < measure.value_time.getTime()) {
this.log.debug(`Got new Measurement for ${measure.data_type}: ${measure.value}`);
await this.setStateAsync(`${device.uuid}.${measure.data_type}`, {
val: measure.value,
ack: true,
ts: measure.value_time.getTime()
});
if (measure.data_type === "battery") {
await this.setStateChangedAsync(`${device.uuid}.lowBat`, {
val: measure.value < 20,
//TODO: evaluate or make configurable...
ack: true,
ts: measure.value_time.getTime()
});
}
if (measure.data_type === "rssi") {
await this.setStateChangedAsync(`${device.uuid}.offline`, {
val: measure.value < 5,
//TODO: evaluate or make configurable...
ack: true,
ts: measure.value_time.getTime()
});
}
} else {
this.log.debug(`Measurement for ${measure.data_type} was already recorded in state db.`);
}
} else {
this.log.debug(
`Did not read ${measure.data_type} for ${device.poolId} because ${JSON.stringify(measure.exclusion_reason)}`
);
}
}
await Promise.all(promises);
} catch (e) {
this.log.warn(`Could not get measurements: ${e}`);
}
}
/**
* Update recommendations for device.
*
* @param device - device to update
*/
async updateRecommendationsOfDevice(device) {
try {
const recommendations = await this.api.getRecommendations(device.poolId);
await this.setObjectNotExistsAsync(`${device.uuid}.recommendations`, {
type: "channel",
common: {
name: "Recommendations"
},
native: {}
});
let lastRecommendation;
const recommendationsStored = [];
for (const recommendation of recommendations) {
if (Date.now() < recommendation.deadline.getTime() && recommendation.status !== "ok") {
recommendationsStored.push(recommendation.id);
} else {
this.log.debug(
`Recommendation ignored, because deadline is over or status is ok: ${Date.now() >= recommendation.deadline.getTime()} - ${recommendation.status} - ${JSON.stringify(recommendation)}`
);
continue;
}
await this.setObjectNotExistsAsync(`${device.uuid}.recommendations.${recommendation.id}`, {
type: "state",
common: {
name: recommendation.id.toString(10),
type: "string",
role: "text",
read: true,
write: false
},
native: {}
});
await this.setState(`${device.uuid}.recommendations.${recommendation.id}`, recommendation.title, true);
if (!lastRecommendation || recommendation.updated_at.getTime() > lastRecommendation.updated_at.getTime()) {
lastRecommendation = recommendation;
}
}
await this.setObjectNotExistsAsync(`${device.uuid}.recommendations.json`, {
type: "state",
common: {
name: "Recommendations",
type: "string",
role: "json",
read: true,
write: false
},
native: {}
});
await this.setState(`${device.uuid}.recommendations.json`, JSON.stringify(recommendations, null, 2), true);
if (lastRecommendation) {
await this.setObjectNotExistsAsync(`${device.uuid}.recommendations.lastRecommendation`, {
type: "state",
common: {
name: "Last recommendation",
type: "string",
role: "text",
read: true,
write: false
},
native: {}
});
await this.setState(
`${device.uuid}.recommendations.lastRecommendation`,
lastRecommendation.title,
true
);
} else {
const lastRecThere = await this.objectExists(`${device.uuid}.recommendations.lastRecommendation`);
if (lastRecThere) {
await this.delStateAsync(`${device.uuid}.recommendations.lastRecommendation`);
}
}
const recommendationObjects = await this.getStatesAsync(`${device.uuid}.recommendations.*`);
for (const id of Object.keys(recommendationObjects)) {
let found = false;
if (!id.includes("lastRecommendation") && !id.includes("json")) {
const recId = Number(id.split(".").pop());
for (const recommendation of recommendationsStored) {
if (recommendation === recId) {
found = true;
break;
}
}
if (!found) {
this.log.debug(`Deleting recommendation ${id}`);
await this.delObjectAsync(id, { recursive: true });
}
}
}
} catch (e) {
this.log.warn(`Could not get recommendations: ${e}`);
}
}
async poll() {
this.log.debug("Polling");
const promises = [];
for (const device of this.devices) {
promises.push(this.updateMeasurementsOfDevice(device));
promises.push(this.updateRecommendationsOfDevice(device));
}
await Promise.all(promises);
this.log.debug(`Update done.`);
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param callback - callback function
*/
onUnload(callback) {
try {
this.unloaded = true;
for (const sleep of this.sleeps) {
clearTimeout(sleep);
}
callback();
} catch (e) {
console.error("Error during unloading:", e);
callback();
}
}
// If you need to react to object changes, uncomment the following block and the corresponding line in the constructor.
// You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`.
// /**
// * Is called if a subscribed object changes
// */
// private onObjectChange(id: string, obj: ioBroker.Object | null | undefined): void {
// if (obj) {
// // The object was changed
// this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`);
// } else {
// // The object was deleted
// this.log.info(`object ${id} deleted`);
// }
// }
/**
* Is called if a subscribed state changes
*/
// private onStateChange(id: string, state: ioBroker.State | null | undefined): void {
// if (state) {
// // The state was changed
// this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
// } else {
// // The state was deleted
// this.log.info(`state ${id} deleted`);
// }
// }
/**
* Store new tokens in adapter config.
*
* @param accessToken - access token
* @param refreshToken - refresh token
* @param noAdapterRestart - if true, don't restart adapter otherwise writing the config object will trigger a restart.
*/
async storeNewTokens(accessToken, refreshToken, noAdapterRestart = false) {
var _a;
const systemConfig = await this.getForeignObjectAsync("system.config");
const secrect = ((_a = systemConfig == null ? void 0 : systemConfig.native) == null ? void 0 : _a.secret) || "RJaeBLRPwvPfh5O";
const instance = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);
instance.native.accessToken = accessToken ? encryptDecrypt(secrect, accessToken) : instance.native.accessToken;
instance.native.refreshToken = refreshToken ? encryptDecrypt(secrect, refreshToken) : instance.native.refreshToken;
if (!noAdapterRestart) {
await this.setForeignObject(`system.adapter.${this.namespace}`, instance);
}
return instance;
}
// If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
/**
* Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
* Using this method requires "common.messagebox" property to be set to true in io-package.json
*
* @param obj - message object
*/
async onMessage(obj) {
if (typeof obj === "object" && obj.message) {
this.log.debug(`Message: ${JSON.stringify(obj)}`);
if (obj.command === "getOAuthStartLink") {
const baseUrl = obj.message.redirectUriBase;
this.redirectURI = `${baseUrl}oauth2_callbacks/${this.namespace}/`;
this.oauthStateCode = `ico-cloud-${Math.floor(Math.random() * 1e5)}-${Date.now()}`;
this.log.debug(`Got redirect URI: ${this.redirectURI}. Storing state ${this.oauthStateCode}`);
const loginUrl = import_api.Api.getLoginUrl(this.redirectURI, this.oauthStateCode);
this.log.debug(`Got login URL: ${loginUrl}`);
if (obj.callback) {
this.sendTo(obj.from, obj.command, { openUrl: loginUrl }, obj.callback);
}
}
if (obj.command === "oauth2Callback") {
this.log.debug(`Got oauth2 callback, trying to get access token. Stored state: ${this.oauthStateCode}`);
if (this.oauthStateCode === obj.message.state) {
const result = await import_api.Api.getToken(obj.message.code, this.redirectURI, this.log);
if (obj.callback) {
if (result) {
const instance = await this.storeNewTokens(result.accessToken, result.refreshToken, true);
this.sendTo(
obj.from,
obj.command,
{ result: "loginSuccessMessage", native: result, saveConfig: true },
obj.callback
);
await this.setForeignObject(`system.adapter.${this.namespace}`, instance);
} else {
this.sendTo(obj.from, obj.command, { error: "loginErrorMessage" }, obj.callback);
}
}
} else {
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: "loginWrongStateMessage" }, obj.callback);
}
}
}
if (obj.command === "resetTokens") {
this.log.debug(`Got reset tokens command.`);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{
native: { accessToken: "", refreshToken: "" },
saveConfig: false
},
obj.callback
);
}
}
}
}
}
if (require.main !== module) {
module.exports = (options) => new IcoCloud(options);
} else {
(() => new IcoCloud())();
}
//# sourceMappingURL=main.js.map