mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[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:
parent
c24ec070bc
commit
2c2097d646
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user