Meltem WRG II integration via Meltem Gateway and Modbus

Thank you for this thread, it is very helpful. I have just a few questions to clarify.

If I want to integrate these Meltem WRG II units into HA smoothly, I need to get units with Modbus support, right?

  1. The Meltem M-WRG-GW gateway connects to the units via WiFi/Ethernet.
  2. The gateway is then connected to HA (say running on RPi) via the Micro-USB (which also powers the gateway).
  3. The gateway also works with non-Modbus units but in that case, it doesn’t expose Modbus COM RTU interface.

I think statements 1 and 2 are confirmed in the initial guide, but I am only guessing statement 3 from what was said. I will message Meltem support about this if needed.

Basically I was wondering if you can get non-Modbus units and still get Modbus control over the gateway (there is a relevant difference in price of Modbus vs standard units).

the GW connects to the units via a proprietary wireless connection. I have the non Modbus variants and I can use the described modbus connection via the GW to communicate with the units wireless.

1 Like

I had to change the modbus integration a little bit otherwise i run into a lot of timeout issues:

modbus: 
  - name: modbus_hub
    type: serial
    port: /dev/ttyACM0
    baudrate: 19200
    bytesize: 8
    method: rtu
    parity: E
    stopbits: 1
    timeout: 5                        # Default is 3. Increase to 5 or 10 seconds.
    delay: 1                          # (Optional) Wait 1s before startup
    message_wait_milliseconds: 200
    sensors: !include modbus.yaml

For me the timeout, delay and message_wait_milliseconds entries fixed the problem.

And if someone is interested in a python script to generate the modbus.yaml:

Generate script
import argparse
from pathlib import Path

# ==========================================
# 1. CONFIGURE YOUR SLAVES HERE
# ==========================================
slaves = [
    {"id": 2, "prefix": "Lüftung Buero Nora", "slug": "bn"},
    {"id": 3, "prefix": "Lüftung Buero Oli", "slug": "bo"},
    {"id": 4, "prefix": "Lüftung Kinderzimmer", "slug": "kz"},
    {"id": 5, "prefix": "Lüftung Schlafzimmer", "slug": "sz"},
    {"id": 6, "prefix": "Lüftung Wohnzimmer", "slug": "wz"},
]

# ==========================================
# 2. DEFINE THE SENSOR TEMPLATE
# ==========================================
# This list contains the logic from your original config, minus 'name' and 'slave'.
sensors_template = [
    {"suffix": "Modus", "address": 41100, "data_type": "int16", "scan_interval": 3},
    {
        "suffix": "Modus Zuluft",
        "address": 41101,
        "data_type": "int16",
        "scan_interval": 5,
    },
    {
        "suffix": "Frostschutzfunktion",
        "address": 41018,
        "data_type": "int16",
        "scan_interval": 60,
    },
    {
        "suffix": "Ablufttemperatur",
        "address": 41000,
        "data_type": "float32",
        "device_class": "temperature",
        "unit": "°C",
        "scan_interval": 60,
        "precision": 1,
        "swap": "word",
    },
    {
        "suffix": "Außenlufttemperatur",
        "address": 41002,
        "data_type": "float32",
        "device_class": "temperature",
        "unit": "°C",
        "scan_interval": 60,
        "precision": 1,
        "swap": "word",
    },
    {
        "suffix": "Fortlufttemperatur",
        "address": 41004,
        "data_type": "float32",
        "device_class": "temperature",
        "unit": "°C",
        "scan_interval": 60,
        "precision": 1,
        "swap": "word",
    },
    {
        "suffix": "Feuchte Abluft",
        "address": 41006,
        "data_type": "int16",
        "device_class": "humidity",
        "unit": "%",
        "scan_interval": 60,
    },
    {
        "suffix": "CO² Abluft",
        "address": 41007,
        "data_type": "int16",
        "device_class": "carbon_dioxide",
        "unit": "ppm",
        "scan_interval": 60,
    },
    {
        "suffix": "Zulufttemperatur",
        "address": 41009,
        "data_type": "float32",
        "device_class": "temperature",
        "unit": "°C",
        "scan_interval": 60,
        "precision": 1,
        "swap": "word",
    },
    {
        "suffix": "Feuchte Zuluft",
        "address": 41011,
        "data_type": "int16",
        "device_class": "humidity",
        "unit": "%",
        "scan_interval": 60,
    },
    {
        "suffix": "Lüfterstufe Abluft",
        "address": 41020,
        "data_type": "int16",
        "device_class": "volume_flow_rate",
        "unit": "m³/h",
        "scan_interval": 5,
    },
    {
        "suffix": "Lüfterstufe Zuluft",
        "address": 41021,
        "data_type": "int16",
        "device_class": "volume_flow_rate",
        "unit": "m³/h",
        "scan_interval": 5,
    },
    {
        "suffix": "Zeit bis Filterwechsel",
        "address": 41027,
        "data_type": "int16",
        "device_class": "duration",
        "unit": "d",
        "scan_interval": 60,
    },
    {
        "suffix": "Modus General",
        "address": 41120,
        "data_type": "int16",
        "scan_interval": 3600,
    },
    {
        "suffix": "Rel. Feuchte Startpunkt",
        "address": 42000,
        "data_type": "int16",
        "device_class": "humidity",
        "unit": "%",
        "scan_interval": 5,
    },
    {
        "suffix": "Min. Lüftungsstufe Feuchteregelung",
        "address": 42001,
        "data_type": "int16",
        "scan_interval": 60,
    },
    {
        "suffix": "Max. Lüftungsstufe Feuchteregelung",
        "address": 42002,
        "data_type": "int16",
        "scan_interval": 60,
    },
    {
        "suffix": "CO² Startpunkt",
        "address": 42003,
        "data_type": "int16",
        "device_class": "carbon_dioxide",
        "unit": "ppm",
        "scan_interval": 60,
    },
    {
        "suffix": "Min. Lüftungsstufe CO²-Regelung",
        "address": 42004,
        "data_type": "int16",
        "scan_interval": 60,
    },
    {
        "suffix": "Max. Lüftungsstufe CO²-Regelung",
        "address": 42005,
        "data_type": "int16",
        "scan_interval": 60,
    },
]


def clean_string(input_str):
    """Helper to make clean Unique IDs"""
    return (
        input_str.lower()
        .replace(" ", "_")
        .replace("ä", "ae")
        .replace("ö", "oe")
        .replace("ü", "ue")
        .replace("²", "2")
        .replace(".", "")
        .replace("-", "_")
    )


if __name__ == "__main__":
    arguments = argparse.ArgumentParser(
        description="Generate sensor configurations for Home Assistant MODBUS."
    )
    arguments.add_argument(
        "--out_file",
        type=Path,
        default="modbus.yaml",
        help="The path to the output YAML file.",
    )
    args = arguments.parse_args()
    with args.out_file.open("w", encoding="utf-8") as f:
        f.write(f"# Auto-generated Modbus config for {len(slaves)} slaves.\n")
        f.write("# DO NOT EDIT MANUALLY - Update the Python script instead.\n\n")

        for slave in slaves:
            f.write("# ---------------------------------------------------------\n")
            f.write(f"# SLAVE {slave['id']}: {slave['prefix']}\n")
            f.write("# ---------------------------------------------------------\n")

            for s in sensors_template:
                # Generate Name
                full_name = f"{slave['prefix']} {s['suffix']}"

                # Generate Unique ID (e.g., mb_sz_3_co2_abluft)
                # Including slave ID in unique_id ensures no conflicts if you move devices later
                clean_suffix = clean_string(s["suffix"])
                unique_id = f"mb_{slave['slug']}_{slave['id']}_{clean_suffix}"

                # Write YAML block
                f.write(f'- name: "{full_name}"\n')
                f.write(f"  unique_id: {unique_id}\n")
                f.write(f"  slave: {slave['id']}\n")
                f.write(f"  address: {s['address']}\n")
                f.write(f"  data_type: {s['data_type']}\n")
                f.write(f"  scan_interval: {s['scan_interval']}\n")

                # Optional fields
                if "device_class" in s:
                    f.write(f"  device_class: {s['device_class']}\n")
                if "unit" in s:
                    f.write(f'  unit_of_measurement: "{s["unit"]}"\n')
                if "precision" in s:
                    f.write(f"  precision: {s['precision']}\n")
                if "swap" in s:
                    f.write(f"  swap: {s['swap']}\n")

                f.write("\n")  # Empty line between sensors

    print(
        f"Success! File '{args.out_file}' has been created with {len(slaves) * len(sensors_template)} sensors."
    )
    print("You can now include this file in your configuration.yaml.")

@Thombo Which solution is currently in use?

Is it “Wi-Fi to Modbus RTU” with an ESP32 or the “Wi-Fi to Meltem Bridge”?

Or are they the same thing?

What are the OTG and 5V solder pads?

Connect the two pads labeled “IN-OUT”.

Does anyone have it running through the ESP bridge?

My connection drops after a few hours. I have to reboot the ESP, then it works again.

@daywalkeraut Could you resolve the issue?

If not, did you try this code change?

Yes, after implementing the above PR (USB watchdog) in the code, the ESP32-S3 bridge has been stably working with my M-WRG-II E with the Meltem gateway (as long as WiFi connection to the ESP32 is sufficient). I use a USB-C-to-Micro-USB cable to connect ESP32 and the gateway. HA integration via Modbus works nicely. Using several scripts I can do everything the app does (and more). Now I can also disconnect the Meltem gateway from the internet (ethernet connection) to operate the system completely offline (as long as I don’t want to use the Meltem app).