Doorbird play audio clip through speaker

Hi,

I would like to play audio clip through my doorbird speaker instead of speaking through their app. I basically just want to play a clip that instructs someone to pull the gate.

Does anyone know how this might be possible?

This works ok assuming you have appdaemon:

Hi,

Thank you very much for your work! I have been looking for a solution to this problem for a long time. I managed to connect to my Doorbird device, the audio (tts to mp3) is streaming, but it comes out as complete crap. Some chunks do get through in an understandable fashion though. Can the streaming be optimized in some way?

Apparently this audio transmission is an integral part of the doorbird binding in openHAB, seems the wait time could be the issue? Checking the code of their binding I found this: openHAB binding

Did you find a solution here?

I got the same issue - the AppDaemon script is working proper but the Audio Quality at the doorbird is totally disturbed / chopped.

My files for test purpose have been converted to μ-law WAV (8Khz, Mono, μ-law) as described in the API documentation

Find the logs here

[13:09:08] INFO: Starting AppDaemon...
s6-rc: info: service legacy-services successfully started
2024-05-27 13:09:10.149301 INFO AppDaemon: AppDaemon Version 4.4.2 starting
2024-05-27 13:09:10.149421 INFO AppDaemon: Python version is 3.11.9
2024-05-27 13:09:10.149524 INFO AppDaemon: Configuration read from: /config/appdaemon.yaml
2024-05-27 13:09:10.149628 INFO AppDaemon: Added log: AppDaemon
2024-05-27 13:09:10.149736 INFO AppDaemon: Added log: Error
2024-05-27 13:09:10.149839 INFO AppDaemon: Added log: Access
2024-05-27 13:09:10.149941 INFO AppDaemon: Added log: Diag
2024-05-27 13:09:10.244897 INFO AppDaemon: Loading Plugin HASS using class HassPlugin from module hassplugin
2024-05-27 13:09:10.339916 INFO HASS: HASS Plugin Initializing
2024-05-27 13:09:10.340002 WARNING HASS: ha_url not found in HASS configuration - module not initialized
2024-05-27 13:09:10.340069 INFO HASS: HASS Plugin initialization complete
2024-05-27 13:09:10.340260 INFO AppDaemon: Initializing HTTP
2024-05-27 13:09:10.340410 INFO AppDaemon: Using 'ws' for event stream
2024-05-27 13:09:10.342993 INFO AppDaemon: Starting API
2024-05-27 13:09:10.344546 INFO AppDaemon: Starting Admin Interface
2024-05-27 13:09:10.344722 INFO AppDaemon: Starting Dashboards
2024-05-27 13:09:10.352267 INFO HASS: Connected to Home Assistant 2024.5.5
2024-05-27 13:09:10.356045 INFO AppDaemon: App 'hello_world' added
2024-05-27 13:09:10.356712 INFO AppDaemon: App 'doorbird_audio' added
2024-05-27 13:09:10.357177 INFO AppDaemon: Found 2 active apps
2024-05-27 13:09:10.357285 INFO AppDaemon: Found 0 inactive apps
2024-05-27 13:09:10.357373 INFO AppDaemon: Found 0 global libraries
2024-05-27 13:09:10.357468 INFO AppDaemon: Starting Apps with 2 workers and 2 pins
2024-05-27 13:09:10.357880 INFO AppDaemon: Running on port 5050
2024-05-27 13:09:10.384307 INFO HASS: Evaluating startup conditions
2024-05-27 13:09:10.388683 INFO HASS: Startup condition met: hass state=RUNNING
2024-05-27 13:09:10.388926 INFO HASS: All startup conditions met
2024-05-27 13:09:10.421501 INFO AppDaemon: Got initial state from namespace default
2024-05-27 13:09:12.361120 INFO AppDaemon: Scheduler running in realtime
2024-05-27 13:09:12.362035 INFO AppDaemon: Adding /config/apps to module import path
2024-05-27 13:09:12.362838 INFO AppDaemon: Loading App Module: /config/apps/doorbird_audio.py
2024-05-27 13:09:12.396271 INFO AppDaemon: Loading App Module: /config/apps/hello.py
2024-05-27 13:09:12.396822 INFO AppDaemon: Loading app hello_world using class HelloWorld from module hello
2024-05-27 13:09:12.397383 INFO AppDaemon: Loading app doorbird_audio using class DoorbirdAudio from module doorbird_audio
2024-05-27 13:09:12.398044 INFO AppDaemon: Calling initialize() for hello_world
2024-05-27 13:09:12.422741 INFO hello_world: Hello from AppDaemon
2024-05-27 13:09:12.423230 INFO hello_world: You are now ready to run Apps!
2024-05-27 13:09:12.423692 INFO AppDaemon: Calling initialize() for doorbird_audio
2024-05-27 13:09:12.424552 INFO AppDaemon: App initialization complete
2024-05-27 13:09:51.088991 WARNING doorbird_audio: ------------------------------------------------------------
2024-05-27 13:09:51.089135 WARNING doorbird_audio: Unexpected error in worker for App doorbird_audio:
2024-05-27 13:09:51.089257 WARNING doorbird_audio: Worker Ags: {'id': '61125763d71a48fc9732bb56e4d94e7c', 'name': 'doorbird_audio', 'objectid': 'c0d6aa6e93fb4c4a8b2e7141909ff011', 'type': 'event', 'event': 'doorbird_audio', 'function': <bound method DoorbirdAudio.doorbird_audio of <doorbird_audio.DoorbirdAudio object at 0x7fc31b40e210>>, 'data': {'device_ip': '192.168.1.220', 'username': 'XXXXX', 'password': 'XXXXX', 'audio_url': 'http://192.168.1.114:8123/local/doorbird.mp3', 'metadata': {'origin': 'LOCAL', 'time_fired': '2024-05-27T11:09:37.438571+00:00', 'context': {'id': '01HYWVVNRYM361TH1NWNN170QN', 'parent_id': None, 'user_id': '26262bdb3712457486cf0ee731a79cce'}}}, 'pin_app': True, 'pin_thread': 1, 'kwargs': {'__thread_id': 'thread-1'}}
2024-05-27 13:09:51.089370 WARNING doorbird_audio: ------------------------------------------------------------
2024-05-27 13:09:51.091348 WARNING doorbird_audio: Traceback (most recent call last):
File "/usr/lib/python3.11/site-packages/appdaemon/threading.py", line 1095, in worker
funcref(args["event"], data, self.AD.events.sanitize_event_kwargs(app, args["kwargs"]))
File "/config/apps/doorbird_audio.py", line 119, in doorbird_audio
doorbird.send_audio(data["audio_url"])
File "/config/apps/doorbird_audio.py", line 97, in send_audio
with requests.post(
^^^^^^^^^^^^^^
File "/usr/lib/python3.11/site-packages/requests/api.py", line 115, in post
return request("post", url, data=data, json=json, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/site-packages/requests/api.py", line 59, in request
return session.request(method=method, url=url, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/site-packages/requests/sessions.py", line 587, in request
resp = self.send(prep, **send_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/site-packages/requests/sessions.py", line 701, in send
r = adapter.send(request, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/site-packages/requests/adapters.py", line 531, in send
r = low_conn.getresponse()
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/http/client.py", line 1395, in getresponse
response.begin()
File "/usr/lib/python3.11/http/client.py", line 325, in begin
version, status, reason = self._read_status()
^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/http/client.py", line 307, in _read_status
raise BadStatusLine(line)
http.client.BadStatusLine:

2024-05-27 13:09:51.091472 WARNING doorbird_audio: ------------------------------------------------------------
2024-05-27 13:09:51.092131 WARNING AppDaemon: Excessive time spent in callback 'doorbird_audio() in doorbird_audio', Thread 'thread.thread-1' - now complete after 14.091939 seconds (limit=10)

i ended up getting rid of my doorbird. It’s a terrible product and I am much happier without it.

Fixed it so far - not perfect (still clipping a little - but easy to understand now)

import appdaemon.plugins.hass.hassapi as hass
import requests
from requests.auth import HTTPBasicAuth
from requests.exceptions import RequestException
import subprocess
from io import BytesIO
from time import sleep, time

class DoorbirdException(Exception):
    """An exception for doorbirds"""

class Doorbird:
    CHUNK_SIZE = 4096  # Moderate chunk size in bytes
    RATE_LIMIT = 8192  # Bytes per second (8KB per second)
    CHUNK_INTERVAL = 0.5  # Interval in seconds (500 ms for larger chunks)

    def __init__(self, device_ip, username, password):
        """
        Connect to a Doorbird

        Args:
            device_ip (str): Doorbird device IP address.
            username (str): Doorbird HTTP username.
            password (str): Doorbird HTTP password.

        Raises:
            DoorbirdException
        """
        self.device_ip = device_ip
        self.username = username
        self.password = password
        self.session_id = self._get_session_id()

    def _get_session_id(self):
        """
        Get session ID from Doorbird device.

        Raises:
            DoorbirdException: If unable to obtain session ID.
        """
        get_session_url = f"http://{self.device_ip}/bha-api/getsession.cgi"
        auth = (self.username, self.password)
        try:
            response = requests.get(get_session_url, auth=auth)
            response.raise_for_status()
            data = response.json()
            return data["BHA"]["SESSIONID"]
        except RequestException as e:
            raise DoorbirdException(f"Failed to obtain session ID: {e}")

    def _convert_audio(self, input_file):
        """
        Convert audio to 8000Hz mono PCM mu-law format.

        Args:
            input_file (str): Path to the input audio file.

        Returns:
            bytes: Converted audio data.
        """
        try:
            process = subprocess.run(
                [
                    'ffmpeg', '-y', '-i', input_file, '-ar', '8000', '-ac', '1', '-f', 'mulaw', 'pipe:1'
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                check=True
            )
            return process.stdout
        except subprocess.CalledProcessError as e:
            raise DoorbirdException(f"FFmpeg conversion failed: {e.stderr.decode()}")

    def _generate_audio_chunks(self, audio_data):
        """
        Generator function to yield chunks of audio data from a file.
        Doorbird is rate-limited to 8KB per second.

        Args:
            audio_data: Converted audio data

        Yields:
            bytes: Chunks of audio data.
        """
        stream = BytesIO(audio_data)
        next_chunk_time = time()
        while True:
            start_time = time()
            chunk = stream.read(self.CHUNK_SIZE)
            if not chunk:
                break
            yield chunk
            elapsed_time = time() - start_time
            next_chunk_time += self.CHUNK_INTERVAL
            sleep_time = max(0, next_chunk_time - time())
            self._log_timing(self.CHUNK_SIZE, elapsed_time, sleep_time)
            sleep(sleep_time)

    def _log_timing(self, chunk_size, elapsed_time, sleep_time):
        """
        Log the timing details for each chunk.

        Args:
            chunk_size (int): Size of the chunk.
            elapsed_time (float): Time taken to process the chunk.
            sleep_time (float): Time to sleep before sending the next chunk.
        """
        print(f"Chunk Size: {chunk_size}, Elapsed Time: {elapsed_time:.6f}, Sleep Time: {sleep_time:.6f}")

    def send_audio(self, audio_url):
        """
        Send audio to Doorbird device.

        Args:
            audio_url (str): URL of the audio file.

        Raises:
            DoorbirdException: If any step in the process fails.
        """
        try:
            # Download the audio file
            audio_response = requests.get(audio_url)
            audio_response.raise_for_status()
            audio_file_path = '/config/downloaded_audio.mp3'
            with open(audio_file_path, 'wb') as audio_file:
                audio_file.write(audio_response.content)

            # Convert the audio file
            audio_data = self._convert_audio(audio_file_path)

            # Transmit the audio file in chunks
            audio_transmit_url = f"http://{self.device_ip}/bha-api/audio-transmit.cgi?sessionid={self.session_id}"
            auth = HTTPBasicAuth(self.username, self.password)

            def audio_stream():
                for chunk in self._generate_audio_chunks(audio_data):
                    yield chunk

            response = requests.post(
                audio_transmit_url,
                headers={"Content-Type": "audio/basic", "Connection": "Keep-Alive", "Cache-Control": "no-cache"},
                data=audio_stream(),
                auth=auth,
                timeout=60
            )

            response.raise_for_status()
            print("Audio transmission completed successfully.")

        except RequestException as e:
            raise DoorbirdException(f"Failed to send audio: {e}")

class DoorbirdAudio(hass.Hass):
    """AppDaemon app to handle Doorbird audio events."""

    def initialize(self):
        """Initialize the AppDaemon app."""
        self.listen_event(self.doorbird_audio, "doorbird_audio")

    def doorbird_audio(self, event_name, data, kwargs):
        """Handle Doorbird audio event."""
        try:
            self.log(f"Received event: {event_name} with data: {data}")
            doorbird = Doorbird(data["device_ip"], data["username"], data["password"])
            doorbird.send_audio(data["audio_url"])
            self.log("Audio transmission completed successfully.")
        except DoorbirdException as e:
            self.log(f"Failed to send audio: {e}")