//////////////////////////////////////////////////////////////////////
// Copyright (C) Hiroshi SUGIMURA 2021.11.11
//////////////////////////////////////////////////////////////////////
/**
* @module mainESM
*/
'use strict'
//////////////////////////////////////////////////////////////////////
// 基本ライブラリ
const cron = require('node-cron');
const Store = require('electron-store');
const eSM = require('e-smartmeter-echonet-lite');
const EL = require('echonet-lite');
const ELconv = require('echonet-lite-conv');
const { Sequelize, Op, esmdataModel, esmrawModel, electricEnergyModel } = require('./models/localDBModels'); // DBデータと連携
const { objectSort, isObjEmpty, mergeDeeply } = require('./mainSubmodule');
let sendIPCMessage = null;
const store = new Store();
let config = {
enabled: false, // 有効/無効
dongleType: 'TESSERA', // 'ROHM' or 'TESSERA', default:TESSERA
id: '', // Bルート認証ID設定, Your B route ID.
password: '', // Bルート認証パスワード設定, Your B route password.
userAmpere: '30', // ユーザの契約アンペア
EPANDESC: {}, // コネクション情報
connectionType: 'stable', // 接続方式, 'stable' or 'fast', stable:No use EPANDESC
debug: false // スマメライブラリのデバッグ有効
};
let persist = {};
//////////////////////////////////////////////////////////////////////
// config
let mainESM = {
isRun: false, // 動作中
observationJob: null,
observationPort: null,
connected: false, // 初回起動のみ実施するためのフラグ, flag for first connection
//////////////////////////////////////////////////////////////////////
// interfaces
//////////////////////////////////////////////////////////////////////
/**
* @func start
* @desc 初期化と開始
* @async
* @param {sendIPCMessage} _sendIPCMessage IPC通信関数
* @return void
* @throw error
*/
start: function (_sendIPCMessage) {
sendIPCMessage = _sendIPCMessage;
if (mainESM.isRun) {
if (persist) {
sendIPCMessage("renewESMConfigView", config);
sendIPCMessage("fclESM", persist);
}
mainESM.sendTodayEnergy(); // 現在持っているデータを送っておく
return;
}
config.enabled = store.get('config.ESM.enabled', false);
config.dongleType = store.get('config.ESM.dongleType', '');
config.id = store.get('config.ESM.id', '');
config.password = store.get('config.ESM.password', '');
config.userAmpere = store.get('config.ESM.userAmpere', '30');
config.connectionType = store.get('config.ESM.connectionType', 'stable');
config.EPANDESC = store.get('config.ESM.EPANDESC', {});
config.debug = store.get('config.ESM.debug', false);
persist = store.get('persist.ESM', {});
sendIPCMessage("renewESMConfigView", config);
if (config.enabled == false) {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.start() desabled.') : 0;
mainESM.isRun = false;
return;
}
mainESM.isRun = true;
try {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.start()') : 0;
mainESM.startObserve(); // 定時処理
} catch (error) {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.start() startObserve error') : 0;
mainESM.isRun = false;
}
if (persist) {
sendIPCMessage("fclESM", persist);
}
mainESM.sendTodayEnergy(); // 現在持っているデータを送っておく
},
/**
* @func stop
* @desc シリアルポートを開放して連携終了、設定や現在の数値を永続化する
* @async
* @param {void}
*/
stop: async function () {
mainESM.isRun = false;
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.stop()') : 0;
mainESM.connected = false;
await mainESM.stopObservation();
await eSM.release();
await mainESM.setConfig();
await store.set('persist.ESM', persist);
},
/**
* @func stopWithoutSave
* @desc シリアルポートを開放して連携終了、設定や現在の数値を永続化しない
* @async
* @param {void}
* @throw error
*/
stopWithoutSave: async function () {
mainESM.isRun = false;
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.stopWithoutSave()') : 0;
mainESM.connected = false;
await mainESM.stopObservation();
await eSM.release();
},
/**
* @func setConfig
* @desc 設定をセットするとともに永続化する、引数なければ保存だけする
* @async
* @param {config} _config 設定、nullなら保存のみ
* @return void
* @throw error
*/
setConfig: async function (_config) {
if (_config) {
config = mergeDeeply(config, _config);
}
await store.set('config.ESM', config);
sendIPCMessage("renewESMConfigView", config); // 保存したので画面に通知
sendIPCMessage("configSaved", 'ESM'); // 保存したので画面に通知
},
/**
* @func getConfig
* @desc 現在の設定を取得する
* @async
* @param {void}
* @return config config
*/
getConfig: function () {
return config;
},
/**
* @func getPersist
* @desc 現在のデータを取得する
* @async
* @param {void}
* @return persist persist
*/
getPersist: function () {
return persist;
},
//////////////////////////////////////////////////////////////////////
// 定時処理のインタフェース
/**
* @func observe
* @desc スマートメータを監視する、初回受信時にトリガー
* @async
* @param {void}
* @return void
* @throw error
*/
startObserve: function () {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.startObserve() start.') : 0;
if (mainESM.observationJob) {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.startObserve() already started.') : 0;
}
// 1分毎に監視タスクは動作する
// 接続状態チェック
// 機器情報の変化を意味付けする
// DBにinsertする
mainESM.observationJob = cron.schedule('*/1 * * * *', async () => {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.startObserve.cron.schedule()') : 0;
// 既に接続していたら機器情報の変化をみる。接続していなかったら接続する
// この処理はmainESM.start()でobserve serialportとして分割した。
if (mainESM.connected) {
// 機器情報の変化の監視
eSM.getMeasuredValues(); // 機器情報の変化を定期的にgetする
mainESM.changeCallback(eSM.facilities); // 機器の変化の監視
mainESM.insertDB(); // データベースに登録
} else {
// 切断状態なら再接続?
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.startObserve.cron.schedule() is NO connection.') : 0;
// 既に接続していたら機器情報の変化をみる。接続していなかったら接続する
if (eSM.state == 'disconnected') {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.startObserve.cron.schedule() eSM.state is disconnected.') : 0;
if (config.connectionType == 'stable') {
config.EPANDESC = {};
}
eSM.initialize(config, mainESM.received); // ライブラリの方でリエントラント制御してるので、ここでは雑に呼ぶ
}
}
});
mainESM.observationJob.start();
},
/**
* @func stopObservation
* @desc 監視をやめる
* @async
* @param {void}
* @return void
*/
stopObservation: function () {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.stopObserve() observation.') : 0;
if (mainESM.observationJob) {
mainESM.observationJob.stop();
mainESM.observationJob = null;
}
},
//////////////////////////////////////////////////////////////////////
// inner functions
//////////////////////////////////////////////////////////////////////
/**
* @func insertDB
* @desc 現在のデータをDBにinsertする、基本的には1分に1回呼ばれる
* @async
* @param {void}
* @return void
*/
insertDB: async () => {
try {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.insertDB() every min') : 0;
let dt = new Date();
// Wi-SUN電力スマートメーターの状態のチェック
if (mainESM.connected && persist && persist.IPs && persist.IPs.length != 0) {
// config.debug ? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.insertDB() persist:\x1b[32m', persist, '\x1b[0m' ):0;
let ip = persist.IPs[0];
let sm = persist[ip];
// 蓄積するほどデータがそろってない場合はスキップ
if (!sm || !sm['低圧スマート電力量メータ01(028801)']) {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.insertDB.() SumartMeter persist.esmData is Null.') : 0;
} else if (!sm['低圧スマート電力量メータ01(028801)']['設置場所(81)']) { // 基本プロパティがなければ取り直す
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.insertDB() SumartMeter esmData.place is Null.') : 0;
eSM.getStatic();
} else if (isObjEmpty(sm.Means)) {
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.insertDB() SumartMeter sm.Means is Empty.') : 0;
} else {
// merge用ベース
let means = {
'積算電力量計測値(正方向計測値)[kWh]': null,
'積算電力量計測値(逆方向計測値)[kWh]': 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"), '| mainESM.insertDB() ESM mergeObj \x1b[32m', mergeObj, '\x1b[0m' ):0;
let instantaneousPower = null;
if (sm['低圧スマート電力量メータ01(028801)']['瞬時電力計測値(E7)']) {
// console.log( 'E7:', sm['低圧スマート電力量メータ01(028801)']['瞬時電力計測値(E7)'] );
instantaneousPower = sm['低圧スマート電力量メータ01(028801)']['瞬時電力計測値(E7)'].split('W')[0];
}
let instantaneousCurrentsR = null;
if (sm['低圧スマート電力量メータ01(028801)']['瞬時電流計測値(E8)']) {
let e8 = JSON.parse(sm['低圧スマート電力量メータ01(028801)']['瞬時電流計測値(E8)'].split('(')[0]);
let rp = e8['RPhase'];
// console.log( rp );
instantaneousCurrentsR = rp.split('[A]')[0];
}
let instantaneousCurrentsT = null;
if (sm['低圧スマート電力量メータ01(028801)']['瞬時電流計測値(E8)']) {
let e8 = JSON.parse(sm['低圧スマート電力量メータ01(028801)']['瞬時電流計測値(E8)'].split('(')[0]);
let tp = e8['TPhase'];
// console.log( tp );
instantaneousCurrentsT = tp.split('[A]')[0];
}
//------------------------------------------------------------
// 整理されたデータベースにする
let q = {
dateTime: dt,
srcType: 'Meter',
place: sm['低圧スマート電力量メータ01(028801)']['設置場所(81)'],
commulativeAmountNormal: mergeObj['積算電力量計測値(正方向計測値)[kWh]'], // E0
commulativeAmountReverse: mergeObj['積算電力量計測値(逆方向計測値)[kWh]'], // E3
instantaneousPower: instantaneousPower, // E7
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"), '| mainESM.insertDB() ESM insert:\x1b[32m', q, '\x1b[0m' ):0;
electricEnergyModel.create(q);
}
};
mainESM.sendTodayEnergy(); // 本日のデータの定期的送信 スマートメータ分
} catch (error) {
console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.insertDB() each 3min, error:', error);
throw error;
}
},
/**
* @func renewPortList
* @desc シリアルポートリストを取得する
* @async
* @param {void}
* @return Array シリアルポートリスト
* @throw error
*/
renewPortList: async function () {
return await eSM.renewPortList();
},
/**
* @func received
* @desc 受信処理
* @async
* @param {eSM} sm スマメオブジェクト
* @param {rinfo} rinfo 送信元のIPアドレス
* @param {ELStructure} els ECHONET Lite Structureの形で受信したデータ
* @param {Error} error エラーオブジェクト、エラーがあったときに情報あり
* @return void
* @throw error
*/
received: function (sm, rinfo, els, error) {
// わからんエラー
if (error) {
sendIPCMessage('Error', { datetime: new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), moduleName: 'mainESM.received()', stackLog: `${error}\nスマートメータの設定をもう一度確認し、一度アプリを再起動してください。または機器を再起動してください。` });
console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.received() error:\x1b[32m', error, '\x1b[0m');
return;
}
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.received() sm:\x1b[32m', sm, '\x1b[0m') : 0;
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.received() rinfo:\x1b[32m', rinfo, '\x1b[0m') : 0;
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.received() els:\x1b[32m', els, '\x1b[0m') : 0;
try {
// 切断された
if (sm.state == 'close') {
mainESM.connected = false; // 未接続にする
return;
}
// 初回接続時, first connection
if (!mainESM.connected && sm.state == 'available') {
config.EPANDESC = eSM.EPANDESC; // 接続できたので接続情報を確保
mainESM.connected = true; // 接続できたフラグ
eSM.getStatic(); // 初回接続時は静的プロパティをもらっておく
}
if (els) {
mainESM.connected = true; // 接続できたフラグ
sendIPCMessage("ESMLinked");
// 受信データを解析してDBに格納
let rawdata = EL.getSeparatedString_ELDATA(els);
ELconv.elsAnarysis(els, function (eljson) {
for (const [key, value] of Object.entries(eljson.EDT)) {
esmdataModel.create({ srcip: rinfo.address, seoj: eljson.SEOJ, deoj: eljson.DEOJ, esv: eljson.ESV, epc: key, edt: value });
}
});
esmrawModel.create({ srcip: rinfo.address, rawdata: rawdata, seoj: els.SEOJ, deoj: els.DEOJ, esv: els.ESV, opc: els.OPC, detail: els.DETAIL });
} else {
// elsが入っていないときは処理しない
config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.received() els is NO Data') : 0;
}
} catch (e) {
console.error(e);
}
},
/**
* @func changeCallback
* @desc 受信処理、変更があった場合に呼ばれる
* @async
* @param {facilities} facilities 変更後データ
* @return void
*/
changeCallback: function (facilities) {
ELconv.refer(objectSort(facilities), function (devs) {
// console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| ESMStart() devs:\x1b[32m', objectSort(devs), '\x1b[0m' );
persist = eSM.objectSort(devs);
sendIPCMessage("fclESM", persist);
});
},
/**
* @func getCases
* @desc 或る日のデータを3分単位で取得するためのwhen文を生成する
* @async
* @param {string} date 或る日
* date: Date="2023-01-06"
* @return {string} when文の文字列
* when createdAt >= "2023-01-05 23:57" and createdAt < "2023-01-06 00:00" then "00:00"
* when createdAt >= "2023-01-06 00:00" and createdAt < "2023-01-06 00:03" then "00:03"
* when createdAt >= "2023-01-06 00:03" and createdAt < "2023-01-06 00:06" then "00:06"
* ...
* when createdAt >= "2023-01-06 23:54" and createdAt < "2023-01-06 23:57" then "23:57"
* else "24:00"
*/
getCases: function (date) {
let T1 = new Date(date);
let T2 = new Date(date);
let T3 = new Date(date);
let T4 = new Date(date);
// UTCだがStringにて表現しているので、なんか複雑
T1.setHours(T1.getHours() - T1.getHours() - 10, 57, 0, 0); // 前日の14時57分xx秒 14:57:00 .. 15:00:00 --> 00:00
T2.setHours(T1.getHours() - T1.getHours() - 10, 58, 0, 0); // T1 + 1min
T3.setHours(T1.getHours() - T1.getHours() - 10, 59, 0, 0); // T1 + 2min
T4.setHours(T1.getHours() - T1.getHours(), 0, 0, 0); // 集約先
let ret = "";
for (let t = 0; t < 480; t += 1) { // 24h * 20 times (= 60min / 3min)
// console.log( T1.toISOString(), ':', T1.toFormat('YYYY-MM-DD HH24:MI'), ', ', T4.toFormat('HH24:MI') );
ret += `WHEN "createdAt" LIKE "${T1.toFormat('YYYY-MM-DD HH24:MI')}%" OR "createdAt" LIKE "${T2.toFormat('YYYY-MM-DD HH24:MI')}%" OR "createdAt" LIKE "${T3.toFormat('YYYY-MM-DD HH24:MI')}%" THEN "${T4.toFormat('HH24:MI')}" \n`;
T1.setMinutes(T1.getMinutes() + 3); // + 3 min
T2.setMinutes(T2.getMinutes() + 3); // + 3 min
T3.setMinutes(T3.getMinutes() + 3); // + 3 min
T4.setMinutes(T4.getMinutes() + 3); // + 3 min
}
return ret + 'ELSE "24:00"';
},
/**
* @func getRows
* @desc DBから今日のデータを取得
* @async
* @param {void}
* @return Array[] rows
*/
getRows: async function () {
try {
let now = new Date(); // 現在
let begin = new Date(now); // 現在時刻UTCで取得
begin.setHours(begin.getHours() - begin.getHours() - 1, 57, 0, 0); // 前日の23時57分0秒にする
let end = new Date(begin); // 現在時刻UTCで取得
end.setHours(begin.getHours() + 25, 0, 0, 0); // 次の日の00:00:00にする
let cases = mainESM.getCases(now);
let subQuery = `CASE ${cases} END`;
// 3分毎データ
let rows = await electricEnergyModel.findAll({
attributes: ['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'],
'createdAt',
[Sequelize.literal(subQuery), 'timeunit']
],
where: {
srcType: 'Meter',
dateTime: { [Op.between]: [begin.toISOString(), end.toISOString()] }
},
group: ['timeunit']
});
return rows;
} catch (error) {
console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.getRows()', error);
}
},
/**
* @func getTodayElectricEnergy
* @desc 今日のデータを配列として取得する
* @async
* @param {void}
* @return Array[Object] 今日のデータ
* @throw error
*/
getTodayElectricEnergy: async function () {
// 画面に今日のデータを送信するためのデータ作る
try {
let rows = await mainESM.getRows();
let T1 = new Date();
T1.setHours(0, 0, 0);
let array = [];
for (let t = 0; t < 480; t += 1) { // 3分が480回で1440=1日
let row = rows.find((row) => row.dataValues.timeunit == T1.toFormat('HH24:MI'));
if (row) {
array.push({
id: t,
time: T1.toISOString(),
srcType: 'electric',
commulativeAmountNormal: row.dataValues.avgCommulativeAmountNormal,
commulativeAmountReverse: row.dataValues.avgCommulativeAmountReverse,
instantaneousPower: row.dataValues.avgInstantaneousPower,
instantaneousCurrentsR: row.dataValues.avgInstantaneousCurrentsR,
instantaneousCurrentsT: row.dataValues.avgInstantaneousCurrentsT
});
} else {
array.push({
id: t,
time: T1.toISOString(),
srcType: 'electric',
commulativeAmountNormal: null,
commulativeAmountReverse: null,
instantaneousPower: null,
instantaneousCurrentsR: null,
instantaneousCurrentsT: null
});
}
T1.setMinutes(T1.getMinutes() + 3); // + 3 min
}
return array;
} catch (error) {
console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.getTodayElectricEnergy()', error);
throw error;
}
},
/**
* @func sendTodayEnergy
* @desc 現在持っているデータをRendererに送る
* @async
* @param {void}
* @return void
* @throw error
*/
sendTodayEnergy: async function () {
let arg = {};
// WI-SUNのスマートメータ
if (config.enabled) {
arg = await mainESM.getTodayElectricEnergy();
// config.debug?console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainESM.sendTodayEnergy() arg:\x1b[32m', arg, '\x1b[0m' ):0;
sendIPCMessage('renewTodayElectricEnergy', JSON.stringify(arg));
}
}
};
module.exports = mainESM;
//////////////////////////////////////////////////////////////////////
// EOF
//////////////////////////////////////////////////////////////////////