coobnoob / ha-appdaemon-doorbird-audio

A simple homeassistant AppDaemon app to send audio to a doorbird device
The Unlicense
1 stars 0 forks source link

Output Sound choppy #1

Open thorbeenwiedemann opened 1 month ago

thorbeenwiedemann commented 1 month ago

Sound output is really choppy, no chance to understand a single bit on the doorbird.

Tried multiple files mp3/wav and preconverted into ulaw 8khz aswell

Please see the AppDaemon Log attached

[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)

thorbeenwiedemann commented 1 month ago

Fixed it so far

`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}")

`