iobroker.plenticore-g3
Version:
Adapter to communicate with a KOSTAL Plenticore Plus via local network (non-modbus)
448 lines (419 loc) • 17.2 kB
JavaScript
//-----------------------------------------------------------------------------
// preset processdata IDs
//-----------------------------------------------------------------------------
const presetProcessDataIDs = [
{ id: 'devices:local/Grid_P', description: 'Grid_P', role: 'value.power' },
{ id: 'devices:local/Dc_P', description: 'Dc_P', role: 'value.power' },
{ id: 'devices:local/Home_P', description: 'Home_P', role: 'value.power' },
{ id: 'devices:local:ac/P', description: 'ac/P', role: 'value.power' },
{ id: 'devices:local:battery/P', description: 'battery/P', role: 'value.power' },
{ id: 'devices:local:battery/SoC', description: 'battery/SoC' },
{ id: 'scb:statistic:EnergyFlow/Statistic:Yield:Day', description: 'Statistic:Yield:Day', role: 'value.energy' },
{
id: 'scb:statistic:EnergyFlow/Statistic:EnergyChargePv:Day',
description: 'Statistic:EnergyChargePv:Day',
role: 'value.energy',
},
{
id: 'scb:statistic:EnergyFlow/Statistic:EnergyHome:Day',
description: 'Statistic:EnergyHome:Day',
role: 'value.energy',
},
{
id: 'scb:statistic:EnergyFlow/Statistic:EnergyHomeBat:Day',
description: 'Statistic:EnergyHomeBat:Day',
role: 'value.energy',
},
{
id: 'scb:statistic:EnergyFlow/Statistic:EnergyHomeGrid:Day',
description: 'Statistic:EnergyHomeGrid:Day',
role: 'value.energy',
},
{
id: 'scb:statistic:EnergyFlow/Statistic:EnergyHomePv:Day',
description: 'Statistic:EnergyHomePv:Day',
role: 'value.energy',
},
{ id: 'scb:update/Update:Version', description: 'Update:Version', role: 'value' },
];
//-----------------------------------------------------------------------------
// preset setting IDs
//-----------------------------------------------------------------------------
const presetSettingIDs = [
{ id: 'devices:local/Battery:MinHomeComsumption', description: 'Battery:MinHomeComsumption', role: 'level' },
{ id: 'devices:local/Battery:MinSoc', description: 'Battery:MinSoc', role: 'level.battery' },
{ id: 'scb:update/Update:Method', description: 'Update:Method', role: 'state' },
];
const EntryID = {
id: '',
description: '',
role: '',
preselected: false,
unit: '',
type: '',
min: 0,
max: 0,
};
const EntryPollPd = {
moduleid: '',
processdataids: {},
};
const EntryPollSetting = {
moduleid: '',
settingids: {},
};
//-----------------------------------------------------------------------------
/**
* Class handles process data and settings
*/
class PlenticoreData {
#adapter;
#I18n;
#type = '';
#presetIDs = [];
#allIDsRaw;
#allIDs = []; // all available IDs <moduelsid>/<id>
#pollIDs = []; // array for api call
#optionalIDs = [];
#isCreateObjects = true;
/**
* @param {object} adapter - adapter object
* @param {object} I18n - used for translations
* @param {string} type - to determine drocessdata and settings
*/
constructor(adapter, I18n, type) {
this.#adapter = adapter;
this.#I18n = I18n;
this.#type = type;
if (type == 'processdata') {
this.#presetIDs = presetProcessDataIDs;
} else {
this.#presetIDs = presetSettingIDs;
}
}
// private methods
#createPollIDs() {
this.#pollIDs = [];
for (const preset of this.#presetIDs) {
let foundInAllIDs = this.#allIDs.find(element => element.id == preset.id);
let key = this.#type == 'processdata' ? 'processdataids' : 'settingids';
if (foundInAllIDs) {
let moduleId = preset.id.substring(0, preset.id.indexOf('/'));
let foundInPoll = this.#pollIDs.find(element => element.moduleid == moduleId);
if (foundInPoll) {
let key = this.#type == 'processdata' ? 'processdataids' : 'settingids';
foundInPoll[key].push(preset.id.substring(preset.id.indexOf('/') + 1));
} else {
let entry = this.#type == 'processdata' ? { ...EntryPollPd } : { ...EntryPollSetting };
let ids = [preset.id.substring(preset.id.indexOf('/') + 1)];
entry['moduleid'] = moduleId;
entry[key] = ids;
this.#pollIDs.push(entry);
}
} else {
this.#adapter.log.warning(`preset not found in list: {preset.id}`);
}
}
for (const optional of this.#optionalIDs) {
let foundInAllIDs = this.#allIDs.find(element => element.id == optional.id);
let key = this.#type == 'processdata' ? 'processdataids' : 'settingids';
// skip optionals that have become presets
if (foundInAllIDs && foundInAllIDs.preset) {
continue;
}
if (foundInAllIDs) {
let moduleId = optional.id.substring(0, optional.id.indexOf('/'));
let foundInPoll = this.#pollIDs.find(element => element.moduleid == moduleId);
if (foundInPoll) {
foundInPoll[key].push(optional.id.substring(optional.id.indexOf('/') + 1));
} else {
let entry = this.#type == 'processdata' ? { ...EntryPollPd } : { ...EntryPollSetting };
let ids = [optional.id.substring(optional.id.indexOf('/') + 1)];
entry['moduleid'] = moduleId;
entry[key] = ids;
this.#pollIDs.push(entry);
}
} else {
this.#adapter.log.warning(`optionsl not found in list: {preset.id}`);
}
}
}
async #deleteUnusedObjects(allObjects) {
for (const [key, value] of Object.entries(allObjects)) {
if (value.type == 'state') {
let topChannel = value.native.type;
let moduleId = value.native.moduleid;
let id = value.native.dataid;
if (!topChannel || !moduleId || !id) {
continue;
}
if (topChannel == this.#type) {
let found = this.#presetIDs.find(elem => elem.id == `${moduleId}/${id}`);
if (!found) {
found = this.#optionalIDs.find(elem => elem.id == `${moduleId}/${id}`);
}
if (!found) {
await this.#adapter.delObjectAsync(key);
this.#adapter.log.info(`deleted unsued object ${key}`);
}
}
}
}
for (const [key, value] of Object.entries(allObjects)) {
if (value.type == 'folder') {
let topChannel = value.native.type;
if (topChannel && topChannel == this.#type) {
let subStates = await this.#adapter.getStatesAsync(`${key}.*`);
if (!subStates || Object.keys(subStates).length === 0) {
await this.#adapter.delObjectAsync(key);
this.#adapter.log.info(`deleted unsued folder ${key}`);
}
}
}
}
}
static #name2id(name) {
return (name || '').replace(/[^A-Za-z0-9-_]/g, '_');
}
static #guessRoleFromId(module, id) {
if (id == 'P' || id.endsWith('_P')) {
return 'value.energy';
} else if (id == 'I' || id.endsWith('_I')) {
return 'value.current';
} else if (id == 'V' || id.endsWith('_V')) {
return 'value.voltage';
} else if (module.includes('EnergyFlow')) {
return 'value.energy';
}
return 'value';
}
async #createObjects(values) {
for (const module of values) {
let folderid = this.#type;
// create folder structure
let moduleids = module.moduleid.split(':');
for (const moduleid of moduleids) {
folderid += `.${PlenticoreData.#name2id(moduleid)}`;
await this.#adapter.extendObjectAsync(
folderid,
{
type: 'folder',
common: {
name: moduleid,
},
native: {
type: this.#type,
moduleid: moduleid,
},
},
{ preserve: { common: ['name'] } },
);
}
// create objects
for (const value of module[this.#type]) {
let description = '';
let found = this.#presetIDs.find(element => element.id == `${module.moduleid}/${value.id}`);
if (!found) {
found = this.#optionalIDs.find(element => element.id == `${module.moduleid}/${value.id}`);
}
if (found) {
description = this.#I18n.getTranslatedObject(found.description);
}
let stateid = `${folderid}.${PlenticoreData.#name2id(value.id)}`;
if (this.#type == 'processdata') {
let pdata = this.#allIDs.find(element => element.id == `${module.moduleid}/${value.id}`);
if (!pdata) {
this.#adapter.log.warning(`processdata not found in allIDS: ${module.moduleid}/${value.id}`);
continue;
}
let role =
pdata.role != '' ? pdata.role : PlenticoreData.#guessRoleFromId(module.moduleid, value.id);
await this.#adapter.extendObjectAsync(
stateid,
{
type: 'state',
common: {
name: value.id.replace('Statistic_', ''),
desc: description,
type: 'number',
role: role,
unit: value.unit,
read: true,
write: false,
},
native: {
type: this.#type,
moduleid: module.moduleid,
dataid: value.id,
},
},
{ preserve: { common: ['name'] } },
);
} else {
let setting = this.#allIDs.find(element => element.id == `${module.moduleid}/${value.id}`);
if (!setting) {
this.#adapter.log.warning(`setting not found in allIDS: ${module.moduleid}/${value.id}`);
continue;
}
let settingtype = setting.type == 'string' ? 'string' : 'number';
let role = setting.role != '' ? setting.role : 'state';
await this.#adapter.extendObjectAsync(
stateid,
{
type: 'state',
common: {
name: value.id,
desc: description,
type: settingtype,
role: role,
unit: setting.unit,
min: setting.min,
max: setting.max,
read: true,
write: true,
},
native: {
type: this.#type,
moduleid: module.moduleid,
dataid: value.id,
},
},
{ preserve: { common: ['name'] } },
);
}
}
}
this.#isCreateObjects = false;
}
// public methods
/**
* Stores all the ID available
*
* @param {object} IDs - raw jason as return from API call
*/
setAllIDs(IDs) {
this.#allIDsRaw = IDs;
}
/**
* Gets an Array with IDs to be polled
*/
getPollIDs() {
return this.#pollIDs;
}
/**
* Initializes the instance. Creates an array with all available IDs in form of <moduleid>/<dataid>
* and marks the preset IDs in this array
*
* @param {Array} optionalIDs - array with user defined optionals (from ui)
* @param {Array} allAdapterObjs - all adapter objects
*/
async init(optionalIDs, allAdapterObjs) {
this.#optionalIDs = optionalIDs || [];
this.#isCreateObjects = true;
// create an array of entries with ID comprised of 'moduleId/dataId'
this.#allIDs = [];
for (const module of this.#allIDsRaw) {
let moduleId = module['moduleid'];
if (this.#type == 'processdata') {
let IDs = module['processdataids'];
for (const ID of IDs) {
let entry = { ...EntryID };
entry.id = `${moduleId}/${ID}`;
entry.description = '';
entry.preselected = false;
this.#allIDs.push(entry);
}
} else if (this.#type == 'settings') {
let settings = module['settings'];
for (const setting of settings) {
if (setting.access != 'readwrite') {
continue;
}
let entry = { ...EntryID };
entry.id = `${moduleId}/${setting.id}`;
entry.description = '';
entry.preselected = false;
entry.unit = setting.unit;
entry.type = setting.type;
if (setting.min == null) {
delete entry.min;
} else {
entry.min = Number(setting.min);
}
if (setting.max == null) {
delete entry.max;
} else {
entry.max = Number(setting.max);
}
this.#allIDs.push(entry);
}
}
}
// add presets
for (const preset of this.#presetIDs) {
let found = this.#allIDs.find(element => element.id == preset.id);
if (found) {
found.description = this.#I18n.getTranslatedObject(preset.description);
found.preselected = true;
found.role = preset.role;
}
}
// update state that hold the array (used by admin)
this.#adapter.setState(`${this.#type}.all-available`, JSON.stringify(this.#allIDs), true);
this.#createPollIDs();
await this.#deleteUnusedObjects(allAdapterObjs);
}
/**
* Processes the data got from API call
*
* @param {Array} values - array values - see API doc
*/
async processData(values) {
if (this.#isCreateObjects) {
await this.#createObjects(values);
}
// write values
for (const module of values) {
let channelname = this.#type;
let moduleids = module.moduleid.split(':');
for (const moduleid of moduleids) {
channelname += `.${PlenticoreData.#name2id(moduleid)}`;
}
for (const value of module[this.#type]) {
let id = `${channelname}.${PlenticoreData.#name2id(value.id)}`;
if (this.#type == 'processdata') {
this.#adapter.setState(id, value.value, true);
} else {
let obj = await this.#adapter.getObjectAsync(id);
let type = obj.common.type;
let val = type == 'string' ? value.value : Number(value.value);
this.#adapter.setState(id, val, true);
}
}
}
}
/**
* Prepares values before write
*
* @param {string} stateId - id of the iobroker state
* @param {state} state - iobroker state
*/
async prepareWriteValue(stateId, state) {
let obj = await this.#adapter.getObjectAsync(stateId);
let moduleid = obj.native.moduleid;
let settingid = obj.native.dataid;
let payload = [
{
moduleid: moduleid,
settings: [
{
id: settingid,
value: `${state.val}`,
},
],
},
];
return payload;
}
}
module.exports = PlenticoreData;