Files
meb-battery/index.js
Giuseppe Raffa 4d5d51c018 Una repositoy che integra un plugin custom su SignalK per ottenere tutti i dati del BMS della batteria
- Introdotta l'implementazione JavaScript per la comunicazione BMS in bmscore.js, inclusi i metodi per il recupero dati e la gestione degli errori.
- Creato errors.js per mappare i codici di errore dal formato Python a quello JavaScript.
2026-05-11 19:45:07 +02:00

151 lines
5.3 KiB
JavaScript

// SignalK plugin — Daly BMS via USB/RS485 (solo USB).
// Polling periodico → delta SignalK. POST read-only on-demand.
'use strict';
const DalyProtocol = require('./lib/daly-protocol');
const DEFAULTS = Object.freeze({
device: '/dev/ttyUSB0',
batteryId: 'house',
pollSocMs: 2000,
pollStatusMs: 30000,
pollCellsMs: 10000,
retries: 5
});
module.exports = function (app) {
const plugin = {
id: 'signalk-daly-bms',
name: 'Daly BMS (USB)',
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: `electrical.batteries.${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('capacity.stateOfCharge', 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();
await bms.getStatus(); // necessario per cell/temp
app.setPluginStatus(`Connesso a ${opts.device}`);
} catch (e) {
app.setPluginError(`init failed: ${e.message}`);
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;
};