<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Wolf boilers on ebusd: solved — read and write]]></title><description><![CDATA[<h1>Wolf boilers on ebusd: the checksum is solved — read <em>and</em> write every register</h1>
<p dir="auto"><strong>TL;DR</strong> — The mystery byte in a Wolf read telegram is a CRC8 with polynomial <code>0x5c</code> over the<br />
low byte of the TelegramNr, XORed with the high byte. That was the last thing standing between<br />
Wolf's own published parameter database and a complete ebusd config. You no longer need to<br />
copy magic bytes out of somebody else's <code>08.csv</code>. There is a script below that generates a<br />
config for <strong>any</strong> of 48 Wolf device templates. Writes work too, and the write format is<br />
different from the read format in a way that explains why so many people's <code>w</code> rows do nothing.</p>
<p dir="auto">Tested on a <strong>Wolf FGB-K-28</strong> (ebusd 25.1, eBUS shield, ioBroker). 22 of 22 generated IDs are<br />
byte-identical to a hand-made config that has worked for years, plus 30 new registers that were<br />
never readable before. Please try it on your CGB / CGB-2 / CGG-2 / COB / CHA / BM-2 and report<br />
back — the derivation is device-independent but I can only prove it on the hardware I own.</p>
<hr />
<h2>1. Why this has been hard</h2>
<p dir="auto">Wolf publishes no eBUS documentation. Every Wolf config in the wild is hand-made, partial, and<br />
propagated by copy-paste. <code>--scanconfig</code> can never help: a Wolf boiler answers the scan with<br />
<code>MF=65; ID=" &amp;"</code>, ebusd does not know manufacturer <code>0x65</code>, and the ID looks like line noise.</p>
<p dir="auto">Reads are <code>ZZ=08</code>, <code>PBSB=5022</code>, three ID bytes:</p>
<pre><code>r,heating,dhw_temp,,,08,5022,cc0e00,...
                            ^^^^^^
                            ?? 0e 00      0x000e = 14 = TelegramNr
</code></pre>
<p dir="auto">The last two bytes are obviously the TelegramNr little-endian. The <strong>first byte</strong> is the problem.<br />
It is different for every register, the forums call it "the CRC", and the accepted workaround has<br />
been: if nobody has published the byte for the register you want, you cannot read that register.</p>
<h2>2. The checksum</h2>
<p dir="auto">It is <strong>linear over GF(2)</strong>, which is what makes it recoverable. Take known-good (byte, TelegramNr)<br />
pairs out of any working Wolf <code>08.csv</code>, treat each output bit as a linear equation over the 16<br />
index bits plus a constant, and solve. On 22 verified pairs that is heavily over-determined and<br />
gives a unique answer, whose bit-effects are exactly:</p>
<pre><code class="language-js">function wolfCrc(telegramNr) {
    let c = telegramNr &amp; 0xff;
    for (let i = 0; i &lt; 8; i++)
        c = (c &amp; 0x80) ? (((c &lt;&lt; 1) &amp; 0xff) ^ 0x5c) : ((c &lt;&lt; 1) &amp; 0xff);
    return (c ^ (telegramNr &gt;&gt; 8)) &amp; 0xff;
}

const hx = n =&gt; n.toString(16).padStart(2, '0');
const readId = tn =&gt; hx(wolfCrc(tn)) + hx(tn &amp; 0xff) + hx((tn &gt;&gt; 8) &amp; 0xff);
</code></pre>
<p dir="auto"><strong>A CRC8 chain with polynomial <code>0x5c</code>, init 0, over the LOW byte only — then the HIGH byte is<br />
XORed in and never shifted.</strong> That last part is why it is not any textbook CRC8 and why<br />
table-driven guesses fail.</p>
<p dir="auto">Checks that matter:</p>
<ul>
<li>It reproduces <strong>22 of 22</strong> IDs in a config that has worked for years.</li>
<li>The linear solve leaves index bits 10–15 <strong>unconstrained</strong> (no training sample used them, since<br />
all known registers were &lt; 1024). The poly-<code>0x5c</code> form pins them as identity on the high byte.<br />
That extrapolation is confirmed on real hardware: TelegramNr <strong>10010</strong> (high byte <code>0x27</code>, far<br />
outside the training set) generates <code>771a27</code> and reads <strong>1.80 bar</strong> of system pressure on my<br />
boiler. Same for 10013, 10067, 10068, 65019.</li>
<li>TelegramNr 554 and 644 both produce <code>de</code> — a genuine collision, because <code>0x2a</code> and <code>0x84</code> shift<br />
to the same value. The high byte disambiguates them. If you have been confused by two registers<br />
sharing a first byte, that is why.</li>
</ul>
<p dir="auto"><strong>Bonus:</strong> running this over an existing config is a good way to find typos. It flagged one in<br />
mine — <code>hg74</code> had <code>d5</code> where the algorithm says <code>91</code>. It had never worked and nobody knew why.</p>
<h2>3. Writes — and why yours probably do nothing</h2>
<p dir="auto">Writes use a <strong>different</strong> address and a <strong>different</strong> ID format:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>Read</th>
<th>Write</th>
</tr>
</thead>
<tbody>
<tr>
<td>Destination</td>
<td><code>ZZ=08</code> (slave)</td>
<td><strong><code>ZZ=03</code> (master)</strong></td>
</tr>
<tr>
<td>PB SB</td>
<td><code>5022</code></td>
<td><code>5023</code></td>
</tr>
<tr>
<td>First ID byte</td>
<td>the checksum</td>
<td><strong><code>00</code> — writes are not checksummed</strong></td>
</tr>
<tr>
<td>Payload</td>
<td>—</td>
<td>value, then a 4-byte suffix <strong><code>9d010000</code></strong></td>
</tr>
</tbody>
</table>
<pre><code>w,heating,dhw_target_set,Warmwassersolltemperatur eingestellt,,03,5023,001300,dhw_target_set,,SIN,10,°C,,suffix,,HEX:4,=9d010000,,
</code></pre>
<p dir="auto">Two things people get wrong:</p>
<ol>
<li><strong>The destination is the master address <code>03</code>, not <code>08</code>.</strong> I had this recorded as "the write<br />
rows are mis-addressed" — that was wrong, and it had simply never been tested. Sending the same<br />
write to <code>ZZ=08</code> gives <code>ERR: read timeout</code>.</li>
<li><strong><code>ebusctl write</code> printing <code>empty</code> is SUCCESS, not an error.</strong> A Wolf write is a master-master<br />
telegram with no data response. <code>empty</code> is what a successful one looks like.</li>
</ol>
<p dir="auto">Proof, on a deliberately harmless register (<code>hg07</code>, heating pump run-on, valid range 0–30 min):</p>
<pre><code>$ ebusctl read -m 0 -c heating hg07
2
$ ebusctl write -c heating hg07 3
empty
$ ebusctl read -m 0 -c heating hg07
3
</code></pre>
<p dir="auto">And the one that matters — the DHW setpoint, TelegramNr 19, write ID <code>001300</code>:</p>
<pre><code>before: 45.0
empty
after:  50.0
live:   50.0      &lt;- TelegramNr 3, the derived setpoint, followed
</code></pre>
<p dir="auto"><strong>Test writes on something harmless first.</strong> <code>hg07</code> or <code>hg09</code> are ideal: in-range values change<br />
nothing you will notice, and they read straight back.</p>
<h2>4. Getting the register list for <em>your</em> Wolf device</h2>
<p dir="auto">You do not have to guess which TelegramNrs exist. Wolf's own SmartSet/ISM7 metadata is public<br />
inside <a href="https://github.com/zivillian/ism7mqtt" rel="nofollow ugc"><code>zivillian/ism7mqtt</code></a> — <code>parameter.xml</code>,<br />
<code>converter.xml</code>  and <code>device.xml</code>  under <code>src/ism7mqtt/Resources/</code>. Between them they give, for<br />
every Wolf device: TelegramNr, data type, unit, min/max, and the value labels for enums.</p>
<p dir="auto"><code>wolf-registers.js</code> (attached) downloads them and does the join:</p>
<pre><code>$ node wolf-registers.js --list
  DTID    WRSDeviceIds  params  Name
  30000   0x18          80      BM
  220000  0x20          139     BM-2
  80000   0x6           45      CGB
  180000  0x21          97      CGB-2
  100000  0x1D          53      CGG-2
  110000  0x1C          53      CGU-2
  270000  0x29          178     CHA
  90000   0x1A          49      COB
  250000  0x26          64      FGB
  ...48 templates
</code></pre>
<p dir="auto"><strong>Which one are you?</strong> Look at your ebusd scan string. Mine says <code>MF=65; ID=" &amp;"</code>. Those ID bytes<br />
are <code>0x20 0x26</code> — and <code>&amp;</code> <strong>is</strong> <code>0x26</code>, which is exactly the <code>WRSDeviceIds</code> of the <code>FGB</code><br />
template. The "garbage" ID is not garbage; it contains your device id. Match it against the list.</p>
<pre><code>$ node wolf-registers.js --device FGB
TelegramNr  readID   writeID  Type    Unit   Range        Name
13          280d00   000d00   SS10    °C                  Kesseltemperatur
14          cc0e00   000e00   SS10    °C                  Warmwassertemperatur
19          541300   001300   SS10    °C     30..80       Warmwassersolltemperatur eingestellt
270         cd0e01   000e01   SS100          0..30        Heizkurve
10010       771a27   001a27   SS100   bar                 Anlagendruck
...

$ node wolf-registers.js --device FGB --csv --write &gt; 08-generated.csv
</code></pre>
<p dir="auto">Type mapping: <code>SS10</code>→<code>SIN</code> divider 10, <code>SS100</code>→<code>SIN</code> divider 100, <code>US10</code>→<code>UIN</code> divider 10,<br />
<code>US</code>→<code>UIN</code>, <code>SS</code>→<code>SIN</code>.</p>
<h2>5. Things that cost me hours</h2>
<p dir="auto"><strong><code>ebusctl read -h</code>  only works for messages ebusd ALREADY KNOWS.</strong> It is not a raw frame sender.<br />
An unknown ID returns <code>ERR: element not found</code> without touching the bus. So you cannot use it to<br />
probe for new registers, and "a wrong checksum is rejected" proves nothing about your boiler — it<br />
only proves ebusd's message table did not match.</p>
<p dir="auto"><strong><code>ebusctl hex</code> and <code>read -def</code> need <code>--enablehex</code> / <code>--enabledefine</code>.</strong> Off by default.</p>
<p dir="auto"><strong>Validate a config offline, as a normal user, before installing it:</strong></p>
<pre><code>ebusd --checkconfig --configpath=/home/you/ebustest/ --device=ens:127.0.0.1:59999
</code></pre>
<p dir="auto">Copy your config tree somewhere writable, edit, check. It prints <code>found messages: N (... M poll)</code><br />
and exits 0 without touching the running daemon or the bus.</p>
<p dir="auto"><strong>Do not force-read many registers back to back.</strong> I ran 32 sequential <code>read -m 0</code> calls and<br />
roughly every other one came back empty, with the symbol rate spiking to 102. It looks exactly<br />
like "this register is unsupported" and it is not. Read the <strong>poll cache</strong> instead —<br />
<code>ebusctl find -a -c heating</code> — giving it a couple of minutes after <code>ebusctl reload</code> to refill.</p>
<p dir="auto"><strong>Adding a write row whose name matches an existing read row does NOT increase <code>messages:</code>.</strong><br />
ebusd merges them into one message with two directions. Check with <code>ebusctl find -w -c heating</code>,<br />
not the count. I briefly misread this as a rejected row.</p>
<p dir="auto"><strong>Polling more costs nothing.</strong> I went from 28 to 60 polled messages and the bus symbol rate<br />
stayed at <strong>12</strong>, exactly as before. Poll priority (<code>r1</code>…<code>r9</code>) controls how often each is polled,<br />
not how much traffic there is in total.</p>
<p dir="auto"><strong>Values reading <code>32768</code> or <code>3276.8</code> are the eBUS null sentinel</strong> — "this register does not exist<br />
on this device". Six of mine had been dead for years. Their TelegramNrs (39, 321, 362, 365, 368)<br />
simply are not in the FGB parameter list; they had been copied from a different Wolf model. No<br />
config change will ever fix those, and now you can tell the difference between "wrong byte" and<br />
"not present" by checking the generated list.</p>
<h2>6. ioBroker integration</h2>
<p dir="auto"><strong>MQTT.</strong> ebusd's own MQTT integration publishes reads fine:</p>
<pre><code>--mqttint=/etc/ebusd/mqtt-integration.cfg --mqtthost=127.0.0.1 --mqttport 1883 ...
</code></pre>
<p dir="auto">Note it ships <code>filter-direction = r|u</code>, i.e. <strong>read-only</strong> — it will not accept writes. To write<br />
from a script, shell out:</p>
<pre><code class="language-js">exec('ebusctl write -c heating dhw_target_set 50', () =&gt; {
    exec('ebusctl read -m 0 -c heating dhw_target_set', (e, out) =&gt; log('now ' + out));
});
</code></pre>
<p dir="auto">Always read back and verify. A write that silently does nothing is the failure mode here.</p>
<p dir="auto"><strong>Poll priority gotcha.</strong> If you use the Home Assistant MQTT profile, <code>filter-seen = 5</code> silently<br />
promotes matched messages to poll priority 5. Remove it and everything goes to <code>no data stored</code><br />
because nothing is polled any more. Declare priority in the CSV instead (<code>r5,heating,...</code>).</p>
<p dir="auto"><strong>Two scripts are attached:</strong></p>
<ul>
<li><code>boiler_status_en.js</code> — turns the raw enum numbers into readable states under<br />
<code>0_userdata.0.heating.boiler.*</code>: <code>status</code>, <code>mode</code>, <code>burner_status</code>, <code>firing</code>, <code>fault</code>, <code>alive</code>,<br />
plus a one-line <code>summary</code> like <code>Firing at 28% — Heating mode</code>. <code>fault</code> is the one worth<br />
alarming on: true when <code>burner_status</code> is 12 (Störung) or the safety limiter has tripped.<br />
<code>alive</code> watches staleness across several polled registers, because the boiler powers the eBUS —<br />
so a dead bus means a dead boiler.</li>
<li><code>boiler_aliases.js</code> — a stable <code>alias.0.boiler.*</code> layer so renaming an ebusd message does not<br />
break every widget and chart. It refuses to create an alias whose source does not exist and<br />
tells you which ones to fix.</li>
</ul>
<p dir="auto"><strong>Useful enum decodings</strong> (from Wolf's own <code>KeyValueList</code>, FGB — check yours with the tool):</p>
<pre><code>burner_status  0 Aus · 1 Ruhekontakt · 2 Vorspülen · 3 Zünden · 4 Stabilisierung · 5 Ein ·
               6 Ventilprüfung · 7 Nachspülen · 8 Softstart · 9 Taktsperre ·
               10 Betrieb ohne Brenner · 11 Abgasklappe · 12 Störung · 13 Gradienten Überwachung ·
               14 Gasdruck · 15 Spreizung hoch · 16 Spreizung KF
operating_mode 0 Test · 1 Start · 2 Frost Heizkreis · 3 Frost Warmwasser · 4 Schornsteinfeger ·
               5 Kombibetrieb · 6 Parallelbetrieb · 7 Warmwasser · 8 Warmwassernachlauf ·
               9 Mindest-Kombizeit · 10 Heizbetrieb · 11 Nachlauf Heizkreispumpe ·
               12 Frostschutz · 13 Standby · 14 Kaskadenbetrieb · 15 GLT-Betrieb ·
               16-19 Kalibration · 20 WW Schnellstart · 21 Externe Deaktivierung
program (10100) 1 Aus · 3 Winterbetrieb · 5 Sommerbetrieb
TelegramNr 370 bit 3          = Brenner
TelegramNr 371 bits 0,1,2,4,5 = Zündung, Ventil 1, Ventil 2, Kesselkreispumpe, 3-Wege-Ventil
</code></pre>
<h2>7. Safety</h2>
<p dir="auto">This is a gas appliance. A few things I would not do:</p>
<ul>
<li><strong>Do not write to a register you have not looked up.</strong> The generated table gives the valid<br />
range; stay inside it. Writing outside a documented range is not something I have tested and<br />
not something I would.</li>
<li><strong>Prove writes on a harmless parameter first</strong> (<code>hg07</code>, <code>hg09</code>), read back, restore.</li>
<li><strong>Do not try to make ebusd the heating controller.</strong> Emulating the controller broadcast means<br />
repeating it forever, and if your automation dies in winter so does your frost protection. For<br />
simple on/off demand a relay across the room-thermostat contact fails safe; this does not.</li>
<li><strong>Back up <code>08.csv</code>  <a href="/assets/uploads/files/1790715679712-boiler_aliases.js">boiler_aliases.js</a> <a href="/assets/uploads/files/1790715679715-boiler_status_en.js">boiler_status_en.js</a> <a href="/assets/uploads/files/1790715679717-wolf-registers.js">wolf-registers.js</a> before every change</strong> and keep <code>ebusd --checkconfig</code> in the loop.</li>
<li>Writes change settings <strong>in the boiler</strong>, persistently. They survive a reboot of everything<br />
else. Write down what you changed.</li>
</ul>
<h2>8. What it got me</h2>
<p dir="auto">A Wolf FGB-K-28 that exposed 28 registers, six of them permanently dead, now exposes 60 — at<br />
identical bus load. New and working:</p>
<p dir="auto"><strong>Anlagendruck</strong> (system pressure, bar) · <strong>Abgastemperatur</strong> (flue gas) · <strong>Modulationsgrad</strong> ·<br />
<strong>Brennerstatus</strong> (17 states) · <strong>Betriebsart</strong> (20 modes) · <strong>Brenner</strong>, <strong>Zündung</strong>,<br />
<strong>Ventil 1/2</strong>, <strong>Kesselkreispumpe</strong>, <strong>3-Wege-Ventil</strong> (six real digital signals) ·<br />
<strong>Drehzahl Kesselkreispumpe / ZHP WW</strong> · <strong>Heizkurve</strong> · <strong>Firmware</strong> · <strong>Anlagendruck</strong> ·<br />
<strong>Auslauftemperatur Warmwasser</strong> · <strong>Sicherheitstemperaturbegrenzer</strong> · <strong>Anzahl Netz-Starts</strong> ·<br />
and roughly fifteen more.</p>
<p dir="auto">Plus working writes, which turned into an automated weekly DHW disinfection cycle and a<br />
vacation setback.</p>
<p dir="auto">Three errors found in a config that had been running for years: a wrong checksum on <code>hg74</code>,<br />
<code>hg75</code>  typed as minutes when TelegramNr 364 is <strong>litres per minute</strong> (it is the DHW flow rate),<br />
and six registers that do not exist on this device at all.</p>
<hr />
<h3>Files</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>wolf-registers.js</code></td>
<td>generates the register table / ebusd CSV for any Wolf device</td>
</tr>
<tr>
<td><code>boiler_status_en.js</code></td>
<td>ioBroker: decoded status states + fault/alive flags</td>
</tr>
<tr>
<td><code>boiler_aliases.js</code></td>
<td>ioBroker: stable <code>alias.0.boiler.*</code>  layer</td>
</tr>
</tbody>
</table>
<h3>Credit</h3>
<p dir="auto">The parameter database is <a href="https://github.com/zivillian/ism7mqtt" rel="nofollow ugc">zivillian/ism7mqtt</a>'s extraction<br />
of Wolf's SmartSet metadata — without it none of this is possible. ebusd is<br />
<a href="https://github.com/john30/ebusd" rel="nofollow ugc">john30/ebusd</a>. The forum threads that got me oriented are<br />
<a href="https://github.com/john30/ebusd/discussions/779" rel="nofollow ugc">ebusd discussion #779</a> and <a href="https://github.com/john30/ebusd/discussions/721" rel="nofollow ugc">#721</a>.</p>
<p dir="auto">Corrections welcome, especially from anyone who can confirm or refute the checksum on a non-FGB<br />
Wolf device.</p>
]]></description><link>https://forum.iobroker.net/topic/85470/wolf-boilers-on-ebusd-solved-read-and-write</link><generator>RSS for Node</generator><lastBuildDate>Wed, 30 Sep 2026 21:14:06 GMT</lastBuildDate><atom:link href="https://forum.iobroker.net/topic/85470.rss" rel="self" type="application/rss+xml"/><pubDate>Tue, 29 Sep 2026 21:05:51 GMT</pubDate><ttl>60</ttl></channel></rss>