index.js

//////////////////////////////////////////////////////////////////////
//	$Date:: 2016-04-03 17:56:33 +0900#$
//	$Rev: 9416 $
//	Copyright (C) Hiroshi SUGIMURA 2016.04.03.
//////////////////////////////////////////////////////////////////////
'use strict'

/**
 * ECHONET Lite プロトコル コンバーターモジュール
 * @module echonet-lite-conv
 * @description ECHONET Lite プロトコルのデータ変換を行うモジュールです。
 * EOJ/EPC/EDTの解析、日時フォーマット変換、各種機器データの変換機能を提供します。
 * @author Hiroshi SUGIMURA
 * @license MIT
 */

//////////////////////////////////////////////////////////////////////
const Encoding = require('encoding-japanese');
const fs = require('fs');

/**
 * ECHONET Lite コンバーターオブジェクト
 * @namespace ELconv
 */
let ELconv = {
	/** @type {Object} 辞書データ */
	m_dict: {},
	/** @type {string} 最新仕様バージョン */
	m_latestSpec: '1.12',
	/** @type {string} 最新付録バージョン */
	m_latestAppendix: 'I',
	/** @type {boolean} 初期化フラグ */
	m_initialized: false
};


/**
 * 辞書ファイルを読み込んで初期化する
 * ノードプロファイル、スーパークラス、デバイスオブジェクト、メーカーコードの辞書を読み込む
 * @function initialize
 * @memberof module:echonet-lite-conv
 */
ELconv.initialize = function () {
	// 本来はすべての辞書をもって,条件分けすべき
	// ELconv.m_dictNod = JSON.parse( fs.readFileSync('./node_modules/echonet-lite-conv/Spec_1.11/nodeProfile.json', 'utf8') );
	ELconv.m_dictNod = JSON.parse(fs.readFileSync('./node_modules/echonet-lite-conv/Spec_1.12/nodeProfile.json', 'utf8'));

	// ELconv.m_dictSup = JSON.parse( fs.readFileSync('./node_modules/echonet-lite-conv/Appendix_G/superClass_G.json', 'utf8') );
	// ELconv.m_dictSup = JSON.parse( fs.readFileSync('./node_modules/echonet-lite-conv/Appendix_H/superClass_H.json', 'utf8') );
	ELconv.m_dictSup = JSON.parse(fs.readFileSync('./node_modules/echonet-lite-conv/Appendix_I/superClass_I.json', 'utf8'));

	// ELconv.m_dictDev = JSON.parse( fs.readFileSync('./node_modules/echonet-lite-conv/Appendix_G/deviceObject_G.json', 'utf8') );
	// ELconv.m_dictDev = JSON.parse( fs.readFileSync('./node_modules/echonet-lite-conv/Appendix_H/deviceObject_H.json', 'utf8') );
	ELconv.m_dictDev = JSON.parse(fs.readFileSync('./node_modules/echonet-lite-conv/Appendix_I/deviceObject_I.json', 'utf8'));

	// メーカーコード辞書
	ELconv.m_dictMakers = JSON.parse(fs.readFileSync('./node_modules/echonet-lite-conv/makers.json', 'utf8'));
};


//////////////////////////////////////////////////////////////////////
// 変換系/内部関数
//////////////////////////////////////////////////////////////////////

/**
 * 1バイトを文字列の16進表現へ変換(1Byteは必ず2文字にする)
 * @function toHexString
 * @memberof module:echonet-lite-conv
 * @param {number} byte - 変換する1バイトの数値
 * @returns {string} 2文字の16進数文字列
 */
ELconv.toHexString = function (byte) {
	// 文字列0をつなげて,後ろから2文字分スライスする
	return (('0' + byte.toString(16)).slice(-2));
};


/**
 * 16進表現の文字列を数値のバイト配列へ変換
 * @function toHexArray
 * @memberof module:echonet-lite-conv
 * @param {string} string - 16進数の文字列
 * @returns {number[]} バイト配列
 */
ELconv.toHexArray = function (string) {
	let ret = [];
	let i, l, r;

	for (i = 0; i < string.length; i += 2) {
		l = string.substr(i, 1);
		r = string.substr(i + 1, 1);
		ret.push((parseInt(l, 16) * 16) + parseInt(r, 16));
	}

	return ret;
};

/**
 * 年月データを文字列に変換 (Year(2byte), Month(1byte))
 * @function YYMtoString
 * @memberof module:echonet-lite-conv
 * @param {string} yym - 年月の16進数文字列
 * @returns {string} 'YYYY.M'形式の文字列
 */
ELconv.YYMtoString = function (yym) {  // Year(2byte), Month(1byte)
	let yy, m;
	yy = yym.substr(0, 4);
	m = yym.substr(4, 2);
	return parseInt(yy, 16) + '.' + parseInt(m, 16);
};

/**
 * 年月日データを文字列に変換 (Year(2byte), Month(1byte), Day(1byte))
 * @function YYMDtoString
 * @memberof module:echonet-lite-conv
 * @param {string} yymd - 年月日の16進数文字列
 * @returns {string} 'YYYY.M.D'形式の文字列
 */
ELconv.YYMDtoString = function (yymd) { // Year(2byte), Month(1byte), Day(1byte)
	let yy, m, d;
	yy = yymd.substr(0, 4);
	m = yymd.substr(4, 2);
	d = yymd.substr(6, 2);
	return parseInt(yy, 16) + '.' + parseInt(m, 16) + '.' + parseInt(d, 16);
};

/**
 * 時分データを文字列に変換 (Hour(1byte), Minute(1byte))
 * @function HMtoString
 * @memberof module:echonet-lite-conv
 * @param {string} hm - 時分の16進数文字列
 * @returns {string} 'H.M'形式の文字列
 */
ELconv.HMtoString = function (hm) {  // Hour(1byte), Minute(1byte)
	let h, m;
	h = hm.substr(0, 2);
	m = hm.substr(2, 2);
	return parseInt(h, 16) + '.' + parseInt(m, 16);
};

/**
 * 時分秒データを文字列に変換 (Hour(1byte), Minute(1byte), Second(1byte))
 * @function HMStoString
 * @memberof module:echonet-lite-conv
 * @param {string} hms - 時分秒の16進数文字列
 * @returns {string} 'H.M.S'形式の文字列
 */
ELconv.HMStoString = function (hms) { // Hour(1byte), Minute(1byte), Second(1byte)
	let h, m, s;
	h = hms.substr(0, 2);
	m = hms.substr(2, 2);
	s = hms.substr(4, 2);
	return parseInt(h, 16) + '.' + parseInt(m, 16) + '.' + parseInt(s, 16);
};

/**
 * 時分フレームデータを文字列に変換 (Hour(1byte), Minute(1byte), Frame(1byte))
 * @function HMFtoString
 * @memberof module:echonet-lite-conv
 * @param {string} hmf - 時分フレームの16進数文字列
 * @returns {string} 'H.M.F'形式の文字列
 */
ELconv.HMFtoString = function (hmf) { // Hour(1byte), Minute(1byte), Frame(1byte)
	let h, m, f;
	h = hmf.substr(0, 2);
	m = hmf.substr(2, 2);
	f = hmf.substr(4, 2);
	return parseInt(h, 16) + '.' + parseInt(m, 16) + '.' + parseInt(f, 16);
};

/**
 * 分秒データを文字列に変換 (Minute(1byte), Second(1byte))
 * @function MStoString
 * @memberof module:echonet-lite-conv
 * @param {string} ms - 分秒の16進数文字列
 * @returns {string} 'M.S'形式の文字列
 */
ELconv.MStoString = function (ms) {  // Minute(1byte), Second(1byte)
	let m, s;
	m = ms.substr(0, 2);
	s = ms.substr(2, 2);
	return parseInt(m, 16) + '.' + parseInt(s, 16);
};

/**
 * 16進数文字列をShift_JIS文字列に変換
 * @function HEXStringtoShiftJIS
 * @memberof module:echonet-lite-conv
 * @param {string} hexString - 16進数文字列
 * @returns {string} Shift_JISでデコードされた文字列
 */
ELconv.HEXStringtoShiftJIS = function (hexString) {
	let array = ELconv.toHexArray(hexString);
	return (Encoding.codeToString(Encoding.convert(array)));
};

/**
 * 16進数文字列をASCII文字列に変換
 * @function HEXStringtoASCII
 * @memberof module:echonet-lite-conv
 * @param {string} hexString - 16進数文字列
 * @returns {string} ASCIIでデコードされた文字列(null文字を除去)
 */
ELconv.HEXStringtoASCII = function (hexString) {
	let array = ELconv.toHexArray(hexString);
	let ret = Buffer(array).toString('ASCII');
	return ret.replace(/\0/g, ''); // null(\0000)が入ってしまうことがあるので
};


/**
 * ビットマップデータを文字列に変換
 * @function BITMAPtoString
 * @memberof module:echonet-lite-conv
 * @param {string} edt - EDTの16進数文字列
 * @param {Array} typeArray - ビット名と値のマッピング配列
 * @returns {string} 各ビットの状態を示す文字列
 */
ELconv.BITMAPtoString = function (edt, typeArray) {
	let ret = '';
	let x = parseInt(edt);

	for (let i = 0; i < typeArray.length; i += 1) {
		ret += typeArray[i].bitName + ':' + typeArray[i].bitValues[(x >> i) & 1] + ',';
	}
	ret = ret.slice(0, -1);

	return ret;
};


/**
 * バイト列文字列を2文字づつスペース区切りにする
 * @function ByteStringSeparater
 * @memberof module:echonet-lite-conv
 * @param {string} bytestring - バイト列の文字列
 * @returns {string} スペース区切りの文字列
 */
ELconv.ByteStringSeparater = function (bytestring) {
	if (bytestring == null) {
		return '';
	}

	let ret = '';
	for (let i = 0; i < bytestring.length; i += 2) {
		ret += bytestring.substr(i, 2) + ' ';
	}
	ret = ret.slice(0, -1);

	return ret;
};


/**
 * EOJ(ECHONET Lite Object)を人間が読める文字列に変換
 * @function refEOJ
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @returns {string} EOJの名称または元の文字列
 */
ELconv.refEOJ = function (eoj) {
	let ret = eoj = eoj.toUpperCase();
	if (eoj.substr(0, 4) == '0EF0') {
		ret = ELconv.m_dictNod.elObjects['0x0EF0'].objectName + eoj.substr(4, 2);
	}
	if (ELconv.m_dictDev.elObjects['0x' + eoj.substr(0, 4)]) {
		ret = ELconv.m_dictDev.elObjects['0x' + eoj.substr(0, 4)].objectName + eoj.substr(4, 2);
	}
	return ret;
};


/**
 * EPC(ECHONET Lite Property)を人間が読める文字列に変換
 * @function refEPC
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @returns {string} EPCの名称または元の文字列
 */
ELconv.refEPC = function (eoj, epc) {
	eoj = eoj.toUpperCase();
	let ret = epc = epc.toUpperCase();

	// F0からFFまではuser defined
	let upper = epc.substr(0, 1);
	if (upper == 'F') {
		ret = 'ユーザ定義領域(' + epc + ')';
	}

	// スーパークラスにあるか?
	if (undefined != ELconv.m_dictSup.elObjects['0x0000'].epcs['0x' + epc]) { // 指定のEPCはあるか?あればまず確保,機器固有があれば上書き
		ret = ELconv.m_dictSup.elObjects['0x0000'].epcs['0x' + epc].epcName + '(' + epc + ')';
	}

	if (eoj.substr(0, 4) == '0EF0') {  // node profile object
		let devProps = ELconv.m_dictNod.elObjects['0x' + eoj.substr(0, 4)];
		if ('0x' + epc in devProps.epcs) { // 指定のEPCはあるか?
			ret = devProps.epcs['0x' + epc].epcName + '(' + epc + ')';
		}
	}

	// 各機器固有のEPCか?
	if (ELconv.m_dictDev.elObjects['0x' + eoj.substr(0, 4)]) {  // 指定のEOJはあるか?
		let devProps = ELconv.m_dictDev.elObjects['0x' + eoj.substr(0, 4)];
		if ('0x' + epc in devProps.epcs) { // 指定のEPCはあるか?
			ret = devProps.epcs['0x' + epc].epcName + '(' + epc + ')';
		}
	}
	return (ret);
};


//////////////////////////////////////////////////////////////////////
// other(referSpec)
//////////////////////////////////////////////////////////////////////

/**
 * 特殊なEPCの仕様に基づいてEDTを解析
 * @function selectReferSpec
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 解析されたEDTの文字列表現
 */
ELconv.selectReferSpec = function (eoj, epc, edt) {
	let ret;
	eoj = eoj.toUpperCase();
	epc = epc.toUpperCase();
	edt = edt.toUpperCase();

	if (epc == '81') { // 設置場所
		ret = ELconv.referSpec81(eoj, epc, edt);
	} else if (epc == '82') { // Version情報
		ret = ELconv.referSpec82(eoj, epc, edt);
	} else if (epc == '83') { // 識別番号
		ret = ELconv.ByteStringSeparater(edt);
	} else if (epc == '8A') { // メーカコード
		ret = ELconv.referSpec8A(eoj, epc, edt);
	} else if (epc == '9D' || epc == '9E' || epc == '9F') {
		ret = ELconv.referSpec9D9E9F(eoj, epc, edt);
	} else {
		ret = 'referSpec' + '(' + ELconv.ByteStringSeparater(edt) + ')';
	}

	return ret;
};


/**
 * EPC 0x81 設置場所の解析
 * @function referSpec81
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 設置場所の文字列表現
 */
ELconv.referSpec81 = function (eoj, epc, edt) {
	let ret;
	eoj = eoj.toUpperCase();
	epc = epc.toUpperCase();
	edt = edt.toUpperCase();
	let edtHexArray = ELconv.toHexArray(edt);
	// 設置場所コード0x01の時は,以降に続く16バイトで緯度・経度・高さの情報とする
	// 設置場所コード0x01の時で,上位8バイトが 00,00,1B,00,00,00,00,03の時には下位8バイトは国土地理院の場所情報コードに従う
	// 設置場所コード0x02から0x07はfor future reserved

	if (edtHexArray[0] == 0) {
		ret = '位置情報未設定' + '(' + ELconv.ByteStringSeparater(edt) + ')';

	} else if (edtHexArray[0] == 1) {
		// 緯度・経度・高さ or 国土地理院
		ret = '位置情報定義';

		if (edt.substr(2, 16) == '00001B0000000003') {
			ret += ':国土地理院場所情報コード:';

			let latitude = ((edtHexArray[9] & 0x3f) << 17) + ((edtHexArray[10] & 0xff) << 9) + ((edtHexArray[11] & 0xff) << 1) + ((edtHexArray[12] & 0x80) >> 7);
			let longitude = ((edtHexArray[12] & 0x7f) << 17) + ((edtHexArray[13] & 0xff) << 9) + ((edtHexArray[14] & 0xff) << 1) + ((edtHexArray[15] & 0x80) >> 7);
			let floor = ((edtHexArray[15] & 0x7f) << 1) + ((edtHexArray[16] & 0x80) >> 7) - 50;
			let floor_mid = ((edtHexArray[16] & 0x40) >> 6);
			let number = (edtHexArray[16] & 0x3f);

			ret += 'latitude=' + latitude + ',longitude=' + longitude + ',floor=' + floor;
			if (floor_mid = 1) {
				ret += '+mid';
			}
			ret += ',number=' + number;

		} else {
			ret += ':緯度・経度・高さ:';
		}

		ret += '(' + ELconv.ByteStringSeparater(edt) + ')';
	} else if (2 < edtHexArray[0] && edtHexArray[0] < 7) {
		ret = 'for future reserved' + '(' + ELconv.ByteStringSeparater(edt) + ')';

	} else if (edtHexArray[0] == 255) {
		ret = '設置場所不定' + '(' + ELconv.ByteStringSeparater(edt) + ')';

	} else {
		// 1バイトの時は表を見る
		let x = parseInt(edt, 16);

		if (x & 0x80) {
			ret = 'フリー定義' + (x & 0x7f) + '(' + ELconv.ByteStringSeparater(edt) + ')';

		} else {
			let highNumber = ((x >> 3) & 0x0F);
			let lowNumber = (x & 0x07);

			switch (highNumber) {
				case 1:
					ret = '居間、リビング' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';

					break;
				case 2:
					ret = '食堂、ダイニング' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 3:
					ret = '台所、キッチン' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 4:
					ret = '浴槽、バス' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 5:
					ret = 'トイレ' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 6:
					ret = '洗面所、脱衣所' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 7:
					ret = '廊下' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 8:
					ret = '部屋' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 9:
					ret = '階段' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 10:
					ret = '玄関' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 11:
					ret = '納屋' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 12:
					ret = '庭、外周' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 13:
					ret = '車庫' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 14:
					ret = 'ベランダ、バルコニー' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				case 15:
					ret = 'その他' + lowNumber + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
				default:
					ret = 'referSpec' + '(' + ELconv.ByteStringSeparater(edt) + ')';
					break;
			}
		}
	}

	return (ret);
};

/**
 * EPC 0x82 Version情報の解析
 * @function referSpec82
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} バージョン情報の文字列表現
 */
ELconv.referSpec82 = function (eoj, epc, edt) {
	let ret = '';
	eoj = eoj.toUpperCase();
	epc = epc.toUpperCase();
	edt = edt.toUpperCase();
	let edtHexArray = ELconv.toHexArray(edt);

	if (eoj.substr(0, 4) == '0EF0') { // プロファイルオブジェクト方式
		ret = 'Ver. ';
		ret += edtHexArray[0] + '.' + edtHexArray[1];

		switch (edtHexArray[2]) {
			case 1:
				ret += ' 規定電文形式';
				break;
			case 2:
				ret += ' 任意電文形式';
				break;
			case 3:
				ret += ' 規定・任意電文形式';
				break;
		}

	} else { // 機器オブジェクト
		ret = 'Release ';
		ret += Buffer([edtHexArray[2]]).toString('ASCII');
	}

	ret += '(' + ELconv.ByteStringSeparater(edt) + ')';

	return ret;
};


/**
 * EPC 0x8A メーカーコードの解析
 * @function referSpec8A
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} メーカー名の文字列表現
 */
ELconv.referSpec8A = function (eoj, epc, edt) {
	edt = edt.toUpperCase();

	let ret = 'Makercode is not found.';

	if (ELconv.m_dictMakers[edt]) {
		ret = ELconv.m_dictMakers[edt];
	}
	ret += '(' + ELconv.ByteStringSeparater(edt) + ')';

	return ret;
};


/**
 * EPC 0x9D, 0x9E, 0x9F プロパティマップの解析
 * @function referSpec9D9E9F
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} プロパティマップの文字列表現
 */
ELconv.referSpec9D9E9F = function (eoj, epc, edt) {
	let ret = '';
	let array = ELconv.toHexArray(edt);

	if (array.length < 16) { // プロパティマップ16バイト未満は記述形式1
		array = array.map(function (e) {
			return ELconv.toHexString(e);
		});
		ret = array[0];
		ret += '[' + array.slice(1).join(', ') + ']';
	} else {
		// 16バイト以上なので記述形式2,EPCのarrayを作り直したら,あと同じ
		array = ELconv.parseMapForm2(array);
		array = array.map(function (e) {
			return ELconv.toHexString(e);
		});
		ret = array[0];
		ret += '[' + array.slice(1).join(', ') + ']';
	}
	ret += '(' + ELconv.ByteStringSeparater(edt) + ')';
	return ret;
};

/**
 * プロパティマップの記述形式2を解析してForm1に変換
 * 16以上のプロパティ数の時に使用
 * @function parseMapForm2
 * @memberof module:echonet-lite-conv
 * @param {number[]} array - バイト配列
 * @returns {number[]} Form1形式のプロパティ配列
 */
ELconv.parseMapForm2 = function (array) {
	let ret = [];
	let val = 0x80;

	// bit loop
	for (let bit = 0; bit < 8; bit += 1) {
		// byte loop
		for (let byt = 1; byt < 17; byt += 1) {
			if ((array[byt] >> bit) & 0x01) {
				ret.push(val);
			}
			val += 1;
		}
	}

	ret.unshift(ret.length);

	return ret;
};


/**
 * 分電盤メータリング(0x0287) EPC 0xC2 積算電力量計測単位の解析
 * @function distributionBoardC2
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {number} 計測単位の係数
 */
ELconv.distributionBoardC2 = function (eoj, epc, edt) {
	let ret = 0.001; // default, 0.001kWh
	switch (edt) {
		case '00':
			ret = 1;
			break;
		case '01':
			ret = 0.1;
			break;
		case '02':
			ret = 0.01;
			break;
		case '03':
			ret = 0.001;
			break;
		case '04':
			ret = 0.0001;
			break;
		case '0A':
			ret = 10;
			break;
		case '0B':
			ret = 100;
			break;
		case '0C':
			ret = 1000;
			break;
		case '0D':
			ret = 10000;
			break;
	}
	return ret;
};


/**
 * 分電盤メータリング チャンネル範囲指定の解析
 * EPC: 0xB2, 0xB4, 0xB6, 0xB9, 0xBB, 0xBD
 * @function distributionBoardB2B4B6B9BBBD
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} チャンネル範囲の文字列表現
 */
ELconv.distributionBoardB2B4B6B9BBBD = function (eoj, epc, edt) {
	return edt.substr(0, 2) + 'ch-' + edt.substr(2, 4) + 'ch' + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 分電盤メータリング EPC 0xB3 積算電力量計測値リスト(片方向)
 * 単位がC2に依存する
 * @function distributionBoardB3
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 積算電力量のJSON文字列
 */
ELconv.distributionBoardB3 = function (eoj, epc, edt) {
	let edtHexArray = ELconv.toHexArray(edt);
	let begin = edtHexArray[0];
	let end = edtHexArray[1];
	let ret = {};

	for (let i = begin; i <= end; i += 1) {
		let value = edtHexArray[i * 2] * 256 + edtHexArray[i * 2 + 1];
		ret['ch' + i] = value + '[xC2 kWh]';  // デフォルトでWhだが,本質的にはC2に依存する
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 分電盤メータリング EPC 0xB5 瞬時電流計測値リスト(片方向)
 * 単位: 0.1A
 * @function distributionBoardB5
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 瞬時電流のJSON文字列
 */
ELconv.distributionBoardB5 = function (eoj, epc, edt) {
	let edtHexArray = ELconv.toHexArray(edt);
	let begin = edtHexArray[0];
	let end = edtHexArray[1];
	let ret = {};

	for (let i = begin; i <= end; i += 1) {
		let value = (edtHexArray[i * 2] * 256 + edtHexArray[i * 2 + 1]) * 0.1;
		ret['ch' + i] = value + '[A]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 分電盤メータリング EPC 0xB7 瞬時電力計測値リスト(片方向)
 * 単位: 1W
 * @function distributionBoardB7
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 瞬時電力のJSON文字列
 */
ELconv.distributionBoardB7 = function (eoj, epc, edt) {
	let edtHexArray = ELconv.toHexArray(edt);
	let begin = edtHexArray[0];
	let end = edtHexArray[1];
	let ret = {};

	for (let i = begin; i <= end; i += 1) {
		let value = edtHexArray[i * 2] * 256 + edtHexArray[i * 2 + 1];
		ret['ch' + i] = value + '[W]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 分電盤メータリング EPC 0xBA 積算電力量計測値リスト(双方向)
 * 単位はC2に従う
 * @function distributionBoardBA
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 積算電力量のJSON文字列(正逆両方向)
 */
ELconv.distributionBoardBA = function (eoj, epc, edt) {
	let edtHexArray = ELconv.toHexArray(edt);
	let begin = edtHexArray[0];
	let end = edtHexArray[1];
	let ret = {};

	for (let i = begin; i <= end; i += 1) {
		let value_f = edtHexArray[i * 2] * 256 + edtHexArray[i * 2 + 1];
		let value_r = edtHexArray[i * 2 + 2] * 256 + edtHexArray[i * 2 + 3];
		ret['ch' + i + 'f'] = value_f + '[xC2 kWh]';
		ret['ch' + i + 'r'] = value_r + '[xC2 kWh]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 分電盤メータリング EPC 0xBC 瞬時電流計測値リスト(双方向)
 * 単位: 0.1A
 * @function distributionBoardBC
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 瞬時電流のJSON文字列(R相/T相)
 */
ELconv.distributionBoardBC = function (eoj, epc, edt) {
	let edtHexArray = ELconv.toHexArray(edt);
	let begin = edtHexArray[0];
	let end = edtHexArray[1];
	let ret = {};

	for (let i = begin; i <= end; i += 1) {
		let value_r = (edtHexArray[i * 2] * 256 + edtHexArray[i * 2 + 1]) * 0.1;
		let value_t = (edtHexArray[i * 2 + 2] * 256 + edtHexArray[i * 2 + 3]) * 0.1;
		ret['ch' + i + 'R'] = value_r + '[A]';
		ret['ch' + i + 'T'] = value_t + '[A]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 分電盤メータリング EPC 0xBE 瞬時電力計測値リスト(双方向)
 * 単位: W
 * @function distributionBoardBE
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 瞬時電力のJSON文字列
 */
ELconv.distributionBoardBE = function (eoj, epc, edt) {
	let edtHexArray = ELconv.toHexArray(edt);
	let begin = edtHexArray[0];
	let end = edtHexArray[1];
	let ret = {};

	for (let i = begin; i <= end; i += 1) {
		let value = edtHexArray[i * 2] * 256 + edtHexArray[i * 2 + 1];
		ret['ch' + i] = value + '[W]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 低圧スマート電力量メータ(0x0288) EPC 0xE8 瞬時電流計測値の解析
 * @function lowVoltageSmartElectricEnergyMeterE8
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 瞬時電流のJSON文字列(R相/T相)
 */
ELconv.lowVoltageSmartElectricEnergyMeterE8 = function (eoj, epc, edt) {
	let rPhase = edt.substr(0, 4);
	let tPhase = edt.substr(4, 4);
	let ret = {};

	ret['RPhase'] = Int16Array.from([parseInt(rPhase, 16)])[0] * 0.1 + '[A]';

	if (tPhase === '7FFE') {
		ret['TPhase'] = 'two-wire';
	} else {
		ret['TPhase'] = Int16Array.from([parseInt(tPhase, 16)])[0] * 0.1 + '[A]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * 低圧スマート電力量メータ EPC 0xEA/0xEB 定時積算電力量計測値の解析
 * @function lowVoltageSmartElectricEnergyMeterEAEB
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列(0xEA:正方向, 0xEB:逆方向)
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 定時積算電力量の文字列表現
 */
ELconv.lowVoltageSmartElectricEnergyMeterEAEB = function (eoj, epc, edt) {
	// 12 34 56 78 90 12 34 56
	// 07 E6 06 0F 11 1E 00 00 0B 94 60
	// yy yy mm dd hh mm ss
	// 2022  6  15 11 30 00 [kWh      ]
	// edt = "07E6060F111E0005F5E0FF";  //05F5E0FFがMax
	let ret;
	let yymd = ELconv.YYMDtoString(edt.substr(0, 8));
	let hms = ELconv.HMStoString(edt.substr(8, 6));
	// console.log( edt.substr(14,20) );
	let pow = parseInt(edt.substr(14, 20), 16);

	ret = yymd + ' ' + hms + ',' + pow + '[xE1 kWh]';

	return ret + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * スマート電力量サブメータ(0x028D) EPC 0xE8 瞬時電流計測値の解析
 * @function smartElectricEnergySubMeterE8
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 瞬時電流のJSON文字列(R相/T相)
 */
ELconv.smartElectricEnergySubMeterE8 = function (eoj, epc, edt) {
	let rPhase = edt.substr(0, 4);
	let tPhase = edt.substr(4, 4);
	let ret = {};

	ret['RPhase'] = Int16Array.from([parseInt(rPhase, 16)])[0] * 0.1 + '[A]';

	if (tPhase === '7FFE') {
		ret['TPhase'] = 'two-wire';
	} else {
		ret['TPhase'] = Int16Array.from([parseInt(tPhase, 16)])[0] * 0.1 + '[A]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * スマート電力量サブメータ EPC 0xE9 瞬時電圧計測値の解析
 * @function smartElectricEnergySubMeterE9
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 瞬時電圧のJSON文字列(R-S/S-T)
 */
ELconv.smartElectricEnergySubMeterE9 = function (eoj, epc, edt) {
	let rPhase = edt.substr(0, 4);
	let tPhase = edt.substr(4, 4);
	let ret = {};

	ret['R-S'] = Int16Array.from([parseInt(rPhase, 16)])[0] * 0.1 + '[V]';

	if (tPhase === '7FFE') {
		ret['S-T'] = 'two-wire';
	} else {
		ret['S-T'] = Int16Array.from([parseInt(tPhase, 16)])[0] * 0.1 + '[V]';
	}

	return JSON.stringify(ret) + '(' + ELconv.ByteStringSeparater(edt) + ')';
};

/**
 * スマート電力量サブメータ EPC 0xEA/0xEB 定時積算電力量計測値の解析
 * @function smartElectricEnergySubMeterEAEB
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列(0xEA:正方向, 0xEB:逆方向)
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 定時積算電力量の文字列表現
 */
ELconv.smartElectricEnergySubMeterEAEB = function (eoj, epc, edt) {
	// 12 34 56 78 90 12 34 56
	// 07 E6 06 0F 11 1E 00 00 0B 94 60
	// yy yy mm dd hh mm ss
	// 2022  6  15 11 30 00 [kWh      ]
	// edt = "07E6060F111E0005F5E0FF";  //05F5E0FFがMax
	let ret;
	let yymd = ELconv.YYMDtoString(edt.substr(0, 8));
	let hms = ELconv.HMStoString(edt.substr(8, 6));
	// console.log( edt.substr(14,20) );
	let pow = parseInt(edt.substr(14, 20), 16);

	ret = yymd + ' ' + hms + ',' + pow + '[xD3 kWh]';

	return ret + '(' + ELconv.ByteStringSeparater(edt) + ')';
};


//////////////////////////////////////////////////////////////////////
// 変換系
//////////////////////////////////////////////////////////////////////

/**
 * ネットワーク内のECHONET Lite機器全体情報を参照し変換する
 * @function refer
 * @memberof module:echonet-lite-conv
 * @param {Object} facilities - 機器情報オブジェクト
 * @param {Function} callback - 変換結果を受け取るコールバック関数
 */
ELconv.refer = function (facilities, callback) {
	let ret = { 'IPs': [] };

	Object.keys(facilities || {}).forEach(function (ip) { // ip
		ret.IPs.push(ip);
		ret[ip] = { 'EOJs': [] }
		Object.keys(facilities[ip] || {}).forEach(function (eoj) { // eoj
			let retEoj = ELconv.refEOJ(eoj) + '(' + eoj + ')';
			ret[ip].EOJs.push(retEoj);
			ret[ip][retEoj] = { 'EPCs': [] };
			Object.keys((facilities[ip] && facilities[ip][eoj]) ? facilities[ip][eoj] : {}).forEach(function (epc) { // epc
				let retEpc = ELconv.refEPC(eoj, epc);
				ret[ip][retEoj].EPCs.push(retEpc);
				ret[ip][retEoj][retEpc] = ELconv.parseEDT(eoj, epc, (facilities[ip] && facilities[ip][eoj]) ? facilities[ip][eoj][epc] : ''); //edt
			});

			let conboEdt = ELconv.EDTconvination(eoj, (facilities[ip] ? facilities[ip][eoj] : null));
			if (conboEdt) {
				ret[ip].Means = conboEdt;
			}

		}); // eoj
	}); // ip
	callback(ret);
};


/**
 * EDT(ECHONET Lite Data)を解析して人間が読める形式に変換
 * @function parseEDT
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {string} epc - EPCの16進数文字列
 * @param {string} edt - EDTの16進数文字列
 * @returns {string} 解析されたEDTの文字列表現
 */
ELconv.parseEDT = function (eoj, epc, edt) {
	eoj = eoj.toUpperCase();
	epc = epc.toUpperCase();
	edt = edt.toUpperCase();

	let contentRule;
	let ret;

	// F0からFFまではuser defined
	let upper = epc.substr(0, 1);
	if (upper == 'F') {
		ret = 'ユーザ定義領域(' + ELconv.ByteStringSeparater(edt) + ')';
		return ret;
	}

	// スーパークラスにあるか?(ガード付き)
	const supObj = ELconv.m_dictSup?.elObjects?.['0x0000']?.epcs?.['0x' + epc]?.edt?.[0]?.content;
	if (supObj) {
		contentRule = supObj;
	}

	if (eoj.substr(0, 4) == '0EF0') {  // node profile object
		const devProps = ELconv.m_dictNod?.elObjects?.['0x' + eoj.substr(0, 4)];
		if (devProps?.epcs && ('0x' + epc) in devProps.epcs) { // 指定のEPCはあるか?
			const nodObj = ELconv.m_dictNod?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0x' + epc]?.edt?.[0]?.content;
			if (nodObj) contentRule = nodObj;
		}
	}

	// 各機器固有のEPCか?
	if (ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]) {  // 指定のEOJはあるか?
		const devProps = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)];
		if (devProps?.epcs && ('0x' + epc) in devProps.epcs) { // 指定のEPCはあるか?
			const devObj = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0x' + epc]?.edt?.[0]?.content;
			if (devObj) contentRule = devObj;
		}
	}

	// 1.content typeをみてみる
	for (let contentType in (contentRule || {})) {
		switch (contentType) {
			case 'rawData':				// rawData
				switch (contentRule.rawData) {
					case 'ASCII':
						ret = ELconv.HEXStringtoASCII(edt) + '(ascii:' + ELconv.ByteStringSeparater(edt) + ')';
						break;
					case 'ShiftJIS':
						ret = ELconv.HEXStringtoShiftJIS(edt) + '(ShiftJIS:' + ELconv.ByteStringSeparater(edt) + ')';
						break;
					case 'binary':
					default:
						ret = contentRule.rawData + '(' + ELconv.ByteStringSeparater(edt) + ')';
						break;
				}
				break;

			case 'numericValue':			// numericValue
				let val = 0;
				switch (contentRule.numericValue.integerType) {
					case 'Signed':
						val = parseInt(edt, 16);
						let bitLen = edt.length * 4;
						let maxVal = Math.pow(2, bitLen - 1) - 1;
						if (val > maxVal) {
							val = val - Math.pow(2, bitLen);
						}
						break;
					case 'Unsigned':
						val = parseInt(edt, 16);
						break;
					default:
						// エラー返すのどうしたらいいかわからん。
						val = parseInt(edt, 16);
						break;
				}

				let mag = 0; // 倍率を10のN乗表記した指数部。省略可。例: -1(x0.1), 2(x100)
				if (contentRule.numericValue.magnification) {
					mag = parseInt(contentRule.numericValue.magnification);
				}

				let unit = ''; //  単位。省略可。例: '℃'
				if (contentRule.numericValue.unit) {
					unit = contentRule.numericValue.unit;
				}

				ret = val * Math.pow(10, mag) + unit + '(' + ELconv.ByteStringSeparater(edt) + ')';
				break;

			case 'level': // Rel.I, クッキングヒータクラスの加熱出力設定には非対応
				let value = parseInt(edt, 16);
				let min = parseInt(contentRule.level.min, 10);
				value -= min + 1;
				ret = 'level ' + value + '(' + ELconv.ByteStringSeparater(edt) + ')';
				break;

			case 'customType':  // 時間
				switch (contentRule.customType) {
					case 'YYM':  // Year(2byte), Month(1byte)
						ret = ELconv.MStoString(edt) + '(' + ELconv.ByteStringSeparater(edt) + ')';
						break;
					case 'YYMD': // Year(2byte), Month(1byte), Day(1byte)
						ret = ELconv.YYMDtoString(edt) + '(' + ELconv.ByteStringSeparater(edt) + ')';
						break;
					case 'HM':   // Hour(1byte), Minute(1byte)
						ret = ELconv.HMtoString(edt) + '(' + ELconv.ByteStringSeparater(edt) + ')';
						break;
					case 'HMS':  // Hour(1byte), Minute(1byte), Second(1byte)
						ret = ELconv.HMStoString(edt) + '(' + ELconv.ByteStringSeparater(edt) + ')';
						break;
					case 'HMF':  // Hour(1byte), Minute(1byte), Frame(1byte)
						ret = ELconv.HMFtoString(edt) + '(' + ELconv.ByteStringSeparater(edt) + ')';
						break;
					case 'MS':   // Minute(1byte), Second(1byte)
						ret = ELconv.MStoString(edt) + '(' + ELconv.ByteStringSeparater(edt) + ')';
						break;
				}
				break;

			case 'bitmap': // bitmap方式
				ret = ELconv.BITMAPtoString(edt, contentRule.bitmap) + '(' + ELconv.ByteStringSeparater(edt) + ')';
				break;

			case 'others': // 各EPC個別対応しかできないかも?
				if (contentRule.others == 'referSpec') {
					ret = ELconv.selectReferSpec(eoj, epc, edt);
				} else {
					ret = contentRule.others + '(' + ELconv.ByteStringSeparater(edt) + ')';
				}
				break;

			default:
				ret = contentType + '(' + ELconv.ByteStringSeparater(edt) + ')';
				break;
		}
	}

	// 2.keyValuesを持つときはそちらを優先するが,undefinedの時は値を更新しない
	for (let contentType in (contentRule || {})) {
		if (contentType == 'keyValues') {

			if (epc == 'D6') {			// 3.keyValuesの特殊な形として,自ノードインスタンスリストSを解決する
				let count = parseInt(edt.substring(0, 2));
				ret = '';
				for (let i = 0; i < count; i++) {
					let st = edt.substr(i * 6 + 2, 6);
					ret += ELconv.refEOJ(st) + ',';
				}
				ret = ret.slice(0, -1) + '(' + ELconv.ByteStringSeparater(edt) + ')';
			}
			else if (epc == 'D7') { // 3.keyValuesの特殊な形として,自ノードクラスリストSを解決する
				let count = parseInt(edt.substring(0, 2));
				ret = '';
				for (let i = 0; i < count; i++) {
					let st = edt.substr(i * 4 + 2, 4);
					ret += ELconv.refEOJ(st) + ',';
				}
				ret = ret.slice(0, -1) + '(' + ELconv.ByteStringSeparater(edt) + ')';
			}
			// 4.プロパティマップ以外はJSON DBのEDT対応表を参照
			else if (contentRule.keyValues['0x' + edt]) {
				ret = contentRule.keyValues['0x' + edt] + '(' + ELconv.ByteStringSeparater(edt) + ')';
			}
		}
	}


	// その他,特殊な形
	// 分電盤メータリング 0287
	if (eoj.substr(0, 4) == '0287') {
		switch (epc) {
			case 'C2':
				ret = ELconv.distributionBoardC2(eoj, epc, edt);
				break;
			case 'B2':
			case 'B4':
			case 'B6':
			case 'B9':
			case 'BB':
			case 'BD':
				ret = ELconv.distributionBoardB2B4B6B9BBBD(eoj, epc, edt);
				break;
			case 'B3':
				ret = ELconv.distributionBoardB3(eoj, epc, edt);
				break;
			case 'B5':
				ret = ELconv.distributionBoardB5(eoj, epc, edt);
				break;
			case 'B7':
				ret = ELconv.distributionBoardB7(eoj, epc, edt);
				break;
			case 'BA':
				ret = ELconv.distributionBoardBA(eoj, epc, edt);
				break;
			case 'BC':
				ret = ELconv.distributionBoardBC(eoj, epc, edt);
				break;
			case 'BE':
				ret = ELconv.distributionBoardBE(eoj, epc, edt);
				break;
		}
	}

	// 低圧スマート電力量メータ01(028801)
	if (eoj.substr(0, 4) === '0288') {
		switch (epc) {
			case 'E8':
				ret = ELconv.lowVoltageSmartElectricEnergyMeterE8(eoj, epc, edt);
				break;
			case 'EA':
			case 'EB':
				ret = ELconv.lowVoltageSmartElectricEnergyMeterEAEB(eoj, epc, edt);
				break;
		}
	}

	// 低圧スマート電力量メータ01(028801)
	if (eoj.substr(0, 4) === '028D') {
		switch (epc) {
			case 'E8':
				ret = ELconv.smartElectricEnergySubMeterE8(eoj, epc, edt);
				break;
			case 'E9':
				ret = ELconv.smartElectricEnergySubMeterE9(eoj, epc, edt);
				break;
			case 'EA':
			case 'EB':
				ret = ELconv.smartElectricEnergySubMeterEAEB(eoj, epc, edt);
				break;
		}
	}


	return ret;
};


/**
 * 複数のEPCが関連して意味をなすデータを組み合わせて解析
 * 主に電力量計測関連の係数と値の組み合わせ処理
 * @function EDTconvination
 * @memberof module:echonet-lite-conv
 * @param {string} eoj - EOJの16進数文字列
 * @param {Object} epcs - EPCとEDTのマッピングオブジェクト
 * @returns {Object|null} 組み合わせ解析結果のオブジェクトまたはnull
 */
ELconv.EDTconvination = function (eoj, epcs) {
	// console.log('# ELconv.EDTconvination', eoj, epcs);
	eoj = eoj.toUpperCase();

	let ret = {};
	// 低圧スマート電力量メータ(0288)
	if (eoj.substr(0, 4) === '0288') {
		if (epcs['e0'] && epcs['e1']) {
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xE1']?.edt?.[0]?.content;
			const coefficientStr = contentRule?.keyValues?.['0x' + epcs['e1']];
			if (coefficientStr != null) {
				const pow = parseInt(epcs['e0'], 16) * parseFloat(coefficientStr);
				ret['積算電力量計測値(正方向計測値)[kWh]'] = pow;
			}
		}
		if (epcs['e3'] && epcs['e1']) {
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xE1']?.edt?.[0]?.content;
			const coefficientStr = contentRule?.keyValues?.['0x' + epcs['e1']];
			if (coefficientStr != null) {
				const pow = parseInt(epcs['e3'], 16) * parseFloat(coefficientStr);
				ret['積算電力量計測値(逆方向計測値)[kWh]'] = pow;
			}
		}
		if (epcs['ea'] && epcs['e1']) {
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xE1']?.edt?.[0]?.content;
			const coefficientStr = contentRule?.keyValues?.['0x' + epcs['e1']];
			if (coefficientStr != null) {
				const yymd = ELconv.YYMDtoString(epcs['ea'].substr(0, 8));
				const hms = ELconv.HMStoString(epcs['ea'].substr(8, 6));
				const pow = parseInt(epcs['ea'].substr(14, 20), 16) * parseFloat(coefficientStr);
				const dt = new Date(yymd.split('.')[0], yymd.split('.')[1] - 1, yymd.split('.')[2],
					hms.split('.')[0], hms.split('.')[1], hms.split('.')[2]);
				ret['定時積算電力量計測値正方向'] = { '日時': dt.toISOString(), '計測値[kWh]': pow };
			}
		}
		if (epcs['eb'] && epcs['e1']) {
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xE1']?.edt?.[0]?.content;
			const coefficientStr = contentRule?.keyValues?.['0x' + epcs['e1']];
			if (coefficientStr != null) {
				const yymd = ELconv.YYMDtoString(epcs['eb'].substr(0, 8));
				const hms = ELconv.HMStoString(epcs['eb'].substr(8, 6));
				const pow = parseInt(epcs['eb'].substr(14, 20), 16) * parseFloat(coefficientStr);
				const dt = new Date(yymd.split('.')[0], yymd.split('.')[1] - 1, yymd.split('.')[2],
					hms.split('.')[0], hms.split('.')[1], hms.split('.')[2]);
				ret['定時積算電力量計測値逆方向'] = { '日時': dt.toISOString(), '計測値[kWh]': pow };
			}
		}
		// if( epcs['e7'] ) {  // 瞬時電力計測値(E7)も使いやすくしたい
		// }

	}
	// スマート電力量サブメータ(028D)
	// D3:係数は任意
	// D4:単位は必須
	else if (eoj.substr(0, 4) === '028D') {
		if (epcs['d3'] && epcs['e7']) {
			let coefficient = epcs['d3'] == '' ? 1 : parseInt(epcs['d3'], 16);
			let pow = parseInt(epcs['e7'], 16) * coefficient;
			ret['瞬時電力計測値[W]'] = pow;
		}
		if (epcs['d3'] && epcs['d4'] && epcs['e1']) {
			const coefficient = epcs['d3'] == '' ? 1 : parseInt(epcs['d3'], 16);
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xD4']?.edt?.[0]?.content;
			const unitStr = contentRule?.keyValues?.['0x' + epcs['d4']];
			if (unitStr != null) {
				const pow = parseInt(epcs['e1'], 16) * coefficient * parseFloat(unitStr);
				ret['積算電力量計測値(正方向計測値)[kWh]'] = pow;
			}
		}
		if (epcs['d3'] && epcs['d4'] && epcs['e3']) {
			const coefficient = epcs['d3'] == '' ? 1 : parseInt(epcs['d3'], 16);
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xD4']?.edt?.[0]?.content;
			const unitStr = contentRule?.keyValues?.['0x' + epcs['d4']];
			if (unitStr != null) {
				const pow = parseInt(epcs['e3'], 16) * coefficient * parseFloat(unitStr);
				ret['積算電力量計測値(逆方向計測値)[kWh]'] = pow;
			}
		}
		if (epcs['d3'] && epcs['d4'] && epcs['ea']) {
			const coefficient = epcs['d3'] == '' ? 1 : parseInt(epcs['d3'], 16);
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xD4']?.edt?.[0]?.content;
			const unitStr = contentRule?.keyValues?.['0x' + epcs['d4']];
			if (unitStr != null) {
				const yymd = ELconv.YYMDtoString(epcs['ea'].substr(0, 8));
				const hms = ELconv.HMStoString(epcs['ea'].substr(8, 6));
				const pow = parseInt(epcs['ea'].substr(14, 20), 16) * parseFloat(coefficient) * parseFloat(unitStr);
				const dt = new Date(yymd.split('.')[0], yymd.split('.')[1] - 1, yymd.split('.')[2],
					hms.split('.')[0], hms.split('.')[1], hms.split('.')[2]);
				ret['定時積算電力量計測値正方向'] = { '日時': dt.toISOString(), '計測値[kWh]': pow };
			}
		}
		if (epcs['d3'] && epcs['d4'] && epcs['eb']) {
			const coefficient = epcs['d3'] == '' ? 1 : parseInt(epcs['d3'], 16);
			const contentRule = ELconv.m_dictDev?.elObjects?.['0x' + eoj.substr(0, 4)]?.epcs?.['0xD4']?.edt?.[0]?.content;
			const unitStr = contentRule?.keyValues?.['0x' + epcs['d4']];
			if (unitStr != null) {
				const yymd = ELconv.YYMDtoString(epcs['eb'].substr(0, 8));
				const hms = ELconv.HMStoString(epcs['eb'].substr(8, 6));
				const pow = parseInt(epcs['eb'].substr(14, 20), 16) * parseFloat(coefficient) * parseFloat(unitStr);
				const dt = new Date(yymd.split('.')[0], yymd.split('.')[1] - 1, yymd.split('.')[2],
					hms.split('.')[0], hms.split('.')[1], hms.split('.')[2]);
				ret['定時積算電力量計測値逆方向'] = { '日時': dt.toISOString(), '計測値[kWh]': pow };
			}
		}
		// if( epcs['e7'] ) {  // 瞬時電力計測値(E7)も使いやすくしたい
		// }

	}
	else {
		return null;
	}
	return ret;
};



/**
 * ESV(ECHONET Lite Service)コードを文字列に変換
 * @function refESV
 * @memberof module:echonet-lite-conv
 * @param {string} esv - ESVの16進数文字列
 * @returns {string} ESVの名称
 */
ELconv.refESV = function (esv) {
	// normalize input to lowercase and handle falsy safely
	esv = (esv || '').toLowerCase();
	let esv_dict = {
		"50": 'SETI_SNA',
		"51": 'SETC_SNA',
		"52": 'GET_SNA',
		"53": 'INF_SNA',
		"5e": 'SETGET_SNA',
		"60": 'SETI',
		"61": 'SETC',
		"62": 'GET',
		"63": 'INF_REQ',
		"6e": 'SETGET',
		"71": 'SET_RES',
		"72": 'GET_RES',
		"73": 'INF',
		"74": 'INFC',
		"7a": 'INFC_RES',
		"7e": 'SETGET_RES'
	};
	return (esv_dict[esv]);
};


/**
 * ECHONET Lite電文(ELS)を解析して人間が読める形式に変換
 * @function elsAnarysis
 * @memberof module:echonet-lite-conv
 * @param {Object} els - ECHONET Lite電文オブジェクト
 * @param {Function} callback - 解析結果を受け取るコールバック関数
 */
ELconv.elsAnarysis = function (els, callback) {

	// 初期化していなかったら初期化する
	if (!ELconv.m_initialized) {
		ELconv.initialize();
	};

	let ret = {
		'EHD': 'ECHONET Lite',
		'TID': '??',
		'SEOJ': '??',
		'DEOJ': '??',
		'ESV': '??',
		'OPC': '??',
		'EDT': {}
	};

	ret.TID = els.TID;
	ret.SEOJ = ELconv.refEOJ(els.SEOJ);
	ret.DEOJ = ELconv.refEOJ(els.DEOJ);
	ret.ESV = ELconv.refESV(els.ESV);
	ret.OPC = els.OPC;
	// normalize ESV for internal branching
	const esv = (els.ESV || '').toLowerCase();

	// EDT だけ少し面倒くさい
	Object.keys(els.DETAILs).forEach(function (epc) { // epc
		let retEpc;
		switch (esv) {
			case '60': // SETIはDEOJを参照
			case '61': // SETC
				retEpc = ELconv.refEPC(els.DEOJ, epc);
				ret['EDT'][retEpc] = ELconv.parseEDT(els.DEOJ, epc, els.DETAILs[epc]); //edt
				break;

			case '62': // GETはEDT=00
			case '63': // INF_REQ
			case '6e':
				retEpc = ELconv.refEPC(els.DEOJ, epc);
				ret['EDT'][retEpc] = 'Request(00)'; //edt
				break;

			// returnはSEOJを参照
			case '50': // seti_sna
			case '51': // setc_sna
			case '52': // get_sna
			case '53': // inf_sna
			case '5e': // setget_sna
			case '71': // set_res
			case '72': // get_res
			case '73': // inf
			case '74': // infc
			case '7a': // infc_res
			case '7e': // setget_res
				retEpc = ELconv.refEPC(els.SEOJ, epc);
				ret['EDT'][retEpc] = ELconv.parseEDT(els.SEOJ, epc, els.DETAILs[epc]); //edt
				break;

			default:
				retEpc = ELconv.refEPC(els.DEOJ, epc);
				ret['EDT'][retEpc] = 'unknown(' + ELconv.ByteStringSeparater(els.DETAILs[epc]) + ')'; //edt
				break;
		}
	});

	callback(ret);
};


//////////////////////////////////////////////////////////////////////
// Aliases for backward compatibility and correct naming
//////////////////////////////////////////////////////////////////////
/**
 * Alias for EDTconvination (corrected spelling: combination)
 * @function EDTCombination
 * @memberof module:echonet-lite-conv
 */
ELconv.EDTCombination = ELconv.EDTconvination;

/**
 * Alias for elsAnarysis (corrected spelling: analysis)
 * @function elsAnalysis
 * @memberof module:echonet-lite-conv
 */
ELconv.elsAnalysis = ELconv.elsAnarysis;


module.exports = ELconv;

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