iobroker.solarmanpv
Version:
Reading data from balcony power plant
584 lines (555 loc) • 16.2 kB
JavaScript
/*
* Created with @iobroker/create-adapter v2.1.1
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
'use strict';
const utils = require('@iobroker/adapter-core');
const crypto5 = require('crypto');
const api = require('./lib/solarmanpvApiClient.js');
class Solarmanpv extends utils.Adapter {
/**
* @param [options] empty
*/
constructor(options) {
super({
...options,
name: 'solarmanpv',
});
this.on('ready', this.onReady.bind(this));
this.on('unload', this.onUnload.bind(this));
api.eventEmitter.on('tokenChanged', this.onTokenChanged.bind(this));
//
this.stationIdList = [];
this.modulList = [];
this.modulSelect = [];
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
// Initialize your adapter here
this.log.debug(`[onReady] started: ${this.namespace}`);
if (!this.config.email || !this.config.password) {
this.log.error(`User email and/or user password empty - please check instance configuration`);
return;
}
if (!this.config.appId || !this.config.appSecret) {
this.log.error(`Solarman APP ID and/or APP Secrets empty - please check instance configuration`);
return;
}
// About User changes
await this.checkUserData();
api.email = this.config.email;
api.password = this.config.password;
api.appId = this.config.appId;
api.appSecret = this.config.appSecret;
api.companyName = this.config.companyName;
const object = this.config.activeToken;
if (typeof object !== 'undefined' && object !== null) {
api.token = this.config.activeToken;
}
// start with delay
await this.delay(Math.floor(Math.random() * 5000));
try {
// [main]
// get station-id via api-call
await this.initializeStation().then(async result => await this.updateStationData(result));
for (const stationId of this.stationIdList) {
await this.initializeInverter(stationId).then(async inverterList => {
await this.manageInverterDevice(inverterList);
for (const inverter of inverterList) {
await this.getDeviceData(inverter.deviceId, inverter.deviceSn)
.then(async data => await this.updateDeviceData(stationId, inverter, data))
.catch(error => {
this.log.debug(`[iterate devices] Device ID: ${inverter.deviceId} skipped`);
this.log.silly(`[iterate devices] ${error}`);
});
}
});
}
} catch (error) {
this.log.debug(`[main] catch ${JSON.stringify(error)}`);
} finally {
this.log.debug(`[onReady] finished - stopping instance`);
this.terminate
? this.terminate('Everything done. Going to terminate till next schedule', 11)
: process.exit(0);
}
// End onReady
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param callback callback
*/
onUnload(callback) {
try {
// Here you must clear all timeouts or intervals that may still be active
this.log.debug('[onUnload] cleaned everything up...');
callback();
} catch (e) {
callback();
this.log.debug(`[onUnload] ${JSON.stringify(e)}`); //eslint no-unused-vars
}
}
/**
* save data in ioBroker datapoints
*
* @param station stationId
* @param device deviceId
* @param name name
* @param description description
* @param value value
* @param role role
* @param unit unit
* @param nullable nullable
*/
async persistData(station, device, name, description, value, role, unit, nullable) {
let dp_Folder;
let sensorName;
if (device == '') {
dp_Folder = String(station);
sensorName = `${station}.${description}`;
} else {
dp_Folder = `${String(station)}.${String(device)}`;
sensorName = `${device}.${description}`;
}
const dp_Device = String(`${dp_Folder}.${name}`);
//this.log.debug(`[persistData] Station "${station}" Device "${device}" Name "${name}" Sensor "${description}" with value: "${value}" and unit "${unit}" as role "${role}`);
this.log.silly(`[persistData] Sensorname "${sensorName}"`);
//
await this.setObjectNotExists(dp_Folder, {
type: 'channel',
common: {
name: {
en: 'Values from device',
de: 'Werte vom Gerät',
ru: 'Значения от устройства',
pt: 'Valores do dispositivo',
nl: 'Waarden van het apparaat',
fr: "Valeurs de l'appareil",
it: 'Valori dal dispositivo',
es: 'Valores desde el dispositivo',
pl: 'Wartości z urządzenia',
uk: 'Ціни від пристрою',
'zh-cn': '来自设备的值',
},
desc: 'generated by solarmanpv',
role: 'info',
},
native: {},
});
//
// Type recognition <number>
if (isNumber(value)) {
value = parseFloat(value);
//
await this.setObjectNotExistsAsync(dp_Device, {
type: 'state',
common: {
name: name,
type: 'number',
role: role,
unit: unit,
read: true,
write: true,
},
native: {},
});
//this.log.debug(`[persistData] Device "${dp_Device}" Key "${key}" with value: "${value}" and unit "${unit}" with role "${role}" as type "number"`);
} else {
// or <string>
await this.setObjectNotExistsAsync(dp_Device, {
type: 'state',
common: {
name: name,
type: 'string',
role: role,
unit: unit,
read: true,
write: true,
},
native: {},
});
//this.log.debug(`[persistData] Device "${dp_Device}" Key "${key}" with value: "${value}" and unit "${unit}" with role "${role}" as type "string"`);
}
// Differentiated writing of data
if (nullable) {
await this.setState(dp_Device, { val: 0, ack: true, q: 0x42 }); // Nullable values while device is not present
} else {
await this.setState(dp_Device, { val: value, ack: true, q: 0x00 });
}
//
function isNumber(n) {
return !isNaN(parseFloat(n)) && !isNaN(n - 0);
}
}
/**
* update inverter data in ioBroker
*
* @param stationId Number of the station
* @param inverter inverter object
* @param data data object
*/
async updateDeviceData(stationId, inverter, data) {
// only selected moduls
for (const obj of this.modulList) {
if (!this.modulSelect.includes(obj['modul'])) {
if (obj['checkSelect']) {
this.modulSelect.push(obj['modul']);
}
}
}
if (this.modulSelect.includes(inverter.deviceId)) {
this.log.debug(`[updateDeviceData] Device ID: ${inverter.deviceId}`);
await this.persistData(
stationId,
inverter.deviceId,
'deviceType',
'deviceType',
inverter.deviceType,
'state',
'',
false,
);
await this.persistData(
stationId,
inverter.deviceId,
'connectStatus',
'connectStatus',
inverter.connectStatus,
'state',
'',
false,
);
await this.persistData(
stationId,
inverter.deviceId,
'collectionTime',
'collectionTime',
inverter.collectionTime * 1000,
'date',
'',
false,
);
const isOffline = inverter.connectStatus == 0 ? true : false;
// blacklist-keys that shall not be updated
for (const obj of data.dataList) {
const result = this.config.deviceBlacklist.includes(obj.key);
if (!result && obj.value != 'none') {
const setToZero = isOffline && this.config.deviceZero.includes(obj.key);
await this.persistData(
stationId,
inverter.deviceId,
obj.key,
obj.name,
obj.value,
'state',
obj.unit,
setToZero,
);
} else {
await this.deleteDeviceState(stationId, inverter.deviceId, obj.key);
}
}
} else {
await this.deleteDeviceObject(stationId, inverter.deviceId);
}
}
/**
* update station data in ioBroker
*
* @param data data object
*/
async updateStationData(data) {
for (const obj of data) {
// define keys that shall be updated
const updateKeys = [
['name', 'state', ''],
['generationPower', 'value.power', 'W'],
['networkStatus', 'state', ''],
['lastUpdateTime', 'date', ''],
];
for (const key of updateKeys) {
if (key[0] == 'lastUpdateTime') {
// special case 'lastUpdateTime'
obj[key[0]] *= 1000;
}
await this.persistData(obj['id'], '', key[0], key[0], obj[key[0]], key[1], key[2], false);
}
}
}
/**
* get inverter data from api
*
* @param deviceId Number of the device
* @param deviceSn Serial number of the device
* @returns data
*/
async getDeviceData(deviceId, deviceSn) {
this.log.debug(`[getDeviceData] Device ID: ${deviceId} with Device SN: ${deviceSn}`);
return api.axios
.post(
'/device/v1.0/currentData?language=en', // language parameter does not show any effect
{
deviceId: deviceId,
deviceSn: deviceSn,
},
)
.then(response => {
return response.data;
})
.catch(error => {
this.log.warn(`[getDeviceData] ${error} ID: ${deviceId}`);
return Promise.reject(error);
});
}
/**
* Collects the device IDs that were read in order to subsequently save
* them in the configuration.
*
* @param inverterList List of inverter objects
*/
async manageInverterDevice(inverterList) {
let modulListChanged = false;
this.modulList = this.config.deviceModules;
let isArray = Array.isArray(this.modulList);
if (!isArray || this.config.clearModules) {
this.modulList = [];
this.log.debug(`[manageInverterDevice] Modullist cleared`);
}
// add new devices
for (const inverter of inverterList) {
if (!this.modulList.find(element => element.modul == inverter.deviceId)) {
this.log.debug(`[manageInverterDevice] ADD: ${inverter.deviceId}`);
this.modulList.push({ modul: inverter.deviceId, checkSelect: true }); //default
modulListChanged = true;
}
}
// write into config if changes
if (modulListChanged) {
this.getForeignObject(`system.adapter.${this.namespace}`, (err, obj) => {
if (err) {
this.log.error(`[manageInverterDevice] ${err}`);
} else {
if (obj) {
obj.native.deviceModules = this.modulList; // modify object
this.setForeignObject(obj._id, obj, err => {
if (err) {
this.log.error(`[manageInverterDevice] Error while DeviceListUpdate: ${err}`);
} else {
this.log.debug(
`[manageInverterDevice] New Devicelist: ${JSON.stringify(this.modulList)}`,
);
}
});
// config.clearModules reset
if (this.config.clearModules) {
this.config.clearModules = false;
obj.native.clearModules = this.config.clearModules;
this.setForeignObject(obj._id, obj, err => {
if (err) {
this.log.error(`[manageInverterDevice] Error while resetting clearModules: ${err}`);
} else {
this.log.debug(`[manageInverterDevice] clearModules resettet`);
}
});
}
}
}
});
}
}
/**
* get inverter-id from api
*
* @param stationId Number of the station
* returns deviceListItems
*/
async initializeInverter(stationId) {
this.log.debug(`[initializeInverter] Station ID: ${stationId}`);
return api.axios
.post(
'/station/v1.0/device?language=en', // language parameter does not show any effect
{
page: 1,
size: 19,
stationId: stationId,
},
)
.then(response => {
return response.data.deviceListItems;
})
.catch(error => {
this.log.warn(`[initializeInverter] error: ${error.code}`);
return Promise.reject(error);
});
}
/**
* Get station id from api (multiple)
*
* @returns stationlist
*/
initializeStation() {
return api.axios
.post(
'/station/v1.0/list?language=en', // language parameter does not show any effect
{
page: 1,
size: 20,
},
)
.then(response => {
for (const obj of response.data.stationList) {
this.stationIdList.push(obj['id']); // StationId's for devices
}
return response.data.stationList;
})
.catch(error => {
this.log.warn(`[initializeStation] error: ${error.code}`);
return Promise.reject(error);
});
}
/**
* Is called when ApiClient has received new token.
*
* @param token new token
*/
async onTokenChanged(token) {
this.log.debug(`[onTokenChanged] token changed: ${token}`);
this.extendForeignObject(`system.adapter.${this.namespace}`, {
native: {
activeToken: token,
},
});
}
/**
* Check whether user data are plausible
*/
async checkUserData() {
let inputData =
this.config.email +
this.config.password +
this.config.appId +
this.config.appSecret +
this.config.companyName;
let crc = crypto5.createHash('md5').update(inputData).digest('hex');
// get oldCRC
const object = await this.getStateAsync('checksumUserData');
if (typeof object !== 'undefined' && object !== null) {
this.oldCrc = object?.val;
}
this.log.debug(`[checkUserData] Crc ${this.oldCrc}`);
// compare to previous config
if (!this.oldCrc || this.oldCrc != crc) {
this.log.debug(`[checkUserData] has changed or is new; previous crc: ${this.oldCrc}`);
// store new crc
this.log.debug(`[checkUserData] store new hash: ${crc}`);
// write datapoint
await this.setObjectNotExistsAsync('checksumUserData', {
type: 'state',
common: {
name: {
en: 'Checksum user data',
de: 'Checksumme Benutzerdaten',
ru: 'Проверьте данные пользователя Checksum',
pt: 'Dados do usuário do checksum',
nl: 'Vertaling:',
fr: 'Vérifier les données utilisateur',
it: 'Dati utente di checksum',
es: 'Datos de usuario de checksum',
pl: 'Checksum data',
uk: 'Перевірити дані користувачів',
'zh-cn': '用户数据',
},
type: 'string',
role: 'state',
read: true,
write: false,
},
native: {},
});
await this.setState('checksumUserData', { val: crc, ack: true });
// delete Token
this.getForeignObject(`system.adapter.${this.namespace}`, (err, obj) => {
if (err) {
this.log.error(`[checkUserData] ${err}`);
} else {
if (obj) {
obj.native.activeToken = '';
this.setForeignObject(obj._id, obj, err => {
if (err) {
this.log.error(`[checkUserData] Error while deleting token: ${err}`);
} else {
this.log.debug(`[checkUserData] Token deleted`);
}
});
}
}
});
}
return;
}
/**
* Deletes states
*
* @param stationID stationId
* @param deviceName deviceId
* @param stateName stateName
*/
async deleteDeviceState(stationID, deviceName, stateName) {
const stateToDelete = `${stationID}.${deviceName}.${stateName}`;
try {
// Verify that associated object exists
const currentObj = await this.getStateAsync(stateToDelete);
if (currentObj) {
await this.delObjectAsync(stateToDelete);
this.log.debug(`[deleteDeviceState] Device ID: (${stateToDelete})`);
} else {
const currentState = await this.getStateAsync(stateName);
if (currentState) {
this.log.debug(`[deleteDeviceState] State: (${stateToDelete})`);
}
}
} catch (e) {
this.log.error(`[deleteDeviceState] error ${e} while deleting: (${stateToDelete})`);
}
}
/**
* Deletes object or states (recursive)
*
* @param stationId StationId
* @param deviceId DeviceId
*/
async deleteDeviceObject(stationId, deviceId) {
const deviceName = `${stationId}.${deviceId}`;
try {
// Verify that associated object exists
const currentObj = await this.getObjectAsync(deviceName);
if (currentObj) {
await this.delObjectAsync(deviceName, { recursive: true });
this.log.debug(`[deleteDeviceObject] Device ID: (${deviceName})`);
} else {
const currentState = await this.getStateAsync(deviceName);
if (currentState) {
await this.deleteStateAsync(deviceName);
this.log.debug(`[deleteDeviceObject] State: (${deviceName})`);
}
}
} catch (e) {
this.log.error(`[deleteDeviceObject] error ${e} while deleting: (${deviceName})`);
}
}
// End Class
}
if (require.main !== module) {
// Export the constructor in compact mode
/**
* @param [options] empty
*/
module.exports = options => new Solarmanpv(options);
} else {
// otherwise start the instance directly
new Solarmanpv();
}