const DalyProtocol = require('./src/bmscore'); const DEFAULTS = Object.freeze({ device: '/dev/ttyUSB0', batteryId: 'house', pollSocMs: 2000, pollStatusMs: 30000, pollCellsMs: 10000, retries: 5 }); module.exports = function (app) { const plugin = { id: 'meb-battery', name: 'MEB Battery BMS', description: 'Legge un BMS Daly via seriale USB/RS485 e pubblica su SignalK' }; plugin.schema = { type: 'object', properties: { device: { type: 'string', default: DEFAULTS.device, title: 'Serial device (USB)' }, batteryId: { type: 'string', default: DEFAULTS.batteryId, title: 'SignalK battery id' }, pollSocMs: { type: 'integer', default: DEFAULTS.pollSocMs, title: 'Polling SOC/V/A (ms)' }, pollStatusMs: { type: 'integer', default: DEFAULTS.pollStatusMs, title: 'Polling status (ms)' }, pollCellsMs: { type: 'integer', default: DEFAULTS.pollCellsMs, title: 'Polling celle/temperature (ms)' }, retries: { type: 'integer', default: DEFAULTS.retries, title: 'Retry per richiesta' } } }; let bms = null; let opts = null; const timers = new Set(); const sk = (path, value) => ({ path: `meb.battery.${opts.batteryId}.${path}`, value }); const sendDelta = values => values && values.length && app.handleMessage(plugin.id, { updates: [{ values }] }); const safe = async (label, fn) => { try { return await fn(); } catch (e) { app.error(`${label}: ${e.message}`); return null; } }; // --- pollers --- async function pollSoc() { const s = await safe('pollSoc', () => bms.getSoc()); if (!s) return; sendDelta([ sk('voltage', s.total_voltage), sk('current', s.current), // SignalK: + = uscita (scarica), come Daly sk('soc', s.soc_percent / 100) ]); } async function pollStatus() { const s = await safe('pollStatus', () => bms.getStatus()); if (!s) return; sendDelta([ sk('cycles', s.cycles), sk('cellsCount', s.cells), sk('chargerRunning', s.charger_running), sk('loadRunning', s.load_running) ]); } async function pollCells() { const [cells, temps, errs] = await Promise.all([ safe('getCellVoltages', () => bms.getCellVoltages()), safe('getTemperatures', () => bms.getTemperatures()), safe('getErrors', () => bms.getErrors()) ]); const values = []; if (cells) for (const [k, v] of Object.entries(cells)) values.push(sk(`cells.${k}.voltage`, v)); if (temps) { const list = Object.values(temps); const toK = c => Math.round((c + 273.15) * 100) / 100; // °C → K, 2 decimali if (list.length) values.push(sk('temperature', toK(list[0]))); for (const [k, v] of Object.entries(temps)) values.push(sk(`temperatures.${k}`, toK(v))); } if (errs) values.push(sk('errors', errs)); sendDelta(values); } const startTimer = (fn, ms) => { const t = setInterval(() => { fn(); }, ms); timers.add(t); }; plugin.start = async function (options = {}) { opts = { ...DEFAULTS, ...options }; app.setPluginStatus(`Apertura ${opts.device}…`); bms = new DalyProtocol({ device: opts.device, retries: opts.retries, log: msg => app.debug(msg) }); try { await bms.open(); let lastErr = null; for (let i=0; i<3; i++) { try { await bms.getStatus(); lastErr = null; break; } catch (error) { lastErr = error; await new Promise(r => setTimeout(r, 500)); } if (lastErr) throw lastErr; } app.setPluginStatus(`Connesso a ${opts.device}`); } catch (e) { app.setPluginError(`init failed: ${e.message}`); if (bms) { await bms.close().catch(()=>{}); bms = null; } return; } // primo giro immediato + polling periodico await Promise.all([pollStatus(), pollSoc(), pollCells()]); startTimer(pollSoc, opts.pollSocMs); startTimer(pollStatus, opts.pollStatusMs); startTimer(pollCells, opts.pollCellsMs); }; plugin.stop = async function () { for (const t of timers) clearInterval(t); timers.clear(); if (bms) { await bms.close().catch(() => {}); bms = null; } app.setPluginStatus('stopped'); }; // --- API REST: solo letture on-demand --- plugin.registerWithRouter = function (router) { const wrap = fn => async (_req, res) => { if (!bms) return res.status(503).json({ error: 'plugin not started' }); try { res.json(await fn()); } catch (e) { res.status(500).json({ error: e.message }); } }; router.post('/soc', wrap(() => bms.getSoc())); router.post('/status', wrap(() => bms.getStatus())); router.post('/temperatures', wrap(() => bms.getTemperatures())); router.post('/cell-voltages', wrap(() => bms.getCellVoltages())); router.post('/cell-range', wrap(() => bms.getCellVoltageRange())); router.post('/temp-range', wrap(() => bms.getTemperatureRange())); router.post('/mosfet', wrap(() => bms.getMosfetStatus())); router.post('/errors', wrap(() => bms.getErrors())); router.post('/all', wrap(async () => { const [soc, status, cell_voltages, temperatures, mosfet, errors] = await Promise.all([ bms.getSoc(), bms.getStatus(), bms.getCellVoltages(), bms.getTemperatures(), bms.getMosfetStatus(), bms.getErrors() ]); return { soc, status, cell_voltages, temperatures, mosfet, errors }; })); }; return plugin; };