]> git.basschouten.com Git - openhab-addons.git/commitdiff
[doorbird] Add audiosink (#14122)
authorGwendal Roulleau <dalgwen@users.noreply.github.com>
Sun, 8 Jan 2023 09:57:04 +0000 (10:57 +0100)
committerGitHub <noreply@github.com>
Sun, 8 Jan 2023 09:57:04 +0000 (10:57 +0100)
* [doorbird] Add audiosink

Add audiosink capability to a doorbird thing

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/DoorbirdHandlerFactory.java
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdAPI.java
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/audio/ConvertedInputStream.java [new file with mode: 0644]
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/audio/DoorbirdAudioSink.java [new file with mode: 0644]
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/handler/DoorbellHandler.java

index c5e31f0d0eb4fc67d39b90a9db1cf29823f28b84..2a217c7743fcc4f426ab870b14eeea1f31cee2d2 100644 (file)
@@ -58,7 +58,7 @@ public class DoorbirdHandlerFactory extends BaseThingHandlerFactory {
     protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
         if (THING_TYPE_D101.equals(thingTypeUID) || THING_TYPE_D210X.equals(thingTypeUID)) {
-            return new DoorbellHandler(thing, timeZoneProvider, httpClient);
+            return new DoorbellHandler(thing, timeZoneProvider, httpClient, bundleContext);
         } else if (THING_TYPE_A1081.equals(thingTypeUID)) {
             return new ControllerHandler(thing);
         }
index 47551dd0a231019858d8ff391082f5adb5233e71..51ba942577cdafc649eebb8b331937d7b9bf70db 100644 (file)
 package org.openhab.binding.doorbird.internal.api;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
 import java.time.Duration;
 import java.time.ZonedDateTime;
+import java.util.Arrays;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -24,6 +27,9 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.DeferredContentProvider;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
@@ -52,6 +58,17 @@ public final class DoorbirdAPI {
     private @Nullable Authorization authorization;
     private @Nullable HttpClient httpClient;
 
+    // define a completed listener when sending audio asynchronously :
+    private Response.CompleteListener complete = new Response.CompleteListener() {
+        @Override
+        public void onComplete(@Nullable Result result) {
+            if (result != null) {
+                logger.debug("Doorbird audio sent. Response status {} {} ", result.getResponse().getStatus(),
+                        result.getResponse().getReason());
+            }
+        }
+    };
+
     public static Gson getGson() {
         return (GSON);
     }
@@ -145,6 +162,59 @@ public final class DoorbirdAPI {
         return downloadImage("/bha-api/history.cgi?event=motionsensor&index=" + imageNumber);
     }
 
+    public void sendAudio(InputStream audioInputStream) {
+        Authorization auth = authorization;
+        HttpClient client = httpClient;
+        if (client == null) {
+            logger.warn("Unable to send audio because httpClient is not set");
+            return;
+        }
+        if (auth == null) {
+            logAuthorizationError("audio-transmit");
+            return;
+        }
+        String url = buildUrl(auth, "/bha-api/audio-transmit.cgi");
+        logger.debug("Executing doorbird API post audio: {}", url);
+        DeferredContentProvider content = new DeferredContentProvider();
+        try {
+            // @formatter:off
+            client.POST(url)
+                    .header("Authorization", "Basic " + auth.getAuthorization())
+                    .header("Content-Type", "audio/basic")
+                    .header("Content-Length", "9999999")
+                    .header("Connection", "Keep-Alive")
+                    .header("Cache-Control", "no-cache")
+                    .content(content)
+                    .send(complete);
+            // @formatter:on
+
+            // It is crucial to send data in small chunks to not overload the doorbird
+            // It means that we have to wait the appropriate amount of time between chunk to send
+            // real time data, as if it were live spoken.
+            int CHUNK_SIZE = 256;
+            int nbByteRead = -1;
+            long nextChunkSendTimeStamp = 0;
+            do {
+                byte[] data = new byte[CHUNK_SIZE];
+                nbByteRead = audioInputStream.read(data);
+                if (nbByteRead > 0) {
+                    if (nbByteRead != CHUNK_SIZE) {
+                        data = Arrays.copyOf(data, nbByteRead);
+                    } // compute exact waiting time needed, by checking previous estimation against current time
+                    long timeToWait = Math.max(0, nextChunkSendTimeStamp - System.currentTimeMillis());
+                    Thread.sleep(timeToWait);
+                    logger.debug("Sending chunk...");
+                    content.offer(ByteBuffer.wrap(data));
+                }
+                nextChunkSendTimeStamp = System.currentTimeMillis() + 30;
+            } while (nbByteRead != -1);
+        } catch (InterruptedException | IOException e) {
+            logger.info("Unable to communicate with Doorbird", e);
+        } finally {
+            content.close();
+        }
+    }
+
     public void openDoorController(String controllerId, String doorNumber) {
         openDoor("/bha-api/open-door.cgi?r=" + controllerId + "@" + doorNumber);
     }
diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/audio/ConvertedInputStream.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/audio/ConvertedInputStream.java
new file mode 100644 (file)
index 0000000..e4ac160
--- /dev/null
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.doorbird.internal.audio;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioFormat.Encoding;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * This class convert a stream to the normalized ulaw
+ * format wanted by doorbird api
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class ConvertedInputStream extends InputStream {
+
+    private static final javax.sound.sampled.AudioFormat INTERMEDIARY_PCM_FORMAT = new javax.sound.sampled.AudioFormat(
+            javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, 8000, 16, 1, 2, 8000, false);
+    private static final javax.sound.sampled.AudioFormat TARGET_ULAW_FORMAT = new javax.sound.sampled.AudioFormat(
+            javax.sound.sampled.AudioFormat.Encoding.ULAW, 8000, 8, 1, 1, 8000, false);
+
+    private AudioInputStream pcmUlawInputStream;
+
+    public ConvertedInputStream(InputStream innerInputStream) throws UnsupportedAudioFileException, IOException {
+
+        pcmUlawInputStream = getULAWStream(new BufferedInputStream(innerInputStream));
+    }
+
+    public AudioInputStream getAudioInputStream() {
+        return pcmUlawInputStream;
+    }
+
+    @Override
+    public int read(byte @Nullable [] b) throws IOException {
+        return pcmUlawInputStream.read(b);
+    }
+
+    @Override
+    public int read(byte @Nullable [] b, int off, int len) throws IOException {
+        return pcmUlawInputStream.read(b, off, len);
+    }
+
+    @Override
+    public byte[] readAllBytes() throws IOException {
+        return pcmUlawInputStream.readAllBytes();
+    }
+
+    @Override
+    public byte[] readNBytes(int len) throws IOException {
+        return pcmUlawInputStream.readNBytes(len);
+    }
+
+    @Override
+    public int readNBytes(byte @Nullable [] b, int off, int len) throws IOException {
+        return pcmUlawInputStream.readNBytes(b, off, len);
+    }
+
+    @Override
+    public int read() throws IOException {
+        return pcmUlawInputStream.read();
+    }
+
+    @Override
+    public void close() throws IOException {
+        pcmUlawInputStream.close();
+    }
+
+    /**
+     * Ensure the right ULAW format by converting if necessary (two pass)
+     *
+     * @param originalInputStream a mark/reset compatible stream
+     *
+     * @return A ULAW stream (1 channel, 8000hz, 16 bit signed)
+     * @throws IOException
+     * @throws UnsupportedAudioFileException
+     */
+    private AudioInputStream getULAWStream(InputStream originalInputStream)
+            throws UnsupportedAudioFileException, IOException {
+
+        try {
+            AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(originalInputStream);
+            AudioFormat format = audioInputStream.getFormat();
+
+            boolean frameRateOk = Math.abs(format.getFrameRate() - 8000) < 1;
+            boolean sampleRateOk = Math.abs(format.getSampleRate() - 8000) < 1;
+
+            if (format.getEncoding().equals(Encoding.ULAW) && format.getChannels() == 1 && frameRateOk && sampleRateOk
+                    && format.getFrameSize() == 1 && format.getSampleSizeInBits() == 8) {
+                return audioInputStream;
+            }
+
+            // we have to use an intermediary format with 16 bits, even if the final target format is 8 bits
+            // this is a limitation of the conversion library, which only accept 16 bits input to convert to ULAW.
+            AudioInputStream targetPCMFormat = audioInputStream;
+            if (format.getChannels() != 1 || !frameRateOk || !sampleRateOk || format.getFrameSize() != 2
+                    || format.getSampleSizeInBits() != 16) {
+                targetPCMFormat = AudioSystem.getAudioInputStream(INTERMEDIARY_PCM_FORMAT, audioInputStream);
+            }
+
+            return AudioSystem.getAudioInputStream(TARGET_ULAW_FORMAT, targetPCMFormat);
+        } catch (IllegalArgumentException iarg) {
+            throw new UnsupportedAudioFileException(
+                    "Cannot convert audio input to ULAW target format. Cause: " + iarg.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/audio/DoorbirdAudioSink.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/audio/DoorbirdAudioSink.java
new file mode 100644 (file)
index 0000000..d6c82b5
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.doorbird.internal.audio;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.doorbird.internal.handler.DoorbellHandler;
+import org.openhab.core.audio.AudioFormat;
+import org.openhab.core.audio.AudioSink;
+import org.openhab.core.audio.AudioStream;
+import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.UnsupportedAudioFormatException;
+import org.openhab.core.audio.UnsupportedAudioStreamException;
+import org.openhab.core.library.types.PercentType;
+
+/**
+ * The audio sink for doorbird
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class DoorbirdAudioSink implements AudioSink {
+
+    private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
+    private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
+
+    private DoorbellHandler doorbellHandler;
+
+    static {
+        SUPPORTED_FORMATS.add(AudioFormat.WAV);
+        SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
+    }
+
+    public DoorbirdAudioSink(DoorbellHandler doorbellHandler) {
+        this.doorbellHandler = doorbellHandler;
+    }
+
+    @Override
+    public String getId() {
+        return doorbellHandler.getThing().getUID().toString();
+    }
+
+    @Override
+    public @Nullable String getLabel(@Nullable Locale locale) {
+        return doorbellHandler.getThing().getLabel();
+    }
+
+    @Override
+    public void process(@Nullable AudioStream audioStream)
+            throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
+        if (audioStream == null) {
+            return;
+        }
+        try (ConvertedInputStream normalizedULAWStream = new ConvertedInputStream(audioStream)) {
+            doorbellHandler.sendAudio(normalizedULAWStream);
+        } catch (UnsupportedAudioFileException | IOException e) {
+            throw new UnsupportedAudioFormatException("Cannot send to the doorbird sink", audioStream.getFormat(), e);
+        }
+    }
+
+    @Override
+    public Set<AudioFormat> getSupportedFormats() {
+        return SUPPORTED_FORMATS;
+    }
+
+    @Override
+    public Set<Class<? extends AudioStream>> getSupportedStreams() {
+        return SUPPORTED_STREAMS;
+    }
+
+    @Override
+    public PercentType getVolume() {
+        return new PercentType(100);
+    }
+
+    @Override
+    public void setVolume(PercentType volume) {
+        // NOT IMPLEMENTED
+    }
+}
index b831eaaf30dde2f9212ded3f8eb558fd2dfc2dce..e625fa8a5e4009edba403cceddc2adc3e844a415 100644 (file)
@@ -19,10 +19,12 @@ import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Hashtable;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -37,8 +39,10 @@ import org.openhab.binding.doorbird.internal.action.DoorbirdActions;
 import org.openhab.binding.doorbird.internal.api.DoorbirdAPI;
 import org.openhab.binding.doorbird.internal.api.DoorbirdImage;
 import org.openhab.binding.doorbird.internal.api.SipStatus;
+import org.openhab.binding.doorbird.internal.audio.DoorbirdAudioSink;
 import org.openhab.binding.doorbird.internal.config.DoorbellConfiguration;
 import org.openhab.binding.doorbird.internal.listener.DoorbirdUdpListener;
+import org.openhab.core.audio.AudioSink;
 import org.openhab.core.common.ThreadPoolManager;
 import org.openhab.core.i18n.TimeZoneProvider;
 import org.openhab.core.library.types.DateTimeType;
@@ -56,6 +60,8 @@ import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
 import org.openhab.core.types.State;
 import org.openhab.core.types.UnDefType;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -88,13 +94,19 @@ public class DoorbellHandler extends BaseThingHandler {
 
     private DoorbirdAPI api = new DoorbirdAPI();
 
+    private BundleContext bundleContext;
+
+    private @Nullable ServiceRegistration<AudioSink> audioSinkRegistration;
+
     private final TimeZoneProvider timeZoneProvider;
     private final HttpClient httpClient;
 
-    public DoorbellHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient) {
+    public DoorbellHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient,
+            BundleContext bundleContext) {
         super(thing);
         this.timeZoneProvider = timeZoneProvider;
         this.httpClient = httpClient;
+        this.bundleContext = bundleContext;
         udpListener = new DoorbirdUdpListener(this);
     }
 
@@ -120,6 +132,7 @@ public class DoorbellHandler extends BaseThingHandler {
         api.setHttpClient(httpClient);
         startImageRefreshJob();
         startUDPListenerJob();
+        startAudioSink();
         updateStatus(ThingStatus.ONLINE);
     }
 
@@ -129,6 +142,7 @@ public class DoorbellHandler extends BaseThingHandler {
         stopImageRefreshJob();
         stopDoorbellOffJob();
         stopMotionOffJob();
+        stopAudioSink();
         super.dispose();
     }
 
@@ -240,6 +254,10 @@ public class DoorbellHandler extends BaseThingHandler {
         api.sipHangup();
     }
 
+    public void sendAudio(InputStream inputStream) {
+        api.sendAudio(inputStream);
+    }
+
     public String actionGetRingTimeLimit() {
         return getSipStatusValue(SipStatus::getRingTimeLimit);
     }
@@ -411,6 +429,23 @@ public class DoorbellHandler extends BaseThingHandler {
         }
     }
 
+    private void startAudioSink() {
+        final DoorbellHandler thisHandler = this;
+        // Register an audio sink in openhab
+        logger.trace("Registering an audio sink for this {}", thing.getUID());
+        audioSinkRegistration = bundleContext.registerService(AudioSink.class, new DoorbirdAudioSink(thisHandler),
+                new Hashtable<>());
+    }
+
+    private void stopAudioSink() {
+        // Unregister the doorbird audio sink
+        ServiceRegistration<AudioSink> audioSinkRegistrationLocal = audioSinkRegistration;
+        if (audioSinkRegistrationLocal != null) {
+            logger.trace("Unregistering the audio sync service for the doorbird thing {}", getThing().getUID());
+            audioSinkRegistrationLocal.unregister();
+        }
+    }
+
     private void updateDoorbellMontage() {
         if (config.montageNumImages == 0) {
             return;