DoorBird: add transmit audio functionality

The DoorBird LAN-API allows for transmitting audio to it’s intercom through the /bha-api/audio-transmit.cgi endpoint (see page 18). This can be useful for sending short TTS messages and for instance letting people know you are not at home.

There is a separate AppDaemon app which seems to add this functionality. I havent tested it as I dont use AppDaemon (yet). I’d like to see this feature integrated in the default DoorBird integration.

I already tested the endpoint and functionality myself using curl and also with a Nodejs script. This works like a charm. Unfortunately my Python skills are to limited to come up with a pull request.

@oblogic7 or @bdraco , you seem to be listed as code owners for the DoorBird integration. Does this feature request sound viable?

For now I have created a work around which doesnt require AddDaemon which I’ll share here for anyone interested in adding this functionality.

All credits to @coobnoob for the script used. I only edited his/her AppDaemon script so it works as a standalone shell script. I’m in no way a Python coder so there are probably lots of improvements possible but the script works.

  1. Add the script below somewhere in your Home Assistant folder. I added it to a folder called custom_scripts.
  2. Add the following configuration to configuration.yaml and make sure you reference the name and location of the script correctly.
shell_command:
  doorbird_transmit_audio: python3 /config/custom_scripts/doorbird_transmit_audio.py {{ address }} {{ username }} {{ password }} {{ audio_file_path }}
  1. Create an automation with an action that looks similar to this, obviously using the correct IP address and credentials of your DoorBird.
action:
  - service: shell_command.doorbird_transmit_audio
    metadata: {}
    data:
      address: 192.168.0.134
      username: gxxxxxxxx2
      password: hxxxxxxxxk
      audio_file_path: /config/www/sounds/doorbird/not_at_home.mp3

The script

import subprocess
import sys
import requests
from requests.exceptions import RequestException
from time import sleep 
from io import BytesIO

# Credits for this script go to Kristian (coobnoob). The base of this script is a copy from https://github.com/coobnoob/ha-appdaemon-doorbird-audio/tree/main

# usage examples
# python3 /config/custom_scripts/doorbird_transmit_audio.py 192.168.0.134 username password /config/www/sounds/doorbird/not_at_home.mp3

# parameters
base_url = "http://%s/bha-api" % (sys.argv[1])
auth = (sys.argv[2], sys.argv[3])

def _generate_audio_chunks(output_stream, chunk_size=8*1024):
  """
  Generator function to yield chunks of audio data from a file.
  Doorbird is rate limited to 8K per second

  Args:
      output_stream: Stream of audio
      chunk_size (int): Size of each chunk in bytes.

  Yields:
      bytes: Chunks of audio data.
  """
  while True:
    chunk = output_stream.read(chunk_size)
    if not chunk:
      break
    sleep(1) # We are rate limiting to chunk_size per second
    yield chunk

def send_audio(audio_file_path):
  """
  Send audio to Doorbird

  Args:
      audio_file_path (str): Path to the audio file to be transmitted.
      
  Raises:
      Exception
  """
  try:
    response = requests.get(f"{base_url}/getsession.cgi", auth=auth)
    response.raise_for_status()
       
    # Check if the request was successful (HTTP status code 200)
    if response.status_code == 200:
      data = response.json()
      session_id = data["BHA"]["SESSIONID"]
    else:
      raise Exception(f"Failed to obtain session ID. Status code: {response.status_code}")

    # Prepare headers
    headers = {
      "Content-Type": "audio/basic",
      "Connection": "Keep-Alive",
      "Cache-Control": "no-cache"
    }

    # Command to convert the file to an a mono, 8000Khz mu-law wav
    command = [
      "ffmpeg",
      "-y",
      "-i", audio_file_path,
      "-ac", "1",
      "-ar", "8000",
      "-f", "wav",
      "-codec:a", "pcm_mulaw",
      "-"
    ]

    # Convert the file to an in-memory, mono, 8000Khz mu-law wav
    result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    result.check_returncode()
    
    # Make the POST request with the audio data generator to chunk the file
    with requests.post(
      f"{base_url}/audio-transmit.cgi?sessionid={session_id}",
      headers=headers,
      data=_generate_audio_chunks(BytesIO(result.stdout), chunk_size=8*1024),
      auth=auth,
      stream=True
    ) as response:
      if response.status_code != 200:
        sys.stderr.write("Failed to transmit audio. \n")
        response.raise_for_status()
        sys.exit()
  except requests.exceptions.RequestException as e:
    sys.stderr.write(str(e))
    sys.exit()

# Make the request
try:
  send_audio(sys.argv[4])
except Exception as e:
  sys.stderr.write(str(e))
  sys.exit()
2 Likes

Thanks for sharing! I will try to implement this next month. The intended use case is to send an audio message, if someone locks the door using the Doorbird keypad and a door or window is still left open.

I hadnt even thought about that use case but that’s a great one as well. Looking forward to the proper implementation.

Thanks

I was using gstreamer for this case.
But every Home Assistant update, I have to install gstreamer again

Hi,there news about this functionality?
It would be incredible, because I also use doubletake to recognize people, at this point if it were possible to launch a TTS on the intercom it would be possible to leave a personalized message for each person!

Hi, thanks for your post. Everything works for me, only mp3 is pronounced very quickly and as if not completely. Is there anything to consider
And as output I have the following
stdout: “”
stderr: (‘Connection aborted.’, BadStatusLine(‘\r\n’))
returncode: 0

Looking at the error it sounds like a bandwith issue. I have my DoorBird connected over PoE. My guess is yours is connected over WiFi and it cant handle this kind of traffic properly. Thats a guess though.

Also connected via poe. This is very strange
I have D2101KV when it’s important

Can you please help me? What else can I do, what can I checkCan you please help me? What else can I do, what can I check?
Can i have your mp3 to test?

Can you please explain how you do it with gstreamer? I installed gstreamer, but I don’t know how to proceed properly.

Mine is also connected via POE and has the same problem as @Artur1990. The audio is played but in a strange way… could it be a conversion problem?

same issue here. did you ever get it resolved?

I tried to solve this issue/problem. But unfortunately I couldn’t implement a satified solution because I’m not a python developer too.
But I could solve issue.

First of all I had to use the Advanced SSH & Web Terminal addon to install some missing python modules (I saw these errors when I executed the python script in the terminal…):

  1. Error:
File "/config/custom_scripts/doorbird_transmit_audio.py", line 3, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

Solution:

python3 -m pip install requests
  1. Error:
[Errno 2] No such file or directory: 'ffmpeg'#

Solution:

apk add ffmpeg

Then the script was executed and the audio was transfered to the doorbird but still with the error:

stdout: “”
stderr: (‘Connection aborted.’, BadStatusLine(‘\r\n’))
returncode: 0

But unfortunately the audio was terrible to listen on the doorbird.

Then I adapted the Python script of @Phuturist to this (like in Doorbird play audio clip through speaker - #7 by Thorbeen with “clipping a little”):

import subprocess
import sys
import requests
from requests.exceptions import RequestException
from time import sleep, time
from io import BytesIO

# Credits for this script go to Kristian (coobnoob). The base of this script is a copy from https://github.com/coobnoob/ha-appdaemon-doorbird-audio/tree/main

# usage examples
# python3 /config/custom_scripts/doorbird_transmit_audio.py 192.168.0.134 username password /config/www/sounds/doorbird/not_at_home.mp3

# parameters
base_url = "http://%s/bha-api" % (sys.argv[1])
auth = (sys.argv[2], sys.argv[3])

def _generate_audio_chunks(output_stream, chunk_size=8*1024):
  """
  Generator function to yield chunks of audio data from a file.
  Doorbird is rate limited to 8K per second

  Args:
      output_stream: Stream of audio
      chunk_size (int): Size of each chunk in bytes.

  Yields:
      bytes: Chunks of audio data.
  """
  next_chunk_time = time()
  while True:
    start_time = time()
    chunk = output_stream.read(chunk_size)
    if not chunk:
      break
    #sleep(1) # We are rate limiting to chunk_size per second
    yield chunk
    elapsed_time = time() - start_time
    next_chunk_time += 0.5
    sleep_time = max(0, next_chunk_time - time())
    #self._log_timing(self.CHUNK_SIZE, elapsed_time, sleep_time)
    sleep(sleep_time)

def send_audio(audio_file_path):
  """
  Send audio to Doorbird

  Args:
      audio_file_path (str): Path to the audio file to be transmitted.
      
  Raises:
      Exception
  """
  try:
    response = requests.get(f"{base_url}/getsession.cgi", auth=auth)
    response.raise_for_status()
       
    # Check if the request was successful (HTTP status code 200)
    if response.status_code == 200:
      data = response.json()
      session_id = data["BHA"]["SESSIONID"]
    else:
      raise Exception(f"Failed to obtain session ID. Status code: {response.status_code}")

    # Prepare headers
    headers = {
      "Content-Type": "audio/basic",
      "Connection": "Keep-Alive",
      "Cache-Control": "no-cache"
    }

    # Command to convert the file to an a mono, 8000Khz mu-law wav
    command = [
      "ffmpeg",
      "-y",
      "-i", audio_file_path,
      "-ac", "1",
      "-ar", "8000",
      "-f", "wav",
      "-codec:a", "pcm_mulaw",
      "-"
    ]

    # Convert the file to an in-memory, mono, 8000Khz mu-law wav
    result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    result.check_returncode()
    
    # Make the POST request with the audio data generator to chunk the file
    with requests.post(
      f"{base_url}/audio-transmit.cgi?sessionid={session_id}",
      headers=headers,
      data=_generate_audio_chunks(BytesIO(result.stdout), chunk_size=8*1024),
      auth=auth,
      stream=True
    ) as response:
      if response.status_code != 200:
        sys.stderr.write("Failed to transmit audio. \n")
        response.raise_for_status()
        sys.exit()
  except requests.exceptions.RequestException as e:
    sys.stderr.write(str(e))
    sys.exit()

# Make the request
try:
  send_audio(sys.argv[4])
except Exception as e:
  sys.stderr.write(str(e))
  sys.exit()

the important part is the _generate_audio_chunks method with the new sleep time. BUT even then the audio file is not played clear and satified on the doorbird. the “still clipping a little” from the mentioned link is a showstopper for me… :frowning:

Maybe there is someone who can solve this issue finally please? That would be great :pray:

Hey, so I’m trying to implement this. I’ve already added the URLs to doorbirdpy and now want to start implementing in HA itself. Would this be the correct idea?

  1. RECEIVE: Media Source (as is used in tts) to play audio
  2. SEND: Get audio as input and send it via POST to the doorbird device. Here also similar to stt.
  3. Or is there a way to do proper 2-way audio in HA? I can’t find any examples…

Or does anyone have any other ideas?

With cURL I have both directions working, just need to get it into HA now.

Hey there,

I think I fixed the audio playback. At least for me it is now playing clear audio.
This is the script I’m using

import subprocess
import sys
import time
from io import BytesIO

import requests
from requests.exceptions import RequestException

def convert_to_ulaw_bytes(audio_file_path: str) -> bytes:
    """
    Convert arbitrary audio input to raw G.711 µ-law (8kHz, mono) bytes.
    Output is raw payload (no WAV/AU container).
    """
    ffmpeg_cmd = [
        "ffmpeg", "-y",
        "-i", audio_file_path,
        "-ac", "1",
        "-ar", "8000",
        "-af", "dcshift=shift=0,highpass=f=400,lowpass=f=3200,alimiter=limit=0.55",
        "-f", "mulaw",
        "-codec:a", "pcm_mulaw",
        "-"
    ]

    p = subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if p.returncode != 0:
        raise RuntimeError(
            "ffmpeg failed:\n" + p.stderr.decode(errors="ignore")
        )
    if not p.stdout:
        raise RuntimeError("ffmpeg produced no audio output.")
    return p.stdout


def realtime_ulaw_generator(ulaw_stream, bytes_per_sec=8000, chunk_size=160):
    """
    Yield µ-law bytes in real-time pacing.

    G.711 µ-law @ 8kHz => 8000 bytes/sec.
    chunk_size=160 bytes => 20ms of audio per chunk (classic telephony frame).
    """
    interval = chunk_size / bytes_per_sec  # e.g. 160/8000 = 0.02s

    next_t = time.monotonic()
    while True:
        chunk = ulaw_stream.read(chunk_size)
        if not chunk:
            break

        yield chunk

        next_t += interval
        sleep_for = next_t - time.monotonic()
        if sleep_for > 0:
            time.sleep(sleep_for)


def send_audio(device_ip: str, username: str, password: str, audio_file_path: str):
    """
    Convert and transmit audio to DoorBird via LAN API.
    """
    url = f"http://{device_ip}/bha-api/audio-transmit.cgi"
    auth = (username, password)

    # DoorBird expects G.711 µ-law, 8000 Hz, and Content-Type audio/basic
    headers = {
        "Content-Type": "audio/basic",
        "Cache-Control": "no-cache",
        "Connection": "close",
    }

    ulaw_bytes = convert_to_ulaw_bytes(audio_file_path)
    stream = BytesIO(ulaw_bytes)

    # Use requests streaming upload with real-time pacing
    try:
        resp = requests.post(
            url,
            headers=headers,
            data=realtime_ulaw_generator(stream, bytes_per_sec=8000, chunk_size=160),
            auth=auth,
            timeout=30,
        )

        # DoorBird may return 204 if user lacks permission (no ring event, no watch-always)
        if resp.status_code == 204:
            raise RuntimeError(
                "DoorBird returned HTTP 204 (no permission: needs watch-always or a ring event within last 5 minutes)."
            )

        resp.raise_for_status()
        return True

    except RequestException as e:
        raise RuntimeError(f"HTTP request failed: {e}") from e


def main():
    if len(sys.argv) != 5:
        sys.stderr.write(
            "Usage: python3 doorbird_transmit_audio.py <device-ip> <username> <password> <audio-file>\n"
        )
        sys.exit(1)

    device_ip = sys.argv[1]
    username = sys.argv[2]
    password = sys.argv[3]
    audio_file_path = sys.argv[4]

    try:
        send_audio(device_ip, username, password, audio_file_path)
        print("OK: Audio transmitted.")
    except Exception as e:
        sys.stderr.write(str(e) + "\n")
        sys.exit(1)


if __name__ == "__main__":
    main()

Best,
Daniel

Great Daniel, thanks a lot! I will try it the next days…
What kind of MP3 file should be used? Any special audio parameters/coding?

Best regards, Oilid

Sure no problem. Looking forward to your feedback. I just used a mp3 file I created with this free online text to mp3 tool here https://ttsmp3.com/. But I guess any mp3 should be fine.

@danielboecker Unfortunately it doesn’t work for me with your script. Nothing will be transferred to the doorbird. BTW: My Doorbird is D2101V.
I think there is a security issue because your script doesn’t ask for a session id before sending the audio file?! Is this maybe the reason? I tried to adapt your script for sending the session id (like in my posted script) but it doesn’t work.
Unfortunately I’m not a python developer :see_no_evil: