mainAutoAssessment.js

//////////////////////////////////////////////////////////////////////
//	Copyright (C) Hiroshi SUGIMURA 2023.10.09
//////////////////////////////////////////////////////////////////////
'use strict'
/**
 * @module mainAutoAssessment
 */

//////////////////////////////////////////////////////////////////////
// 基本ライブラリ
const cron = require('node-cron');
require("date-utils");

const { sqlite3, Op, IOT_MajorResultsModel, IOT_MinorResultsModel, IOT_QuestionnaireAnswersModel } = require('./models/localDBModels');   // DB models

const { getToday, getYesterday, roundFloat, checkValue } = require('./mainSubmodule');


//////////////////////////////////////////////////////////////////////
// config

let config = {
	debug: false
}

const minorSchema = {
	date: "",
	assessmentSource: "HAL",
	r_1_1: null,
	r_1_2: null,
	r_1_3: null,
	r_1_4: null,
	r_1_5: null,
	r_1_6: null,
	r_1_7: null,
	r_1_8: null,
	r_1_9: null,
	r_1_10: null,
	r_1_11: null,
	r_1_12: null,
	r_1_13: null,
	r_1_14: null,
	r_1_15: null,
	r_2_1: null,
	r_2_2: null,
	r_2_3: null,
	r_2_4: null,
	r_2_5: null,
	r_2_6: null,
	r_2_7: null,
	r_2_8: null,
	r_2_9: null,
	r_2_10: null,
	r_2_11: null,
	r_2_12: null,
	r_2_13: null,
	r_2_14: null,
	r_2_15: null,
	r_3_1: null,
	r_3_2: null,
	r_3_3: null,
	r_3_4: null,
	r_3_5: null,
	r_3_6: null,
	r_3_7: null,
	r_3_8: null,
	r_3_9: null,
	r_3_10: null,
	r_3_11: null,
	r_3_12: null,
	r_3_13: null,
	r_3_14: null,
	r_3_15: null,
	r_4_1: null,
	r_4_2: null,
	r_4_3: null,
	r_4_4: null,
	r_4_5: null,
	r_4_6: null,
	r_4_7: null,
	r_4_8: null,
	r_4_9: null,
	r_4_10: null,
	r_4_11: null,
	r_4_12: null,
	r_4_13: null,
	r_4_14: null,
	r_4_15: null,
	r_5_1: null,
	r_5_2: null,
	r_5_3: null,
	r_5_4: null,
	r_5_5: null,
	r_5_6: null,
	r_5_7: null,
	r_5_8: null,
	r_5_9: null,
	r_5_10: null,
	r_5_11: null,
	r_5_12: null,
	r_5_13: null,
	r_5_14: null,
	r_5_15: null,
	r_6_1: null,
	r_6_2: null,
	r_6_3: null,
	r_6_4: null,
	r_6_5: null,
	r_6_6: null,
	r_6_7: null,
	r_6_8: null,
	r_6_9: null,
	r_6_10: null,
	r_6_11: null,
	r_6_12: null,
	r_6_13: null,
	r_6_14: null,
	r_6_15: null
};

const majorSchema = {
	date: "",
	assessmentSource: "HAL",
	smartLifeIndex: 3,
	totalPoint: 0,
	totalRank: 0,
	clothingPoint: 0,
	clothingRawScore: 0,
	foodPoint: 0,
	foodRawScore: 0,
	housingPoint: 0,
	housingRawScore: 0,
	physicalHealthPoint: 0,
	physicalHealthRawScore: 0,
	mentalHealthPoint: 0,
	mentalHealthRawScore: 0,
	ecologyPoint: 0,
	ecologyRawScore: 0,
	comments: "やったね!"
};

let sendIPCMessage = null;


//////////////////////////////////////////////////////////////////////
// モジュール

let mainAutoAssessment = {
	isRun: false,  // 機能が利用可能になったか?
	observationJob: null,

	/**
	 * @func lastMR2mr
	 * @desc 以前のMinorResultsからMinorResults(1日経過処理)
	 * @async
	 */
	lastMR2mr: async function (mrRow, mr) {

		await Object.keys(mr).forEach(async function (key) {
			// await console.log( `mr[${key}] == ${mr[key]}  <==  mrRow[${key}] == ${mrRow[key]}` );
			if (mr[key] == null) {
				mr[key] = mrRow[key] - 1;
			}
		});

		return mr;
	},


	/**
	 * @func calcMajorResults
	 * @desc MinorResultsから MajorResultsを計算する
	 * @async
	 */
	calcMajorResults: function (mr) {
		let clothing_sum = 0;
		let clothing_num = 0;
		let food_sum = 0;
		let food_num = 0;
		let housing_sum = 0;
		let housing_num = 0;
		let physical_sum = 0;
		let physical_num = 0;
		let mental_sum = 0;
		let mental_num = 0;
		let ecology_sum = 0;
		let ecology_num = 0;
		let comments = "";

		for (let [k, v] of Object.entries(mr)) {
			if (v === null) {
				continue;
			}
			if (k.startsWith('r_1_')) {
				clothing_sum += v;
				clothing_num++;
			} else if (k.startsWith('r_2_')) {
				food_sum += v;
				food_num++;
			} else if (k.startsWith('r_3_')) {
				housing_sum += v;
				housing_num++;
			} else if (k.startsWith('r_4_')) {
				physical_sum += v;
				physical_num++;
			} else if (k.startsWith('r_5_')) {
				mental_sum += v;
				mental_num++;
			} else if (k.startsWith('r_6_')) {
				ecology_sum += v;
				ecology_num++;
			}
		}

		// 平均値を求める
		let clothing_point = clothing_sum / clothing_num;
		let food_point = food_sum / food_num;
		let housing_point = housing_sum / housing_num;
		let physical_point = physical_sum / physical_num;
		let mental_point = mental_sum / mental_num;
		let ecology_point = ecology_sum / ecology_num;

		// totalPoint は平均値の和
		let total_point = (clothing_point + food_point + housing_point + physical_point + mental_point + ecology_point) / 6;

		// totalRank の特定
		let total_rank = '';

		if (total_point >= 90) {
			total_rank = 'SSS';
			comments = '最高です!';
		} else if (total_point >= 80) {
			total_rank = 'SS';
			comments = 'いい調子だね!';
		} else if (total_point >= 70) {
			total_rank = 'S';
			comments = '頑張りました~';
		} else if (total_point >= 60) {
			total_rank = 'A';
			comments = 'なかなかぐっどだよ';
		} else if (total_point >= 50) {
			total_rank = 'B';
			comments = 'けっこうすごいじゃん';
		} else if (total_point >= 40) {
			total_rank = 'C';
			comments = '普通くらいには頑張れてるね';
		} else if (total_point >= 30) {
			total_rank = 'D';
			comments = 'がんばれそう';
		} else if (total_point >= 20) {
			total_rank = 'E';
			comments = '改善点を探したいね';
		} else {
			total_rank = 'F';
			comments = 'しょっく';
		}

		let data = {
			clothingPoint: roundFloat(clothing_point),
			clothingRawScore: roundFloat(clothing_point * 5),
			foodPoint: roundFloat(food_point),
			foodRawScore: roundFloat(food_point * 5),
			housingPoint: roundFloat(housing_point),
			housingRawScore: roundFloat(housing_point * 5),
			physicalHealthPoint: roundFloat(physical_point),
			physicalHealthRawScore: roundFloat(physical_point * 5),
			mentalHealthPoint: roundFloat(mental_point),
			mentalHealthRawScore: roundFloat(mental_point * 5),
			ecologyPoint: roundFloat(ecology_point),
			ecologyRawScore: roundFloat(ecology_point * 5),
			totalPoint: roundFloat(total_point),
			totalRank: total_rank,
			comments: comments
		};

		return data;
	},


	/**
	 * @func assessment
	 * @desc 評価シーケンス全体
	 * @async
	 */
	assessment: async function (today, yesterday) {
		console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainAutoAssessment - today:', today, ' yesterday:', yesterday);

		let now = new Date();

		// DB トランザクション開始
		let t = await sqlite3.transaction();

		try {
			// 初期化
			let minorResults = await Object.assign({}, minorSchema);
			let majorResults = await Object.assign({}, majorSchema);

			////////////////////////////////////////////////////////////////////////////////////////
			// MinorResults
			// await console.log( '-- MinorResults' );

			//--------------------------------------------------------
			// アンケートデータでまずは点数をつける
			// await console.log('questionnaire');
			// SELECT * FROM halexp.IOT_QuestionnaireAnswersModel
			// where UID="U517290377a4861b16cc91c2d111f116d" and createdAt like "2022-05-11%" order by createdAt desc;
			let qaRow = await IOT_QuestionnaireAnswersModel.findOne({
				where: { createdAt: { [Op.like]: yesterday + "%" } },
				order: [["createdAt", "desc"]]
			});
			if (qaRow) { // アンケートがあれば
				minorResults = await mainAutoAssessment.qa2mr(qaRow, minorResults);
				// console.log( 'There is questionnaire.' );
			}
			// console.dir( minorResults );


			//--------------------------------------------------------
			// IoTデータで点数をつける
			// await console.log('IoT');




			//--------------------------------------------------------
			// nullの場所だけ、前日以前の得点を利用する。各得点は-1することで放置していると0点になる
			// await console.log('latest MinorResults');
			// SELECT * FROM halexp.IOT_MinorResults
			// where UID="U517290377a4861b16cc91c2d111f116d" order by date desc;
			let mrRow = await IOT_MinorResultsModel.findOne({
				order: [["date", "desc"]]
			});
			if (mrRow) { // 以前のMinorResultsがあれば
				minorResults = await mainAutoAssessment.lastMR2mr(mrRow, minorResults);
				// console.log( 'There is latest MinorResults.' );
			}

			// それでも残るnullは0点
			// await console.log('Null fix');
			await Object.keys(minorResults).forEach(function (key) {
				if (minorResults[key] == undefined || minorResults[key] == null) {
					minorResults[key] = 0;
				}
			});

			// 最終的な値のチェック、min = 0, max = 100
			// await console.log('checkValues');
			await Object.keys(minorResults).forEach(function (key) {
				minorResults[key] = checkValue(minorResults[key], 0, 100);
			});
			// console.dir( minorResults );


			////////////////////////////////////////////////////////////////////////////////////////
			// 完成した成績表の登録
			// IOT_MinorResults テーブルから該当ユーザーの今日のレコードを取得
			let minorToday = await IOT_MinorResultsModel.findOne({
				where: {
					date: today
				}
			});

			// 該当ユーザーの今日の minorResults レコードが見つかればUpdate、なければCreate
			if (minorToday) {
				// minorResults を UPDATE
				minorResults.updatedAt = now;
				minorResults.date = today;
				// console.log('Update IOT_MinorResultsModel', minorResults);
				await IOT_MinorResultsModel.update(minorResults, {
					where: {
						idIOT_MinorResults: minorToday.dataValues.idIOT_MinorResults
					}
				});
			} else {
				// minorResultsをCreate
				minorResults.createdAt = now;
				minorResults.updatedAt = now;
				minorResults.assessmentSource = 'HEMS-Logger';
				minorResults.date = today;
				// console.log('Create IOT_MinorResultsModel:', minorResults);
				await IOT_MinorResultsModel.create(minorResults);
			}


			//--------------------------------------------------------------------------------------
			// MajorResults
			// await console.log( '-- MajorResults' );
			majorResults = mainAutoAssessment.calcMajorResults(minorResults);  // minorからmajorを計算

			// IOT_MajorResults テーブルから該当ユーザーの今日のレコードを取得
			let majorToday = await IOT_MajorResultsModel.findOne({
				where: {
					date: today
				}
			});

			// 該当ユーザーの今日の majorResults レコードが見つかればUpdate、なければCreate
			if (majorToday) {
				// majorResults を UPDATE
				majorResults.updatedAt = now;
				majorResults.date = today;
				// console.log('Update IOT_MajorResultsModel', majorResults);
				await IOT_MajorResultsModel.update(majorResults, {
					where: {
						idIOT_MajorResults: majorToday.dataValues.idIOT_MajorResults
					}
				});
			} else {
				// majorResultsをCreate
				majorResults.createdAt = now;
				majorResults.updatedAt = now;
				majorResults.date = today;
				majorResults.assessmentSource = 'HAL';
				// console.log('Create IOT_MajorResultsModel:', majorResults);
				await IOT_MajorResultsModel.create(majorResults);
			}

			// コミット
			await t.commit();
			// console.log('commit');
		} catch (error) {
			// ロールバック
			await t.rollback();
			// console.log('rollback');
			console.error(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '|', error);
		}

	},


	//////////////////////////////////////////////////////////////////////
	/**
	 * @func start
	 * @desc 自動評価システムの開始処理(定時実行、EntryPoint)
	 * @async
	 */
	start: function (_sendIPCMessage) {
		config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainAutoAssessment.start()') : 0;

		if (mainAutoAssessment.isRun) {
			return; // 多重起動防止
		}
		mainAutoAssessment.isRun = true;
		sendIPCMessage = _sendIPCMessage;

		mainAutoAssessment.observationJob = cron.schedule('0 0 9 * * *', () => {  // 本番用の AM9:00
			// mainAutoAssessment.observationJob = cron.schedule('*/10 * * * * *', () => {  // debug用の0秒毎
			config.debug ? console.log(new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainAutoAssessment.start().observationJob') : 0;

			let today = getToday();
			let yesterday = getYesterday();

			mainAutoAssessment.assessment(today, yesterday);
		});

		mainAutoAssessment.observationJob.start();
	},

	/**
 * @func stop
 * @desc 停止
 * @async
 */
	stop: async function () {
		await mainAutoAssessment.observationJob.stop();
		mainAutoAssessment.observationJob = null;
		mainAutoAssessment.isRun = false;
	}
};


//////////////////////////////////////////////////////////////////////
// EOF
//////////////////////////////////////////////////////////////////////
module.exports = mainAutoAssessment;