@smartdcc/gbcs-parser
Version:
GBCS parser based on henrygiraldo.github.io
268 lines • 11.2 kB
JavaScript
;
/*
* Created on Mon Jul 31 2023
*
* Copyright (c) 2023 Smart DCC Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeDLMSDateTime = decodeDLMSDateTime;
exports.minifyList = minifyList;
exports.decodeECS24 = decodeECS24;
const node_util_1 = require("node:util");
function decodeDLMSDateTime(b, logger) {
if (b.length !== 12 && b.length !== 5) {
logger?.('buffer should be 12 (date time) or 5 (date) bytes');
return null;
}
const ret = {};
const year = (b[0] << 8) | b[1];
if (year !== 0xffff) {
ret.year = year;
}
const month = b[2];
if (month === 0xfd || month === 0xfe) {
logger?.(`month specified as ${month.toString(16)} but daylight_savings_end and daylight_savings_begin are not supported`);
return null;
}
if (month !== 0xff) {
ret.month = month;
}
const dayOfMonth = b[3];
if (dayOfMonth !== 0xff) {
ret.dayOfMonth = dayOfMonth;
}
const dayOfWeek = b[4];
if (dayOfWeek !== 0xff) {
ret.dayOfWeek = dayOfWeek;
}
if (b.length === 12) {
const time = b.subarray(5, 9);
if (time.some((x) => x !== 0)) {
logger?.(`time should be 00:00:00 but found ${time.toString('hex')}`);
return null;
}
const deviation = (b[9] << 8) | b[10];
if (deviation !== 0x8000 && deviation !== 0) {
logger?.(`deviation should be in UTC but ${deviation.toString(16)} specified`);
return null;
}
if (b[11] !== 0xff) {
logger?.('expected clock status to not be specified');
return null;
}
}
return ret;
}
function minifyList(l) {
while (l[l.length - 1] === 0) {
l = l.slice(0, l.length - 1);
}
return l;
}
function decodeECS24(message, logger) {
if (message['Grouping Header']?.['Other Information Length'].children?.['Message Code'].notes !== 'ECS24 Read ESME Tariff Data') {
logger?.('wrong message code');
return null;
}
if (message['Grouping Header']?.['CRA Flag']?.notes !== 'Response') {
logger?.('not a response');
return null;
}
const ListofAccessResponseSpecification = message.Payload?.['DLMS Access Response'].children?.['List of Access Response Specification'].children ?? {};
if (Object.keys(ListofAccessResponseSpecification).filter((key) => {
const result = ListofAccessResponseSpecification[key].children?.['Data Access Result']
?.notes;
if (result !== 'Success') {
logger?.(`${key} data access result was not "Success": ${result}`);
return false;
}
return true;
}).length !== 17) {
logger?.('ECS24 payload was not successful, unable to decode');
return null;
}
const ListofAccessResponseData = message.Payload?.['DLMS Access Response'].children?.['List of Access Response Data'].children ?? {};
/*
As the above checks have passed, it is assumed that the payload is of the correct format
correct format. So error cases are not tested for.
*/
/* 0 (Primary)ActiveTariffPrice.value */
/* n/a */
/* 1 (Primary)ActiveTariffPrice.scale */
/* n/a */
/* 2 TariffSwitchingTable.currentSeasons */
const currentSeasonsDLMS = ListofAccessResponseData['[2] Array'].children ?? {};
const currentSeasonsDLMS_len = Number(ListofAccessResponseData['[2] Array']?.notes?.split(' ')[0]);
const seasons = Array.from(Array(currentSeasonsDLMS_len).keys()).map((i) => {
const s = currentSeasonsDLMS[`[${i}] Structure`]?.children ?? {};
logger?.(`parsing season ${i}: ${(0, node_util_1.inspect)(s, {
breakLength: Infinity,
compact: true,
})}`);
const date = decodeDLMSDateTime(Buffer.from(s['[1] Octet String']?.hex.replace(/ /g, ''), 'hex').subarray(2), logger);
return {
name: `${s['[0] Octet String']?.notes}`.slice(1, -1),
weekProfile: Number(s['[2] Octet String'].hex.split(' ').slice(-1)[0]),
...date,
};
});
/* 3 TariffSwitchingTable.currentWeeks */
const currentWeeksDLMS = ListofAccessResponseData['[3] Array'].children ?? {};
const currentWeeksDLMS_len = Number(ListofAccessResponseData['[3] Array']?.notes?.split(' ')[0]);
const weekProfiles = Array.from(Array(currentWeeksDLMS_len).keys())
.map((i) => {
const wp = currentWeeksDLMS[`[${i}] Structure`]?.children ?? {};
return {
id: Number(wp['[0] Octet String'].hex.split(' ').slice(-1)[0]),
weekProfile: [
Number(wp['[1] Unsigned']?.notes),
Number(wp['[2] Unsigned']?.notes),
Number(wp['[3] Unsigned']?.notes),
Number(wp['[4] Unsigned']?.notes),
Number(wp['[5] Unsigned']?.notes),
Number(wp['[6] Unsigned']?.notes),
Number(wp['[7] Unsigned']?.notes),
],
};
})
.sort((a, b) => a.id - b.id)
.map(({ weekProfile }) => weekProfile);
/* 4 TariffSwitchingTable.currentDayIdentifiers */
const currentDayIdentifiersDLMS = ListofAccessResponseData['[4] Array'].children ?? {};
const currentDayIdentifiersDLMS_len = Number(ListofAccessResponseData['[4] Array']?.notes?.split(' ')[0]);
const dayProfiles = Array.from(Array(currentDayIdentifiersDLMS_len).keys())
.map((i) => {
const dp = currentDayIdentifiersDLMS[`[${i}] Structure`]?.children ?? {};
const pss = dp['[1] Array']?.children ?? {};
const pss_len = Number(dp['[1] Array']?.notes?.split(' ')[0]);
return {
id: Number(dp['[0] Unsigned']?.notes),
dayProfile: Array.from(Array(pss_len).keys()).map((i) => {
const ps = pss[`[${i}] Structure`]?.children ?? {};
const startTimeBinary = Buffer.from(ps['[0] Octet String']?.hex.replace(/ /g, ''), 'hex');
const startTime = startTimeBinary[2] * 60 * 60 +
startTimeBinary[3] * 60 +
startTimeBinary[4];
const action = Number(ps['[2] Long Unsigned']?.notes);
if (action >= 1 && action <= 48) {
return {
startTime,
action: action,
mode: 'tou',
};
}
if (action >= 101 && action <= 108) {
return {
startTime,
action: (action - 100),
mode: 'block',
};
}
/* action 201 ... 204 is secondary element tou */
logger?.(`failed to interpret action ${action}, setting default of 1`);
return {
startTime,
action: 1,
mode: 'tou',
};
}),
};
})
.sort((a, b) => a.id - b.id)
.map(({ dayProfile }) => dayProfile);
/* 5 TariffSwitchingTable.specialDays */
const specialDaysDLMS = ListofAccessResponseData['[5] Array'].children ?? {};
const specialDaysDLMS_len = Number(ListofAccessResponseData['[5] Array']?.notes?.split(' ')[0]);
const specialDays = Array.from(Array(specialDaysDLMS_len).keys())
.map((i) => {
const sd = specialDaysDLMS[`[${i}] Structure`]?.children ?? {};
const date = decodeDLMSDateTime(Buffer.from(sd['[1] Octet String']?.hex.replace(/ /g, ''), 'hex').subarray(2), logger);
return {
id: Number(sd['[0] Long Unsigned']?.notes),
specialDay: {
...date,
dayProfile: Number(sd['[2] Unsigned'].notes),
},
};
})
.sort((a, b) => a.id - b.id)
.map(({ specialDay }) => specialDay);
/* 6-13 TariffThresholdMatrixBlock.thresholdCurrent */
const thresholds = [];
for (let z = 6; z < 14; z++) {
const thresholdCurrentDLMS = ListofAccessResponseData[`[${z}] Array`].children ?? {};
const thresholdCurrentDLMS_len = Number(ListofAccessResponseData[`[${z}] Array`]?.notes?.split(' ')[0]);
const thresholdCurrent = Array.from(Array(thresholdCurrentDLMS_len).keys()).map((i) => {
return Number(thresholdCurrentDLMS[`[${i}] Double Long Unsigned`].notes);
});
thresholds.push({
thresholds: thresholdCurrent,
});
}
/* 14 CurrencyUnit.valueCurrent */
/* 15 StandingCharge */
const standingChargeDLMS = ListofAccessResponseData['[15] Structure'].children ?? {};
const standingChargeScale = Number(standingChargeDLMS['[0] Structure']?.children?.['[1] Integer']?.notes);
const standingCharge = Number(standingChargeDLMS['[2] Array']?.children?.['[0] Structure']?.children?.['[1] Long']?.notes);
/* 16 Charge */
const chargeDLMS = ListofAccessResponseData['[16] Structure'].children ?? {};
const priceScale = Number(chargeDLMS['[0] Structure']?.children?.['[1] Integer']?.notes);
/* list of 80 charges initialised to 0, 48 for tou and 32 for block */
const charges = Array(80).fill(0);
const chargesDLMS = chargeDLMS['[2] Array']?.children ?? {};
const chargesDLMS_len = Number(chargeDLMS['[2] Array']?.notes?.split(' ')[0]);
for (let i = 0; i < chargesDLMS_len; i++) {
// todo: keep eye on whether some meters return out of order results
/*
const id = parseInt(
chargesDLMS[`[${i}] Structure`].children?.['[0] Octet String']?.hex
.split(' ')
.slice(-1)[0] as string,
16,
)
*/
const c = Number(chargesDLMS[`[${i}] Structure`].children?.['[1] Long']?.notes);
charges[i] = c;
}
/* assemble the block actions */
const blocks = thresholds.map((t, i) => {
const b = 48 + i;
const prices = [
charges[b],
charges[b + 8],
charges[b + 8 * 2],
charges[b + 8 * 3],
];
return {
...t,
prices: prices.slice(0, t.thresholds.length + 1),
};
});
return {
seasons,
weekProfiles,
dayProfiles,
specialDays,
tous: minifyList(charges.slice(0, 48)),
blocks,
pricing: {
priceScale,
standingCharge,
standingChargeScale,
},
};
}
//# sourceMappingURL=tariff.js.map