mainNetatmo.js

//////////////////////////////////////////////////////////////////////
//	Copyright (C) Hiroshi SUGIMURA 2018.03.16
//////////////////////////////////////////////////////////////////////
/**
 * @module mainNetatmo
 */
'use strict'

//////////////////////////////////////////////////////////////////////
// 基本ライブラリ
const Store = require('electron-store');
const netatmo = require('netatmo');
const cron = require('node-cron');
require('date-utils');// for log
const { Sequelize, Op, netatmoModel, roomEnvModel } = require('./models/localDBModels');// DBデータと連携
const  {mergeDeeply} = require('./mainSubmodule');

let sendIPCMessage = null;
const store = new Store();

let config = {
	enabled: false,
	id: "",
	secret: "",
	username: "",
	password: "",
	debug: false
};

let persist = {};


//////////////////////////////////////////////////////////////////////
// config
let mainNetatmo = {
	api: null,
	observationJob: null,
	data: {},
	debug: false,
	callback: null,
	isRun: false,

	//////////////////////////////////////////////////////////////////////
	/**
	 * @func start
	 * @desc start
	 * @async
	 * @param {void} 
	 * @return void
	 * @throw error
	 */
	// netatmo start
	start: function ( _sendIPCMessage ) {
		sendIPCMessage = _sendIPCMessage;

		if( mainNetatmo.isRun ) {
			sendIPCMessage( "renewNetatmoConfigView", config );
			sendIPCMessage( "renewNetatmo", persist );
			mainNetatmo.sendTodayRoomEnv();// 現在持っているデータを送っておく
			return;
		}

		config.enabled  = store.get('config.Netatmo.enabled', false);
		config.id       = store.get('config.Netatmo.id', '');
		config.secret   = store.get('config.Netatmo.secret', '');
		config.username = store.get('config.Netatmo.username', '');
		config.password = store.get('config.Netatmo.password', '');
		config.debug    = store.get('config.Netatmo.debug', false);
		sendIPCMessage( "renewNetatmoConfigView", config );

		persist = store.get('persist.Netatmo', {});

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

		// configがなければ実行しない。
		if( config.id == '' || config.secret == '' || config.username == '' || config.password == '') {
			config.debug?console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.start() no config.'):0;
			return;
		}

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

		try{
			mainNetatmo.api = new netatmo({ 'client_id' : config.id, 'client_secret' : config.secret, 'username' : config.username, 'password' : config.password });
			mainNetatmo.data = {};
			mainNetatmo.callback = function (err, devices) {
				if(err) {
					console.error(err);
					return;
				}
				persist = devices;
				sendIPCMessage( "renewNetatmo", persist );
				netatmoModel.create({ detail: JSON.stringify(persist) });// dbに入れる
			};

			mainNetatmo.api.on('get-stationsdata', (err, devices) => {// イベント登録
				mainNetatmo.callback(err, devices);
			});

			mainNetatmo.setObserve();// 定期的チェック開始
		} catch(e) {
			console.error( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.start() error:', e);
		}

		sendIPCMessage( "renewNetatmo", persist );

		mainNetatmo.sendTodayRoomEnv();// 現在持っているデータを送っておく
	},


	//////////////////////////////////////////////////////////////////////
	// Netatmoの処理


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

		await mainNetatmo.setConfig( config );
		await store.set('persist.Netatmo', persist);
		await mainNetatmo.stopObservation();
	},

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


	/**
	 * @func setConfig
	 * @desc setConfig
	 * @async
	 * @param {void} 
	 * @return void
	 * @throw error
	 */
	setConfig: async function ( _config ) {
		if( _config ) {
			config = mergeDeeply( config, _config );
		}
		await store.set('config.Netatmo', config);
		sendIPCMessage( "renewNetatmoConfigView", config );
		sendIPCMessage( "configSaved", 'Netatmo' );// 保存したので画面に通知
	},

	/**
	 * @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;
	},


	//////////////////////////////////////////////////////////////////////
	// innser functions

	/**
	 * @func getCases
	 * @desc getRows
	 * @async
	 * @param {void} 
	 * @return void
	 * @throw error
	 */
	// 定時処理、部屋環境のデータ送信
	/*
	getCases
	input
		date: Date="2023-01-06"

	output
		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 void
	 * @throw error
	 */
	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 = mainNetatmo.getCases( now );

			let subQuery = `CASE ${cases} END`;

			// 3分毎データ
			let rows = await roomEnvModel.findAll( {
				attributes: ['id',
							 [Sequelize.fn('AVG', Sequelize.col('temperature')), 'avgTemperature'],
							 [Sequelize.fn('AVG', Sequelize.col('humidity')), 'avgHumidity'],
							 [Sequelize.fn('AVG', Sequelize.col('pressure')), 'avgPressure'],
							 [Sequelize.fn('AVG', Sequelize.col('CO2')), 'avgCO2'],
							 [Sequelize.fn('AVG', Sequelize.col('noise')), 'avgNoise'],
							 'createdAt',
							 [Sequelize.literal(subQuery), 'timeunit']
							 ],
				where: {
					srcType: 'netatmo',
					dateTime: { [Op.between] : [begin.toISOString(), end.toISOString()] }
				},
				group: ['timeunit']
			} );

			return rows;
		} catch( error ) {
			console.error( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.getRows()', error);
		}
	},

	/**
	 * @func getTodayRoomEnv
	 * @desc getTodayRoomEnv
	 * @async
	 * @param {void} 
	 * @return void
	 * @throw error
	 */
	getTodayRoomEnv: async function( ) {
		// 画面に今日のデータを送信するためのデータ作る
		try {
			let rows = await mainNetatmo.getRows();

			let T1 = new Date();
			T1.setHours( 0, 0, 0);

			let array = [];
			for( let t=0; t<480; t+=1 ) {
				let row = rows.find( (row) => row.dataValues.timeunit == T1.toFormat('HH24:MI') );

				if( row ) {
					array.push( {
						id: t,
						time: T1.toISOString(),
						srcType: 'netatmo',
						temperature: row.dataValues.avgTemperature,
						humidity: row.dataValues.avgHumidity,
						pressure: row.dataValues.avgPressure,
						noise: row.dataValues.avgNoise,
						CO2: row.dataValues.avgCO2
					} );
				}else{
					array.push( {
						id: t,
						time: T1.toISOString(),
						srcType: 'omron',
						temperature: null,
						humidity: null,
						pressure: null,
						noise: null,
						CO2: null
					});
				}

				T1.setMinutes( T1.getMinutes() +3 );// + 3 min
			}
			return array;

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

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

		if( config.enabled ) {
			arg = await mainNetatmo.getTodayRoomEnv();
			sendIPCMessage( 'renewRoomEnvNetatmo', JSON.stringify(arg));
		}
	},

	/**
	 * @func setObservesetObserve
	 * @func setObserve
	 * @desc netatmoを監視する
	 * @async
	 * @param {void} 
	 * @return void
	 * @throw error
	 */
	setObserve: function() {
		if( mainNetatmo.observationJob ) {
			config.debug?console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.observe() is already started.' ):0;
		}
		config.debug?console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.observe() start.' ):0;

		// 監視はcronで実施、1分毎
		mainNetatmo.observationJob = cron.schedule('*/1 * * * *', () => {
			try{
				config.debug?console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.cron.schedule() every 1min'):0;

				// 部屋の環境を記録、Netatmo
				mainNetatmo.api.getStationsData();

				let dt = new Date();

				//------------------------------------------------------------
				// 部屋の環境を記録、Netatmo
				if( config.enabled && persist.length != 0 ) {
					// config.debug ? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.cron.schedule() Store Netatmo'):0;
					let n = persist[0];
					if( n ) {
						roomEnvModel.create( {
							dateTime: dt,
							srcType: 'netatmo',
							place: n.home_name,
							temperature: n.dashboard_data.Temperature,
							humidity: n.dashboard_data.Humidity,
							pressure: n.dashboard_data.Pressure,
							noise: n.dashboard_data.Noise,
							CO2: n.dashboard_data.CO2} );
					}
				}

				mainNetatmo.sendTodayRoomEnv();// 本日のデータの定期的送信
			} catch( error ) {
				console.error( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.cron.schedule() each 1min, error:', error);
			}
		});

		mainNetatmo.observationJob.start();
	},


	/**
	 * @func stopObservation
	 * @desc 監視をやめる
	 * @async
	 * @param {void} 
	 * @return void
	 * @throw error
	 */
	stopObservation: function() {
		config.debug ? console.log( new Date().toFormat("YYYY-MM-DDTHH24:MI:SS"), '| mainNetatmo.stop() observation.' ):0;

		if( mainNetatmo.observationJob ) {
			mainNetatmo.observationJob.stop();
			mainNetatmo.observationJob = null;
		}
	}
};


module.exports = mainNetatmo;
//////////////////////////////////////////////////////////////////////
// EOF
//////////////////////////////////////////////////////////////////////