src/specification.js
/*! resol-vbus | Copyright (c) 2013-present, Daniel Wippermann | MIT license */
const crypto = require('crypto');
const { sprintf } = require('sprintf-js');
const I18N = require('./i18n');
const SpecificationFile = require('./specification-file');
const {
applyDefaultOptions,
deepFreezeObjectTree,
hasOwnProperty,
isNumber,
isObject,
isString,
roundNumber,
} = require('./utils');
const globalSpecificationFile = SpecificationFile.getDefaultSpecificationFile();
let globalSpecificationData = null;
if (globalSpecificationFile) {
globalSpecificationData = deepFreezeObjectTree(globalSpecificationFile.getSpecificationData());
}
let globalSpecification = undefined;
const conversionFactors = {
BtusPerWattHour: 3.412128,
GramsCO2OilPerWattHour: 0.568,
GramsCO2GasPerWattHour: 0.2536,
GallonsPerLiter: 0.264172,
PoundsForcePerSquareInchPerBar: 14.5037738,
};
const numberFormatCache = new Map();
/**
* @typedef UnitSpecification
* @type {object}
* @property {String} unitId Unit identifier
* @property {String} unitCode Unit code
* @property {String} unitFamily Unit family
* @property {String} unitText Unit text
*/
/**
* @typedef TypeSpecification
* @type {object}
* @property {String} typeId Type identifier
* @property {String} rootTypeId Root type identifier
* @property {number} precision Precision for numeral values
* @property {UnitSpecification} unit Unit object
*/
/**
* @typedef DeviceSpecification
* @type {object}
* @property {string} deviceId Device identifier
* @property {number} channel VBus channel
* @property {number} selfAddress VBus address of the device itself
* @property {number} peerAddress VBus address of the device's peer
* @property {string} name Name of the device
* @property {string} fullName Name of the device optionally prefixed with VBus channel (if it is not 0)
*/
/**
* @typedef PacketSpecification
* @type {object}
* @property {string} packetId Packet identifier
* @property {number} channel VBus channel
* @property {number} destinationAddress VBus address of the destination device
* @property {number} sourceAddress VBus address of the source device
* @property {number} protocolVersion VBus protocol version
* @property {number} command VBus command
* @property {number} info Additional info for sorting purposes
* @property {DeviceSpecification} destinationDevice DeviceSpecification object of the destination device
* @property {DeviceSpecification} sourceDevice DeviceSpecification object of the source device
* @property {PacketFieldSpecification[]} packetFields Array of PacketFieldSpecification objects
*/
/**
* @typedef packetFieldGetRawValue
* @type {function}
* @param {Buffer} buffer Buffer object
* @param {number} start Start index in the buffer
* @param {number} end End index in the buffer
*/
/**
* @typedef PacketFieldSpecification
* @type {object}
* @property {string} fieldId Field identifier
* @property {object} name Object containing names by language code
* @property {TypeSpecification} type TypeSpecification object
* @property {packetFieldGetRawValue} getRawValue Function to get raw value from a buffer
*/
/**
* @typedef PacketField
* @type {object}
* @property {string} id Packet field identifier
* @property {Packet} packet Packet
* @property {PacketSpecification} packetSpec
* @property {PacketFieldSpecification} packetFieldSpec
* @property {PacketFieldSpecification} origPacketFieldSpec
* @property {string} name
* @property {number} rawValue Raw value
* @property {function} formatTextValue Function to format this packet field's raw value into textual form
*/
/**
* @typedef FilteredPacketFieldSpecification
* @type {object}
* @property {string} filteredPacketFieldId
* @property {string} packetId
* @property {string} fieldId
* @property {string} name
* @property {string} type
* @property {string} getRawValue
*/
/**
* @typedef BlockTypeSection
* @type {object}
* @property {string} sectionId Section identifier
* @property {string} surrogatePacketId Surrogate packet identifier
* @property {Packet} packet Packet object
* @property {PacketSpecification} packetSpec PacketSpecification object
* @property {number} startOffset Offset of section start within Packet frame data
* @property {number} endOffset Offset of section end within Packet frame data
* @property {number} type Section type
* @property {number} payloadCount Count of payload elements
* @property {number} frameCount Count of frames
* @property {Buffer} frameData Frame data
*/
class Specification {
/**
* Creates a new Specification instance and optionally initializes its members with the given values.
*
* @constructs
* @param {object} options Initialization values for this instance's members
* @param {string} options.language {@link Specification#language}
* @param {string} options.specificationData {@link Specification#specificationData}
*/
constructor(options) {
applyDefaultOptions(this, options, /** @lends Specification.prototype */ {
/**
* Language code (ISO 639-1)
* @type {string}
*/
language: 'en',
});
this.i18n = new I18N(this.language);
this.deviceSpecCache = {};
this.packetSpecCache = {};
this.blockTypePacketSpecCache = {};
const loadSpecificationDataOptions = {};
let rawSpecificationData;
if (!options) {
// nop
} else if (options.specificationData) {
rawSpecificationData = options.specificationData;
} else if (options.specificationFile) {
loadSpecificationDataOptions.specificationData = options.specificationFile.getSpecificationData();
}
this.specificationData = Specification.loadSpecificationData(rawSpecificationData, loadSpecificationDataOptions);
}
/**
* Gets the UnitSpecification object matching the given identifier.
*
* @param {string} id Unit identifier
* @returns {UnitSpecification} Unit object
*
* @example
* > console.log(spec.getUnitById('DegreesCelsius'));
* { unitId: 'DegreesCelsius',
* unitCode: 'DegreesCelsius',
* unitText: ' °C' }
* undefined
* >
*/
getUnitById(id) {
return this.specificationData.units [id];
}
/**
* Gets the TypeSpecification object matching the given identifier.
*
* @param {string} id Type identifier
* @returns {TypeSpecification} Type object
*
* @example
* > console.log(spec.getTypeById('Number_0_1_DegreesCelsius'));
* { typeId: 'Number_0_1_DegreesCelsius',
* rootTypeId: 'Number',
* precision: 1,
* unit:
* { unitId: 'DegreesCelsius',
* unitCode: 'DegreesCelsius',
* unitText: ' °C' } }
* undefined
* >
*/
getTypeById(id) {
return this.specificationData.types [id];
}
/**
* Gets the DeviceSpecification object matching the given arguments.
*
* @memberof Specification#
* @name getDeviceSpecification
* @method
*
* @param {number} selfAddress VBus address of the device itself
* @param {number} peerAddress VBus address of the device's peer
* @param {number} [channel=0] VBus channel of the device
* @returns {DeviceSpecification} DeviceSpecification object
*
* @example
* > console.log(spec.getDeviceSpecification(0x7E11, 0x0000, 1));
* { name: 'DeltaSol MX [Regler]',
* deviceId: '01_7E11_0000',
* channel: 1,
* selfAddress: 32273,
* peerAddress: 0,
* fullName: 'VBus #1: DeltaSol MX [Regler]' }
* undefined
* >
*/
/**
* Gets the DeviceSpecification object matching the given header and direction.
*
* @param {Header} header Header instance
* @param {string} which Either `'source'` or `'destination'`
* @returns {DeviceSpecification} DeviceSpecification object
*/
getDeviceSpecification(selfAddress, peerAddress, channel) {
if (typeof selfAddress === 'object') {
if (peerAddress === 'source') {
({ channel } = selfAddress);
peerAddress = selfAddress.destinationAddress;
selfAddress = selfAddress.sourceAddress;
} else if (peerAddress === 'destination') {
({ channel } = selfAddress);
peerAddress = selfAddress.sourceAddress;
selfAddress = selfAddress.destinationAddress;
} else {
throw new Error('Invalid arguments');
}
} else if (typeof selfAddress === 'string') {
const md = selfAddress.match(/^(?:([0-9a-f]{2})_)?([0-9a-f]{4})(?:_([0-9a-f]{4})(?:_.*)?)?$/i);
if (!md) {
throw new Error('Invalid device ID');
}
selfAddress = parseInt(md [2], 16);
peerAddress = parseInt(md [3], 16);
channel = parseInt(md [1], 16);
}
if (channel === undefined) {
channel = 0;
}
const deviceId = sprintf('%02X_%04X_%04X', channel, selfAddress, peerAddress);
if (!hasOwnProperty(this.deviceSpecCache, deviceId)) {
let origDeviceSpec;
if (!origDeviceSpec && this.specificationData.getDeviceSpecification) {
origDeviceSpec = this.specificationData.getDeviceSpecification(selfAddress, peerAddress);
}
if (!origDeviceSpec && this.specificationData.deviceSpecs) {
origDeviceSpec = this.specificationData.deviceSpecs ['_' + deviceId];
}
const deviceSpec = {
...origDeviceSpec,
deviceId,
channel,
selfAddress,
peerAddress,
};
if (!hasOwnProperty(deviceSpec, 'name')) {
deviceSpec.name = this.i18n.t('specification.unknownDevice', selfAddress);
}
if (!hasOwnProperty(deviceSpec, 'fullName')) {
let fullNameFormatter;
if (channel) {
fullNameFormatter = 'specification.fullNameWithChannel';
} else {
fullNameFormatter = 'specification.fullNameWithoutChannel';
}
deviceSpec.fullName = this.i18n.t(fullNameFormatter, channel, deviceSpec.name);
}
this.deviceSpecCache [deviceId] = Object.freeze(deviceSpec);
}
return this.deviceSpecCache [deviceId];
}
/**
* Gets the PacketSpecification object matching the given arguments.
*
* @memberof Specification#
* @name getPacketSpecification
* @method
*
* @param {number} channel VBus channel
* @param {number} destinationAddress VBus address of destination device
* @param {number} sourceAddress VBus address of source device
* @param {number} command VBus command
* @returns {PacketSpecification} PacketSpecification object
*
* @example
* > console.log(spec.getPacketSpecification(1, 0x0010, 0x7E21, 0x0100));
* { packetId: '01_0010_7E21_10_0100',
* packetFields:
* [ { fieldId: '000_2_0',
* name: [Object],
* type: [Object],
* getRawValue: [Function] },
* { fieldId: '002_1_0',
* name: [Object],
* type: [Object],
* getRawValue: [Function] } ],
* channel: 1,
* destinationAddress: 16,
* sourceAddress: 32289,
* protocolVersion: 16,
* command: 256,
* info: 0,
* destinationDevice:
* { name: 'DFA',
* deviceId: '01_0010_7E21',
* channel: 1,
* selfAddress: 16,
* peerAddress: 32289,
* fullName: 'VBus #1: DFA' },
* sourceDevice:
* { name: 'DeltaSol MX [Heizkreis #1]',
* deviceId: '01_7E21_0010',
* channel: 1,
* selfAddress: 32289,
* peerAddress: 16,
* fullName: 'VBus #1: DeltaSol MX [Heizkreis #1]' },
* fullName: 'VBus #1: DeltaSol MX [Heizkreis #1]' }
* undefined
* >
*/
/**
* Gets the PacketSpecification object matching the given arguments.
*
* @memberof Specification#
* @name getPacketSpecification
* @method
*
* @param {string} packetSpecId PacketSpecification identifier
* @returns {PacketSpecification} PacketSpecification object
*
* @example
* > console.log(spec.getPacketSpecification('01_0010_7E21_10_0100'));
* { packetId: '01_0010_7E21_10_0100',
* packetFields:
* [ { fieldId: '000_2_0',
* name: [Object],
* type: [Object],
* getRawValue: [Function] },
* { fieldId: '002_1_0',
* name: [Object],
* type: [Object],
* getRawValue: [Function] } ],
* channel: 1,
* destinationAddress: 16,
* sourceAddress: 32289,
* protocolVersion: 16,
* command: 256,
* info: 0,
* destinationDevice:
* { name: 'DFA',
* deviceId: '01_0010_7E21',
* channel: 1,
* selfAddress: 16,
* peerAddress: 32289,
* fullName: 'VBus #1: DFA' },
* sourceDevice:
* { name: 'DeltaSol MX [Heizkreis #1]',
* deviceId: '01_7E21_0010',
* channel: 1,
* selfAddress: 32289,
* peerAddress: 16,
* fullName: 'VBus #1: DeltaSol MX [Heizkreis #1]' },
* fullName: 'VBus #1: DeltaSol MX [Heizkreis #1]' }
* undefined
* >
*/
/**
* Gets the PacketSpecification object matching the given packet.
*
* @param {Packet} packet VBus packet
* @returns {PacketSpecification} PacketSpecification object
*/
getPacketSpecification(headerOrChannel, destinationAddress, sourceAddress, command) {
if (typeof headerOrChannel === 'object') {
({ command, sourceAddress, destinationAddress } = headerOrChannel);
headerOrChannel = headerOrChannel.channel;
} else if (typeof headerOrChannel === 'string') {
const md = headerOrChannel.match(/^([0-9a-f]{2})_([0-9a-f]{4})_([0-9a-f]{4})(?:_10)?_([0-9a-f]{4})/i);
if (!md) {
throw new Error('Invalid packet ID');
}
command = parseInt(md [4], 16);
sourceAddress = parseInt(md [3], 16);
destinationAddress = parseInt(md [2], 16);
headerOrChannel = parseInt(md [1], 16);
}
const packetId = sprintf('%02X_%04X_%04X_10_%04X', headerOrChannel, destinationAddress, sourceAddress, command);
if (!hasOwnProperty(this.packetSpecCache, packetId)) {
let origPacketSpec;
if (!origPacketSpec && this.specificationData.getPacketSpecification) {
origPacketSpec = this.specificationData.getPacketSpecification(destinationAddress, sourceAddress, command);
}
if (!origPacketSpec && this.specificationData.packetSpecs) {
origPacketSpec = this.specificationData.packetSpecs ['_' + packetId];
}
const destinationDeviceSpec = this.getDeviceSpecification(destinationAddress, sourceAddress, headerOrChannel);
const sourceDeviceSpec = this.getDeviceSpecification(sourceAddress, destinationAddress, headerOrChannel);
let { fullName } = sourceDeviceSpec;
if (destinationAddress !== 0x0010) {
fullName += ' => ' + destinationDeviceSpec.name;
}
const packetSpec = {
...origPacketSpec,
packetId,
channel: headerOrChannel,
destinationAddress,
sourceAddress,
protocolVersion: 0x10,
command,
info: 0,
destinationDevice: destinationDeviceSpec,
sourceDevice: sourceDeviceSpec,
fullName,
};
if (!hasOwnProperty(packetSpec, 'packetFields')) {
packetSpec.packetFields = [];
}
this.packetSpecCache [packetId] = Object.freeze(packetSpec);
}
return this.packetSpecCache [packetId];
}
/**
* Gets the PacketFieldSpecification object matching the given arguments.
*
* @memberof Specification#
* @name getPacketFieldSpecification
* @method
*
* @param {PacketSpecification} packetSpec PacketSpecification object
* @param {string} fieldId Field identifier
* @returns {PacketFieldSpecification} PacketFieldSpecification object
*
* @example
* > var packetSpec = spec.getPacketSpecification('01_0010_7E21_10_0100');
* undefined
* > console.log(spec.getPacketFieldSpecification(packetSpec, '000_2_0'));
* { fieldId: '000_2_0',
* name:
* { ref: 'Flow set temperature',
* en: 'Flow set temperature',
* de: 'Vorlauf-Soll-Temperatur',
* fr: 'Température nominale départ' },
* type:
* { typeId: 'Number_0_1_DegreesCelsius',
* rootTypeId: 'Number',
* precision: 1,
* unit:
* { unitId: 'DegreesCelsius',
* unitCode: 'DegreesCelsius',
* unitText: ' °C' } },
* getRawValue: [Function] }
* undefined
* >
*/
/**
* Gets the PacketFieldSpecification object matching the given arguments.
*
* @param {string} packetFieldId Packet field identifier
* @returns {PacketFieldSpecification} PacketFieldSpecification object
*
* @example
* > console.log(spec.getPacketFieldSpecification('01_0010_7E21_10_0100_000_2_0'));
* { fieldId: '000_2_0',
* name:
* { ref: 'Flow set temperature',
* en: 'Flow set temperature',
* de: 'Vorlauf-Soll-Temperatur',
* fr: 'Température nominale départ' },
* type:
* { typeId: 'Number_0_1_DegreesCelsius',
* rootTypeId: 'Number',
* precision: 1,
* unit:
* { unitId: 'DegreesCelsius',
* unitCode: 'DegreesCelsius',
* unitText: ' °C' } },
* getRawValue: [Function] }
* undefined
* >
*/
getPacketFieldSpecification(packetSpecOrId, fieldId) {
let packetFieldSpec;
if (typeof packetSpecOrId === 'string') {
if (this.specificationData.filteredPacketFieldSpecs) {
packetFieldSpec = this.specificationData.filteredPacketFieldSpecs.find(pfs => pfs.filteredPacketFieldId === packetSpecOrId);
}
if (!packetFieldSpec) {
const md = packetSpecOrId.match(/^([0-9a-f]{2}_[0-9a-f]{4}_[0-9a-f]{4}(?:_10)?_[0-9a-f]{4})_(.*)$/i);
if (!md) {
throw new Error('Invalid packet field ID');
}
fieldId = md [2];
packetSpecOrId = this.getPacketSpecification(md [1]);
}
}
if (!packetFieldSpec && packetSpecOrId) {
packetFieldSpec = packetSpecOrId.packetFields.find(pfs => pfs.fieldId === fieldId);
}
return packetFieldSpec;
}
/**
* Gets the raw value of a packet field from a buffer.
*
* @param {PacketFieldSpecification} packetField PacketFieldSpecification object
* @param {Buffer} buffer Buffer object
* @param {number} [start=0] Start index in the buffer
* @param {number} [end=buffer.length] End index in the buffer
* @returns {number} Raw value
*
* @example
* > var packetFieldSpec = spec.getPacketFieldSpecification('01_0010_7721_10_0100_000_2_0');
* undefined
* > var buffer = Buffer.from('b822', 'hex');
* undefined
* > console.log(spec.getRawValue(packetFieldSpec, buffer));
* 888.8000000000001
* undefined
* >
*/
getRawValue(packetField, buffer, start, end) {
if (start === undefined) {
start = 0;
}
if (end === undefined) {
end = buffer ? buffer.length : 0;
}
let rawValue;
if (packetField && packetField.getRawValue) {
rawValue = packetField.getRawValue(buffer, start, end);
} else if (packetField && packetField.packetFieldSpec) {
rawValue = this.getRawValue(packetField.packetFieldSpec, buffer, start, end);
if (isNumber(rawValue)) {
if (packetField.conversions) {
({ rawValue } = this.convertRawValue(rawValue, packetField.conversions));
} else {
({ rawValue } = this.convertRawValue(rawValue, packetField.packetFieldSpec.type.unit, packetField.type.unit));
}
}
} else {
rawValue = null;
}
return rawValue;
}
getRoundedRawValue(packetField, buffer, start, end) {
const rawValue = this.getRawValue(packetField, buffer, start, end);
const precision = (packetField && packetField.type && packetField.type.precision) || 0;
const roundedRawValue = roundNumber(rawValue, -precision);
return roundedRawValue;
}
invertConversions(conversions) {
if (!Array.isArray(conversions)) {
return conversions;
}
return conversions.reverse().map((conversion) => {
const invertedConversion = {};
if (isNumber(conversion.offset)) {
invertedConversion.offset = conversion.offset * -1;
}
if (isNumber(conversion.factor)) {
invertedConversion.factor = 1 / conversion.factor;
}
if (isNumber(conversion.power)) {
if (conversion.power !== 0) {
invertedConversion.power = 1 / conversion.power;
} else {
invertedConversion.power = conversion.power;
}
}
if (conversion.sourceUnit) {
invertedConversion.targetUnit = conversion.sourceUnit;
}
if (conversion.targetUnit) {
invertedConversion.sourceUnit = conversion.targetUnit;
}
return invertedConversion;
});
}
setRawValue(packetField, rawValue, buffer, start, end) {
if (start === undefined) {
start = 0;
}
if (end === undefined) {
end = buffer ? buffer.length : 0;
}
if (packetField && packetField.setRawValue) {
packetField.setRawValue(rawValue, buffer, start, end);
} else if (packetField && packetField.packetFieldSpec) {
if (isNumber(rawValue)) {
if (packetField.conversions) {
({ rawValue } = this.convertRawValue(rawValue, this.invertConversions(packetField.conversions)));
} else {
({ rawValue } = this.convertRawValue(rawValue, packetField.type.unit, packetField.packetFieldSpec.type.unit));
}
}
this.setRawValue(packetField.packetFieldSpec, rawValue, buffer, start, end);
}
}
/**
* Converts a raw number value from one unit to another. The units must be in the same unit family.
*
* @param {number} rawValue Raw number value to convert from
* @param {Unit} sourceUnit Unit to convert from
* @param {Unit} targetUnit Unit to convert to
* @return {object} Result containing a `rawValue` property with the conversion result and a `unit` property with the associated unit.
*/
convertRawValue(rawValue_, sourceUnit_, targetUnit_) {
const that = this;
let conversions;
if (Array.isArray(sourceUnit_)) {
conversions = sourceUnit_;
} else {
conversions = [{
power: null,
factor: null,
offset: null,
sourceUnit: sourceUnit_,
targetUnit: targetUnit_,
}];
}
const result = conversions.reduce((valueInfo, conversion) => {
let { rawValue } = valueInfo;
const { sourceUnit, targetUnit } = conversion;
const unitFamily = sourceUnit && sourceUnit.unitFamily;
const hasPower = isNumber(conversion.power);
const hasFactor = isNumber(conversion.factor);
const hasOffset = isNumber(conversion.offset);
const autoConvert = !hasFactor && !hasOffset && !hasPower;
if (hasPower) {
if (rawValue === 0 && conversion.power < 0) {
rawValue = 0; // Infinity
} else {
rawValue = Math.pow(rawValue, conversion.power);
}
}
if (hasFactor) {
rawValue = rawValue * conversion.factor;
}
if (hasOffset) {
rawValue = rawValue + conversion.offset;
}
if (autoConvert && !sourceUnit) {
throw new Error('Must provide a source unit');
} else if (!targetUnit) {
// nop, no conversion requested
} else if (sourceUnit.unitCode === targetUnit.unitCode) {
// nop, no conversion for same unit
} else if (targetUnit.unitCode === 'None') {
// nop, just ignore the unit suffix
} else if (!autoConvert) {
// nop, already multiplied by factor above and allows to change unit family
} else if (unitFamily !== targetUnit.unitFamily) {
throw new Error('Unit families of source and target unit must match');
} else if (!unitFamily) {
// nop, no conversion for unknown unit family
} else if (unitFamily === 'Temperature') {
rawValue = that._convertTemperatureRawValue(rawValue, sourceUnit.unitCode, targetUnit.unitCode);
} else if (unitFamily === 'Volume') {
rawValue = that._convertVolumeRawValue(rawValue, sourceUnit.unitCode, targetUnit.unitCode);
} else if (unitFamily === 'VolumeFlow') {
rawValue = that._convertVolumeFlowRawValue(rawValue, sourceUnit.unitCode, targetUnit.unitCode);
} else if (unitFamily === 'Pressure') {
rawValue = that._convertPressureRawValue(rawValue, sourceUnit.unitCode, targetUnit.unitCode);
} else if (unitFamily === 'Energy') {
rawValue = that._convertEnergyRawValue(rawValue, sourceUnit.unitCode, targetUnit.unitCode);
} else if (unitFamily === 'Power') {
rawValue = that._convertPowerRawValue(rawValue, sourceUnit.unitCode, targetUnit.unitCode);
} else if (unitFamily === 'Time') {
rawValue = that._convertTimeRawValue(rawValue, sourceUnit.unitCode, targetUnit.unitCode);
} else {
throw new Error('Unsupported unit family ' + JSON.stringify(sourceUnit.unitFamily));
}
return {
rawValue,
unit: targetUnit || sourceUnit,
};
}, {
rawValue: rawValue_,
unit: sourceUnit_,
});
return result;
}
_convertTemperatureRawValue(rawValue, sourceUnitCode, targetUnitCode) {
switch (sourceUnitCode) {
case 'DegreesCelsius':
// nop
break;
case 'DegreesFahrenheit':
rawValue = (rawValue - 32) / 1.8;
break;
default:
throw new Error('Unsupported source unit ' + JSON.stringify(sourceUnitCode));
}
switch (targetUnitCode) {
case 'DegreesCelsius':
// nop
break;
case 'DegreesFahrenheit':
rawValue = (rawValue * 1.8) + 32;
break;
default:
throw new Error('Unsupported target unit ' + JSON.stringify(targetUnitCode));
}
return rawValue;
}
_convertVolumeRawValue(rawValue, sourceUnitCode, targetUnitCode) {
switch (sourceUnitCode) {
case 'Liters':
// nop
break;
case 'CubicMeters':
rawValue = rawValue * 1000;
break;
case 'Gallons':
rawValue = rawValue / conversionFactors.GallonsPerLiter;
break;
default:
throw new Error('Unsupported source unit ' + JSON.stringify(sourceUnitCode));
}
switch (targetUnitCode) {
case 'Liters':
// nop
break;
case 'CubicMeters':
rawValue = rawValue / 1000;
break;
case 'Gallons':
rawValue = rawValue * conversionFactors.GallonsPerLiter;
break;
default:
throw new Error('Unsupported target unit ' + JSON.stringify(targetUnitCode));
}
return rawValue;
}
_convertVolumeFlowRawValue(rawValue, sourceUnitCode, targetUnitCode) {
switch (sourceUnitCode) {
case 'LitersPerHour':
// nop
break;
case 'LitersPerMinute':
rawValue = rawValue * 60;
break;
case 'CubicMetersPerHour':
rawValue = rawValue * 1000;
break;
case 'GallonsPerHour':
rawValue = rawValue / conversionFactors.GallonsPerLiter;
break;
case 'GallonsPerMinute':
rawValue = rawValue * 60 / conversionFactors.GallonsPerLiter;
break;
default:
throw new Error('Unsupported source unit ' + JSON.stringify(sourceUnitCode));
}
switch (targetUnitCode) {
case 'LitersPerHour':
// nop
break;
case 'LitersPerMinute':
rawValue = rawValue / 60;
break;
case 'CubicMetersPerHour':
rawValue = rawValue / 1000;
break;
case 'GallonsPerHour':
rawValue = rawValue * conversionFactors.GallonsPerLiter;
break;
case 'GallonsPerMinute':
rawValue = rawValue / 60 * conversionFactors.GallonsPerLiter;
break;
default:
throw new Error('Unsupported target unit ' + JSON.stringify(targetUnitCode));
}
return rawValue;
}
_convertPressureRawValue(rawValue, sourceUnitCode, targetUnitCode) {
switch (sourceUnitCode) {
case 'Bars':
// nop
break;
case 'PoundsForcePerSquareInch':
rawValue = rawValue / conversionFactors.PoundsForcePerSquareInchPerBar;
break;
default:
throw new Error('Unsupported source unit ' + JSON.stringify(sourceUnitCode));
}
switch (targetUnitCode) {
case 'Bars':
// nop
break;
case 'PoundsForcePerSquareInch':
rawValue = rawValue * conversionFactors.PoundsForcePerSquareInchPerBar;
break;
default:
throw new Error('Unsupported target unit ' + JSON.stringify(targetUnitCode));
}
return rawValue;
}
_convertEnergyRawValue(rawValue, sourceUnitCode, targetUnitCode) {
switch (sourceUnitCode) {
case 'WattHours':
// nop
break;
case 'KilowattHours':
rawValue = rawValue * 1000;
break;
case 'MegawattHours':
rawValue = rawValue * 1000000;
break;
case 'Btus':
rawValue = rawValue / conversionFactors.BtusPerWattHour;
break;
case 'KiloBtus':
rawValue = rawValue * 1000 / conversionFactors.BtusPerWattHour;
break;
case 'MegaBtus':
rawValue = rawValue * 1000000 / conversionFactors.BtusPerWattHour;
break;
case 'GramsCO2Gas':
rawValue = rawValue / conversionFactors.GramsCO2GasPerWattHour;
break;
case 'KilogramsCO2Gas':
rawValue = rawValue * 1000 / conversionFactors.GramsCO2GasPerWattHour;
break;
case 'TonsCO2Gas':
rawValue = rawValue * 1000000 / conversionFactors.GramsCO2GasPerWattHour;
break;
case 'GramsCO2Oil':
rawValue = rawValue / conversionFactors.GramsCO2OilPerWattHour;
break;
case 'KilogramsCO2Oil':
rawValue = rawValue * 1000 / conversionFactors.GramsCO2OilPerWattHour;
break;
case 'TonsCO2Oil':
rawValue = rawValue * 1000000 / conversionFactors.GramsCO2OilPerWattHour;
break;
default:
throw new Error('Unsupported source unit ' + JSON.stringify(sourceUnitCode));
}
switch (targetUnitCode) {
case 'WattHours':
// nop
break;
case 'KilowattHours':
rawValue = rawValue / 1000;
break;
case 'MegawattHours':
rawValue = rawValue / 1000000;
break;
case 'Btus':
rawValue = rawValue * conversionFactors.BtusPerWattHour;
break;
case 'KiloBtus':
rawValue = rawValue / 1000 * conversionFactors.BtusPerWattHour;
break;
case 'MegaBtus':
rawValue = rawValue / 1000000 * conversionFactors.BtusPerWattHour;
break;
case 'GramsCO2Gas':
rawValue = rawValue * conversionFactors.GramsCO2GasPerWattHour;
break;
case 'KilogramsCO2Gas':
rawValue = rawValue / 1000 * conversionFactors.GramsCO2GasPerWattHour;
break;
case 'TonsCO2Gas':
rawValue = rawValue / 1000000 * conversionFactors.GramsCO2GasPerWattHour;
break;
case 'GramsCO2Oil':
rawValue = rawValue * conversionFactors.GramsCO2OilPerWattHour;
break;
case 'KilogramsCO2Oil':
rawValue = rawValue / 1000 * conversionFactors.GramsCO2OilPerWattHour;
break;
case 'TonsCO2Oil':
rawValue = rawValue / 1000000 * conversionFactors.GramsCO2OilPerWattHour;
break;
default:
throw new Error('Unsupported target unit ' + JSON.stringify(targetUnitCode));
}
return rawValue;
}
_convertPowerRawValue(rawValue, sourceUnitCode, targetUnitCode) {
switch (sourceUnitCode) {
case 'Watts':
// nop
break;
case 'Kilowatts':
rawValue = rawValue * 1000;
break;
default:
throw new Error('Unsupported source unit ' + JSON.stringify(sourceUnitCode));
}
switch (targetUnitCode) {
case 'Watts':
// nop
break;
case 'Kilowatts':
rawValue = rawValue / 1000;
break;
default:
throw new Error('Unsupported target unit ' + JSON.stringify(targetUnitCode));
}
return rawValue;
}
_convertTimeRawValue(rawValue, sourceUnitCode, targetUnitCode) {
switch (sourceUnitCode) {
case 'Seconds':
// nop
break;
case 'Minutes':
rawValue = rawValue * 60;
break;
case 'Hours':
rawValue = rawValue * 3600;
break;
case 'Days':
rawValue = rawValue * 86400;
break;
default:
throw new Error('Unsupported source unit ' + JSON.stringify(sourceUnitCode));
}
switch (targetUnitCode) {
case 'Seconds':
// nop
break;
case 'Minutes':
rawValue = rawValue / 60;
break;
case 'Hours':
rawValue = rawValue / 3600;
break;
case 'Days':
rawValue = rawValue / 86400;
break;
default:
throw new Error('Unsupported target unit ' + JSON.stringify(targetUnitCode));
}
return rawValue;
}
/**
* Formats a raw value into its textual representation.
*
* @param {PacketFieldSpecification} packetField PacketFieldSpecification object
* @param {number} rawValue Raw value
* @param {string|UnitSpecification|null} [unit] Unit to format to
* @returns {string} Textual representation of the raw value
*
* @example
* > var packetFieldSpec = spec.getPacketFieldSpecification('01_0010_7721_10_0100_000_2_0');
* undefined
* > var rawValue = 888.8000000000001;
* undefined
* > console.log(spec.formatTextValueFromRawValue(packetFieldSpec, rawValue, 'DegreesCelsius'));
* 888.8 °C
* undefined
* >
*/
formatTextValueFromRawValue(packetField, rawValue, unit) {
let textValue;
if ((rawValue !== undefined) && (rawValue !== null)) {
if (typeof unit === 'string') {
if (hasOwnProperty(this.specificationData.units, unit)) {
unit = this.specificationData.units [unit];
} else {
throw new Error('Unknown unit named "' + unit + '"');
}
}
if (packetField && packetField.type) {
const { type } = packetField;
if (type.formatTextValue) {
textValue = type.formatTextValue(rawValue, unit);
} else {
textValue = this.formatTextValueFromRawValueInternal(rawValue, unit, type.rootTypeId, type.precision, type.unit);
}
} else {
textValue = rawValue.toString();
if (unit && unit.unitText) {
textValue += unit.unitText;
}
}
} else {
textValue = '';
}
return textValue;
}
formatTextValueFromRawValueInternal(rawValue, unit, rootType, precision, defaultUnit) {
const unitText = unit ? unit.unitText : defaultUnit ? defaultUnit.unitText : '';
let result, textValue, format;
if ((rawValue === undefined) || (rawValue === null)) {
result = '';
} else if (rootType === 'Time') {
textValue = this.i18n.moment(rawValue * 60000).utc().format('HH:mm');
result = textValue + unitText;
} else if (rootType === 'Weektime') {
textValue = this.i18n.moment((rawValue + 5760) * 60000).utc().format('dd,HH:mm');
result = textValue + unitText;
} else if (rootType === 'DateTime') {
textValue = this.i18n.moment((rawValue + 978307200) * 1000).utc().format('L HH:mm:ss');
result = textValue + unitText;
} else if (precision === 0) {
textValue = this.i18n.numeral(rawValue).format('0');
result = textValue + unitText;
} else if (precision === 1) {
textValue = this.i18n.numeral(rawValue).format('0.0');
result = textValue + unitText;
} else if (precision === 2) {
textValue = this.i18n.numeral(rawValue).format('0.00');
result = textValue + unitText;
} else if (precision === 3) {
textValue = this.i18n.numeral(rawValue).format('0.000');
result = textValue + unitText;
} else if (precision === 4) {
textValue = this.i18n.numeral(rawValue).format('0.0000');
result = textValue + unitText;
} else {
if (!numberFormatCache.has(precision)) {
format = '0.';
for (let i = 0; i < precision; i++) {
format = format + '0';
}
numberFormatCache.set(precision, format);
}
textValue = this.i18n.numeral(rawValue).format(numberFormatCache.get(precision));
result = textValue + unitText;
}
return result;
}
/**
* Gets an array of PacketField objects for the provided Packet objects.
*
* @param {Header[]} headers Array of Header objects
* @returns {PacketField[]} Array of PacketField objects
*/
getPacketFieldsForHeaders(headers) {
const _this = this;
// filter out all packets
const packets = headers.reduce((memo, header) => {
if ((header.getProtocolVersion() & 0xF0) === 0x10) {
memo.push(header);
}
return memo;
}, []);
const packetFields = [];
const { filteredPacketFieldSpecs } = this.specificationData;
if (filteredPacketFieldSpecs) {
const packetById = packets.reduce((memo, packet) => {
const packetSpec = _this.getPacketSpecification(packet);
memo [packetSpec.packetId] = packet;
return memo;
}, {});
for (const fpfs of filteredPacketFieldSpecs) {
const packetField = {
id: fpfs.filteredPacketFieldId,
packet: packetById [fpfs.packetId],
packetSpec: fpfs.packetSpec,
packetFieldSpec: fpfs,
origPacketFieldSpec: fpfs.packetFieldSpec,
};
packetFields.push(packetField);
}
} else {
for (const packet of packets) {
const packetSpec = _this.getPacketSpecification(packet);
if (packetSpec) {
for (const packetFieldSpec of packetSpec.packetFields) {
const packetField = {
id: packetSpec.packetId + '_' + packetFieldSpec.fieldId,
packet,
packetSpec,
packetFieldSpec,
origPacketFieldSpec: packetFieldSpec,
};
packetFields.push(packetField);
}
}
}
}
const { language } = this;
for (const packetField of packetFields) {
const pfsName = packetField.packetFieldSpec.name;
let name;
if (isString(pfsName)) {
name = pfsName;
} else if (isObject(pfsName)) {
name = pfsName [language] || pfsName.en || pfsName.de || pfsName.ref;
}
let rawValue;
if (packetField.packetFieldSpec && packetField.packet) {
const frameData = packetField.packet.frameData.slice(0, packetField.packet.frameCount * 4);
rawValue = _this.getRawValue(packetField.packetFieldSpec, frameData);
}
let precision;
if (packetField.packetFieldSpec && packetField.packetFieldSpec.type) {
precision = packetField.packetFieldSpec.type.precision || 0;
}
Object.assign(packetField, {
name,
rawValue,
formatTextValue(unit) {
return _this.formatTextValueFromRawValue(packetField.packetFieldSpec, rawValue, unit);
},
getRoundedRawValue() {
return roundNumber(rawValue, -precision);
},
});
}
return packetFields;
}
setPacketFieldRawValues(packetFields, rawValues) {
const _this = this;
const packetFieldById = packetFields.reduce((memo, packetField) => {
memo [packetField.id] = packetField;
const { fieldId } = packetField.packetFieldSpec;
if (memo [fieldId] === undefined) {
memo [fieldId] = packetField;
} else {
memo [fieldId] = null;
}
return memo;
}, {});
for (const key of Object.getOwnPropertyNames(rawValues)) {
const rawValue = rawValues [key];
const packetField = packetFieldById [key];
if (packetField === undefined) {
throw new Error('Unknown raw value ID ' + JSON.stringify(key));
} else if (packetField === null) {
throw new Error('Non-unique raw value ID ' + JSON.stringify(key));
} else {
const frameData = packetField.packet.frameData.slice(0, packetField.packet.frameCount * 4);
_this.setRawValue(packetField.packetFieldSpec, rawValue, frameData);
}
}
}
getFilteredPacketFieldSpecificationsForHeaders(headers) {
const filteredPacketFieldSpecs = [];
const packetFields = this.getPacketFieldsForHeaders(headers);
for (const packetField of packetFields) {
const { packetSpec, packetFieldSpec } = packetField;
if (packetSpec && packetFieldSpec) {
const filteredPacketFieldSpec = {
...packetFieldSpec,
filteredPacketFieldId: packetSpec.packetId + '_' + packetFieldSpec.fieldId,
packetId: packetSpec.packetId,
name: packetField.name,
};
filteredPacketFieldSpecs.push(filteredPacketFieldSpec);
}
}
return filteredPacketFieldSpecs;
}
/**
* Gets an array of BlockType sections from a collection of headers.
*
* @param {Header[]} headers Array of Header objects
* @return {BlockTypeSection[]} Array of BlockTypeSection objects
*/
getBlockTypeSectionsForHeaders(headers) {
const _this = this;
return headers.reduce((memo, header) => {
if (((header.getProtocolVersion() & 0xF0) === 0x10) && (header.destinationAddress === 0x0015) && (header.command === 0x0100)) {
const packetSpec = _this.getPacketSpecification(header);
const length = header.frameCount * 4, { frameData } = header;
let startOffset = 0;
while (startOffset + 4 <= length) {
const frameCount = frameData [startOffset] & 255;
const endOffset = startOffset + 4 + 4 * frameCount;
if (endOffset <= length) {
const type = frameData [startOffset + 1] & 255;
let payloadSize = null, payloadCount = null;
// TODO(daniel): refine the payload count based on the type
if (type === 1) {
payloadSize = 2;
} else if (type === 5) {
payloadSize = 4;
} else if (type === 8) {
payloadSize = 1;
} else if (type === 10) {
payloadSize = 8;
} else if (type === 11) {
payloadSize = 4;
} else if (type === 12) {
payloadSize = 4;
} else if (type === 13) {
payloadSize = 4;
} else if (type === 14) {
payloadSize = 1;
} else {
payloadSize = 1;
}
if (!payloadCount && payloadSize) {
payloadCount = Math.floor((endOffset - startOffset - 4) / payloadSize);
}
const sectionId = sprintf('%s_%02X_%02X_%d', packetSpec.packetId, frameCount, type, payloadCount);
const shasum = crypto.createHash('sha1');
shasum.update(Buffer.from(sectionId, 'utf8'));
const surrogatePacketIdHash = shasum.digest('hex').toUpperCase();
const surrogatePacketIdHashPart1 = surrogatePacketIdHash.slice(0, 4);
const surrogatePacketIdHashPart2 = surrogatePacketIdHash.slice(4, 8);
const surrogatePacketId = sprintf('%02X_%04X_%s_%02X_%s', header.channel, header.destinationAddress | 0x8000, surrogatePacketIdHashPart1, 0x10, surrogatePacketIdHashPart2);
memo.push({
sectionId,
surrogatePacketId,
packet: header,
packetSpec,
startOffset,
endOffset,
type,
payloadCount,
frameCount,
frameData: frameData.slice(startOffset, endOffset),
});
}
startOffset = endOffset;
}
if (startOffset !== length) {
throw new Error('Malformed block type packet, ending prematurely at offset ' + startOffset);
}
}
return memo;
}, []);
}
_createUInt8BlockTypeFieldSpecification(fieldIdPrefix, offset, name, typeId, factor) {
return {
fieldId: sprintf('%s_%03d_1_0', fieldIdPrefix, offset),
name,
type: this.getTypeById(typeId),
factor,
parts: [{
offset,
mask: 255,
isSigned: false,
factor: 1,
}],
getRawValue(buffer, start, end) {
let rawValue = 0, valid = false;
if (start + offset < end) {
rawValue += buffer.readUInt8(start + offset);
valid = true;
}
if (valid) {
rawValue = rawValue * factor;
} else {
rawValue = null;
}
return rawValue;
},
setRawValue(newValue, buffer, start, end) {
newValue = Math.round(newValue / factor);
let rawValue;
if (start + offset < end) {
rawValue = newValue & 255;
buffer.writeUInt8(rawValue, start + offset);
}
},
};
}
_createInt16BlockTypeFieldSpecification(fieldIdPrefix, offset, name, typeId, factor) {
return {
fieldId: sprintf('%s_%03d_2_0', fieldIdPrefix, offset),
name,
type: this.getTypeById(typeId),
factor,
parts: [{
offset,
mask: 255,
isSigned: false,
factor: 1,
}, {
offset: offset + 1,
mask: 255,
isSigned: true,
factor: 256,
}],
getRawValue(buffer, start, end) {
let rawValue = 0, valid = false;
if (start + offset < end) {
rawValue += buffer.readUInt8(start + offset);
valid = true;
}
if (start + offset + 1 < end) {
rawValue += buffer.readInt8(start + offset + 1) * 256;
valid = true;
}
if (valid) {
rawValue = rawValue * factor;
} else {
rawValue = null;
}
return rawValue;
},
setRawValue(newValue, buffer, start, end) {
newValue = Math.round(newValue / factor);
let rawValue;
if (start + offset < end) {
rawValue = newValue & 255;
buffer.writeUInt8(rawValue, start + offset);
}
if (start + offset + 1 < end) {
rawValue = (newValue / 256) & 255;
buffer.writeUInt8(rawValue, start + offset + 1);
}
},
};
}
_createUInt32BlockTypeFieldSpecification(fieldIdPrefix, offset, name, typeId, factor) {
return {
fieldId: sprintf('%s_%03d_4_0', fieldIdPrefix, offset),
name,
type: this.getTypeById(typeId),
factor,
parts: [{
offset,
mask: 255,
isSigned: false,
factor: 1,
}, {
offset: offset + 1,
mask: 255,
isSigned: false,
factor: 256,
}, {
offset: offset + 2,
mask: 255,
isSigned: false,
factor: 65536,
}, {
offset: offset + 3,
mask: 255,
isSigned: false,
factor: 16777216,
}],
getRawValue(buffer, start, end) {
let rawValue = 0, valid = false;
if (start + offset < end) {
rawValue += buffer.readUInt8(start + offset);
valid = true;
}
if (start + offset + 1 < end) {
rawValue += buffer.readUInt8(start + offset + 1) * 256;
valid = true;
}
if (start + offset + 2 < end) {
rawValue += buffer.readUInt8(start + offset + 2) * 65536;
valid = true;
}
if (start + offset + 3 < end) {
rawValue += buffer.readUInt8(start + offset + 3) * 16777216;
valid = true;
}
if (valid) {
rawValue = rawValue * factor;
} else {
rawValue = null;
}
return rawValue;
},
setRawValue(newValue, buffer, start, end) {
newValue = Math.round(newValue / factor);
let rawValue;
if (start + offset < end) {
rawValue = newValue & 255;
buffer.writeUInt8(rawValue, start + offset);
}
if (start + offset + 1 < end) {
rawValue = (newValue / 256) & 255;
buffer.writeUInt8(rawValue, start + offset + 1);
}
if (start + offset + 2 < end) {
rawValue = (newValue / 65536) & 255;
buffer.writeUInt8(rawValue, start + offset + 2);
}
if (start + offset + 3 < end) {
rawValue = (newValue / 16777216) & 255;
buffer.writeUInt8(rawValue, start + offset + 3);
}
},
};
}
/**
* Gets the PacketSpecification objects matching the given BlockTypeSection objects.
*
* @param {BlockTypeSection[]} sections Array of BlockTypeSection objects
* @return {PacketSpecification[]} Array of PacketSpecificationObjects
*/
getBlockTypePacketSpecificationsForSections(sections) {
const _this = this;
return sections.reduce((memo, section) => {
const { sectionId } = section;
if (!hasOwnProperty(_this.blockTypePacketSpecCache, sectionId)) {
const fieldIdPrefix = section.sectionId;
const forEachPayload = function(iterator) {
const count = section.payloadCount;
for (let i = 0; i < count; i++) {
const suffix = (count > 1) ? (' ' + (i + 1)) : '';
iterator(i, suffix);
}
};
const packetFieldSpecs = [];
if (section.type === 1) {
// temperatures
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createInt16BlockTypeFieldSpecification(fieldIdPrefix, 4 + index * 2, 'Temperatur Sensor' + suffix, 'Number_0_1_DegreesCelsius', 0.1));
});
} else if (section.type === 5) {
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createUInt32BlockTypeFieldSpecification(fieldIdPrefix, 4 + index * 4, 'Wärmemenge' + suffix, 'Number_1_WattHours', 1));
});
} else if (section.type === 8) {
// Relais speeds
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createUInt8BlockTypeFieldSpecification(fieldIdPrefix, 4 + index, 'Drehzahl Relais' + suffix, 'Number_1_Percent', 1));
});
} else if (section.type === 10) {
// SmartDisplay
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createInt16BlockTypeFieldSpecification(fieldIdPrefix, 4 + index * 8, 'Temperatur Kollektor' + suffix, 'Number_0_1_DegreesCelsius', 0.1));
packetFieldSpecs.push(_this._createInt16BlockTypeFieldSpecification(fieldIdPrefix, 6 + index * 8, 'Temperatur Speicher' + suffix, 'Number_0_1_DegreesCelsius', 0.1));
packetFieldSpecs.push(_this._createUInt32BlockTypeFieldSpecification(fieldIdPrefix, 8 + index * 8, 'Wärmemenge' + suffix, 'Number_1_WattHours', 1));
});
} else if (section.type === 11) {
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createUInt32BlockTypeFieldSpecification(fieldIdPrefix, 4 + index * 4, 'Fehlermaske' + suffix, 'Number_1_None', 1));
});
} else if (section.type === 12) {
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createUInt32BlockTypeFieldSpecification(fieldIdPrefix, 4 + index * 4, 'Warnungsmaske' + suffix, 'Number_1_None', 1));
});
} else if (section.type === 13) {
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createUInt32BlockTypeFieldSpecification(fieldIdPrefix, 4 + index * 4, 'Statusmaske' + suffix, 'Number_1_None', 1));
});
} else if (section.type === 14) {
forEachPayload((index, suffix) => {
packetFieldSpecs.push(_this._createUInt8BlockTypeFieldSpecification(fieldIdPrefix, 4 + index, 'Segmentmaske' + suffix, 'Number_1_None', 1));
});
}
_this.blockTypePacketSpecCache [sectionId] = {
...section.packetSpec,
packetId: section.surrogatePacketId,
sectionId,
packetFields: packetFieldSpecs,
};
}
const packetSpec = _this.blockTypePacketSpecCache [sectionId];
memo.push(packetSpec);
return memo;
}, []);
}
/**
* Gets an array of PacketField objects for the provided BlockTypeSection objects.
*
* @param {BlockTypeSection[]} sections Array of BlockTypeSection objects.
* @return {PacketField[]} Array of PacketField objects
*/
getBlockTypeFieldsForSections(sections) {
const _this = this;
const sectionByBlockTypeId = sections.reduce((memo, section) => {
memo [section.sectionId] = section;
return memo;
}, {});
const packetSpecs = this.getBlockTypePacketSpecificationsForSections(sections);
const packetFields = [];
for (const packetSpec of packetSpecs) {
for (const packetFieldSpec of packetSpec.packetFields) {
const section = sectionByBlockTypeId [packetSpec.sectionId];
const packetField = {
id: packetSpec.packetId + '_' + packetFieldSpec.fieldId,
section,
packet: section.packet,
packetSpec,
packetFieldSpec,
origPacketFieldSpec: packetFieldSpec,
};
packetFields.push(packetField);
}
}
const { language } = this;
for (const packetField of packetFields) {
const pfsName = packetField.packetFieldSpec.name;
let name;
if (isString(pfsName)) {
const key = 'specificationData.packetFieldName.' + pfsName;
name = _this.i18n.t(key);
if (name === key) {
name = pfsName;
}
} else if (isObject(pfsName)) {
name = pfsName [language] || pfsName.en || pfsName.de || pfsName.ref;
}
let rawValue;
if (packetField.packetFieldSpec && packetField.section) {
const { frameData } = packetField.section;
rawValue = _this.getRawValue(packetField.packetFieldSpec, frameData);
}
Object.assign(packetField, {
name,
rawValue,
formatTextValue(unit) {
return _this.formatTextValueFromRawValue(packetField.packetFieldSpec, rawValue, unit);
},
});
}
return packetFields;
}
static loadSpecificationData(rawSpecificationData, options) {
if (rawSpecificationData === undefined) {
rawSpecificationData = {};
}
if (options === undefined) {
options = {};
}
const rawFilteredPacketFieldSpecs = rawSpecificationData.filteredPacketFieldSpecs;
const specification = options.specification || globalSpecification || {};
const specificationData = options.specificationData || specification.specificationData || globalSpecificationData || {};
let filteredPacketFieldSpecs;
if (rawFilteredPacketFieldSpecs) {
const resolve = function(value, collectionKey) {
const collection = specificationData [collectionKey];
if (hasOwnProperty(collection, value)) {
value = collection [value];
}
return value;
};
filteredPacketFieldSpecs = rawFilteredPacketFieldSpecs.map((rfpfs) => {
const packetSpec = specification.getPacketSpecification(rfpfs.packetId);
const packetFieldSpec = specification.getPacketFieldSpecification(packetSpec, rfpfs.fieldId);
let { name } = rfpfs;
if (typeof name === 'string') {
name = { ref: name };
}
return {
...rfpfs,
packetSpec,
packetFieldSpec,
name,
type: resolve(rfpfs.type, 'types'),
conversions: rfpfs.conversions && rfpfs.conversions.map((rawConversion) => {
return {
factor: rawConversion.factor,
offset: rawConversion.offset,
sourceUnit: rawConversion.sourceUnit && resolve(rawConversion.sourceUnit, 'units'),
targetUnit: rawConversion.targetUnit && resolve(rawConversion.targetUnit, 'units'),
};
}),
getRawValue: resolve(rfpfs.getRawValue, 'getRawValueFunctions'),
setRawValue: resolve(rfpfs.setRawValue, 'setRawValueFunctions'),
};
});
}
const result = {
...specificationData,
filteredPacketFieldSpecs,
};
return result;
}
static storeSpecificationData(options) {
if (options === undefined) {
options = {};
}
if (options instanceof Specification) {
options = { specification: options };
}
const specification = options.specification || globalSpecification || {};
const specificationData = options.specificationData || specification.specificationData || globalSpecificationData || {};
const filteredPacketFieldSpecs = options.filteredPacketFieldSpecs || specificationData.filteredPacketFieldSpecs;
let rawFilteredPacketFieldSpecs;
if (filteredPacketFieldSpecs) {
const link = function(value, valueIdKey, collectionKey) {
const collection = specificationData [collectionKey];
let valueId;
if (valueIdKey) {
valueId = value [valueIdKey];
}
if (!valueId) {
valueId = Object.getOwnPropertyNames(collection).find(key => {
return (value === collection [key]);
});
}
if (valueId && hasOwnProperty(collection, valueId) && (collection [valueId] === value)) {
value = valueId;
}
return value;
};
rawFilteredPacketFieldSpecs = filteredPacketFieldSpecs.map((fpfs) => {
const rfpfs = {
filteredPacketFieldId: fpfs.filteredPacketFieldId,
packetId: fpfs.packetId,
fieldId: fpfs.fieldId,
name: fpfs.name,
type: link(fpfs.type, 'typeId', 'types'),
getRawValue: link(fpfs.getRawValue, null, 'getRawValueFunctions'),
setRawValue: link(fpfs.setRawValue, null, 'setRawValueFunctions'),
};
if (fpfs.conversions) {
rfpfs.conversions = fpfs.conversions.map((conversion) => {
const rawConversion = {};
if (isNumber(conversion.factor)) {
rawConversion.factor = conversion.factor;
}
if (isNumber(conversion.offset)) {
rawConversion.offset = conversion.offset;
}
if (conversion.sourceUnit) {
rawConversion.sourceUnit = link(conversion.sourceUnit, 'unitId', 'units');
}
if (conversion.targetUnit) {
rawConversion.targetUnit = link(conversion.targetUnit, 'unitId', 'units');
}
return rawConversion;
});
}
return rfpfs;
});
}
const rawSpecificationData = {
filteredPacketFieldSpecs: rawFilteredPacketFieldSpecs,
};
return rawSpecificationData;
}
static getDefaultSpecification() {
return globalSpecification;
}
}
Object.assign(Specification.prototype, /** @lends Specification.prototype */ {
/**
* Language code (ISO 639-1)
* @type {string}
*/
language: 'en',
deviceSpecCache: null,
packetSpecCache: null,
blockTypePacketSpecCache: null,
/**
* I18N instance
* @type {I18N}
*/
i18n: null,
/**
* Custom specification data to be mixed-in to built-in specification.
* @type {object}
*/
specificationData: null,
});
globalSpecification = new Specification();
module.exports = Specification;