Cheap Bits, Solid Data: Building a Whole‑House Climate Monitor
share forum

Cheap Bits, Solid Data: Building a Whole‑House Climate Monitor


Technology • by Sven Reifschneider • 14 May 2025 • 0 comments

Why I Wanted My Own Climate Feed

I like numbers more than guesses. More than five winters ago I put a pair of TFA Dostmann temperature‑humidity sensors—one in the living room, one in the basement—just to see how quickly the house cooled overnight. The €20 plastic pucks had LCDs, but walking around recording values with a notebook got old fast. I needed the data in one place, continuously, and preferably on a dashboard.

The obvious solution—Wi‑Fi IoT gadgets—felt wrong: higher cost, batteries that last months not years, and proprietary clouds. Then I found baycom/tfrec, a tiny open‑source program by Georg Acher & Deti Fliegl that decodes the 868 MHz packets those sensors already broadcast. All it needs is the same RTL2832U DVB‑T dongle many of us have in a parts drawer. This tool decodes data sent by the KlimaLogg Pro and similar temperature sensors made by TFA Dostmann or other Technoline/LaCrosse-compatible sensors. Have a look at their sensors.txt, a wide range is supported, including weather sensors like wind or rain.

That was the eureka moment: Cheap hardware I already trust, software I can compile, and battery‑friendly sensors I can buy in any electronics store.

Bill of Materials

Item Typical price Notes
RTL2832U DVB‑T USB stick (with R820T tuner) €20–30 Acts as a general‑purpose Software Defined Radio (SDR) receiver.
868 MHz temp/humidity sensor (TFA Dostmann / Technoline / La Crosse) €15–20 LCD read‑out, 2x AA batteries that last 4-5 years. Models for pools, wind, rain available.
Linux host (Raspberry Pi, old laptop, VM) from €15 (Pi Zero 2 W) Any system that can run rtl_sdr.
Optional: better 868 MHz antenna €5–10 The stock whip works across most houses; a bigger external antenna buys extra metres.

I started with two sensors. When that worked, I added some more units over the years to cover most rooms in my house—twelve total today. The RF range comfortably covers my two‑floor + basement brick house with the stock DVB‑T antenna on a shelf. A great addition to my weather station.

Since a wide range of sensors is supported, you'll always find a suiting one for your needs.

Installing tfrec

sudo apt install git build-essential rtl-sdr   # Debian/Ubuntu; Arch users know the drill
git clone https://github.com/baycom/tfrec
cd tfrec
make
sudo cp tfrec /usr/bin/   # or anywhere in $PATH

What tfrec does:

  • Listens to raw I/Q samples from rtl_sdr.
  • Detects the on‑off‑keying pattern used by KlimaLogg Pro‑compatible sensors.
  • Prints each packet’s sensor ID, temperature °C, humidity %, RSSI, battery flag, and timestamp.
  • Optionally executes a command, passing those values as arguments.

No kernel modules, no black magic—just libusb.

Running It Manually (First Sanity Check)

Run it with -d for debug mode:

sudo /opt/bin/tfrec -d

You should see lines like:

16b4  +23.8  41  27  0  -60
92af  +22.1  45  18  0  -63

ID + Temp + Humidity + Sequence + BatteryLow + SignalStrength(dB).

If nothing appears, move the antenna away from USB noise or check the sensor battery / error output.

Automating with systemd

I want:

  • 60 s “listening windows” every 30 min
  • automatic restart if rtl_sdr hangs (rare but happened a few times over the years, timeout is 180 s = 3 min)
  • everything logged via journalctl

Service Unit

We create a simple systemd service that runs our custom tfrec script at /etc/systemd/system/tfrec.service:

[Unit]
Description=Receive 868 MHz climate data and pass to handler

[Service]
Type=simple
User=root
ExecStart=/bin/bash /opt/tfrec.sh
TimeoutStartSec=180

Timer Unit

And we create a suiting systemd timer which calls our service every 30 minutes (minute 0 and 30 of every hour) at /etc/systemd/system/tfrec.timer:

[Unit]
Description=Run tfrec every 30 minutes

[Timer]
OnCalendar=*:0,30
AccuracySec=1min
Persistent=true

[Install]
WantedBy=timers.target

Enable both:

sudo systemctl daemon-reload
sudo systemctl enable --now tfrec.timer

The Wrapper Script

In our example, we put this script at /opt/tfrec.sh. Put it anywhere logical and accessible.

#!/usr/bin/env bash
/usr/bin/tfrec -q -w 60 -e "php /var/www/cli.php climate:add"
exit 0
  • -q quiet (no human output)
  • -w 60 listen for 60 s
  • -e execute the PHP handler for every packet

Don't forget to chmod +x tfrec.sh after saving it, so we can execute it.

Storing Readings with PHP

If you prefer Python or another language, skip to the next heading—the logic is the same: Have a script which accepts the arguments in order as tfrec adds them to your script, process them, ignore invalid entries and store / publish them.

My code is written for the Charm Framework but it's very close to Laravel and Symfony, you'll easily understand it.

CLI Command climate:add

I created a simple CLI command based on symfony/console. The full code is below. Highlights:

  1. Checks Climate::$sensors so unknown IDs are ignored.
  2. Drops duplicates within 15 minutes (tfrec can see the same packet twice in one window).
  3. Saves to MySQL (or SQLite/PostgreSQL—Laravel's Eloquent abstracts it).
  4. Publishes fresh readings on MQTT via php-mqtt/client.
use App\Models\Climate;
use Carbon\Carbon;
use Charm\Bob\Command;
use Charm\Vivid\C;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;
use Symfony\Component\Console\Input\InputArgument;

class ClimateAdd extends Command
{
    protected function configure()
    {
        $this->setName("climate:add")
             ->setDescription("Store tfrec sensor reading")
             ->addArgument('id',       InputArgument::REQUIRED)
             ->addArgument('temp',     InputArgument::REQUIRED)
             ->addArgument('hum',      InputArgument::REQUIRED)
             ->addArgument('seq',      InputArgument::REQUIRED)
             ->addArgument('batfail',  InputArgument::REQUIRED)
             ->addArgument('rssi',     InputArgument::REQUIRED)
             ->addArgument('timestamp',InputArgument::OPTIONAL);
    }

    public function main(): bool
    {
        $id        = $this->io->getArgument('id');
        $temp      = (double)$this->io->getArgument('temp');
        $hum       = (int)$this->io->getArgument('hum');
        $batfail   = (bool)$this->io->getArgument('batfail');
        $rssi      = (int)$this->io->getArgument('rssi');
        $timestamp = $this->io->getArgument('timestamp');

        // Validate sensor
        if (!array_key_exists($id, Climate::$sensors)) {
            return true;
        }

        // 15‑minute duplicate check
        $recent = Climate::where('sensor_id', $id)
                         ->where('time', '>=', Carbon::now()->subMinutes(15))
                         ->first();
        if ($recent) {
            return true;
        }

        $c                = new Climate();
        $c->sensor_id     = $id;
        $c->temperature   = str_replace("+", "", $temp);
        $c->humidity      = $hum;
        $c->rssi          = $rssi;
        $c->low_battery   = $batfail;
        $c->time          = Carbon::createFromTimestamp($timestamp);
        $c->save();

        $this->publishViaMqtt($c);
        return true;
    }

    private function publishViaMqtt(Climate $c)
    {
        $client = new MqttClient(
            C::Config()->get('connections:mqtt.server'),
            C::Config()->get('connections:mqtt.port'),
            C::Config()->get('connections:mqtt.client_id')
        );
        $settings = (new ConnectionSettings())
            ->setUsername(C::Config()->get('connections:mqtt.username'))
            ->setPassword(C::Config()->get('connections:mqtt.password'));

        $client->connect($settings, true);
        $client->publish("/tfrec/{$c->sensor_id}/temp",  $c->temperature);
        $client->publish("/tfrec/{$c->sensor_id}/hygro", $c->humidity);
        $client->disconnect();
    }
}

Database Model

class Climate extends Model
{
    protected $table = 'tfrec';
    protected $casts = ['time' => 'datetime'];
    public $timestamps = false;

    public static $sensors = [
        '16b4' => 'Entrance',
        '92af' => 'Kitchen',
        '5d43' => 'Livingroom',
        '6ac9' => 'Bedroom',
    ];

    public static function getTableStructure(): \Closure
    {
        return function (Blueprint $table) {
            $table->increments('id');
            $table->string('sensor_id')->index();
            $table->decimal('temperature', 4, 1);
            $table->decimal('humidity',    4, 1);
            $table->tinyInteger('rssi')->nullable();
            $table->tinyInteger('low_battery')->nullable();
            $table->dateTime('time');
        };
    }
}

Feel free to swap in SQLite for a lightweight setup or to just only publish the data via MQTT.

MQTT Integration: Why It Makes Life Easy

No matter how your script looks like, it makes sense that it pushes the climate data to MQTT, so you can use it easily across your apps:

  • OpenHAB / Home Assistant subscribe to /tfrec/# and auto‑discover sensors.
  • Node‑RED can add thresholds, alerts, or InfluxDB logging with a drag‑and‑drop flow.
  • Grafana graphs look great on a wall‑mounted tablet.
  • Any language on any device can listen—MQTT is just lightweight TCP.

After import and a bit of configuration, my OpenHAB dashboard now shows the temperature and humidity on the room cards, provides graphs and more.

Reliability After 5+ Years

Aspect Observation
Sensor battery Batteries last 1–3 years, rarely need replacements (indoor, 20 °C).
Packet loss 1–2 drops per week on the farthest sensor; always reappears next interval.
tfrec stability One rtl_sdr hang every ~8-12 weeks—systemd watchdog handles it silently.
Range Stock antenna covers 15 m through two brick walls, more than enough for me.

When a sensor does disappear it’s usually a weak battery; swapping it fixes reception instantly.

Why RTL‑SDR + DVB‑T Sticks Shine for This Job

  • They’re everywhere. Any electronics hobby shop stocks them.
  • Flexible. The same dongle can read ADS‑B aircraft beacons, pager traffic, or weather satellites—just change software.
  • Cheap to replace. If it burns out, €20 buys a new one.
  • Driver support. rtl-sdr is in every mainstream Linux distro, maintained, and well‑documented.

Wrap‑Up

With a handful of parts and an afternoon of tinkering, you can:

  1. Capture room‑level temperature & humidity continuously.
  2. Store data in your own database—no vendor lock‑in.
  3. Feed dashboards or automations through MQTT.
  4. Expand at will: pool temperature, wind speed, even rain gauges—tfrec already supports them.

It started as a two‑sensor experiment and grew into a house‑wide climate feed I trust. If you give it a try, let me know how it works out—and what you end up building on top. Reliable data has a habit of inspiring new ideas.

Happy hacking!

This post was created by myself with support from AI (GPT o3). Illustrations were generated by myself with Sora. Explore how AI can inspire your content – Neoground GmbH.


Share this post

If you enjoyed this article, why not share it with your friends and acquaintances? It helps me reach more people and motivates me to keep creating awesome content for you. Just use the sharing buttons below to share the post on your favorite social media platforms. Thank you!

Please consider sharing this post
Please consider donating

Support the Blog

If you appreciate my work and this blog, I would be thrilled if you'd like to support me! For example, you can buy me a coffee to keep me refreshed while working on new articles, or simply contribute to the ongoing success of the blog. Every little bit of support is greatly appreciated!

currency_bitcoin Donate via Crypto
Bitcoin (BTC):1JZ4inmKVbM2aP5ujyvmYpzmJRCC6xS6Fu
Ethereum (ETH):0xC66B1D5ff486E7EbeEB698397F2a7b120e17A6bE
Litecoin (LTC):Laj2CkWBD1jt4ZP6g9ZQJu1GSnwEtsSGLf
Sven Reifschneider
About the author

Sven Reifschneider

Greetings! I'm Sven, a tech innovator and enthusiastic photographer from scenic Wetterau, near the vibrant Frankfurt/Rhein-Main area. This blog is where I fuse my extensive tech knowledge with artistic passion to craft stories that captivate and enlighten. Leading Neoground, I push the boundaries of AI consulting and digital innovation, advocating for change that resonates through community-driven technology.

Photography is my portal to expressing the ephemeral beauty of life, blending it seamlessly with technological insights. Here, art meets innovation, each post striving for excellence and sparking conversations that inspire.

Curious to learn more? Follow me on social media or click on "learn more" to explore the essence of my vision.


No comments yet

Add a comment

You can use **Markdown** in your comment. Your email won't be published. Find out more about our data protection in the privacy policy.