<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Mqtt on Andy Little</title><link>https://andylittle.net/tags/mqtt/</link><description>Recent content in Mqtt on Andy Little</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 18 Sep 2026 07:00:00 -0700</lastBuildDate><atom:link href="https://andylittle.net/tags/mqtt/index.xml" rel="self" type="application/rss+xml"/><item><title>Two Drivers, One weewx: Adding MQTT Sensors Next to the Weather Station</title><link>https://andylittle.net/blog/2026/weewx-metadriver-mqtt-second-driver/</link><pubDate>Fri, 18 Sep 2026 07:00:00 -0700</pubDate><guid>https://andylittle.net/blog/2026/weewx-metadriver-mqtt-second-driver/</guid><description>weewx runs exactly one station driver. I wanted the Vevor station&amp;rsquo;s data and an indoor sensor that only exists as MQTT messages from rtl_433. weewx-metadriver runs both drivers side by side, and an old MQTT driver needed a rewrite before it would work with weewx 5 and paho-mqtt 2.</description><content:encoded><![CDATA[<p>In the <a href="/blog/2026/weewx-interceptor-isolated-weather-station/">last post</a> a
Raspberry Pi tricked a Vevor weather station into uploading to it, and
<a href="https://weewx.com/">weewx</a> has been recording the outdoor readings since. The
Vevor console sends nothing about indoors, though. Meanwhile an rtl_433 box in
the house already receives a Fine Offset WH25 indoor sensor and publishes its
temperature, humidity and pressure to the house MQTT broker, one value per
topic:</p>
<pre tabindex="0"><code>rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/temperature_C  =&gt; 24.3
rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/humidity       =&gt; 46
rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/pressure_hPa   =&gt; 1010.1
</code></pre><p>I wanted those three numbers in the same weewx database as the outdoor data,
so the reports show <code>inTemp</code>, <code>inHumidity</code> and station <code>pressure</code> next to
everything else.</p>
<p>The catch: weewx loads exactly one station driver. <code>station_type</code> names it,
and that driver&rsquo;s <code>genLoopPackets()</code> is the only source of loop packets. The
interceptor is already that driver.</p>
<h2 id="metadriver">MetaDriver</h2>
<p>Tom Keffer, who writes weewx, published
<a href="https://github.com/tkeffer/weewx-metadriver">weewx-metadriver</a>. It is a
driver whose job is to run other drivers. You list the station types you want,
it loads each one with the normal <code>loader()</code> call, runs each on its own
thread, and merges their loop packets into one queue that the engine reads
from. The first driver in the list is the <em>primary</em>: its archive records and
clock functions are the ones weewx uses. The others only contribute loop
packets. Every packet gets a <code>source</code> key naming the child it came from.</p>
<p>Installation is the usual extension install:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">~/weewx-venv/bin/python ~/weewx/src/weectl.py extension install --yes <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://github.com/tkeffer/weewx-metadriver/archive/refs/heads/master.zip
</span></span></code></pre></div><p>I skipped the suggested <code>weectl station reconfigure</code> and edited <code>weewx.conf</code>
by hand, so nothing else in the file moved:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Station]</span>
</span></span><span class="line"><span class="cl">    <span class="na">station_type</span> <span class="o">=</span> <span class="s">MetaDriver</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[MetaDriver]</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The first entry is the primary station.</span>
</span></span><span class="line"><span class="cl">    <span class="na">station_types</span> <span class="o">=</span> <span class="s">Interceptor, wxMesh
</span></span></span><span class="line"><span class="cl"><span class="s">    driver = user.metadriver</span>
</span></span></code></pre></div><p>The <code>[Interceptor]</code> stanza stays exactly as it was. <code>record_generation = software</code> in <code>[StdArchive]</code> stays too. That&rsquo;s what makes this work: with
software record generation, weewx builds each archive record by accumulating
every loop packet from the interval, whichever child it came from. The WH25
values and the Vevor values land in the same record.</p>
<p>Two things I checked before trusting it. The string <code>source</code> key goes through
the accumulator like any other field, and weewx 5.5&rsquo;s scalar accumulator
quietly converts non-numeric values to <code>None</code> rather than crashing. And
<code>[StdConvert] target_unit = US</code> converts each loop packet before it is
accumulated, so one child sending metric and the other sending US units is
fine.</p>
<h2 id="an-mqtt-driver-from-2016">An MQTT driver from 2016</h2>
<p>For the MQTT side I used
<a href="https://github.com/morrowwm/weewxMQTT">weewxMQTT</a>, a small driver called
<code>wxMesh</code> that subscribes to a topic and turns messages into loop packets. It
is from the weewx 3 and 4 era and hasn&rsquo;t been updated, which showed up in
several ways:</p>
<ul>
<li><strong>The installer doesn&rsquo;t run.</strong> <code>install.py</code> imports <code>setup</code>, which weewx 5
no longer provides, and its config block isn&rsquo;t valid Python. So I copied
<code>bin/user/wxMesh.py</code> into <code>~/weewx-data/bin/user/</code> by hand and wrote the
stanza myself.</li>
<li><strong>paho-mqtt 2.x rejects the old constructor.</strong> <code>pip install paho-mqtt</code>
gives you 2.1.0 today, and <code>mqtt.Client(client_id=...)</code> raises
<code>ValueError: Unsupported callback API version</code> unless you pass
<code>CallbackAPIVersion.VERSION1</code> first.</li>
<li><strong>A broker outage at start-up kills it for good.</strong> The driver called
<code>client.connect()</code> in <code>__init__</code>, which raises if the broker is down.
MetaDriver logs a failed secondary child and drops it. It never retries.</li>
<li><strong>No <code>dateTime</code> unless the message carries one.</strong> weewx services index
packets by <code>dateTime</code>; a packet without it crashes them.</li>
<li><strong>Wrong message format for rtl_433.</strong> The driver wants one message with a
payload like <code>temp:24.3,humidity:46</code>. rtl_433 publishes a bare number per
topic. Feeding it <code>24.3</code> makes <code>datum.split(&quot;:&quot;)</code> fail to unpack, which
ends the generator, which MetaDriver treats as a dead child.</li>
</ul>
<p>The last point is the interesting one. In the original design the payload&rsquo;s
keys name the observations. With rtl_433 the <em>topic</em> is the key.</p>
<h2 id="the-rewrite">The rewrite</h2>
<p>I kept the driver&rsquo;s shape and rewrote the internals. The upstream file stays
next to it as <code>wxMesh.py.orig</code>. The parts that matter:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">kwargs</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">client_id</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">client_id</span><span class="p">,</span> <span class="n">clean_session</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">protocol</span><span class="o">=</span><span class="n">mqtt</span><span class="o">.</span><span class="n">MQTTv311</span><span class="p">,</span> <span class="n">transport</span><span class="o">=</span><span class="s2">&#34;tcp&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">mqtt</span><span class="p">,</span> <span class="s1">&#39;CallbackAPIVersion&#39;</span><span class="p">):</span>      <span class="c1"># paho-mqtt &gt;= 2.0</span>
</span></span><span class="line"><span class="cl">    <span class="bp">self</span><span class="o">.</span><span class="n">client</span> <span class="o">=</span> <span class="n">mqtt</span><span class="o">.</span><span class="n">Client</span><span class="p">(</span><span class="n">mqtt</span><span class="o">.</span><span class="n">CallbackAPIVersion</span><span class="o">.</span><span class="n">VERSION1</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="bp">self</span><span class="o">.</span><span class="n">client</span> <span class="o">=</span> <span class="n">mqtt</span><span class="o">.</span><span class="n">Client</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">reconnect_delay_set</span><span class="p">(</span><span class="n">min_delay</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">max_delay</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">connect_async</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">host</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">port</span><span class="p">,</span> <span class="mi">60</span><span class="p">)</span>   <span class="c1"># never raises here</span>
</span></span><span class="line"><span class="cl"><span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">loop_start</span><span class="p">()</span>
</span></span></code></pre></div><p><code>connect_async()</code> hands the connection to paho&rsquo;s network thread, which
retries with back-off if the broker isn&rsquo;t there. Subscribing happens in
<code>on_connect</code>, so a reconnect re-subscribes too.</p>
<p>Each message is parsed one of two ways. If the payload contains a colon it is
the classic <code>key:value,key:value</code> form. Otherwise the topic is the key and the
payload is the value:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_parse</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">topic</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="s1">&#39;:&#39;</span> <span class="ow">in</span> <span class="n">payload</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">datum</span> <span class="ow">in</span> <span class="n">payload</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">key</span><span class="p">,</span> <span class="n">sep</span><span class="p">,</span> <span class="n">value</span> <span class="o">=</span> <span class="n">datum</span><span class="o">.</span><span class="n">partition</span><span class="p">(</span><span class="s2">&#34;:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">sep</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">data</span><span class="p">[</span><span class="n">key</span><span class="o">.</span><span class="n">strip</span><span class="p">()]</span> <span class="o">=</span> <span class="n">value</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">data</span><span class="p">[</span><span class="n">topic</span><span class="p">]</span> <span class="o">=</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">data</span>
</span></span></code></pre></div><p>Turning that into a packet uses the <code>label_map</code> from the config. A key is kept
if the map knows it, or if it already is a weewx observation name. Anything
else is dropped. That matters with a wildcard subscription, because rtl_433
also publishes <code>battery_ok</code>, <code>mic</code>, <code>id</code> and a <code>time</code> string under the same
prefix, and none of those belong in a loop packet.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_to_packet</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">packet</span> <span class="o">=</span> <span class="p">{</span><span class="s1">&#39;usUnits&#39;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">units</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">key</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">label_map</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">name</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">label_map</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">weewx</span><span class="o">.</span><span class="n">units</span><span class="o">.</span><span class="n">obs_group_dict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">name</span> <span class="o">=</span> <span class="n">key</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">packet</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">pass</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="p">[</span><span class="n">k</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">packet</span> <span class="k">if</span> <span class="n">k</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="s1">&#39;usUnits&#39;</span><span class="p">,</span> <span class="s1">&#39;dateTime&#39;</span><span class="p">)]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="n">ts</span> <span class="o">=</span> <span class="n">packet</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;dateTime&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">packet</span><span class="p">[</span><span class="s1">&#39;dateTime&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span> <span class="k">if</span> <span class="n">ts</span> <span class="k">else</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">packet</span>
</span></span></code></pre></div><p>A packet with only one field is completely normal for weewx. The accumulator
takes whatever each packet has. So each rtl_433 message becomes its own tiny
packet, and the archive record at the end of the interval has all three
values.</p>
<p>The rest of the changes are small: a <code>units</code> option instead of a hard-coded
unit system, a <code>port</code> option, the configured client id actually being used,
<code>topic</code> accepting a list, and logging through weewx&rsquo;s logger so the messages
show up under <code>journalctl -u weewx</code> with the rest.</p>
<h2 id="the-stanza">The stanza</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[wxMesh]</span>
</span></span><span class="line"><span class="cl">    <span class="na">driver</span> <span class="o">=</span> <span class="s">user.wxMesh
</span></span></span><span class="line"><span class="cl"><span class="s">    host = &lt;broker-address&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">    port = 1883
</span></span></span><span class="line"><span class="cl"><span class="s">    username = &lt;user&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">    password = &lt;password&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">    client = weewx-pi3-1
</span></span></span><span class="line"><span class="cl"><span class="s">    # Quote these. An unquoted # starts a comment in weewx.conf.
</span></span></span><span class="line"><span class="cl"><span class="s">    topic = &#34;rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/#&#34;, &#34;weewx/loop&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">    units = METRIC          # rtl_433 sends degC and hPa
</span></span></span><span class="line"><span class="cl"><span class="s">    poll_interval = 2
</span></span></span><span class="line"><span class="cl"><span class="s">    [[label_map]]
</span></span></span><span class="line"><span class="cl"><span class="s">        TIME = dateTime
</span></span></span><span class="line"><span class="cl"><span class="s">        rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/temperature_C = inTemp
</span></span></span><span class="line"><span class="cl"><span class="s">        rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/humidity = inHumidity
</span></span></span><span class="line"><span class="cl"><span class="s">        rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/pressure_hPa = pressure</span>
</span></span></code></pre></div><p>The <code>weewx/loop</code> topic isn&rsquo;t used by anything yet. It&rsquo;s there so a Home
Assistant automation can publish a <code>TIME:0,extraTemp1:21.5,...</code> payload later
without touching the driver again.</p>
<p><code>pressure</code> is station pressure, which is what the WH25 measures. <code>barometer</code>
still comes from the Vevor. <code>[StdWXCalculate]</code> has both set to
<code>prefer_hardware</code>, so weewx uses the measured value when a packet has one and
computes it otherwise.</p>
<h2 id="the--that-wasnt-a-wildcard">The <code>#</code> that wasn&rsquo;t a wildcard</h2>
<p>The first restart looked fine in the log, until I read the subscription line:</p>
<pre tabindex="0"><code>INFO user.wxMesh: wxMesh: subscribed to rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/
</code></pre><p>The trailing <code>#</code> was gone. <code>weewx.conf</code> is a ConfigObj file, and in ConfigObj
an unquoted <code>#</code> starts an inline comment. The driver had subscribed to a
topic that nothing publishes to. Quoting the values in the <code>topic</code> list fixed
it. It&rsquo;s an easy mistake to make with MQTT, where <code>#</code> shows up in almost every
subscription.</p>
<h2 id="checking">Checking</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">journalctl -u weewx <span class="p">|</span> grep wxMesh
</span></span></code></pre></div><pre tabindex="0"><code>INFO user.wxMesh: wxMesh 0.1-pi3-1: broker &lt;broker-address&gt;:1883, client id weewx-pi3-1, ...
INFO user.wxMesh: wxMesh: connected to &lt;broker-address&gt;:1883 with result code 0
INFO user.wxMesh: wxMesh: subscribed to rtl_433/&lt;host&gt;/devices/Fineoffset-WH25/170/#
INFO user.wxMesh: wxMesh: subscribed to weewx/loop
</code></pre><p>Then the next archive record, from the venv&rsquo;s <code>sqlite3</code> module:</p>
<pre tabindex="0"><code>20:00:00  inTemp=75.68  inHumidity=46.0  pressure=29.8185  barometer=29.93  outTemp=67.25
19:55:00  inTemp=None   inHumidity=None  pressure=29.8142  barometer=29.93  outTemp=67.4
</code></pre><p>24.3 °C in, 75.68 °F out, in the same row as the Vevor&rsquo;s outdoor temperature.</p>
<h2 id="gotchas-collected">Gotchas, collected</h2>
<ul>
<li><strong>Quote topics containing <code>#</code></strong> in <code>weewx.conf</code>, or the wildcard is eaten as
a comment.</li>
<li><strong>MetaDriver never restarts a failed child.</strong> Make sure the secondary driver
can&rsquo;t raise during construction. <code>connect_async()</code> instead of <code>connect()</code>
is the whole fix for MQTT.</li>
<li><strong>A wildcard subscription needs a whitelist.</strong> Map the fields you want and
drop the rest, or <code>battery_ok</code>, <code>mic</code> and friends end up in the packets.</li>
<li><strong>paho-mqtt 2.x needs <code>CallbackAPIVersion</code>.</strong> Any weewx MQTT driver written
before 2024 will hit this.</li>
<li><strong>Restarts log a warning</strong> from MetaDriver: &ldquo;worker thread for driver
&lsquo;Interceptor&rsquo; did not close within 5.0 seconds&rdquo;. Each child&rsquo;s generator
blocks until its next packet, so the stop takes a little longer. The threads
are daemons and the process exits anyway.</li>
<li><strong>Stay with <code>record_generation = software</code>.</strong> It&rsquo;s what lets packets from
two drivers share one archive record.</li>
</ul>
<p>The Pi now records an outdoor station that thinks it&rsquo;s talking to Weather
Underground and an indoor sensor that arrives over MQTT from a different
computer, in one weewx, with one archive.</p>
]]></content:encoded></item></channel></rss>