iobroker.plenticore-g3
Version:
Adapter to communicate with a KOSTAL Plenticore Plus via local network (non-modbus)
286 lines (258 loc) • 10.5 kB
JavaScript
'use strict';
/*
* Created with @iobroker/create-adapter v2.6.5
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
const I18n = require('@iobroker/adapter-core').I18n;
// Load your modules here, e.g.:
// const fs = require("fs");
const path = require('path');
const PlenticoreAPI = require('./lib/plenticoreAPI.js');
const PlenticoreData = require('./lib/plenticoredata.js');
class PlenticoreG3 extends utils.Adapter {
#plenticoreAPI;
#processdata;
#settings;
#mainlooptimer;
#isInitialized = false;
#lastUpdateCheck = 0;
/**
* @param {Partial<utils.AdapterOptions>} [options] - options given from iobroker
*/
constructor(options) {
super({
...options,
name: 'plenticore-g3',
});
this.on('ready', this.onReady.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
// this.on("objectChange", this.onObjectChange.bind(this));
// this.on("message", this.onMessage.bind(this));
this.on('unload', this.onUnload.bind(this));
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
// Initialize your adapter here
await I18n.init(path.join(__dirname, 'lib'), this);
// make sure interval is between expected values
if (this.config.pollinginterval < 5) {
this.config.pollinginterval = 5;
} else if (this.config.pollinginterval > 60) {
this.config.pollinginterval = 60;
}
// Reset the connection indicator during startup
this.setState('info.connection', false, true);
this.#processdata = new PlenticoreData(this, I18n, 'processdata');
this.#settings = new PlenticoreData(this, I18n, 'settings');
this.#plenticoreAPI = new PlenticoreAPI(
this.config.host,
this.config.port,
this.config.https,
this.config.password,
this.log,
);
this.mainloop();
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param {() => void} callback - call back into iobroker
*/
onUnload(callback) {
try {
// Here you must clear all timeouts or intervals that may still be active
this.clearTimeout(this.#mainlooptimer);
this.#plenticoreAPI = null;
this.setState('info.connection', false, true);
callback();
} catch (e) {
this.log.error(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
// * @param {string} id
// * @param {ioBroker.Object | null | undefined} obj
// */
// onObjectChange(id, obj) {
// 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
*
* @param {string} id - id of the state changed
* @param {ioBroker.State | null | undefined} state - state that was changed
*/
async onStateChange(id, state) {
if (state && !state.ack) {
// The state was changed
this.log.silly(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
if (!id) {
return;
}
let payload = await this.#settings.prepareWriteValue(id, state);
try {
await this.#plenticoreAPI.writeSetting(payload);
this.setState(id, state, true);
this.log.silly('successfully written value');
} catch (e) {
this.log.warn(`cannot write state ${id} with value ${state.val} - error: ${e}`);
}
}
}
// 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 {ioBroker.Message} obj
// */
// onMessage(obj) {
// if (typeof obj === "object" && obj.message) {
// if (obj.command === "send") {
// // e.g. send email or pushover or whatever
// this.log.info("send command");
// // Send response in callback if required
// if (obj.callback) this.sendTo(obj.from, obj.command, "Message received", obj.callback);
// }
// }
// }
// main loop for polling and sending data
async mainloop() {
if (!this.#plenticoreAPI.loggedIn) {
this.#isInitialized = false;
this.setState('info.connection', false, true);
try {
await this.#plenticoreAPI.login();
} catch (e) {
if (e == 'auth') {
// stop loop due to authorization issue
this.log.warn('terminating because of authentication issue');
this.terminate();
} else {
this.nextLoop();
}
return;
}
}
if (!this.#isInitialized) {
try {
// wait until inverter is in state FeedIn, otherwise REST API is not stable
let inverterStatePoll = PlenticoreData.getPollInverterStateID();
try {
let inverterStateResp = await this.#plenticoreAPI.getProcessData(inverterStatePoll);
let inverterState = inverterStateResp[0].processdata[0].value;
if (inverterState != 6) {
throw new Error('inverter not in state FeedIn');
}
} catch (e) {
throw new Error(e);
}
// want to delete unused objects later
let allAdapterObjs = await this.getAdapterObjectsAsync();
// init processdata
let allProcessData = await this.#plenticoreAPI.getAllProcessData();
let optionalProcessData;
try {
optionalProcessData = JSON.parse(this.config.pdoptionals);
} catch {
optionalProcessData = [];
}
this.#processdata.setAllIDs(allProcessData);
await this.#processdata.init(optionalProcessData, allAdapterObjs);
// init settings
let allSettings = await this.#plenticoreAPI.getAllSettings();
let optionalSettings;
try {
optionalSettings = JSON.parse(this.config.settingoptionals);
} catch {
optionalSettings = [];
}
this.#settings.setAllIDs(allSettings);
await this.#settings.init(optionalSettings, allAdapterObjs);
// subscribe to all settings that are writable
this.subscribeStates('settings.*');
// init done
this.setState('info.connection', true, true);
this.#isInitialized = true;
this.#lastUpdateCheck = 0;
this.log.info('init completed');
} catch (e) {
if (e == 'auth') {
// stop loop due to authorization issue
this.log.warn('terminating because of authentication issue');
this.terminate();
} else {
this.log.debug(`init failed with error: ${e}`);
this.nextLoop();
return;
}
}
}
// poll process data and settings
try {
let pdPollIDs = this.#processdata.getPollIDs();
let processData = await this.#plenticoreAPI.getProcessData(pdPollIDs);
await this.#processdata.processData(processData);
let settingPollIDs = this.#settings.getPollIDs();
let settings = await this.#plenticoreAPI.getSettings(settingPollIDs);
await this.#settings.processData(settings);
} catch (e) {
if (e == 'auth') {
// stop loop due to authorization issue
this.log.warn('terminating because of authentication issue');
this.terminate();
} else {
this.nextLoop();
return;
}
}
// check if an update is available to be installed.
const today = new Date().getDate();
if (today != this.#lastUpdateCheck) {
let method = await this.getStateAsync('settings.scb.update.Update_Method');
if (method != 2) {
this.log.info('check for update');
let version = await this.getStateAsync('processdata.scb.update.Update_Version');
if (version.val != 0) {
this.log.info('new update available');
let text = I18n.translate('available version');
await this.registerNotification('plenticore-g3', 'update', `${text} : ${version.val}`);
}
}
this.#lastUpdateCheck = today;
}
// timer for next interval
this.nextLoop();
}
nextLoop() {
this.#mainlooptimer = this.setTimeout(
() => {
this.mainloop();
},
parseInt(this.config.pollinginterval) * 1000,
);
}
}
if (require.main !== module) {
// Export the constructor in compact mode
/**
* @param {Partial<utils.AdapterOptions>} [options] - options given from iobroker
*/
module.exports = options => new PlenticoreG3(options);
} else {
// otherwise start the instance directly
new PlenticoreG3();
}