UNPKG

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
"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;