UNPKG

iobroker.sun2000

Version:
405 lines (368 loc) 13.2 kB
'use strict'; const { deviceType, storeType, dataRefreshRate, dataType } = require(`${__dirname}/../types.js`); const { Logging, RegisterMap } = require(`${__dirname}/../tools.js`); class DriverBase { constructor(stateInstance, device, options) { this.state = stateInstance; this.adapter = stateInstance.adapter; this.stateCache = stateInstance.stateCache; this.deviceInfo = device; this._modbusClient = null; //NEW!! //https://wiki.selfhtml.org/wiki/JavaScript/Operatoren/Nullish_Coalescing_Operator //https://stackoverflow.com/questions/2851404/what-does-options-options-mean-in-javascript options = options || {}; ((this._modbusId = options.modbusId ?? device.modbusId), (this._modelId = options?.modelId)); ((this._driverClass = options?.driverClass), (this._name = options?.name)); this._errorCount = 0; this._modbusAllowed = true; //modbus request is allowed this._deviceStatus = -1; //device shutdown or standby this._regMap = new RegisterMap(); this.control = null; //Sdongle Service Queue, Emma Service Queue this.log = new Logging(this.adapter); //my own Logger this.registerFields = []; this.postUpdateHooks = []; this._now = new Date(); //this._newNow(); } /** * Modbus ID of the device */ get modbusId() { return this._modbusId; } get info() { return { driverClass: this._driverClass, modelId: this._modelId, name: this._name, modbusAllowed: this._modbusAllowed, deviceStatus: this._deviceStatus, }; } get modbusAllowed() { return this._modbusAllowed; } get deviceStatus() { return this._deviceStatus; } _newNowTime() { this._now = new Date(); return this._now.getTime(); } addHoldingRegisters(startAddr, data) { this._regMap.set(startAddr, data); } getHoldingRegisters(startAddr, length) { const data = this._regMap.get(startAddr, length, this.adapter.isReady); if (this.isTestMode() && startAddr === 30302) { this.log.info(`### TestMode ### get raw data from device/id/address/data ${this._name}/${this._modbusId}/${startAddr}/${data}`); } return data; } /** * Checks if the device is in test mode * @returns {boolean} True if the device is in test mode, false otherwise */ isTestMode() { return false; } logHoldingRegisters(startAddr, length) { if (startAddr === 30302) { const data = this.getHoldingRegisters(startAddr, length); this.log.info(`### TestMode ### read data from device/id/address/data ${this._name}/${this._modbusId}/${startAddr}/${data}`); } } _fromArray(data, address, field) { //nullish coalescing Operator (??) const len = field.register.length ?? dataType.size(field.register.type); const pos = field.register.reg - address; return dataType.convert(data.slice(pos, pos + len), field.register.type); } _getStatePath(type) { let path = ''; if (type !== deviceType.meter) { path = this.deviceInfo.path; } if (path !== '') { path += '.'; } return path; } //v0.8.x _checkValidValueRange(value, register) { if (typeof value === 'number') { if (value === 0) { return value; } //always correct let smallest = 0; let biggest = 0; switch (register.type) { case dataType.int16: //–32.768 bis 32.767 biggest = 32767; smallest = -biggest - 1; break; case dataType.uint16: //0 bis 65.535 biggest = 65535; break; case dataType.int32: //-2,147,483,648 bis 2,147,483,647 biggest = 2147483647; smallest = -biggest - 1; break; case dataType.uint32: // biggest = 4294967295; break; default: biggest = Number.MAX_SAFE_INTEGER; smallest = -biggest; break; } if (value > smallest && value < biggest) { return value; } this.log.debug(`_checkValidValueRange ${value} smallest: ${smallest} biggest: ${biggest} register: ${register.reg}`); return 0; } return value; } async _processRegister(reg, data) { this.addHoldingRegisters(reg.address, data); //store row Data of modbus registers const path = this._getStatePath(reg.type); //pre hook if (reg.preHook) { reg.preHook(path, reg); } if (reg.states) { for (const field of reg.states) { const state = field.state; if (field.store !== storeType.never && !state.initState) { await this.state.initState(path, state); state.initState = true; } if (field.register) { let value = this._fromArray(data, reg.address, field); if (value !== null) { //v0.8.x if (field.register.type) { value = this._checkValidValueRange(value, field.register); } if (field.register.gain) { value /= field.register.gain; } if (field.mapper) { value = await field.mapper(value); } this.stateCache.set(path + state.id, value, { renew: field?.store === storeType.always, stored: field?.store === storeType.never, }); } } } } //post hook if (reg.postHook) { reg.postHook(path); } } /** * Read the device list for a given modbusId. * @param {ModbusClient} modbusClient - The modbus client to use. * @param {number} [modbusId] - The modbus ID to query. * @returns {Promise<[number, { [key: string]: string }]>} * The first element of the array is the number of devices, * the second element is an object with the device information. */ async readDeviceList(modbusClient, modbusId = 0) { this.log.debug('Read Device List (OID=0x87)…'); const allInfo = {}; let objectId = 0x87; try { const resp = await modbusClient.readDeviceIdentification(modbusId, 3, objectId, this.log); this.log.debug(`Device List: ${JSON.stringify(resp)}`); Object.assign(allInfo, resp); } catch (e) { throw new Error(`readDeviceList: No answer for OID=0x${objectId.toString(16).toUpperCase()}: ${e.message}`); } const numDevices = parseInt(JSON.stringify(allInfo['135'] || '').replace(/[^0-9]/g, '')); return [numDevices, allInfo]; } _parseDeviceDescription(descBytes) { const descStr = descBytes.toString('ascii', 0, descBytes.length).replace(/\0/g, ''); const attrs = {}; descStr.split(';').forEach(pair => { if (!pair.includes('=')) return; const [k, v] = pair.split('=', 2); try { attrs[parseInt(k)] = v; } catch { this.log.debug(`Unbekannter Schlüssel ${k} in Beschreibung: ${v}`); } }); return attrs; } /** * Identifies subdevices connected to a Modbus network by name and returns detailed information. * * This function queries the Modbus network for devices, parses their descriptions, and returns * a list of subdevices matching the specified device name. Each identified subdevice includes * its object ID, attributes, and slave ID. * * Many thanks for the implementation in python by WookyDO/huawei_emma_charger * * @param {string} deviceName - The name of the device to search for. * @param {number} [modbusId] - The Modbus ID to query. Defaults to 0. * @returns {Promise<Array<object>>} A promise that resolves to an array of objects, where each * object contains the following properties: * - {string} obj_id: The object ID of the subdevice. * - {Object} attrs: The attributes of the subdevice. * - {number} slave_id: The slave ID of the subdevice. */ async identifySubdevices(deviceName, modbusId = 0) { const [count, info] = await this.readDeviceList(this._modbusClient, modbusId); this.log.debug(`Total devices found: ${count}`); deviceName = deviceName.toUpperCase(); const chargers = []; for (const [oid, raw] of Object.entries(info)) { const numOid = parseInt(oid); if (numOid == 0x87) continue; const attrs = this._parseDeviceDescription(raw); //this.log.debug(`attrs: ${JSON.stringify(attrs)}`); let compareValue = (attrs[8] || '').toUpperCase(); if (deviceName === 'SUN2000') { compareValue = (attrs[1] || '').toUpperCase().slice(0, 7); } if (compareValue === deviceName.toUpperCase()) { const sidVal = attrs[5]; let sid; try { sid = parseInt(sidVal); } catch { this.log.warn(`identifySubdevices: Invalid Slave-ID at OID=0x${numOid.toString(16).toUpperCase()}: ${sidVal}`); continue; } chargers.push({ obj_id: oid, attrs: attrs, slave_id: sid }); this.log.debug(`identifySubdevices: ${deviceName} found: OID=0x${numOid.toString(16).toUpperCase()}, Slave ID=${sid}`); } } return chargers; } /** * Updates the states of the device. * * This function sets the Modbus client, then calls the battery control and active power control * if the refresh rate is not high. It then iterates over the register fields and calls * the readHoldingRegisters function on the Modbus client to read the data from the registers. * For each register, it checks if the device is allowed to communicate with the Modbus network, * if the refresh rate is equal to the register's refresh rate, and if the register's checkIfActive * function returns true. If all conditions are met, it calls the _processRegister function to process * the data from the register. * If the refresh rate is low or empty, it checks if the last read time is older than the specified * interval. If so, it calls the readHoldingRegisters function to read the data from the register. * If any errors occur during the read operation, it catches the error and logs a warning message. * If the error is a connection error, it stops the update loop. * Finally, it calls the _runPostUpdateHooks function to run any post update hooks and stores the states. * * @param {ModbusClient} modbusClient - The Modbus client to use for communication. * @param {dataRefreshRate} refreshRate - The refresh rate to use for updating the states. * @param {number} [duration] - The duration in milliseconds for which the states should be updated. * @returns {Promise<number>} - A promise that resolves to the number of registers read. */ async updateStates(modbusClient, refreshRate, duration) { this._modbusClient = modbusClient; if (this._modbusId >= 0) { modbusClient.setID(this._modbusId); } const start = this._newNowTime(); //battery control and active power control if (this.control && refreshRate !== dataRefreshRate.high) { await this.control.process(modbusClient); } //The number of Registers reads let readRegisters = 0; for (const reg of this.registerFields) { if (duration) { if (this._newNowTime() - start > duration - this.adapter.settings.modbusDelay) { this.log.debug(`### Duration: ${Math.round(duration / 1000)} used time: ${(this._now.getTime() - start) / 1000}`); break; } } //if the device is down or standby we cannot read or write anythink?! if (!this.modbusAllowed && reg.standby !== true) { continue; } //standby - v0.6.2 if (!dataRefreshRate.compare(refreshRate, reg.refresh)) { continue; } //refreshrate unequal if (reg.type == deviceType.meter && !this.deviceInfo?.meter) { continue; } //meter if (reg.type == deviceType.gridPowerControl && !this.deviceInfo?.meter) { continue; } //Grid Power Control - v0.8.x if (reg.checkIfActive && !reg.checkIfActive()) { continue; } //NEW, PATH //refresh rate low or empty const lastread = reg.lastread; if (refreshRate !== dataRefreshRate.high) { if (lastread) { if (!reg.refresh) { continue; } let interval = this.adapter.settings.lowInterval; if (reg.refresh === dataRefreshRate.medium) { interval = this.adapter.settings.mediumInterval; } if (start - lastread < interval) { this.log.debug(`Last read reg for ${start - lastread} ms - ${reg?.info}`); continue; } } } try { this.log.debug(`Try to read data from id/address ${modbusClient.id}/${reg.address}/${reg.refresh}/${reg.info}`); const data = await modbusClient.readHoldingRegisters(reg.address, reg.length, this.log); //my Logger reg.lastread = this._newNowTime(); await this._processRegister(reg, data); readRegisters++; this._errorCount = 0; } catch (err) { //Only increase if modbus is not connected ?? if (err.modbusCode === undefined) { this._errorCount++; } if (!reg.readErrorHook || !reg.readErrorHook(err, reg)) { this.log.warn( `Error while reading from ${modbusClient.ipAddress} [Reg: ${reg.address}, Len: ${reg.length}, modbusID: ${modbusClient.id}] with: ${err.message}`, ); if (err.code == 'EHOSTUNREACH' || err.modbusCode === 5 || err.modbusCode === 6) { this.log.debug('Update loop stopped'); break; } } } } //Einschubfunktionen await this._runPostUpdateHooks(refreshRate); this.state.storeStates(); //fire and forget return readRegisters; } numberBatteryUnits() { return 0; } //inverter async _runPostUpdateHooks(refreshRate) { const path = this._getStatePath(deviceType.inverter); for (const hook of this.postUpdateHooks) { if (dataRefreshRate.compare(refreshRate, hook.refresh)) { const state = hook.state; if (state && !hook.initState) { await this.state.initState(path, state); hook.initState = true; } hook.fn(path); } } } } module.exports = DriverBase;