iobroker.sun2000
Version: 
685 lines (633 loc) • 20.4 kB
JavaScript
'use strict';
/*
 * Created with @iobroker/create-adapter v2.5.0
 */
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
// Load your modules here, e.g.:
const Registers = require(`${__dirname}/lib/register.js`);
const ModbusConnect = require(`${__dirname}/lib/modbus/modbus_connect.js`);
const ModbusServer = require(`${__dirname}/lib/modbus/modbus_server.js`);
const { driverClasses, dataRefreshRate } = require(`${__dirname}/lib/types.js`);
const { Logging, getAstroDate, isSunshine } = require(`${__dirname}/lib/tools.js`);
const ConfigMap = require(`${__dirname}/lib/controls/config_map.js`);
class Sun2000 extends utils.Adapter {
	/**
	 * @param [options]
	 */
	constructor(options) {
		super({
			...options,
			name: 'sun2000',
			useFormatDate: true,
		});
		this.lastTimeUpdated = new Date().getTime();
		this.lastStateUpdatedHigh = 0;
		this.lastStateUpdatedLow = 0;
		this.isConnected = false;
		this.isReady = false;
		this.devices = [];
		this.settings = {
			highInterval: 20000,
			lowInterval: 60000,
			mediumInterval: 30000,
			address: '',
			port: 520,
			modbusTimeout: 10000,
			modbusConnectDelay: 5000,
			modbusDelay: 0,
			modbusAdjust: false,
			ms: {
				address: '0.0.0.0',
				port: 520,
				active: false,
			},
			sl: {
				meterId: 11,
			},
			sd: {
				active: false,
				sDongleId: 100,
			},
			cb: {
				tou: false,
			},
			ds: {
				batteryUnits: true,
				batteryPacks: false,
			},
		};
		//v0.6.
		this.logger = new Logging(this); //only for adapter
		//1.1.0
		this.control = new ConfigMap(this);
		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));
	}
	async initDevicePath(item) {
		if (item.driverClass == driverClasses.inverter) {
			const path = `inverter.${item.index.toString()}`;
			item.path = path;
			await this.extendObject(path, {
				type: 'channel',
				common: {
					name: `channel inverter ${item.index.toString()}`,
					role: 'indicator',
				},
				native: {},
			});
			await this.extendObject(`${path}.grid`, {
				type: 'channel',
				common: {
					name: 'channel grid',
				},
				native: {},
			});
			await this.extendObject(`${path}.info`, {
				type: 'channel',
				common: {
					name: 'channel info',
					role: 'info',
				},
				native: {},
			});
			/*
				await this.extendObject(path+'.battery', {
					type: 'channel',
					common: {
						name: 'channel battery'
					},
					native: {}
				});
				*/
			await this.extendObject(`${path}.string`, {
				type: 'channel',
				common: {
					name: 'channel string',
				},
				native: {},
			});
			await this.extendObject(`${path}.derived`, {
				type: 'channel',
				common: {
					name: 'channel derived',
				},
				native: {},
			});
		}
		if (item.driverClass == driverClasses.sdongle) {
			item.path = '';
			await this.extendObject(`${item.path}sdongle`, {
				type: 'device',
				common: {
					name: 'device SDongle',
				},
				native: {},
			});
		}
		if (item.driverClass == driverClasses.logger) {
			item.path = '';
			await this.extendObject(`${item.path}slogger`, {
				type: 'device',
				common: {
					name: 'device SmartLogger',
				},
				native: {},
			});
		}
		if (item.driverClass == driverClasses.loggerMeter) {
			item.path = '';
		}
		if (item.driverClass == driverClasses.emma) {
			item.path = '';
			await this.extendObject(`${item.path}emma`, {
				type: 'device',
				common: {
					name: 'device Emma',
				},
				native: {},
			});
		}
		if (item.driverClass == driverClasses.emmaCharger) {
			if (item.index == 0) {
				await this.extendObject('charger', {
					type: 'device',
					common: {
						name: 'device charger',
					},
					native: {},
				});
			}
			item.path = `charger.${item.index.toString()}`;
			await this.extendObject(item.path, {
				type: 'channel',
				common: {
					name: `channel charger ${item.index.toString()}`,
					role: 'indicator',
				},
				native: {},
			});
		}
	}
	async initPath() {
		await this.extendObject('control', {
			type: 'channel',
			common: {
				name: 'channel control',
			},
			native: {},
		});
		//inverter
		await this.extendObject('meter', {
			type: 'device',
			common: {
				name: 'device meter',
			},
			native: {},
		});
		await this.extendObject('collected', {
			type: 'channel',
			common: {
				name: 'channel collected',
			},
			native: {},
		});
		await this.extendObject('inverter', {
			type: 'device',
			common: {
				name: 'device inverter',
			},
			native: {},
		});
		for (const item of this.devices) {
			await this.initDevicePath(item);
		}
	}
	async StartProcess() {
		await this.initPath();
		await this.control.init();
		this.state = new Registers(this);
		await this.atMidnight();
		if (this.settings.modbusAdjust) {
			this.settings.modbusAdjust = isSunshine(this);
			//this.logger.debug('Sunshine: '+this.settings.modbusAdjust);
		}
		this.modbusClient = new ModbusConnect(this, this.settings);
		this.modbusClient.setCallback(this.endOfmodbusAdjust.bind(this));
		this.dataPolling();
		this.runWatchDog();
		if (this.settings.ms?.active) {
			this.modbusServer = new ModbusServer(this, this.settings.ms.address, this.settings.ms.port);
			this.modbusServer.connect();
		}
	}
	async atMidnight() {
		this.settings.sunrise = getAstroDate(this, 'sunrise');
		this.settings.sunset = getAstroDate(this, 'sunset');
		const now = new Date();
		const night = new Date(
			now.getFullYear(),
			now.getMonth(),
			now.getDate() + 1, // the next day, ...
			0,
			0,
			0, // ...at 00:00:00 hours
		);
		const msToMidnight = night.getTime() - now.getTime();
		if (this.mitnightTimer) {
			this.clearTimeout(this.mitnightTimer);
		}
		this.mitnightTimer = this.setTimeout(async () => {
			await this.state.mitnightProcess(); //      the function being called at midnight.
			this.atMidnight(); //      reset again next midnight.
		}, msToMidnight);
	}
	/*
	sendToSentry (msg)  {
		if (this.supportsFeature && this.supportsFeature('PLUGINS')) {
			const sentryInstance = this.getPluginInstance('sentry');
			if (sentryInstance) {
				const Sentry = sentryInstance.getSentryObject();
				if (Sentry) this.logger.info('send to Sentry value: '+msg);
				Sentry && Sentry.withScope(scope => {
					scope.setLevel('info');
					scope.setExtra('key', 'value');
					Sentry.captureMessage(msg, 'info'); // Level "info"
				});
			}
		}
	}
	*/
	async endOfmodbusAdjust(info) {
		if (!info.modbusAdjust) {
			this.settings.modbusAdjust = info.modbusAdjust;
			this.settings.modbusDelay = Math.round(info.delay);
			//siehe jsonConfig.json
			if (this.settings.modbusDelay > 6000) {
				this.settings.modbusDelay = 6000;
			}
			this.settings.modbusTimeout = Math.round(info.timeout);
			if (this.settings.modbusTimeout > 30000) {
				this.settings.modbusTimeout = 30000;
			}
			if (this.settings.modbusTimeout < 5000) {
				this.settings.modbusTimeout = 5000;
			}
			this.settings.modbusConnectDelay = Math.round(info.connectDelay);
			if (this.settings.modbusConnectDelay > 10000) {
				this.settings.modbusConnectDelay = 10000;
			}
			if (this.settings.modbusConnectDelay < 2000) {
				this.settings.modbusConnectDelay = 2000;
			}
			//orignal Interval
			this.settings.highInterval = this.config.updateInterval * 1000;
			this.config.autoAdjust = this.settings.modbusAdjust;
			this.config.connectDelay = this.settings.modbusConnectDelay;
			this.config.delay = this.settings.modbusDelay;
			this.config.timeout = this.settings.modbusTimeout;
			this.updateConfig(this.config); //-> restart
			this.logger.info('New modbus settings are stored.');
			//this.sendToSentry(JSON.stringify(info));
		}
	}
	async adjustInverval() {
		if (this.settings.modbusAdjust) {
			this.settings.highInterval = 10000 * this.settings.modbusIds.length;
		} else {
			let minInterval = this.settings.modbusIds.length * this.settings.modbusDelay * 2.5; //len*5*delay/2
			if (this.settings.integration > 0) {
				//SmartLogger, Emma
				minInterval += 5000;
			} else {
				for (const device of this.devices) {
					if (device.duration) {
						minInterval += device.duration;
					}
				}
			}
			if (minInterval > this.settings.highInterval) {
				this.settings.highInterval = Math.round(minInterval);
			}
		}
		this.settings.lowInterval = 60000;
		if (this.settings.highInterval > this.settings.lowInterval) {
			this.settings.lowInterval = this.settings.highInterval;
		}
		this.settings.mediumInterval = Math.round(this.settings.lowInterval / 2);
		const newHighInterval = Math.round(this.settings.highInterval / 1000);
		if (!this.settings.modbusAdjust) {
			if (this.config.updateInterval < newHighInterval) {
				this.logger.warn(`The interval is too small. The value has been changed on ${newHighInterval} sec.`);
				this.logger.warn('Please check your configuration!');
			}
		}
		await this.setState('info.modbusUpdateInterval', { val: newHighInterval, ack: true });
	}
	/**
	 * Is called when databases are connected and adapter received configuration.
	 */
	async onReady() {
		// Initialize your adapter here
		// tiemout is now in ms
		if (this.config.timeout <= 10) {
			this.config.timeout = this.config.timeout * 1000;
			this.updateConfig(this.config);
		}
		if (this.config.sl_active) {
			//old Smartlogger
			this.config.sl_active = false;
			this.config.integration = 1;
			this.updateConfig(this.config);
		}
		await this.setState('info.ip', { val: this.config.address, ack: true });
		await this.setState('info.port', { val: this.config.port, ack: true });
		await this.setState('info.modbusIds', { val: this.config.modbusIds, ack: true });
		await this.setState('info.modbusTimeout', { val: this.config.timeout, ack: true });
		await this.setState('info.modbusConnectDelay', { val: this.config.connectDelay, ack: true });
		await this.setState('info.modbusDelay', { val: this.config.delay, ack: true });
		await this.setState('info.modbusTcpServer', { val: this.config.ms_active, ack: true });
		// Load user settings
		if (this.config.address != '' && this.config.port > 0 && this.config.modbusIds != '' && this.config.updateInterval > 0) {
			this.settings.address = this.config.address;
			this.settings.port = this.config.port;
			this.settings.modbusTimeout = this.config.timeout; //ms
			this.settings.modbusDelay = this.config.delay; //ms
			this.settings.modbusConnectDelay = this.config.connectDelay; //ms
			this.settings.modbusAdjust = this.config.autoAdjust;
			this.settings.modbusIds = this.config.modbusIds.split(',').map(n => {
				return Number(n);
			});
			/*
			this.settings.chargerIds = this.config.chargerIds.split(',').map(n => {
				return Number(n);
			});
			*/
			//SmartDongle
			this.settings.sd.active = this.config.sd_active;
			// eslint-disable-next-line no-constant-binary-expression
			this.settings.sd.sDongleId = Number(this.config.sDongleId) ?? 0;
			if (this.settings.sd.sDongleId < 0 || this.settings.sd.sDongleId >= 255) {
				this.settings.sd.active = false;
			}
			this.settings.highInterval = this.config.updateInterval * 1000; //ms
			//Modbus-Proxy
			this.settings.ms.address = this.config.ms_address;
			this.settings.ms.port = this.config.ms_port;
			this.settings.ms.active = this.config.ms_active;
			this.settings.ms.log = this.config.ms_log;
			//SmartLogger
			this.settings.integration = this.config.integration;
			this.settings.sl.meterId = this.config.sl_meterId;
			//battery charge control
			this.settings.cb.tou = this.config.cb_tou;
			//further battery register
			this.settings.ds.batteryUnits = this.config.ds_bu;
			this.settings.ds.batteryPacks = this.config.ds_bp;
			if (this.settings.modbusAdjust) {
				await this.setState('info.JSONhealth', { val: '{message: "Adjust modbus settings"}', ack: true });
			} else {
				await this.setState('info.JSONhealth', { val: '{message : "Information is collected"}', ack: true });
			}
			if (this.settings.modbusIds.length > 0 && this.settings.modbusIds.length < 6) {
				//ES6 use a for (const [index, item] of array.entries()) of loop
				for (const [i, id] of this.settings.modbusIds.entries()) {
					this.devices.push({
						index: i,
						duration: 5000,
						modbusId: id,
						driverClass: driverClasses.inverter,
						meter: i == 0 && this.settings.integration === 0,
					});
				}
				//SDongle
				if (this.settings.integration === 0 && this.settings.sd.active) {
					this.devices.push({
						index: 0,
						duration: 0,
						modbusId: this.settings.sd.sDongleId,
						driverClass: driverClasses.sdongle,
					});
				}
				//SmartLogger
				if (this.settings.integration === 1) {
					this.devices.push({
						index: 0,
						duration: 0,
						modbusId: 0,
						driverClass: driverClasses.logger,
					});
					if (this.settings.sl.meterId > 0) {
						this.devices.push({
							index: 0,
							duration: 0,
							meter: true,
							modbusId: this.settings.sl.meterId,
							driverClass: driverClasses.loggerMeter,
						});
					}
				}
				//EMMA
				if (this.settings.integration === 2) {
					this.devices.push({
						index: 0,
						duration: 0,
						//modbusId: 1,
						modbusId: 0,
						meter: true,
						driverClass: driverClasses.emma,
					});
				}
				await this.adjustInverval();
				await this.StartProcess();
			} else {
				this.adapterDisable("*** Adapter deactivated, can't parse modbusIds! ***");
			}
		} else {
			this.adapterDisable('*** Adapter deactivated, Adapter Settings incomplete! ***');
		}
	}
	async dataPolling() {
		function timeLeft(target, factor = 1) {
			const left = Math.round((target - new Date().getTime()) * factor);
			if (left < 0) {
				return 0;
			}
			return left;
		}
		const start = new Date().getTime();
		this.logger.debug(`### DataPolling START ${Math.round((start - this.lastTimeUpdated) / 1000)} sec ###`);
		if (this.lastTimeUpdated > 0 && (start - this.lastTimeUpdated) / 1000 > this.settings.highInterval / 1000 + 1) {
			this.logger.debug(`Interval ${(start - this.lastTimeUpdated) / 1000} sec`);
		}
		this.lastTimeUpdated = start;
		const nextLoop = this.settings.highInterval - (start % this.settings.highInterval) + start;
		//High Loop
		for (const item of this.devices) {
			this.lastStateUpdatedHigh += await this.state.updateStates(item, this.modbusClient, dataRefreshRate.high, timeLeft(nextLoop));
		}
		await this.state.runPostProcessHooks(dataRefreshRate.high);
		//Low Loop
		if (timeLeft(nextLoop) > 0) {
			for (const [i, item] of this.devices.entries()) {
				//this.log.debug('+++++ Loop: '+i+' Left Time: '+timeLeft(nextLoop,(i+1)/this.devices.length)+' Faktor '+((i+1)/this.devices.length));
				this.lastStateUpdatedLow += await this.state.updateStates(
					item,
					this.modbusClient,
					dataRefreshRate.low,
					timeLeft(nextLoop, (i + 1) / this.devices.length),
				);
			}
		}
		await this.state.runPostProcessHooks(dataRefreshRate.low);
		if (this.pollingTimer) {
			this.clearTimeout(this.pollingTimer);
		}
		this.pollingTimer = this.setTimeout(() => {
			this.dataPolling(); //recursiv
		}, timeLeft(nextLoop));
		this.logger.debug('### DataPolling STOP ###');
	}
	runWatchDog() {
		this.watchDogHandle && this.clearInterval(this.watchDogHandle);
		this.watchDogHandle = this.setInterval(() => {
			const sinceLastUpdate = new Date().getTime() - this.lastTimeUpdated; //ms
			this.logger.debug(`### Watchdog: time since last update ${sinceLastUpdate / 1000} sec`);
			const lastIsConnected = this.isConnected;
			//this.isConnected = this.lastStateUpdatedHigh > 0 && sinceLastUpdate < this.settings.highInterval*3;
			this.isConnected = this.lastStateUpdatedHigh > 0 || this.lastStateUpdatedLow > 0;
			if (this.isConnected !== lastIsConnected) {
				this.setState('info.connection', this.isConnected, true);
			}
			if (!this.settings.modbusAdjust) {
				if (!this.isConnected) {
					this.setState('info.JSONhealth', { val: '{errno:1, message: "Can\'t connect to inverter"}', ack: true });
				}
				const ret = this.state.CheckReadError(this.settings.lowInterval * 2);
				this.logger.debug(JSON.stringify(this.modbusClient.info));
				//v0.8.x
				if (!this.isReady) {
					this.isReady = this.isConnected && !ret.errno;
				}
				// after 2 Minutes
				if (this.toggleRunWatchDog) {
					if (ret.errno) {
						this.logger.warn(ret.message);
					}
					const obj = { ...ret, modbus: { ...this.modbusClient.info } };
					this.setState('info.JSONhealth', { val: JSON.stringify(obj), ack: true });
				}
				if (this.modbusServer) {
					!this.modbusServer.isConnected && this.modbusServer.connect();
					if (this.settings.ms.log) {
						//const stat = this.modbusServer.info?.stat;
						//object is not empty
						//if (Object.keys(stat).length > 0) this.log.info('Modbus tcp server: '+JSON.stringify(this.modbusServer.info));
						this.logger.info(`Modbus-proxy: ${JSON.stringify(this.modbusServer.info)}`);
					}
				}
			}
			this.toggleRunWatchDog = !this.toggleRunWatchDog;
			this.lastStateUpdatedLow = 0;
			this.lastStateUpdatedHigh = 0;
			if (sinceLastUpdate > this.settings.highInterval * 10) {
				this.setState('info.JSONhealth', { val: '{errno:2, message: "Internal loop error"}', ack: true });
				this.logger.warn('watchdog: restart Adapter...');
				this.restart();
			}
		}, this.settings.lowInterval);
	}
	adapterDisable(errMsg) {
		this.logger.error(errMsg);
		this.setForeignState(`system.adapter.${this.namespace}.alive`, false);
	}
	/**
	 * Is called when adapter shuts down - callback has to be called under any circumstances!
	 *
	 * @param callback
	 */
	onUnload(callback) {
		try {
			this.logger.info('cleaned everything up...');
			this.modbusServer && this.modbusServer.close();
			this.pollingTimer && this.clearTimeout(this.pollingTimer);
			this.mitnightTimer && this.clearTimeout(this.mitnightTimer);
			this.watchDogHandle && this.clearInterval(this.watchDogHandle);
			this.modbusClient && this.modbusClient.close();
			this.setState('info.connection', false, true);
			callback();
		} catch {
			callback();
		}
	}
	/**
	 * Is called if a subscribed state changes
	 *
	 * @param {string} id
	 * @param {ioBroker.State | null | undefined} state
	 */
	onStateChange(id, state) {
		if (state) {
			// The state was changed
			if (state.ack) {
				//this.logger.info(`state ${id} was changed but the ack flag was set. Therefore, no processing takes place!`);
				return;
			}
			const idArray = id.split('.');
			// sun2000.0.inverter.0.control
			if (idArray[2] == 'inverter') {
				const control = this.devices[Number(idArray[3])].instance.control;
				if (control) {
					let serviceId = idArray[5];
					for (let i = 6; i < idArray.length; i++) {
						serviceId += `.${idArray[i]}`;
					}
					control.set(serviceId, state);
				}
				//this.log.info(`### state ${id} changed: ${state.val} (ack = ${state.ack})`);
			}
			// sun2000.0.inverter.0.config
			if (idArray[2] == 'control') {
				let serviceId = idArray[3];
				for (let i = 4; i < idArray.length; i++) {
					serviceId += `.${idArray[i]}`;
				}
				//this.log.info(`### id: ${serviceId} state ${id} changed: ${state.val} (ack = ${state.ack})`);
				this.control.set(serviceId, state);
			}
		} else {
			// The state was deleted
			this.logger.info(`state ${id} deleted`);
		}
	}
	// 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);
	// 		}
	// 	}
	// }
}
if (require.main !== module) {
	// Export the constructor in compact mode
	/**
	 * @param [options]
	 */
	module.exports = options => new Sun2000(options);
} else {
	// otherwise start the instance directly
	new Sun2000();
}