index.js

//////////////////////////////////////////////////////////////////////
//	Copyright (C) Hiroshi SUGIMURA 2020.09.19
//////////////////////////////////////////////////////////////////////
'use strict'

const tradfriLib = require("node-tradfri-client");
const TradfriClient = tradfriLib.TradfriClient;
const discoverGateway = tradfriLib.discoverGateway;
const AccessoryTypes = tradfriLib.AccessoryTypes;
const TradfriError = tradfriLib.TradfriError;
const TradfriErrorCodes = tradfriLib.TradfriErrorCodes;

const cron = require('node-cron');


//////////////////////////////////////////////////////////////////////
/**
 * Tradfri管理オブジェクト
 * 注意: 複数のTradfriゲートウェイを管理する機能はありません。
 * @namespace Tradfri
 */
let Tradfri = {
	/** @type {AccessoryTypes} AccessoryTypesへの参照 */
	AccessoryTypes: AccessoryTypes,

	// user config
	/** @type {string} ゲートウェイ裏面のセキュリティコード (初回認証時のみ必要) */
	securityCode: '',
	/** @type {string} 接続用 Identity */
	identity: '',
	/** @type {string} 接続用 Pre-shared key */
	psk: '',
	/** @type {Object|function} コールバック関数 */
	userFunc: {},

	// private
	/** @type {boolean} 多重起動防止フラグ */
	enabled: false,
	/** @type {Object} ゲートウェイ情報 */
	gw: {},
	/** @type {string} ゲートウェイIPアドレス */
	gwAddress: '',
	/** @type {Object} TradfriClient インスタンス */
	client: {},
	/** @type {boolean} 自動状態取得フラグ (true = 自動) */
	autoGet: true,
	/** @type {boolean} デバッグモードフラグ */
	debugMode: false,
	/** @type {Object|null} 自動取得用cronジョブ (永続監視のため現在は未使用) */
	autoGetCron: null,
	/** @type {boolean} 初期化キャンセル管理フラグ */
	canceled: false,

	// public
	/** @type {Object} 全機器情報リスト */
	facilities: {},
	/** @type {Object} ライトリスト */
	lights: {},
	/** @type {Object} ブラインドリスト */
	blinds: {},

	////////////////////////////////////////
	// inner functions

	/**
	 * 指定時間スリープする
	 * @param {number} ms - スリープする時間(ミリ秒)
	 * @returns {Promise<void>}
	 */
	sleep: async function (ms) {
		return new Promise(function (resolve) {
			setTimeout(function () { resolve() }, ms);
		})
	},

	/**
	 * オブジェクトが空かどうか判定する
	 * 注意: `obj == {}` では判定できません
	 * @param {Object} obj - 判定対象のオブジェクト
	 * @returns {boolean} 空の場合はtrue、それ以外はfalse
	 */
	isObjEmpty: function (obj) {
		if (!obj) return true; // null or undefined check
		return Object.keys(obj).length === 0;
	},


	//////////////////////////////////////////////////////////////////////
	// Tradfri特有の手続き
	//////////////////////////////////////////////////////////////////////
	/**
	 * userFuncが未定義の場合のダミー関数
	 * @param {string} addr - アドレス (未使用)
	 * @param {Object} dev - デバイスオブジェクト (未使用)
	 * @param {Object} err - エラーオブジェクト (未使用)
	 */
	dummy: function (addr, dev, err) {
		Tradfri.debugMode ? console.log('Tradfri.dummy( addr:', addr, ', dev:', dev, 'err:', err, ')') : 0;
	},

	/**
	 * デバイス更新時のコールバック
	 * @param {Object} device - 更新されたデバイスオブジェクト
	 */
	_deviceUpdated: function (device) {
		// Tradfri.debugMode? console.log('_deviceUpdated, device:', device): 0;

		Tradfri.facilities = Tradfri.client.devices;  // 機器情報更新

		if (Tradfri.userFunc) {
			Tradfri.userFunc(Tradfri.gwAddress, device, null); // アップデートのあったデバイス情報だけ通知
		}

		if (device.type === AccessoryTypes.lightbulb) {
			Tradfri.lights[device.instanceId] = device;
		} else if (device.type === AccessoryTypes.blind) {
			Tradfri.blinds[device.instanceId] = device;
		}
	},

	/**
	 * デバイス削除時のコールバック
	 * 内部リストから削除を行います
	 * @param {number} instanceId - 削除されたデバイスのID
	 */
	_deviceRemoved: function (instanceId) {
		Tradfri.debugMode ? console.log('_deviceRemoved', instanceId) : 0;
		if (Tradfri.facilities[instanceId]) delete Tradfri.facilities[instanceId];
		if (Tradfri.lights[instanceId]) delete Tradfri.lights[instanceId];
		if (Tradfri.blinds[instanceId]) delete Tradfri.blinds[instanceId];
	},

	/**
	 * 通知監視用コールバック
	 */
	_observeNotifications: function () {
		Tradfri.debugMode ? console.log('observeNotifications') : 0;
	},


	//////////////////////////////////////////////////////////////////////
	// 初期化
	/**
	 * Tradfriハンドラの初期化
	 * @param {string} securityCode - セキュリティコード
	 * @param {function} userFunc - ユーザーコールバック関数
	 * @param {Object} [Options] - オプション
	 * @param {string} [Options.identity=''] - Identity
	 * @param {string} [Options.psk=''] - PSK
	 * @param {boolean} [Options.autoGet=true] - 自動状態取得
	 * @param {boolean} [Options.debugMode=false] - デバッグモード
	 * @returns {Promise<Object|null>} identityとpskを含むオブジェクト、キャンセルの場合はnull
	 */
	initialize: async function (securityCode, userFunc, Options = { identity: '', psk: '', autoGet: true, debugMode: false }) {
		// 多重起動防止
		if (Tradfri.enabled) return;
		Tradfri.enabled = true;

		Tradfri.canceled = false;

		Tradfri.gw = {};
		Tradfri.facilities = {};
		Tradfri.securityCode = securityCode == undefined ? '' : securityCode;
		Tradfri.userFunc = userFunc == undefined ? Tradfri.dummy : userFunc;
		Tradfri.debugMode = Options.debugMode == undefined || Options.debugMode == false ? false : true;   // true: show debug log
		Tradfri.autoGet = Options.autoGet != false ? true : false;	// 自動的な状態取得の有無
		Tradfri.identity = Options.identity == undefined || Options.identity === '' ? '' : Options.identity;
		Tradfri.psk = Options.psk == undefined || Options.psk === '' ? '' : Options.psk;

		if (Tradfri.debugMode == true) {
			console.log('==== tradfri-handler.js ====');
			console.log('securityCode:', Tradfri.securityCode, ', identity:', Tradfri.identity, ', psk:', Tradfri.psk);
			console.log('autoGet:', Tradfri.autoGet);
		}

		while (Tradfri.isObjEmpty(Tradfri.gw)) {
			if (Tradfri.canceled) {  // 初期化中にキャンセルがきた
				Tradfri.userFunc(null, 'Canceled', null);
				Tradfri.enabled = false;
				return null;
			}

			try {
				// GWを探す
				Tradfri.gw = await discoverGateway();
				if (Tradfri.isObjEmpty(Tradfri.gw)) {
					// 失敗したら30秒まつ (1秒x30回でキャンセルを確認しながら待つ)
					for (let i = 0; i < 30; i += 1) {
						if (Tradfri.canceled) {
							break;
						}
						await Tradfri.sleep(1000);
					}
				}
			} catch (error) {
				console.error('Error: tradfri-handler.initialize().discoverGateway', error);
				Tradfri.enabled = false;
				throw error;
			}
		}

		Tradfri.gwAddress = Tradfri.gw.addresses[0];  // 一つしか管理しない

		Tradfri.debugMode ? console.log('tradfri-handler.initialize() address:', Tradfri.gwAddress, ', gw', Tradfri.gw) : 0;

		Tradfri.client = new TradfriClient(Tradfri.gwAddress);

		if (Tradfri.identity === '') { // 新規リンク
			Tradfri.debugMode ? console.log('tradfri-handler.initialize().authenticate, securityCode:', Tradfri.securityCode) : 0;
			try {
				const ret = await Tradfri.client.authenticate(Tradfri.securityCode);
				Tradfri.identity = ret.identity;
				Tradfri.psk = ret.psk;
				Tradfri.debugMode ? console.log('tradfri-handler.initialize() ret identity:', Tradfri.identity) : 0;
				Tradfri.debugMode ? console.log('tradfri-handler.initialize() ret psk:', Tradfri.psk) : 0;
			} catch (error) {
				console.error('Error: tradfri-handler.initialize().authenticate', error);
				console.log('securityCode:', Tradfri.securityCode);
				Tradfri.enabled = false;
				throw error;
			}
		}

		Tradfri.client.on("device updated", Tradfri._deviceUpdated);
		Tradfri.client.on("device removed", Tradfri._deviceRemoved);
		Tradfri.client.on('device notified', Tradfri._observeNotifications);
		try {
			await Tradfri.client.connect(Tradfri.identity, Tradfri.psk);
		} catch (error) {
			switch (error.code) {
				case TradfriErrorCodes.ConnectionTimedOut: {
					// ゲートウェイに到達できないか、応答がありませんでした
					console.error('Error: tradfri-handler.initialize() TradfriErrorCodes.ConnectionTimedOut.');
					console.error('identity:', Tradfri.identity, ', psk:', Tradfri.psk);
					break;
				}
				case TradfriErrorCodes.AuthenticationFailed: {
					// 認証情報が無効です。`authenticate()`を使用して再認証する必要があります。
					console.error('Error: tradfri-handler.identity() TradfriErrorCodes.AuthenticationFailed.');
					console.error('identity:', Tradfri.identity, ', psk:', Tradfri.psk);
					break;
				}
				case TradfriErrorCodes.ConnectionFailed: {
					// 接続時に不明なエラーが発生しました
					console.error('Error: tradfri-handler.identity() TradfriErrorCodes.ConnectionFailed.');
					console.error('identity:', Tradfri.identity, ', psk:', Tradfri.psk);
					break;
				}
			}
			throw error;
		}

		if (Tradfri.canceled) {  // 初期化中にキャンセルがきた
			Tradfri.userFunc(null, 'Canceled', null);
			Tradfri.enabled = false;
			return null;
		}

		if (Tradfri.autoGet == true) {
			Tradfri.autoGetStart();
		}
		Tradfri.getState();

		return { identity: Tradfri.identity, psk: Tradfri.psk };
	},

	//====================================================================
	/**
	 * 初期化キャンセル
	 */
	initializeCancel: function () {
		Tradfri.canceled = true;
	},

	//====================================================================
	/**
	 * 解放処理
	 * @returns {Promise<void>}
	 */
	release: async function () {
		if (!Tradfri.enabled) return; // 多重開放の防止
		Tradfri.enabled = false;
		await Tradfri.autoGetStop();
		if (Tradfri.client && typeof Tradfri.client.destroy === 'function') {
			await Tradfri.client.destroy();
		}

		// 内部変数のリセット
		Tradfri.gw = {};
		Tradfri.facilities = {};
		Tradfri.lights = {};
		Tradfri.blinds = {};
		Tradfri.client = {};
	},


	//////////////////////////////////////////////////////////////////////
	// request(options, function (error, response, body) { })
	/**
	 * 状態取得(監視開始)
	 */
	getState: function () {
		// 監視開始
		Tradfri.client.observeDevices();
	},


	/**
	 * デバイスの状態を設定する
	 * @param {number} devId - デバイスID
	 * @param {string} devType - デバイスタイプ ('light' または 'blind')
	 * @param {Object} command - コマンドオブジェクト
	 */
	setState: async function (devId, devType, command) {
		// const response = await Tradfri.client.request(devId, "post", stateJson);
		Tradfri.debugMode ? console.log('Tradfri.setState() devId:', devId, ', devType:', devType, ', command:', command) : 0;
		switch (devType) {
			case 'light':
				Tradfri.client.operateLight(Tradfri.lights[devId], command);
				break;

			case 'blind':
				Tradfri.client.operateBlind(Tradfri.blinds[devId], command);
				break;

			default:
				Tradfri.debugMode ? console.log('Tradfri.setState() unknown devType:', devType) : 0;
				throw new Error('unknown devType: ' + devType);
		}
	},


	//////////////////////////////////////////////////////////////////////
	// 定期的なデバイスの監視
	/**
	 * 自動状態取得の開始 (実際には永続的な監視のみ開始)
	 */
	autoGetStart: function () {
		// configファイルにobservationDevsが設定されていれば実施
		Tradfri.debugMode ? console.log('tradfri-handler.autoGetStart()') : 0;
		// Tradfri.client.observeDevices() は永続的なため、cronで繰り返す必要はありません
	},

	/**
	 * 自動状態取得の停止
	 */
	autoGetStop: function () {
		Tradfri.debugMode ? console.log('tradfri-handler.autoGetStop()') : 0;
		// cronジョブは削除されました
	}
};


module.exports = Tradfri;

//////////////////////////////////////////////////////////////////////
// EOF
//////////////////////////////////////////////////////////////////////