homebridge-obis-powermeter
Version:
Plugin for OBIS smart meter devices (SML/D0) to read power, energy, and voltages.
499 lines (498 loc) • 24.5 kB
JavaScript
"use strict";
/**
* Homebridge OBIS Power Meter Plugin
* Reads power/energy data from a Smart Meter via OBIS using smartmeter-obis.
* Provides Homebridge accessories for power consumption, return, voltage, and energy import.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HomebridgeObisPowerConsumption = void 0;
const smartmeter_obis_1 = __importDefault(require("smartmeter-obis"));
const fs_1 = __importDefault(require("fs"));
const PowerConsumption_1 = __importDefault(require("./Accessories/PowerConsumption"));
const PowerReturn_1 = __importDefault(require("./Accessories/PowerReturn"));
const VoltageSensor_1 = __importDefault(require("./Accessories/VoltageSensor"));
const EnergyImport_1 = __importDefault(require("./Accessories/EnergyImport"));
class HomebridgeObisPowerConsumption {
// prefer warn when plugin debugLevel is enabled so logs are visible even if child-bridge log level is warn
d(msg, level = 1) {
var _a;
const dl = Number((_a = this.obisOptions.debug) !== null && _a !== void 0 ? _a : 0);
if (dl >= level) {
this.log.warn(msg);
}
else {
this.log.debug(msg);
}
}
constructor(log, config, api) {
var _a;
this.log = log;
this.config = config;
this.api = api;
this.accessories = [];
this.REGISTER_PLUGIN_NAME = 'homebridge-obis-powermeter';
this.PLATFORM_NAME = 'OBIS';
this.UUID_NAMESPACE = 'homebridge-obis-power-consumption';
this.devices = [];
this.dataDevices = [];
this.device = null;
this.obisOptions = {
protocol: 'SmlProtocol',
transport: 'SerialResponseTransport',
transportSerialPort: '',
requestInterval: 10,
obisNameLanguage: 'en',
obisFallbackMedium: 6,
debug: 0,
protocolSmlIgnoreInvalidCRC: false,
protocolSmlInputEncoding: 'binary',
// SML defaults for encoding/CRC are used
};
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.heartBeatInterval = (this.config.pollInterval || 60) * 1000;
// Configure debug level from config or env (OBIS_DEBUG). Coerce strings -> numbers and clamp 0..2.
const envRaw = process.env.OBIS_DEBUG;
const envNum = envRaw !== undefined && envRaw !== '' ? Number(envRaw) : NaN;
const cfgNum = Number((_a = this.config.debugLevel) !== null && _a !== void 0 ? _a : NaN);
const base = (Number.isFinite(envNum) && envNum >= 0)
? envNum
: (Number.isFinite(cfgNum) && cfgNum >= 0 ? cfgNum : 0);
const dbg = (base <= 0 ? 0 : base >= 2 ? 2 : 1);
this.obisOptions.debug = dbg;
this.api.on('didFinishLaunching', () => {
this.initialize();
});
}
configureAccessory(accessory) {
this.accessories.push(accessory);
}
validateConfig() {
return this.config.serialPort.length > 0;
}
syncObisOptionsFromConfig() {
var _a;
const protocol = (_a = this.config.protocol) !== null && _a !== void 0 ? _a : 'SmlProtocol';
this.obisOptions.protocol = protocol;
// Choose sensible default transport per protocol
if (protocol === 'D0Protocol') {
this.obisOptions.transport = 'SerialRequestResponseTransport';
}
else {
this.obisOptions.transport = 'SerialResponseTransport';
}
}
async validateSerialPort() {
return new Promise((resolve) => {
var _a, _b, _c, _d;
try {
this.syncObisOptionsFromConfig();
this.obisOptions.transportSerialPort = this.config.serialPort;
// forward optional serial settings
const so = this.obisOptions;
if (this.config.serialBaudRate) {
so['transportSerialBaudrate'] = this.config.serialBaudRate;
}
if (this.config.serialDataBits) {
so['transportSerialDataBits'] = this.config.serialDataBits;
}
if (this.config.serialStopBits) {
so['transportSerialStopBits'] = this.config.serialStopBits;
}
if (this.config.serialParity) {
so['transportSerialParity'] = this.config.serialParity;
}
// Basic FS existence/perm check
try {
if (!fs_1.default.existsSync(this.obisOptions.transportSerialPort)) {
this.log.warn(`[OBIS] Serial device does not exist: ${this.obisOptions.transportSerialPort}`);
}
else {
const stat = fs_1.default.statSync(this.obisOptions.transportSerialPort);
this.d(`[OBIS] Serial device exists. Mode: ${stat.mode.toString(8)} Size: ${stat.size}`, 2);
}
}
catch (e) {
this.d(`[OBIS] FS check failed for serial device: ${String(e)}`, 2);
}
// Log validation parameters split across lines to satisfy max-len
const logHeader = `[OBIS] Validating serial port ${this.obisOptions.transportSerialPort}`;
const logDetails = '('
+ `protocol=${this.obisOptions.protocol}, `
+ `transport=${this.obisOptions.transport}, `
+ `reqInterval=${this.obisOptions.requestInterval}, `
+ `debug=${this.obisOptions.debug}, `
+ `baud=${(_a = this.config.serialBaudRate) !== null && _a !== void 0 ? _a : ''}, `
+ `dataBits=${(_b = this.config.serialDataBits) !== null && _b !== void 0 ? _b : ''}, `
+ `stopBits=${(_c = this.config.serialStopBits) !== null && _c !== void 0 ? _c : ''}, `
+ `parity=${(_d = this.config.serialParity) !== null && _d !== void 0 ? _d : ''}`
+ ')';
this.log.debug(logHeader);
this.log.debug(logDetails);
const summarize = (data) => {
const keys = data ? Object.keys(data) : [];
const preview = keys.slice(0, 5).join(', ');
return `keys=${keys.length}${preview ? ` [${preview}]` : ''}`;
};
let settled = false;
const smTransport = smartmeter_obis_1.default.init(this.obisOptions, (error, data) => {
var _a, _b, _c, _d, _f, _g, _h, _j;
if (settled) {
return false;
}
if (error) {
this.log.error(`[OBIS] SmartMeter error during validate: ${error.message}`);
(_a = smTransport.stop) === null || _a === void 0 ? void 0 : _a.call(smTransport);
settled = true;
resolve(false);
return false;
}
try {
this.d(`[OBIS] validate callback data: ${summarize(data)}`, 2);
const hasAnyData = data && Object.keys(data).length > 0;
if (hasAnyData) {
const getStr = (k) => { var _a, _b; return (_b = (_a = data === null || data === void 0 ? void 0 : data[k]) === null || _a === void 0 ? void 0 : _a.valueToString) === null || _b === void 0 ? void 0 : _b.call(_a); };
this.device = {
product_name: (_b = getStr('1-0:96.50.1*1')) !== null && _b !== void 0 ? _b : 'Unknown',
product_type: (_c = getStr('1-0:96.50.1*1')) !== null && _c !== void 0 ? _c : 'Unknown',
serial: (_d = getStr('1-0:96.1.0*255')) !== null && _d !== void 0 ? _d : '',
firmware_version: (_f = getStr('1-0:0.2.0*0')) !== null && _f !== void 0 ? _f : '',
api_version: (_g = getStr('1-0:0.2.0*0')) !== null && _g !== void 0 ? _g : '',
};
this.log.info('[OBIS] Serial validation succeeded (first frame received).');
(_h = smTransport.stop) === null || _h === void 0 ? void 0 : _h.call(smTransport);
settled = true;
resolve(true);
return true;
}
return false;
}
catch (e) {
this.log.warn(`[OBIS] Serial validation got data but parsing failed: ${String(e)}`);
(_j = smTransport.stop) === null || _j === void 0 ? void 0 : _j.call(smTransport);
settled = true;
resolve(true);
return true;
}
});
this.d('[OBIS] Starting serial processing for validation...', 1);
smTransport.process();
setTimeout(() => {
var _a;
if (!settled) {
this.log.error('[OBIS] Timeout while validating the serial port. No data received in time.');
(_a = smTransport.stop) === null || _a === void 0 ? void 0 : _a.call(smTransport);
settled = true;
resolve(false);
}
}, 130000);
}
catch (e) {
this.log.error(`Failed to open serial port '${this.config.serialPort}': ${String(e)}`);
resolve(false);
}
});
}
async initialize() {
if (!this.validateConfig()) {
this.log.error('Configuration error. Please provide your Power‑Meter `serialPort`.');
return;
}
const ok = await this.validateSerialPort();
if (!ok) {
this.log.error('Your Power‑meter\'s Serial Port seems to be incorrect. No connection possible.');
return;
}
this.setupAccessoires();
await this.heartBeat();
if (this.hbTimer) {
clearInterval(this.hbTimer);
}
this.hbTimer = setInterval(() => {
this.heartBeat();
}, this.heartBeatInterval);
}
setupAccessoires() {
// Power Consumption
const powerConsumptionName = 'Power Consumption';
const powerConsumptionUuid = this.api.hap.uuid.generate(`${this.UUID_NAMESPACE}:power-consumption`);
const powerConsumptionExistingAccessory = this.accessories.find((accessory) => accessory.UUID === powerConsumptionUuid);
if (this.config.hidePowerConsumptionDevice !== true) {
if (powerConsumptionExistingAccessory) {
this.devices.push(new PowerConsumption_1.default(this.config, this.log, this.api, powerConsumptionExistingAccessory, this.device));
}
else {
this.log.info(`${powerConsumptionName} added as accessory`);
const accessory = new this.api.platformAccessory(powerConsumptionName, powerConsumptionUuid);
this.devices.push(new PowerConsumption_1.default(this.config, this.log, this.api, accessory, this.device));
this.api.registerPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [accessory]);
}
}
else if (powerConsumptionExistingAccessory) {
this.api.unregisterPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [
powerConsumptionExistingAccessory,
]);
}
// Power Return
const powerReturnName = 'Power Return';
const powerReturnUuid = this.api.hap.uuid.generate(`${this.UUID_NAMESPACE}:power-return`);
const powerReturnExistingAccessory = this.accessories.find((accessory) => accessory.UUID === powerReturnUuid);
// Hidden by default: only show when explicitly configured as not hidden (false)
if (this.config.hidePowerReturnDevice === false) {
if (powerReturnExistingAccessory) {
this.devices.push(new PowerReturn_1.default(this.config, this.log, this.api, powerReturnExistingAccessory, this.device));
}
else {
this.log.info(`${powerReturnName} added as accessory`);
const accessory = new this.api.platformAccessory(powerReturnName, powerReturnUuid);
this.devices.push(new PowerReturn_1.default(this.config, this.log, this.api, accessory, this.device));
this.api.registerPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [accessory]);
}
}
else if (powerReturnExistingAccessory) {
this.api.unregisterPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [powerReturnExistingAccessory]);
}
// Energy Import (Total, kWh)
const eImpName = 'Energy Import';
const eImpUuid = this.api.hap.uuid.generate(`${this.UUID_NAMESPACE}:energy-import`);
const eImpExisting = this.accessories.find(a => a.UUID === eImpUuid);
if (eImpExisting) {
this.dataDevices.push(new EnergyImport_1.default(this.config, this.log, this.api, eImpExisting, this.device));
}
else {
this.log.info(`${eImpName} added as accessory`);
const accessory = new this.api.platformAccessory(eImpName, eImpUuid);
this.dataDevices.push(new EnergyImport_1.default(this.config, this.log, this.api, accessory, this.device));
this.api.registerPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [accessory]);
}
// Voltage L1
const v1Name = 'Voltage L1';
const v1Uuid = this.api.hap.uuid.generate(`${this.UUID_NAMESPACE}:voltage-l1`);
const v1Existing = this.accessories.find(a => a.UUID === v1Uuid);
if (v1Existing) {
this.dataDevices.push(new VoltageSensor_1.default(this.config, this.log, this.api, v1Existing, this.device, {
obisKey: '1-0:32.7.0*255', name: v1Name, serialSuffix: 'voltage-l1',
}));
}
else {
this.log.info(`${v1Name} added as accessory`);
const accessory = new this.api.platformAccessory(v1Name, v1Uuid);
this.dataDevices.push(new VoltageSensor_1.default(this.config, this.log, this.api, accessory, this.device, {
obisKey: '1-0:32.7.0*255', name: v1Name, serialSuffix: 'voltage-l1',
}));
this.api.registerPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [accessory]);
}
// Voltage L2
const v2Name = 'Voltage L2';
const v2Uuid = this.api.hap.uuid.generate(`${this.UUID_NAMESPACE}:voltage-l2`);
const v2Existing = this.accessories.find(a => a.UUID === v2Uuid);
if (v2Existing) {
this.dataDevices.push(new VoltageSensor_1.default(this.config, this.log, this.api, v2Existing, this.device, {
obisKey: '1-0:52.7.0*255', name: v2Name, serialSuffix: 'voltage-l2',
}));
}
else {
this.log.info(`${v2Name} added as accessory`);
const accessory = new this.api.platformAccessory(v2Name, v2Uuid);
this.dataDevices.push(new VoltageSensor_1.default(this.config, this.log, this.api, accessory, this.device, {
obisKey: '1-0:52.7.0*255', name: v2Name, serialSuffix: 'voltage-l2',
}));
this.api.registerPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [accessory]);
}
// Voltage L3
const v3Name = 'Voltage L3';
const v3Uuid = this.api.hap.uuid.generate(`${this.UUID_NAMESPACE}:voltage-l3`);
const v3Existing = this.accessories.find(a => a.UUID === v3Uuid);
if (v3Existing) {
this.dataDevices.push(new VoltageSensor_1.default(this.config, this.log, this.api, v3Existing, this.device, {
obisKey: '1-0:72.7.0*255', name: v3Name, serialSuffix: 'voltage-l3',
}));
}
else {
this.log.info(`${v3Name} added as accessory`);
const accessory = new this.api.platformAccessory(v3Name, v3Uuid);
this.dataDevices.push(new VoltageSensor_1.default(this.config, this.log, this.api, accessory, this.device, {
obisKey: '1-0:72.7.0*255', name: v3Name, serialSuffix: 'voltage-l3',
}));
this.api.registerPlatformAccessories(this.REGISTER_PLUGIN_NAME, this.PLATFORM_NAME, [accessory]);
}
}
async heartBeat() {
this.d('[OBIS] Heartbeat: starting read cycle...', 1);
let settled = false;
try {
this.syncObisOptionsFromConfig();
const summarize = (data) => {
const keys = data ? Object.keys(data) : [];
const preview = keys.slice(0, 3).join(', ');
return `keys=${keys.length}${preview ? ` [${preview}]` : ''}`;
};
const smTransport = smartmeter_obis_1.default.init(this.obisOptions, (error, data) => {
var _a, _b, _c;
if (settled) {
return false;
}
if (error) {
this.log.error(`[OBIS] SmartMeter read error: ${error.message}`);
(_a = smTransport.stop) === null || _a === void 0 ? void 0 : _a.call(smTransport);
settled = true;
return false;
}
try {
this.d(`[OBIS] Heartbeat data: ${summarize(data)}`, 2);
// update voltage sensors first (does not depend on power)
try {
if (data) {
this.dataDevices.forEach(d => d.beatWithData(data));
}
}
catch (e) {
this.d(`[OBIS] Voltage update failed: ${String(e)}`, 1);
}
const { value, src } = this.computeActivePower(data);
if (!Number.isFinite(value)) {
const available = data ? Object.keys(data).slice(0, 10).join(', ') : '';
throw new Error(`Active power not available. Known keys: ${available}`);
}
this.devices.forEach((device) => {
device.beat(value);
});
this.d(`[OBIS] Heartbeat value=${value} (src=${src})`, 1);
(_b = smTransport.stop) === null || _b === void 0 ? void 0 : _b.call(smTransport);
settled = true;
return true;
}
catch (e) {
this.log.error(`[OBIS] Cannot read active power consumption: ${String(e)}`);
(_c = smTransport.stop) === null || _c === void 0 ? void 0 : _c.call(smTransport);
settled = true;
return false;
}
});
this.d('[OBIS] Starting serial processing for heartbeat...', 1);
smTransport.process();
setTimeout(() => {
var _a;
if (!settled) {
this.log.error('[OBIS] Timeout reading active power. Check the Power‑meter Serial Port name.');
(_a = smTransport.stop) === null || _a === void 0 ? void 0 : _a.call(smTransport);
settled = true;
}
}, 30000);
}
catch (error) {
this.log.error('[OBIS] Something went wrong in heartbeat; please double‑check the Power‑meter Serial Port name.');
this.log.debug(String(error));
}
}
shutdown() {
if (this.hbTimer) {
clearInterval(this.hbTimer);
this.hbTimer = undefined;
}
}
floatOf(m) {
if (!m) {
return NaN;
}
try {
// Prefer parsing from valueToString so we can detect units
if (typeof m.valueToString === 'function') {
const s = String(m.valueToString());
const match = s.match(/-?\d+(?:[.,]\d+)?/);
if (match) {
let v = Number(match[0].replace(',', '.'));
const unit = s.toLowerCase();
if (unit.includes('kw')) {
v = v * 1000;
}
// treat plain numbers or 'w' as watts
if (Number.isFinite(v)) {
return v;
}
}
}
// Fallback to first numeric value in the measurement values
const vals = typeof m.getValues === 'function' ? m.getValues() : m.values;
if (Array.isArray(vals) && vals.length > 0) {
const { value, unit } = vals[0];
if (Number.isFinite(value)) {
const u = String(unit || '').toLowerCase();
if (u.includes('kw')) {
return value * 1000;
}
return value;
}
}
}
catch (_e) {
// ignore parse errors
}
return NaN;
}
computeActivePower(data) {
if (!data) {
return { value: NaN, src: 'none' };
}
const has = (k) => Object.prototype.hasOwnProperty.call(data, k);
const get = (k) => this.floatOf(data[k]);
// 1) Direct net power total (16.7.0)
const netCandidates = ['1-0:16.7.0*255', '1-0:16.7.0'];
for (const k of netCandidates) {
if (has(k)) {
const v = get(k);
if (Number.isFinite(v)) {
return { value: v, src: k };
}
}
}
// 2) Import/export totals (1.7.0 import, 2.7.0 export)
const importCandidates = ['1-0:1.7.0*255', '1-0:1.7.0'];
const exportCandidates = ['1-0:2.7.0*255', '1-0:2.7.0'];
let imp = NaN;
let exp = NaN;
for (const k of importCandidates) {
if (has(k)) {
const v = get(k);
if (Number.isFinite(v)) {
imp = v;
break;
}
}
}
for (const k of exportCandidates) {
if (has(k)) {
const v = get(k);
if (Number.isFinite(v)) {
exp = v;
break;
}
}
}
if (Number.isFinite(imp) || Number.isFinite(exp)) {
const iv = Number.isFinite(imp) ? imp : 0;
const ev = Number.isFinite(exp) ? exp : 0;
return { value: iv - ev, src: '1.7-2.7' };
}
// 3) Per-phase import/export sums
const phaseImport = ['1-0:21.7.0*255', '1-0:41.7.0*255', '1-0:61.7.0*255', '1-0:21.7.0', '1-0:41.7.0', '1-0:61.7.0'];
const phaseExport = ['1-0:22.7.0*255', '1-0:42.7.0*255', '1-0:62.7.0*255', '1-0:22.7.0', '1-0:42.7.0', '1-0:62.7.0'];
const sumKeys = (keys) => keys.reduce((acc, k) => acc + (Number.isFinite(get(k)) ? get(k) : 0), 0);
const impSum = sumKeys(phaseImport);
const expSum = sumKeys(phaseExport);
if (impSum > 0 || expSum > 0) {
return { value: impSum - expSum, src: 'phase-import-export' };
}
// 4) Fallback: some meters expose L1/L2/L3 instantaneous as 36/56/76
const phaseAlt = ['1-0:36.7.0*255', '1-0:56.7.0*255', '1-0:76.7.0*255', '1-0:36.7.0', '1-0:56.7.0', '1-0:76.7.0'];
const altSum = sumKeys(phaseAlt);
if (altSum !== 0) {
return { value: altSum, src: 'phases-sum-alt' };
}
return { value: NaN, src: 'not-found' };
}
}
exports.HomebridgeObisPowerConsumption = HomebridgeObisPowerConsumption;