mainEL.js

//////////////////////////////////////////////////////////////////////
//	Copyright (C) Hiroshi SUGIMURA 2020.10.30
//////////////////////////////////////////////////////////////////////
/**
 * @module mainEL
 */
'use strict'

//////////////////////////////////////////////////////////////////////
// 基本ライブラリ
const fs = require('fs');
const path = require('path');
const Store = require('electron-store');
const cron = require('node-cron');  // 監視はechonet-liteモジュールに頼らないで自前でやる
const EL = require('echonet-lite');
const ELconv = require('echonet-lite-conv');
const mainArp = require('./mainArp');     // arpの管理
const mainSystem = require('./mainSystem');     // systemの管理(network部分を利用)
const { Sequelize, Op, elrawModel, eldataModel, electricEnergyModel } = require('./models/localDBModels');   // DBデータと連携
const { objectSort, isObjEmpty, mergeDeeply } = require('./mainSubmodule');

// 基礎設定
const appDir = process.env.NODE_ENV === 'development' ? __dirname : __dirname;
let sendIPCMessage = null;
const store = new Store();

let config = {
	enabled: true,
	oldSearch: false,
	debug: false
};

let network = {
	IPver: 0,
	IPv4: "auto",
	IPv6: "auto"
};

let persist = {
	facilities: {},
	parsed: {}
};


//////////////////////////////////////////////////////////////////////
// EL関連
let mainEL = {
	objList: ['05ff01'],  // 自分のELオブジェクトリスト
	controllerObj: {  // 自分はELコントローラ
		// super
		"80": [0x30], // 動作状態
		"81": [0xff], // 設置場所
		"82": [0x00, 0x00, 0x46, 0x00], // release F
		"88": [0x42], // 異常状態
		"8a": [0x00, 0x00, 0x77], // maker code
		"9d": [0x04, 0x80, 0x8f, 0xa0, 0xb0], // inf map, 1 Byte目は個数
		"9e": [0x04, 0x80, 0x8f, 0xa0, 0xb0], // set map, 1 Byte目は個数
		"9f": [0x09, 0x80, 0x81, 0x82, 0x88, 0x8a, 0x8f, 0x9d, 0x9e, 0x9f] // get map, 1 Byte目は個数
		// child
	},
	localaddresses: null,  // localaddress
	elsocket: null,   // port 3610のbind
	observationTask: null,  // cronオブジェクト
	changeTask: null,  // facilities監視するcron
	isRun: false,  // 実行中か?

	//////////////////////////////////////////////////////////////////////
	// インタフェース
	/**
	 * @func start
	 * @desc 初期化
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	start: async function (_sendIPCMessage, _localaddresses) {
		sendIPCMessage = _sendIPCMessage;

		if (mainEL.isRun) {  // 重複起動対応
			if (config.enabled && persist.parsed && !isObjEmpty(persist.parsed)) {
				sendIPCMessage("renewELConfigView", config);
				sendIPCMessage("fclEL", persist.parsed);
			}
			return;
		}

		config = store.get('config.EL', config);
		persist = store.get('persist.EL', persist);
		network = mainSystem.getConfig();
		mainEL.localaddresses = _localaddresses;
		sendIPCMessage("renewELConfigView", config);   // 保存したので画面に通知

		config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.start()') : 0;

		if (config.enabled == false) {
			config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.start() EL is desabled.') : 0;
			mainEL.isRun = false;
			return;
		}
		mainEL.isRun = true;

		// mainEL初期設定
		await mainEL.init();

		await mainEL.sendTodayEnergy(); 	// 本日のスマメデータの定期的送信、一発目

		if (config.enabled && persist.parsed && !isObjEmpty(persist.parsed)) {
			sendIPCMessage("fclEL", persist.parsed);
		}

		EL.sendOPC1('224.0.23.0', [0x0e, 0xf0, 0x01], [0x0e, 0xf0, 0x01], 0x60, 0x80, [0x30]);// 立ち上がったのでONの宣言
		mainEL.search();

		mainEL.setCron();		// 定時処理設定
	},


	/**
	 * @func stop
	 * @desc ELの機能を停止する。
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	stop: async function () {
		config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.stop()') : 0;

		await mainEL.stopObservation();
		await EL.release();

		await mainEL.setConfig();
		await store.set('persist.EL', persist);
	},

	/**
	 * @func stopWithoutSave
	 * @desc stopWithoutSave
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	stopWithoutSave: async function () {
		config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.stopWithoutSave()') : 0;

		await mainEL.stopObservation();
		await EL.release();
	},

	/**
	 * @func setConfig
	 * @desc setConfig
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	setConfig: async function (_config) {
		config.debug ?? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.setConfig() _config:', _config);

		if (_config) {
			config = mergeDeeply(config, _config);
		}
		await store.set('config.EL', config);
		mainSystem.setConfig(network);

		sendIPCMessage("renewELConfigView", config);  // 保存したので画面に通知
		sendIPCMessage("configSaved", 'EL');  // 保存したので画面に通知
	},

	/**
	 * @func getConfig
	 * @desc getConfig
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	getConfig: function () {
		return config;
	},

	/**
	 * @func getPersist
	 * @desc getPersist
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	getPersist: function () {
		return persist;
	},

	//////////////////////////////////////////////////////////////////////
	// 内部
	/**
	 * @func received
	 * @desc EL受け取った後の処理
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	received: function (rinfo, els, error) {
		if (error) {
			console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.received() error:', error);
			console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.received() rinfo:', rinfo);
			console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.received() els:', els);
			sendIPCMessage('Error', {
				datetime: new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"),
				moduleName: 'mainEL.received()',
				stackLog: `EL packets that cannot be analyzed. From: ${rinfo.address}, Detail: ${error}`
			});
			// throw error;
			return;
		}

		// EL controller
		if (els.DEOJ.substr(0, 4) == '05ff') {
			// ESVで振り分け,主に0x60系列に対応すればいい
			switch (els.ESV) {
				////////////////////////////////////////////////////////////////////////////////////
				// 0x6x
				case EL.SETI:// "60
					break;
				case EL.SETC:// "61",返信必要あり
					break;

				case EL.GET:// 0x62,Get
					for (var epc in els.DETAILs) {
						if (mainEL.controllerObj[epc]) {// 持ってるEPCのとき
							EL.replyOPC1(rinfo.address, EL.toHexArray(els.TID), [0x05, 0xFF, 0x01], EL.toHexArray(els.SEOJ), EL.GET_RES, EL.toHexArray(epc), mainEL.controllerObj[epc]);
						} else {// 持っていないEPCのとき, SNA
							EL.replyOPC1(rinfo.address, EL.toHexArray(els.TID), [0x05, 0xFF, 0x01], EL.toHexArray(els.SEOJ), EL.GET_SNA, EL.toHexArray(epc), [0x00]);
						}
					}
					break;

				case EL.INFREQ:// 0x63
					break;

				case EL.SETGET:// "6e"
					break;

				default:
					break;
			}
		}

		// databaseに登録
		// 確認
		let rawdata = EL.getSeparatedString_ELDATA(els);

		ELconv.elsAnarysis(els, function (eljson) {
			for (const [key, value] of Object.entries(eljson.EDT)) {
				eldataModel.create({ srcip: rinfo.address, srcmac: mainArp.toMAC(rinfo.address), seoj: eljson.SEOJ, deoj: eljson.DEOJ, esv: eljson.ESV, epc: key, edt: value });
			}
		});
		elrawModel.create({ srcip: rinfo.address, srcmac: mainArp.toMAC(rinfo.address), dstip: mainEL.localaddresses[0], dstmac: mainArp.toMAC(mainEL.localaddresses[0]), rawdata: rawdata, seoj: els.SEOJ, deoj: els.DEOJ, esv: els.ESV, opc: els.OPC, detail: els.DETAIL });
	},

	/**
	 * @func sendMsg
	 * @desc sendMsg
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	sendMsg: function (_ip, _msg) {
		// 送信は自分のログも残しておく
		let rawdata = _msg;
		let els = EL.parseString(_msg);

		ELconv.elsAnarysis(els, function (eljson) {
			for (const [key, value] of Object.entries(eljson.EDT)) {
				eldataModel.create({ srcip: mainEL.localaddresses[0], srcmac: mainArp.toMAC(mainEL.localaddresses[0]), seoj: eljson.SEOJ, deoj: eljson.DEOJ, esv: eljson.ESV, epc: key, edt: value });
			}
		});
		elrawModel.create({ srcip: mainEL.localaddresses[0], srcmac: mainArp.toMAC(mainEL.localaddresses[0]), dstip: _ip, dstmac: mainArp.toMAC(_ip), rawdata: rawdata, seoj: els.SEOJ, deoj: els.DEOJ, esv: els.ESV, opc: els.OPC, detail: els.DETAIL });
		EL.sendString(_ip, _msg);
	},

	/**
	 * @func sendOPC1
	 * @desc sendOPC1
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	sendOPC1: function (_ip, _seoj, _deoj, _esv, _epc, _edt) {
		EL.sendOPC1(_ip, _seoj, _deoj, _esv, _epc, _edt);
	},

	/**
	 * @func search
	 * @desc search
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	search: function () {
		EL.search();

		if (config.oldSearch) {  // Ver. 1.0系Search有効時
			EL.sendOPC1(EL.EL_Multi, '0ef001', '0ef001', '63', 'd5', '00');
		}
	},


	/**
	 * @func sendTodayEnergy
	 * @desc sendTodayEnergy
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	sendTodayEnergy: async function () {
		let arg = {};

		// Ether等でつながるスマート電力量サブメータ
		if (config.enabled && persist.parsed) {
			arg = await mainEL.getTodayElectricEnergy_submeter();
			// config.debug?console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.getTodayElectricEnergy_submeter() arg:\x1b[32m', arg, '\x1b[0m' ):0;

			if (arg.filter((d) => { return d.instantaneousPower != null }).length) {  // 何もないと [] が来るので、lengthで判定してNodataならフロントに送らない
				sendIPCMessage('renewTodayElectricEnergy_submeter', JSON.stringify(arg));
			}
		}
	},


	/**
	 * @func setCron
	 * @desc 3分毎にチェックする
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	setCron: function () {
		// cron.schedule('*/3 * * * *', async () => {
		cron.schedule('*/1 * * * *', async () => {
			try {
				config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() every 3min') : 0;

				let dt = new Date();

				// スマート電力サブメーターの状態のチェック
				if (config.enabled && persist.parsed && persist.parsed.IPs && persist.parsed.IPs.length != 0) {
					// config.debug ? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() persist.elParsed:\x1b[32m', persist.elParsed, '\x1b[0m' ):0;

					// persist.elParsed の中にスマート電力サブメータあるか?あればIP取得
					let ip = '';
					for (let i of persist.parsed.IPs) {
						for (let o of persist.parsed[i].EOJs) {
							if (o == 'スマート電力量サブメータ01(028d01)') {
								ip = i;
							}
						}
					}

					if (ip != '') {  // スマート電力サブメータ みつかった
						let sm = persist.parsed[ip];

						// 蓄積するほどデータがそろってない場合はやらない
						if (isObjEmpty(sm.Means)) {
							config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() SubMeter sm.Means is Empty.') : 0;
						}

						// 設置場所は取得する
						else if (!sm['スマート電力量サブメータ01(028d01)']['設置場所(81)']) {
							config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() SubMeter place is Null.') : 0;
							EL.sendOPC1(ip, [0x0e, 0xf0, 0x01], [0x02, 0x88, 0x01], EL.GET, [0x81], [0x00]);  // サブメータの設置場所

						} else {
							// config.debug ? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() SubMeter sm:\x1b[32m', sm, '\x1b[0m' ):0;

							// merge用ベース
							let means = {
								'積算電力量計測値(正方向計測値)[kWh]': null,
								'積算電力量計測値(逆方向計測値)[kWh]': null,
								'瞬時電力計測値[W]': null,
								'定時積算電力量計測値正方向': {
									'日時': null,
									'計測値[kWh]': null
								},
								'定時積算電力量計測値逆方向': {
									'日時': null,
									'計測値[kWh]': null
								}
							};

							// merge用ベースとesmDataとマージ
							let mergeObj = mergeDeeply(means, sm.Means);
							// config.debug ? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() SubMeter mergeObj \x1b[32m', mergeObj, '\x1b[0m' ):0;

							let instantaneousCurrentsR = null;
							if (sm['スマート電力量サブメータ01(028d01)']['瞬時電流計測値(E8)']) {
								let e8 = JSON.parse(sm['スマート電力量サブメータ01(028d01)']['瞬時電流計測値(E8)'].split('(')[0]);
								let rp = e8['RPhase'];
								// console.log( rp );
								instantaneousCurrentsR = rp.split('[A]')[0];
							}

							let instantaneousCurrentsT = null;
							if (sm['スマート電力量サブメータ01(028d01)']['瞬時電流計測値(E8)']) {
								let e8 = JSON.parse(sm['スマート電力量サブメータ01(028d01)']['瞬時電流計測値(E8)'].split('(')[0]);
								let tp = e8['TPhase'];
								// console.log( tp );
								instantaneousCurrentsT = tp.split('[A]')[0];
							}
							//------------------------------------------------------------
							// 整理されたデータベースにする
							let q = {
								dateTime: dt,
								srcType: 'SubMeter',
								place: sm['スマート電力量サブメータ01(028d01)']['設置場所(81)'],
								commulativeAmountNormal: mergeObj['積算電力量計測値(正方向計測値)[kWh]'], // E1 * D3 * D4
								commulativeAmountReverse: mergeObj['積算電力量計測値(逆方向計測値)[kWh]'], // E3 * D3 * D4
								instantaneousPower: mergeObj['瞬時電力計測値[W]'],  // E7 * D3
								instantaneousCurrentsR: instantaneousCurrentsR, // E8
								instantaneousCurrentsT: instantaneousCurrentsT,  // E8
								commulativeAmountsFixedTimeNormalDaytime: mergeObj['定時積算電力量計測値正方向']['日時'],  // EA
								commulativeAmountsFixedTimeNormalPower: mergeObj['定時積算電力量計測値正方向']['計測値[kWh]'],
								commulativeAmountsFixedTimeReverseDaytime: mergeObj['定時積算電力量計測値逆方向']['日時'], // EB
								commulativeAmountsFixedTimeRiversePower: mergeObj['定時積算電力量計測値逆方向']['計測値[kWh]']
							};

							// config.debug ? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() SubMeter insert:\x1b[32m', q, '\x1b[0m' ):0;
							electricEnergyModel.create(q);
						}
					}
				};

				mainEL.sendTodayEnergy(); 		// 本日のデータの定期的送信 スマートメータ分

			} catch (error) {
				console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.cron.schedule() each 3min, error:', error);
				mainEL.isRun = false;
				throw error;
			}
		});
	},


	//////////////////////////////////////////////////////////////////////
	/**
	 * @func init
	 * @desc ELの処理開始
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	init: async function () {

		// 辞書の読み込みをオーバーライド
		ELconv.initialize = function () {
			ELconv.m_dictNod = JSON.parse(fs.readFileSync(path.join(appDir, 'nodeProfile.json'), 'utf8'));
			ELconv.m_dictSup = JSON.parse(fs.readFileSync(path.join(appDir, 'superClass_I.json'), 'utf8'));
			ELconv.m_dictDev = JSON.parse(fs.readFileSync(path.join(appDir, 'deviceObject_I.json'), 'utf8'));
			ELconv.m_dictMakers = JSON.parse(fs.readFileSync(path.join(appDir, 'makers.json'), 'utf8'));
		};
		ELconv.initialize();

		EL.facilities = persist.facilities;

		config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.start() config:\x1b[32m', config, '\x1b[0m') : 0;

		// ECHONET Lite socket
		mainEL.elsocket = EL.initialize(mainEL.objList, mainEL.received, network.IPver,
			{
				v4: network.IPv4 == 'auto' ? '' : network.IPv4,
				v6: network.IPv6 == 'auto' ? '' : network.IPv6,
				ignoreMe: true,
				autoGetProperties: true,
				autoGetDelay: 1000,
				debugMode: false
			});


		// 監視対象機器は、定期的にEPCを取得する。cronで実施、3分毎
		// 未取得EPCの補完も3分毎に確認
		mainEL.observationTask = cron.schedule('*/3 * * * *', async () => {
			config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.cron.schedule() observationTask') : 0;
			EL.complementFacilities();
			await mainEL.observation();
		});

		// facilitiesの変化を監視して、変化があったらcallbackする、1分毎
		let oldVal = JSON.stringify(EL.objectSort(EL.facilities));
		mainEL.changeTask = cron.schedule('*/1 * * * *', async () => {
			config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.cron.schedule() changeTask') : 0;
			const newVal = JSON.stringify(EL.objectSort(EL.facilities));
			if (oldVal == newVal) return;

			// 変化があったのでmainに通知、全体監視して変更があったときに全体データとして呼ばれる
			persist.facilities = objectSort(EL.facilities);
			ELconv.refer(persist.facilities, function (devs) {
				persist.parsed = objectSort(devs);
				if (!isObjEmpty(persist.parsed)) {
					sendIPCMessage("fclEL", persist.parsed);
				}
			});

			oldVal = newVal;
		});
	},



	//////////////////////////////////////////////////////////////////////
	/**
	 * @func getStatic
	 * @desc 基礎的なデバイスの情報取得
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	getStatic: async function () {
		await EL.sendString(EL.Multi, "1081000405ff01028d016206E100E300E700E800D300D400");  // サブメータ
		await mainEL.sleep(5000);
	},


	//////////////////////////////////////////////////////////////////////
	// 定期的なデバイスの監視、監視はIPアドレスが変更される可能性があることに注意すべし

	/**
	 * @func observation
	 * @desc 監視シーケンス
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	observation: async function () {
		// config.debug ? console.log('mainEL.observation() network:', network):0;
		// ipv4, or 0 and 4
		if (network.IPver == '0' || network.IPver == '4' || network.IPver == 0 || network.IPver == 4) {
			// console.log('mainEL.observation() ipv4');
			await EL.sendOPC1(EL.Multi, [0x0e, 0xf0, 0x01], [0x00, 0x22, 0x00], EL.GET, [0xe0], [0x00]);  // 電力センサ
			await EL.sendOPC1(EL.Multi, [0x0e, 0xf0, 0x01], [0x02, 0x81, 0x00], EL.GET, [0xe0], [0x00]);  // 水道量メータ
			await EL.sendOPC1(EL.Multi, [0x0e, 0xf0, 0x01], [0x02, 0x82, 0x00], EL.GET, [0xe0], [0x00]);  // ガスメータ
			await EL.sendOPC1(EL.Multi, [0x0e, 0xf0, 0x01], [0x02, 0x87, 0x00], EL.GET, [0xc0], [0x00]);  // 分電盤メータ
			await EL.sendOPC1(EL.Multi, [0x0e, 0xf0, 0x01], [0x02, 0x88, 0x00], EL.GET, [0xe0], [0x00]);  // 低圧スマート電力量メータ

			// サブメータ
			await EL.sendString(EL.Multi, "1081000405ff01028d016206E100E300E700E800D300D400");
			await mainEL.sleep(5000);
			await EL.sendString(EL.Multi, "1081000505ff01028d016202EA00EB00");
		}

		// ipv6, or 0 and 6
		if (network.IPver == '0' || network.IPver == '6' || network.IPver == 0 || network.IPver == 6) {
			// console.log('mainEL.observation() ipv6');
			await EL.sendOPC1(EL.Multi6, [0x0e, 0xf0, 0x01], [0x00, 0x22, 0x00], EL.GET, [0xe0], [0x00]);  // 電力センサ
			await EL.sendOPC1(EL.Multi6, [0x0e, 0xf0, 0x01], [0x02, 0x81, 0x00], EL.GET, [0xe0], [0x00]);  // 水道量メータ
			await EL.sendOPC1(EL.Multi6, [0x0e, 0xf0, 0x01], [0x02, 0x82, 0x00], EL.GET, [0xe0], [0x00]);  // ガスメータ
			await EL.sendOPC1(EL.Multi6, [0x0e, 0xf0, 0x01], [0x02, 0x87, 0x00], EL.GET, [0xc0], [0x00]);  // 分電盤メータ
			await EL.sendOPC1(EL.Multi6, [0x0e, 0xf0, 0x01], [0x02, 0x88, 0x00], EL.GET, [0xe0], [0x00]);  // 低圧スマート電力量メータ

			// スマートサブメータ
			await EL.sendString(EL.Multi6, "1081000405ff01028d016206E100E300E700E800D300D400");
			await mainEL.sleep(5000);
			await EL.sendString(EL.Multi6, "1081000505ff01028d016202EA00EB00");
		}
	},


	/**
	 * @func stopObservation
	 * @desc 監視行動をやめて,タイマーも解放する
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	stopObservation: async function () {
		config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainEL.stopObservation()') : 0;

		if (mainEL.observationTask) {
			await mainEL.observationTask.stop();
			mainEL.observationTask = null;
		}

		if (mainEL.changeTask) {
			await mainEL.changeTask.stop();
			mainEL.changeTask = null;
		}
	},

	/**
	 * @func sleep
	 * @desc Wait必要な時
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	sleep: function (ms) {
		return new Promise(resolve => setTimeout(resolve, ms));
	},

	/**
	 * @func getTodayElectricEnergy_submeter
	 * @desc 定時処理用, 電力(サブメータ)
	 * @async
	 * @param {void}
	 * @return void
	 * @throw error
	 */
	getTodayElectricEnergy_submeter: async function () {
		// 画面に今日のデータを送信するためのデータ作る
		try {
			// グラフに表示される値1時間で取れたデータの平均値とする。
			// つまり、開始は前日の23時から当日の0時の値を、当日の0時の値とする
			let begin = new Date();  // 現在時刻UTCで取得
			begin.setHours(begin.getHours() - begin.getHours() - 1, 57, 0, 0); // 前日の23時0分0秒にする
			let end = new Date(begin);  // 現在時刻UTCで取得
			end.setMinutes(begin.getMinutes() + 3); // begin + 3min

			// config.debug? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| sendTodayRoomEnv: begin:\x1b[32m', begin.toISOString(), '\x1b[0mend:\x1b[32m', end.toISOString(), '\x1b[0m' ):0;

			// 24h x 3分(=20回)
			let rows = [];
			for (let i = 0; i < (24 * 20); i += 1) {

				// 1時間分
				let r = await electricEnergyModel.findAll({
					attributes: [[Sequelize.literal(`${i}`), 'id'],
					[Sequelize.fn('AVG', Sequelize.col('commulativeAmountNormal')), 'avgCommulativeAmountNormal'],
					[Sequelize.fn('AVG', Sequelize.col('commulativeAmountReverse')), 'avgCommulativeAmountReverse'],
					[Sequelize.fn('AVG', Sequelize.col('instantaneousPower')), 'avgInstantaneousPower'],
					[Sequelize.fn('AVG', Sequelize.col('instantaneousCurrentsR')), 'avgInstantaneousCurrentsR'],
					[Sequelize.fn('AVG', Sequelize.col('instantaneousCurrentsT')), 'avgInstantaneousCurrentsT'],
					[Sequelize.fn('AVG', Sequelize.col('commulativeAmountsFixedTimeNormalPower')), 'avgCommulativeAmountsFixedTimeNormalPower'],
					[Sequelize.fn('AVG', Sequelize.col('commulativeAmountsFixedTimeRiversePower')), 'avgCommulativeAmountsFixedTimeRiversePower'],
					],
					where: {
						srcType: 'SubMeter',
						dateTime: { [Op.between]: [begin.toISOString(), end.toISOString()] }
					}
				});

				rows.push({ t: end.toISOString(), v: r[0].dataValues });

				begin.setMinutes(begin.getMinutes() + 3); // begin + 3min
				end.setMinutes(begin.getMinutes() + 3); // begin + 3min
			}

			let array = [];

			for (const row of rows) {
				array.push({
					id: row.v.id,
					time: row.t,
					srcType: 'electricSub',
					commulativeAmountNormal: row.v.avgCommulativeAmountNormal,
					commulativeAmountReverse: row.v.avgCommulativeAmountReverse,
					instantaneousPower: row.v.avgInstantaneousPower,
					instantaneousCurrentsR: row.v.avgInstantaneousCurrentsR,
					instantaneousCurrentsT: row.v.avgInstantaneousCurrentsT,
					commulativeAmountsFixedTimeNormalPower: row.v.avgCommulativeAmountsFixedTimeNormalPower,
					commulativeAmountsFixedTimeRiversePower: row.v.avgCommulativeAmountsFixedTimeRiversePower
				});
			}

			return array;

		} catch (error) {
			console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| main.getTodayElectricEnergy_submeter()', error);
		}
	}

};


module.exports = mainEL;
//////////////////////////////////////////////////////////////////////
// EOF
//////////////////////////////////////////////////////////////////////