Geberit Duo Fresh with Bluetooth

Hi,

I just wonder is someone ever has connected to Geberit Duo Fresh control Panel (this is a Toilet Flush with integrated sensor) using Bluetooth.

Reason is that i like to access the sensor and start the ventilation in the Restroom. So if someone sits down on the Toilette the Fan starts.
The Sensor works very well so it would be cool to automate this.
Only other way I could think of is using a Shelly uni and use the 12 V switch of the Sensor to activate the Fan. But I do not want to cut the Cable and lose warranty

I found that Geberit has created a Gateway - well it costs 2.500 Euro what is a little bit to much for me.
They are using a bus system called „GEBUS“
My original idea to just cut the cable that comes from the sensor and goes to the fan using a Shelly uni is not possible in that case.

They use a 4 pin Bus system. So I can not hack the hardware and even not the proprietary Bluetooth integration.
Just another company that tries to crate closed environments and selling expensive gateways.



Well, let us assume you have the geberit App installed. Is there a counter incrementing with each usage? Additional, you can enable the adb Debugging on your phone and load this into
Wireshark… There are some guides on yt how to Do those works…

Maybe this way you can read out the times the Sensor started… A first start…

Not sure if you are still looking into it. But I started on it also. I think bluetooth integration would be the way to go. (Not sure the DuoFresh even has GEBUS) The trick is going to be understanding the bluetooth commands. They seem to have 2 ‘unknown’ servcies that are used by the app to communicate with the DuoFresh.
I am using nRF connect & logger to see if I can get some insights into it

Also interested into prospectively getting a duo fresh system but wondering if you made any progress figuring out the cable running to the extractor fan? One thought to avoid warranty issues - if the plug on that cable can be found, you could insert your own extension length between the fan cable and the sensor, and this length can be wired into a shelly uni.

Another idea: put a power meter to measure power going to the transformer. Presumably power would go up when fan is on. Spec sheet says 0.5w standby, 7w operating.

Hi, I am confused. We’re also planning to get several Duo Fresh toilets, but the documentation states that the ventilation inside the bowl stats automatically once a person is detected on the seat. So no manual control required?

Correct, I got two of these.

They have a presence detection system.

As soon as you open the door and move into the area, the fan starts and will stop a few minutes after you leave.

Dont get why the TS want to do this: “Reason is that i like to access the sensor and start the ventilation in the Restroom. So if someone sits down on the Toilette the Fan starts.

Its standard functionality.

@Arn1, why do you want to create an automation for this? Is it because you can? or something else?

I have 2 Geberit Tuma series toilets. Plus I allready use bluetooth proxys for venetian shades. It would be fun to have the data from Geberit via bluetooth into HA, fe system descaling, frimware updates, cleaning shower head, etc. We only need an integration. I thought maybe another brand uses the same software, but haven’t found anything yet.

There is a Python app that would probably allow you to connect your Tumas via MQTT GitHub - jens62/geberit-aquaclean: Python library to connect to the Geberit AquaClean Mera toilet ( port from Thomas Bingels C# library)

My previous comment is deleted by mistake :frowning:

Hi, I have a bluetooth DuoFresh toilette with light and fan. I can give remote access (VNC, adb, etc.) to a rooted android phone with Geberit app if somebody can decode the bluetooth communication and write a HACS addon :slight_smile:

1 Like

starting from the videos below, maybe you can make the logfiles available for everyone?

All possible communications is in the following hci snoop log file:
https://dwd.hu/geberit_duofresh_hci_snoop/geberit_duofresh_hci_snoop.cfa

Can anybody decode and write a HACS addon?

(ChatGPT can but its not working fine :/)

Here is a simple HACS integration with communication to the toilet.
With the help of this, others can also start analyzing and decode the communication.
More eyes see more :slight_smile:

I think UUID: 559eb110-2390-11e8-b467-0ed5f89f718b is where the magic happens, but it needs to be decoded for all option changes, BUT I cant connect with the app to the toilet if the integration is connected (for logging state changes).
I am stuck here a little.

custom_components/geberit_duofresh_bt/

manifest.json:

{
  "domain": "geberit_duofresh_bt",
  "name": "Geberit Duofresh Bluetooth",
  "version": "1.0.0",
  "requirements": ["bleak"],
  "dependencies": [],
  "config_flow": true
}

__init__.py:

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from .const import DOMAIN
from .bluetooth_processor import BluetoothProcessor

async def async_setup(hass: HomeAssistant, config: dict) -> bool:
    return True

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    address = entry.data["address"]
    processor = BluetoothProcessor(address)
    await processor.connect()

    await processor.start_watchdog(retry_interval=5.0)

    if DOMAIN not in hass.data:
        hass.data[DOMAIN] = {}
    hass.data[DOMAIN][entry.entry_id] = processor

    await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])
    return True

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    processor = hass.data[DOMAIN].pop(entry.entry_id, None)
    if processor:
        await processor.disconnect()
    return await hass.config_entries.async_unload_platforms(entry, ["sensor"])

bluetooth_processor.py:

import logging
import asyncio
from bleak import BleakClient
#from .const import NOTIFY_UUID, WRITE_UUID
from .const import WRITE_HANDLE

_LOGGER = logging.getLogger(__name__)

class BluetoothProcessor:
    def __init__(self, address):
        self.address = address
        self.client = None
        self.data = None
        self.polling_task = None
        self.watchdog_task = None
        self._char_cache = {}
        self._excluded_chars = set()

    async def connect(self):
        if self.client and self.client.is_connected:
            _LOGGER.debug(f"Already connected to Geberit Duofresh at {self.address}")
            return

        _LOGGER.debug(f"Connecting to Geberit Duofresh at {self.address}...")

        self.client = BleakClient(self.address)

        try:
            await self.client.connect()
            _LOGGER.info(f"Connected to Geberit Duofresh at {self.address}")

            #await self.client.start_notify(NOTIFY_UUID, self._notification_handler)
            #await self.client.start_notify(NOTIFY_HANDLE, self._notification_handler)
	    
            for char in self.client.services.characteristics.values():
                _LOGGER.debug(f"Characteristic found. UUID: {char.uuid}, handle: {char.handle}, properties: {char.properties}")
                if "notify" in char.properties or "indicate" in char.properties:
                    try:
                        await self.client.start_notify(char.handle, self._notification_handler)
                        _LOGGER.info(f"Enabling notify on characteristic: {char.uuid} (handle: {char.handle})")
                    except Exception as e:
                        _LOGGER.warning(f"Failed to start notify on {char.uuid}: {e}")

            await self.poll_characteristics(0.1)

        except asyncio.CancelledError:
            _LOGGER.warning("Connection attempt cancelled.")
            raise
        except Exception as e:
            _LOGGER.warning(f"Initial connection failed: {e}")

    async def disconnect(self):
        if self.polling_task:
            self.polling_task.cancel()
            self.polling_task = None
        if self.client and self.client.is_connected:
            await self.client.disconnect()
            _LOGGER.info(f"Disconnected from Geberit Duofresh at {self.address}")

    async def start_watchdog(self, retry_interval: float = 5.0):
        async def monitor():
            while True:
                if not self.client or not self.client.is_connected:
                    _LOGGER.info("Not connected to Geberit Duofresh. Attempting to connect...")
                    await self.connect()
                await asyncio.sleep(retry_interval)

        self.watchdog_task = asyncio.create_task(monitor())

    async def write_command(self, value: bytes):
        if self.client and self.client.is_connected:
            #await self.client.write_gatt_char(WRITE_UUID, value, response=False)
            await self.client.write_gatt_char(WRITE_HANDLE, value, response=False)
        else:
            _LOGGER.error("Client is not connected; cannot write command")

    def _notification_handler(self, handle, data):
        self.data = data
        _LOGGER.info(f"Notification received. Handle: {handle}, value: {data.hex()}")

    async def read_notify(self):
        _LOGGER.debug(f"read_notify() called, returning: {self.data.hex() if self.data else 'None'}")
        return self.data or b""

    async def poll_characteristics(self, interval: float = 0.1):
        if self.polling_task:
            self.polling_task.cancel()

        if not self.client or not self.client.is_connected:
            _LOGGER.warning("Cannot poll characteristics: not connected")
            return

        async def poll():
            while True:
                for char in self.client.services.characteristics.values():
                    key = f"{char.uuid}:{char.handle}"
                    if key in self._excluded_chars:
                        continue
                    if "read" in char.properties:
                        try:
                            value = await self.client.read_gatt_char(char.handle)

                            #_LOGGER.debug(f"Polled UUID: {char.uuid}, handle: {char.handle}, value: {value.hex()}")

                            prev = self._char_cache.get(key)
                            if prev != value:
                                _LOGGER.info(f"State changed. UUID: {char.uuid}, handle: {char.handle}, value: {value.hex()}")
                                self._char_cache[key] = value
                        except Exception as e:
                            if "NotPermitted" in str(e):
                                self._excluded_chars.add(key)
                                _LOGGER.warning(f"Excluded UUID {char.uuid} (handle {char.handle}) due to permission error: {e}")
                            else:
                                _LOGGER.warning(f"Failed to read characteristic {char.uuid} (handle {char.handle}): {e}")
                await asyncio.sleep(interval)

        self.polling_task = asyncio.create_task(poll())

config_flow.py:

from homeassistant import config_entries
import voluptuous as vol
import re
from .const import DOMAIN

MAC_REGEX = re.compile(r"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$")

class GeberitConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    VERSION = 1

    async def async_step_user(self, user_input=None):
        errors = {}
        if user_input is not None:
            mac = user_input.get("address", "").strip()
            if not MAC_REGEX.match(mac):
                errors["address"] = "invalid_mac"
            else:
                return self.async_create_entry(title="Geberit Duofresh", data={"address": mac})

        return self.async_show_form(
            step_id="user",
            data_schema=vol.Schema({
                vol.Required("address", description="Geberit Duofresh MAC address (format: XX:XX:XX:XX:XX:XX)"): str
            }),
            errors=errors
        )

const.py:

DOMAIN = "geberit_duofresh_bt"

#NOTIFY_UUID = "559eb102-2390-11e8-b467-0ed5f89f71b"
#NOTIFY_HANDLE = 0x0014
#WRITE_UUID = "559eb101-2390-11e8-b467-0ed5f89f71b"
WRITE_HANDLE = 0x0011

sensor.py:

import logging
from homeassistant.components.sensor import SensorEntity
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

CHARACTERISTIC_MAP = {
    "00002a24-0000-1000-8000-00805f9b34fb": "Model Number",
    "00002a25-0000-1000-8000-00805f9b34fb": "Serial Number",
    "00002a26-0000-1000-8000-00805f9b34fb": "Firmware Revision",
    "00002a27-0000-1000-8000-00805f9b34fb": "Hardware Revision",
    "00002a28-0000-1000-8000-00805f9b34fb": "Software Revision",
    "00002a29-0000-1000-8000-00805f9b34fb": "Manufacturer"
}

class GeberitStaticSensor(SensorEntity):
    def __init__(self, processor, uuid, name):
        self._processor = processor
        self._uuid = uuid
        self._attr_name = f"{name}"
        self._attr_unique_id = f"geberit_duofresh_{uuid.replace('-', '')}"
        self._value = None

    @property
    def native_value(self):
        return self._value

    async def async_update(self):
        if not self._processor.client or not self._processor.client.is_connected:
            return
        try:
            value = await self._processor.client.read_gatt_char(self._uuid)
            decoded = bytes(value).decode(errors="ignore")
            self._value = decoded
        except Exception as e:
            _LOGGER.warning(f"Failed to read {self._uuid}: {e}")

async def async_setup_entry(hass, config_entry, async_add_entities):
    processor = hass.data[DOMAIN][config_entry.entry_id]
    entities = []
    for uuid, name in CHARACTERISTIC_MAP.items():
        entities.append(GeberitStaticSensor(processor, uuid, name))
    async_add_entities(entities)

You need to set the loglevel to debug (in configuration.yaml) for the integration to see the messages in home-assistant.log.

logger:
  ...
  logs:
    custom_components.geberit_duofresh_bt: debug
    ...

Possible log messsages:

Connecting to Geberit Duofresh at E4:85:01:04:60:FF...
Connected to Geberit Duofresh at E4:85:01:04:60:FF
Characteristic found. UUID: 559eb002-2390-11e8-b467-0ed5f89f718b, handle: 47, properties: ['notify']
Enabling notify on characteristic: 559eb002-2390-11e8-b467-0ed5f89f718b (handle: 47)
Characteristic found. UUID: 00002a05-0000-1000-8000-00805f9b34fb, handle: 2, properties: ['indicate']
Enabling notify on characteristic: 00002a05-0000-1000-8000-00805f9b34fb (handle: 2)
Characteristic found. UUID: 559eb001-2390-11e8-b467-0ed5f89f718b, handle: 45, properties: ['write-without-response']
Characteristic found. UUID: 559eb101-2390-11e8-b467-0ed5f89f718b, handle: 29, properties: ['write-without-response']
Characteristic found. UUID: 559eb110-2390-11e8-b467-0ed5f89f718b, handle: 31, properties: ['read']
Characteristic found. UUID: 00002a24-0000-1000-8000-00805f9b34fb, handle: 15, properties: ['read']
Characteristic found. UUID: 00002a25-0000-1000-8000-00805f9b34fb, handle: 17, properties: ['read']
Characteristic found. UUID: 00002a26-0000-1000-8000-00805f9b34fb, handle: 21, properties: ['read']
Characteristic found. UUID: 00002a27-0000-1000-8000-00805f9b34fb, handle: 19, properties: ['read']
Characteristic found. UUID: 00002a28-0000-1000-8000-00805f9b34fb, handle: 23, properties: ['read']
Characteristic found. UUID: 00002a29-0000-1000-8000-00805f9b34fb, handle: 13, properties: ['read']
State changed. UUID: 559eb110-2390-11e8-b467-0ed5f89f718b, handle: 31, value: 0105fc00b1900d0095040120010500010002
State changed. UUID: 00002a24-0000-1000-8000-00805f9b34fb, handle: 15, value: 3833312e3439372e30302e30
State changed. UUID: 00002a25-0000-1000-8000-00805f9b34fb, handle: 17, value: 3437353838
State changed. UUID: 00002a26-0000-1000-8000-00805f9b34fb, handle: 21, value: 31302e37204f5441342e33203230323430323032
State changed. UUID: 00002a27-0000-1000-8000-00805f9b34fb, handle: 19, value: 3036
State changed. UUID: 00002a28-0000-1000-8000-00805f9b34fb, handle: 23, value: 312e352e3020312e302e32
State changed. UUID: 00002a29-0000-1000-8000-00805f9b34fb, handle: 13, value: 47656265726974

Happy to provide a helping hand. Are you guys are using it with a Bluetooth proxy?

@schmidtfx Yes I use a bluetooth proxy.

I tried your custom component @dwd, with my Geberit Aquaclean Mera Classic. The bluetooth name is GEBERIT AC PRO. Not yet a connection established, what can I do to help develop the integration. Shall we set up a Github repo for this?

2025-07-02 09:44:56.077 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish
2025-07-02 09:45:02.424 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish

2025-07-02 09:45:12.868 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish

2025-07-02 09:45:17.869 INFO (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Not connected to Geberit Duofresh. Attempting to connect...

2025-07-02 09:45:17.869 DEBUG (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Connecting to Geberit Duofresh at 00:35:FF:38:10:A2...

2025-07-02 09:45:19.344 DEBUG (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Connecting to Geberit Duofresh at 00:35:FF:38:10:A2...

2025-07-02 09:45:19.422 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish

2025-07-02 09:45:19.426 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish

2025-07-02 09:45:19.426 INFO (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Not connected to Geberit Duofresh. Attempting to connect...

2025-07-02 09:45:19.426 DEBUG (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Connecting to Geberit Duofresh at 00:35:FF:38:10:A2...

2025-07-02 09:45:20.345 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish

2025-07-02 09:45:24.426 INFO (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Not connected to Geberit Duofresh. Attempting to connect...

2025-07-02 09:45:24.427 DEBUG (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Connecting to Geberit Duofresh at 00:35:FF:38:10:A2...

2025-07-02 09:45:25.347 INFO (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Not connected to Geberit Duofresh. Attempting to connect...

2025-07-02 09:45:25.347 DEBUG (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Connecting to Geberit Duofresh at 00:35:FF:38:10:A2...

2025-07-02 09:45:25.775 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish

2025-07-02 09:45:25.775 WARNING (MainThread) [custom_components.geberit_duofresh_bt.bluetooth_processor] Initial connection failed: Error ESP_GATT_CONN_FAIL_ESTABLISH while connecting: Connection failed to establish

I’ve edited the code, added a option to choose between duofresh and mera classic and support for devices.

twoenter/geberit_home_bt

Released a basic version with the diagnostic sensors in the device:


2 Likes

Do you have any news about control these devices?

I Have a mera comfort, and with the proxy and the custom component that you made, i found the device, but no control at all.

Thank you!

2 Likes

In my last version I added 3 notify sensors, wich should update when it changes, but it does never get updated. Maybe because the code is incorrect, maybe because the toilets doens’t update the values. Don’t know yet. I want to let ChatGPT analyse the wireshark file for some new clues. It’s on my to-do list :wink:

1 Like