[doorbird] Add audiosink (#14122)

* [doorbird] Add audiosink

Add audiosink capability to a doorbird thing

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
This commit is contained in:
Gwendal Roulleau 2023-01-08 10:57:04 +01:00 committed by GitHub
parent c24ec070bc
commit 2c2097d646
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 331 additions and 2 deletions

View 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);
}

View File

@ -13,8 +13,11 @@
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);
}

View File

@ -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());
}
}
}

View File

@ -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
}
}

View 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;