NEWS
Buderus 4121 - Einbindung über EcoCan [läuft]
-
Hallo zusammen,
ich habe meine Buderus 4121 selbst über einen Raspberry zero 2 W und einem CAN-Hat (Habe den von waveshare genutzt, würde aber im erneuten Fall ein anderes nehmen, mit 8/16 Mhz - das von waveshare hat 12MHz und die Abfragen auf auf dem PI zeigen was anderes an.. war ein wenig Fehlersuche..) angebunden über zusammen mit chatgpt ein JavaScript geschrieben um die Daten per MQTT zu übertragen.
#!/usr/bin/env node /** * Buderus System 4000 CAN -> MQTT Decoder (ALL MODULES, FULL BITS, RAW) * --------------------------------------------------------------------- * - SocketCAN (ID 0x400 Monitor), 60-Byte-Reassembly je Typ 0x80..0x9E * - Sprechende Felder + vollständige Bit-Dekodierung gemäß Doku * - Zusätzlich immer: Block-JSON, RAW-Bytes b00..b59, Wörter w00..w58 (u16be), * Triplets t00..t57 (u24). Keine Wertebegrenzung/Clamps. * * ENV: * CAN_IF=can0 * MQTT_URL=mqtt://USER:PASS@<BROKER_IP>:1883 * MQTT_BASE=buderus/eco * PUBLISH_ONLY_SEEN=1 # nur publizieren, wenn Block wirklich empfangen (Default 1) * STALE_MS=0 # >0: unterdrückt Veröffentlichung je Typ nach Inaktivität * PUBLISH_BLOCK_JSON=1 # Block-JSON an/aus * PUBLISH_RAW_BYTES=1 # b00..b59 * PUBLISH_WORDS=1 # w00,w02,...,w58 * PUBLISH_TRIPLETS=1 # t00..t57 */ var mqtt = require('mqtt'); var can = require('socketcan'); // ---------- Konfig ---------- var CAN_IF = process.env.CAN_IF || 'can0'; var MQTT_URL = process.env.MQTT_URL || 'mqtt://127.0.0.1:1883'; var MQTT_BASE = (process.env.MQTT_BASE || 'buderus/eco').replace(/\/+$/,''); var RETAIN = true, QOS = 0; var PUBLISH_ONLY_SEEN = (process.env.PUBLISH_ONLY_SEEN || '1') === '1'; var STALE_MS = parseInt(process.env.STALE_MS || '0', 10); var PUBLISH_BLOCK_JSON = (process.env.PUBLISH_BLOCK_JSON || '1') === '1'; var PUBLISH_RAW_BYTES = (process.env.PUBLISH_RAW_BYTES || '1') === '1'; var PUBLISH_WORDS = (process.env.PUBLISH_WORDS || '1') === '1'; var PUBLISH_TRIPLETS = (process.env.PUBLISH_TRIPLETS || '1') === '1'; // ---------- Helper ---------- function u8(buf,i){ return (buf[i]!==undefined)?buf[i]:0; } function i8(buf,i){ var v=u8(buf,i); return v>=128?(v-256):v; } function u16be(buf,i){ return ((u8(buf,i)<<8)|u8(buf,i+1))>>>0; } function i16be(buf,i){ var v=u16be(buf,i); return (v&0x8000)?(v-0x10000):v; } function u24(b3,b2,b1){ return (((b3&0xFF)<<16)|((b2&0xFF)<<8)|(b1&0xFF))>>>0; } function bits(byte, names){ var o={}, b; for(b=0;b<8;b++){ var key = names && names[b] ? names[b] : ('b'+(b+1)); o[key] = !!(byte & (1<<b)); } return o; } function put(obj, path, val){ var p=path.split('/'),cur=obj; for (var i=0;i<p.length-1;i++){ cur[p[i]]=cur[p[i]]||{}; cur=cur[p[i]]; } cur[p[p.length-1]] = val; } function tname(t){ var map = { 0x80:'hk1',0x81:'hk2',0x82:'hk3',0x83:'hk4',0x8A:'hk5',0x8B:'hk6',0x8C:'hk7',0x8D:'hk8',0x8E:'hk9', 0x84:'warmwasser',0x88:'kessel',0x89:'konfig',0x8F:'strategie_boden',0x90:'lap', 0x92:'uba1',0x93:'uba2',0x94:'uba3',0x95:'uba4',0x96:'uba5',0x97:'uba6',0x98:'uba7',0x99:'uba8', 0x9B:'waermemenge',0x9C:'stoermodul',0x9D:'unterstation',0x9E:'solar' }; return map[t] || ('type_'+t.toString(16)); } function ts(){ return new Date().toISOString(); } // ---------- Bit-Labels aus Doku ---------- var HK_BW1 = ['ausschaltopt','einschaltopt','automatik','ww_vorrang','estrichtrocknung','ferien','frostschutz','manuell']; var HK_BW2 = ['sommer','tag','fb_keine_kommunikation','fb_fehlerhaft','fehler_vorlauf','max_vorlauf','ext_stoereingang','party_pause']; var HK_EIN = ['eingang_wf2','eingang_wf3',null,null,null,'schalter_0','schalter_hand','schalter_aut']; var WW_BW1 = ['automatik','desinfektion','nachladung','ferien','fehler_desinfektion','fehler_fuehler','ww_bleibt_kalt','fehler_anode']; var WW_BW2 = ['laden','manuell','nachladen','ausschaltopt','einschaltopt','tag','warm','vorrang']; var WW_PUMP = ['ladepumpe','zirkulationspumpe','absenkung_solar',null,null,null,null,null]; var WW_ZIRK = ['tag','automatik','ferien','einmallauf_3min',null,null,null,null]; var SOL_BW1 = ['fehler_einst_hysterese','sp2_auf_max_temp','sp1_auf_max_temp','kollektor_auf_max_temp',null,null,null,null]; var SOL_BW2 = ['fehler_fuehler_arl_bypass','fehler_fuehler_sp_mitte_bypass','fehler_volumenstrom_wz','fehler_fuehler_rl_wz','fehler_fuehler_vl_wz','fehler_fuehler_sp2_unten','fehler_fuehler_sp1_unten','fehler_fuehler_kollektor']; var SOL_BW3 = ['usv_sp2_zu','usv_sp2_auf_oder_ladepumpe2','usv_bypass_zu','usv_bypass_auf','sekundaerpumpe_sp2_betrieb',null,null,null]; // UBA/EMS Bits (wandhängend) var UBA_STATUS = ['untergruppe_b0','untergruppe_b1','untergruppe_b2','hauptgruppe_b0','hauptgruppe_b1','hauptgruppe_b2','hauptgruppe_b3','blockierender_fehler_uba']; var UBA_HD = ['ww_anforderung','ein_aus_raumthermostat','anforderung_schnittstelle','frostschutz','pumpennachlauf_wegen_ww','ww_anforderung_fuehler','ww_anforderung_durchfluss','brenner_an']; var UBA_BETRZ = ['heizanforderung','ww_anforderung','jumper_11kw_entfernt','betriebstemperatur_betrieb','kesselschutz_taupunkt','verriegelt_service','blockiert','servicemeldung']; var UBA_REL1 = ['magnetventil_stufe1','magnetventil_stufe2','geblaese','zuendung','oelvorwaermung_abgasklappe','kesselkreispumpe_heizkreispumpe','3_wegeventil','ww_zirkulationspumpe']; var UBA_REL2 = ['ww_ladepumpe','fluessiggasventil','gwp_umwaelzpumpe',null,null,null,null,null]; var UBA_EMS_FEHLER = ['luftfuehler_fa_defekt','betriebstemperatur_nicht_erreicht','oelvorwaermer_dauersignal','oelvorwaermer_ohne_signal',null,null,null,null]; var UBA_FEHL_EINST = ['jumper_11kw_kaskade','bc10_notbetrieb','ww_poti_nicht_aut','kesselpoti_nicht_aut_90c','anforderung_klemme_wa',null,'kommunikation_vorhanden_fm458','keine_kommunikation_fm458']; // Kessel bodenstehend (0x88) – Brennerstatusbits (34) var KES_BRENNERSTATUS = ['abgastest','brenner_runter_aus','brenner_auto','brenner_1_stufe','unter_betrieb','leistung_frei','leistung_hoch','betriebsstunden_2_stufe']; // ---------- Schema-Bausteine ---------- function F(name, byte, type, unit, scale){ return {name:name, byte:byte, type:type, unit:unit, scale:scale}; } function Fb(name, byte, names){ return {name:name, byte:byte, type:'u8_bits', bitnames:names}; } function Fu24(name, b3,b2,b1, unit){ return {name:name, bytes:[b3,b2,b1], type:'u24', unit:unit}; } function Fu16be(name, b, unit, scale){ return {name:name, bytes:[b,b+1], type:'u16be', unit:unit, scale:scale}; } // ---------- Schemata ---------- // Heizkreise 0x80..0x83,0x8A..0x8E function HK_SCHEMA(){ return [ Fb('betriebswerte1',0,HK_BW1), Fb('betriebswerte2',1,HK_BW2), F('vorlauf_soll',2,'u8','°C'), F('vorlauf_ist',3,'u8','°C'), F('raum_soll',4,'u8','°C',0.5), F('raum_ist',5,'u8','°C',0.5), F('einschaltopt_min',6,'u8','min'), F('ausschaltopt_min',7,'u8','min'), F('pumpe',8,'u8','%'), F('stellglied',9,'u8','%'), Fb('hk_eingang',10,HK_EIN), // 11..17 „FREI/Heizkennlinie“ -> RAW deckt ab ]; } // Warmwasser 0x84 var SCHEMA_84 = [ Fb('betriebswerte1',0,WW_BW1), Fb('betriebswerte2',1,WW_BW2), F('ww_soll',2,'u8','°C'), F('ww_ist',3,'u8','°C'), F('einschaltopt_min',4,'u8','min'), Fb('pumpen_bits',5,WW_PUMP), // 6..7 reserviert/RAW Fb('zirkulationspumpe',8,WW_ZIRK), Fb('wf_eingaenge',9,['eingang2','eingang3',null,null,null,'handschalter_0','handschalter_hand','handschalter_aut']), F('start_ladung',10,'u8','°C'), F('ende_ladung',11,'u8','°C'), F('t_speicher_unten',12,'u8','°C'), F('t_waermetauscher',13,'u8','°C'), F('mischer_soll',14,'u8','%'), F('mischer_ist',15,'u8','%'), F('prim_pumpe',16,'u8','%'), F('sek_pumpe',17,'u8','%') ]; // Kessel bodenstehend 0x88 (repräsentative/benannte Felder + Brenner-Statusbits) var SCHEMA_88 = [ F('kessel_vl_soll',0,'u8','°C'), F('kessel_vl_ist',1,'u8','°C'), // 2..5 RAW / spezifisch pro ZM F('brenner_ansteuerung',8,'u8'), F('abgastemperatur',9,'u8','°C'), F('mod_brenner_stellglied',10,'u8','%'), F('mod_brenner_ist_leistung',11,'u8','%'), // 12..33 diverse Laufzeiten/Starts – RAW deckt Zahlen vollständig ab Fb('brenner_statusbits',34,KES_BRENNERSTATUS) ]; // Strategie Boden 0x8F (Auszug + RAW) var SCHEMA_8F = [ F('anlagen_vl_soll',0,'u8','°C'), F('vl_ist',1,'u8','°C'), F('rl_soll',2,'u8','°C'), F('rl_ist',3,'u8','°C') ]; // Konfiguration 0x89 (vollständig laut Tabelle) var SCHEMA_89 = [ F('aussentemperatur',0,'i8','°C'), F('aussentemperatur_gedaempft',1,'i8','°C'), // 2,3 Versionen (Direktmodus) -> RAW F('slot1',6,'u8'), F('slot2',7,'u8'), F('slot3',8,'u8'), F('slot4',9,'u8'), F('slotA',10,'u8'), // 12..16 Fehlermeldungen je Slot -> RAW zusätzlich vorhanden F('anlagen_vl_soll',18,'u8','°C'), F('anlagen_vl_ist',19,'u8','°C'), Fb('anlagen_flags',20,['puffer_bleibt_kalt','fuehler_ust_fk_defekt','wartezeit_laeuft',null,null,null,null,null]), F('max_pumpen_ansteuerung',21,'u8','%'), F('max_stellglied',22,'u8','%'), F('rg_vorlauf_ist',23,'u8','°C') ]; // LAP 0x90 var SCHEMA_90 = [ F('ww_soll',4,'u8','°C'), F('ww_ist',6,'u8','°C'), F('einschaltopt_min',8,'u8','min'), F('start_ladung',20,'u8','°C'), F('ende_ladung',21,'u8','°C'), F('ist_speicher_unten',24,'u8','°C'), F('ist_waermetauscher',26,'u8','°C'), F('mischer_soll',28,'u8','%'), F('mischer_ist',30,'u8','%'), F('primaerpumpe',32,'u8','%'), F('sekundaerpumpe',34,'u8','%') ]; // UBA (wandhängend) 0x92..0x99 var SCHEMA_UBA = [ F('soll_modulation',0,'u8','%'), F('ist_modulation',1,'u8','%'), Fu24('brennerstunden_h',2,3,4,'h'), F('brennerminuten',5,'u8','min'), F('kessel_vl_soll',6,'u8','°C'), F('kessel_vl_ist',7,'u8','°C'), F('ww_soll',8,'u8','°C'), F('ww_ist',9,'u8','°C'), F('antipendel_min',10,'u8','min'), Fb('betriebsflag_regelgeraet',11,['antipendel','keine_komm_kse',null,null,null,null,null,null]), Fb('betriebsflags_uba_kse',12,['umwaelzpumpe','schornsteinfeger','keine_komm_uba','keine_komm_kse','antipendel','umschaltventil_ww','abgaswaechter','pumpenschalter']), Fb('status_uba',13,UBA_STATUS), Fb('hd_mode_uba',14,UBA_HD), Fu24('brennerstarts',15,16,17,'count'), F('version_uba',18,'u8'), F('kim_nummer',19,'u8'), F('ruecklauf',20,'u8','°C'), F('pumpen_mod_uba',21,'u8','%'), Fb('anlagenfehler_ems_kessel',22,UBA_EMS_FEHLER), // 23: „ems ww“ in älterer Tabelle – als RAW ohnehin da {name:'ems_code1', byte:24, type:'ascii'}, {name:'ems_code2', byte:25, type:'ascii'}, F('fehlernummer_hi',26,'u8'), F('fehlernummer_lo',27,'u8'), Fb('brennertyp',28,['bit1','bit2','bit3','bit4','bit5','bit6','bit7','bit8']), // 29 zusammen mit 54 als u16be {name:'max_leistung_kw', bytes:[54,29], type:'u16be', unit:'kW'}, F('min_leistung_pct',30,'u8','%'), F('flammenstrom_uA',31,'u8','µA'), F('abgas_temp',32,'u8','°C'), F('ansaugluft',33,'i8','°C'), F('wasserdruck',34,'u8','bar'), Fb('brennerzustand',35,UBA_BETRZ), Fb('relais1',36,UBA_REL1), Fb('relais2',37,UBA_REL2), F('vl_soll_feuerungsautomat',38,'u8','°C'), F('ww_ladeart',39,'u8'), Fb('fehleinstellungen_ems_kessel',40,UBA_FEHL_EINST), Fb('ems_servicemeldungen',41,['keine_meldung',null,null,null,null,null,null,null]), // 42..53 Versions-/Kennbytes -> RAW F('betriebstemperatur_konst',55,'u8','°C') ]; // Wärmemenge 0x9B (Triplets/Zähler kommen zusätzlich als RAW/Triplets) var SCHEMA_9B = [ // einzelne Byte-Zähler exemplarisch – RAW (t..) liefert komplette Zahlen F('impulse_heute_hi',5,'u8'), F('impulse_heute_lo',6,'u8'), F('impulse_woche_hi',11,'u8'), F('impulse_woche_lo',12,'u8') ]; // Störmodul 0x9C / Unterstation 0x9D – Grundfelder + RAW var SCHEMA_9C = [ Fu16be('vl_soll_0_10v',0,'°C') ]; var SCHEMA_9D = [ Fu16be('anlagen_vorlauf',0,'°C'), Fu16be('vl_soll_0_10v',2,'°C'), F('zubringerpumpe',3,'u8','%') ]; // Solar 0x9E var SCHEMA_9E = [ Fb('betriebswerte1',0,SOL_BW1), Fb('betriebswerte2',1,SOL_BW2), Fb('betriebswerte3',2,SOL_BW3), {name:'kollektor_temp', bytes:[3,4], type:'u16be', unit:'°C', scale:0.1}, F('pumpe_speicher_mod',5,'u8','%'), F('t_sp1_unten',6,'u8','°C'), F('status_sp1',7,'u8'), F('t_sp2_unten',8,'u8','°C'), F('status_sp2',9,'u8'), F('t_sp_mitte_bypass',10,'u8','°C'), F('t_anl_rl_bypass',11,'u8','°C'), F('t_wmz_vorlauf',12,'u8','°C'), F('t_wmz_ruecklauf',13,'u8','°C'), Fu16be('volumenstrom_lh',14,'l/h'), Fu16be('leistung_w',16,'W'), Fu24('wm_sp1_100wh',18,19,20,'100Wh'), Fu24('wm_sp2_100wh',21,22,23,'100Wh'), Fu24('bh_sp1_min',24,25,26,'min'), F('ww_sollabsenkung_solar',27,'u8','K'), F('ww_sollabsenkung_kapaz',28,'u8','K'), F('kollektor_temp_1C',29,'u8','°C'), Fu24('bh_sp2_min',30,31,32,'min') ]; // Zuordnung Typ -> (Prefix, Schema) var TYPE_SCHEMAS = new Map([ [0x80,{prefix:'hk1', schema:HK_SCHEMA()}], [0x81,{prefix:'hk2', schema:HK_SCHEMA()}], [0x82,{prefix:'hk3', schema:HK_SCHEMA()}], [0x83,{prefix:'hk4', schema:HK_SCHEMA()}], [0x8A,{prefix:'hk5', schema:HK_SCHEMA()}], [0x8B,{prefix:'hk6', schema:HK_SCHEMA()}], [0x8C,{prefix:'hk7', schema:HK_SCHEMA()}], [0x8D,{prefix:'hk8', schema:HK_SCHEMA()}], [0x8E,{prefix:'hk9', schema:HK_SCHEMA()}], [0x84,{prefix:'warmwasser', schema:SCHEMA_84}], [0x88,{prefix:'kessel', schema:SCHEMA_88}], [0x8F,{prefix:'strategie_boden', schema:SCHEMA_8F}], [0x89,{prefix:'konfig', schema:SCHEMA_89}], [0x90,{prefix:'lap', schema:SCHEMA_90}], [0x92,{prefix:'uba1', schema:SCHEMA_UBA}], [0x93,{prefix:'uba2', schema:SCHEMA_UBA}], [0x94,{prefix:'uba3', schema:SCHEMA_UBA}], [0x95,{prefix:'uba4', schema:SCHEMA_UBA}], [0x96,{prefix:'uba5', schema:SCHEMA_UBA}], [0x97,{prefix:'uba6', schema:SCHEMA_UBA}], [0x98,{prefix:'uba7', schema:SCHEMA_UBA}], [0x99,{prefix:'uba8', schema:SCHEMA_UBA}], [0x9B,{prefix:'waermemenge', schema:SCHEMA_9B}], [0x9C,{prefix:'stoermodul', schema:SCHEMA_9C}], [0x9D,{prefix:'unterstation',schema:SCHEMA_9D}], [0x9E,{prefix:'solar', schema:SCHEMA_9E}] ]); // ---------- Decoder ---------- function decodeByField(buf, f){ var val; if (f.bytes){ if (f.type==='u24'){ val = u24(u8(buf,f.bytes[0]), u8(buf,f.bytes[1]), u8(buf,f.bytes[2])); } else if (f.type==='u16be'){ val = u16be(buf, f.bytes[0]); } else { val = 0; } } else { if (f.type==='u8') val = u8(buf,f.byte); else if (f.type==='i8') val = i8(buf,f.byte); else if (f.type==='ascii'){ var b=u8(buf,f.byte); val=(b>=32&&b<=126)?String.fromCharCode(b):''; } else if (f.type==='u8_bits'){ val = bits(u8(buf,f.byte), f.bitnames); } else if (f.type==='i16be'){ val = i16be(buf,f.byte); } else if (f.type==='u16be'){ val = u16be(buf,f.byte); } else val = u8(buf,f.byte); } if (typeof f.scale==='number') val = Number((val * f.scale).toFixed(2)); if (f.unit) return {v:val, unit:f.unit}; return val; } function decodeBlock(typ, buf){ var t = TYPE_SCHEMAS.get(typ); if (!t) return {}; var out = {}, i, f, decoded, key; for (i=0;i<t.schema.length;i++){ f = t.schema[i]; decoded = decodeByField(buf, f); key = f.name; if (f.type==='u8_bits') put(out, t.prefix+'/'+key, decoded); else if (decoded && typeof decoded==='object' && decoded.v!==undefined && f.unit){ put(out, t.prefix+'/'+key, decoded.v); put(out, t.prefix+'/'+key+'@unit', f.unit); } else { put(out, t.prefix+'/'+key, decoded); if (f.unit) put(out, t.prefix+'/'+key+'@unit', f.unit); } } return out; } // ---------- Reassembly ---------- var blocks = new Map(); var seen = new Map(); // typ -> lastTs TYPE_SCHEMAS.forEach(function(_, typ){ blocks.set(typ, new Uint8Array(60)); }); // ---------- CAN ---------- var channel; try{ channel = can.createRawChannel(CAN_IF, true); channel.addListener('onMessage', function (msg){ if ((msg.id & 0x7FF) !== 0x400) return; var d = msg.data; if (!d || d.length<2) return; var typ = d[0], off = d[1], buf = blocks.get(typ); if (!buf) return; for (var i=2, j=0; i<d.length && (off+j)<buf.length; i++, j++) buf[off+j]=d[i]; seen.set(typ, Date.now()); }); channel.start(); console.error('[CAN] listening on '+CAN_IF+' (ID 0x400)'); }catch(e){ console.error('[CAN] open failed: '+e.message); process.exit(1); } // ---------- MQTT ---------- var client = mqtt.connect(MQTT_URL, { reconnectPeriod:3000 }); client.on('connect', function(){ console.error('[MQTT] connected '+MQTT_URL); }); client.on('error', function(err){ console.error('[MQTT] error: '+err.message); }); setInterval(function(){ TYPE_SCHEMAS.forEach(function(tdef, typ){ if (PUBLISH_ONLY_SEEN){ var last = seen.get(typ); if (!last) return; if (STALE_MS>0 && (Date.now()-last)>STALE_MS) return; } var buf = blocks.get(typ); if (!buf) return; // 1) Sprechende Felder publishTree(decodeBlock(typ, buf)); var name = tname(typ); // 2) Block JSON if (PUBLISH_BLOCK_JSON){ var payload = { ts: ts(), type: typ, type_name: name, length: buf.length, bytes: Array.from(buf) }; client.publish(MQTT_BASE+'/blocks/'+typ.toString(16), JSON.stringify(payload), {retain:RETAIN, qos:QOS}); } // 3) RAW bytes if (PUBLISH_RAW_BYTES){ for (var b=0;b<60;b++){ client.publish(MQTT_BASE+'/raw/'+name+'/b'+String(b).padStart(2,'0'), String(buf[b]), {retain:RETAIN, qos:QOS}); } } // 4) Wörter u16be if (PUBLISH_WORDS){ for (var w=0; w<=58; w+=2){ client.publish(MQTT_BASE+'/raw/'+name+'/w'+String(w).padStart(2,'0'), String(u16be(buf,w)), {retain:RETAIN, qos:QOS}); } } // 5) Triplets u24 if (PUBLISH_TRIPLETS){ for (var t=0; t<=57; t++){ client.publish(MQTT_BASE+'/raw/'+name+'/t'+String(t).padStart(2,'0'), String(u24(buf[t],buf[t+1],buf[t+2])), {retain:RETAIN, qos:QOS}); } } }); client.publish(MQTT_BASE+'/heartbeat', String(Date.now()), {retain:RETAIN, qos:QOS}); }, 2000); function publishTree(tree, base){ base = base || MQTT_BASE; for (var k in tree){ if (!Object.prototype.hasOwnProperty.call(tree,k)) continue; var v = tree[k]; if (v && typeof v==='object' && !Array.isArray(v)) publishTree(v, base+'/'+k); else client.publish(base+'/'+k, String(v), {retain:RETAIN, qos:QOS}); } } process.on('SIGINT', function(){ try{ channel.stop(); }catch(e){} try{ client.end(); }catch(e){} process.exit(0); });
Funkioniert, wenn man die Can Verbindung zum EcoCan Anschluss unter Can0 hergestellt hat.