]> git.basschouten.com Git - openhab-addons.git/blob
a357b6631886d0c64ab7c9efc5d98e5a8f20165d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.pulseaudio.internal.handler;
14
15 import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
16
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.Hashtable;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSink;
34 import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSource;
35 import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
36 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
37 import org.openhab.binding.pulseaudio.internal.items.Sink;
38 import org.openhab.binding.pulseaudio.internal.items.SinkInput;
39 import org.openhab.binding.pulseaudio.internal.items.Source;
40 import org.openhab.core.audio.AudioFormat;
41 import org.openhab.core.audio.AudioSink;
42 import org.openhab.core.audio.AudioSource;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.IncreaseDecreaseType;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.PercentType;
48 import org.openhab.core.library.types.StringType;
49 import org.openhab.core.thing.Bridge;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.ThingStatusInfo;
55 import org.openhab.core.thing.ThingTypeUID;
56 import org.openhab.core.thing.ThingUID;
57 import org.openhab.core.thing.binding.BaseThingHandler;
58 import org.openhab.core.thing.binding.ThingHandler;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.RefreshType;
61 import org.openhab.core.types.State;
62 import org.openhab.core.types.UnDefType;
63 import org.osgi.framework.BundleContext;
64 import org.osgi.framework.ServiceRegistration;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 /**
69  * The {@link PulseaudioHandler} is responsible for handling commands, which are
70  * sent to one of the channels.
71  *
72  * @author Tobias Bräutigam - Initial contribution
73  * @author Miguel Álvarez - Register audio source and refactor
74  */
75 @NonNullByDefault
76 public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
77
78     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
79             .unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
80                     SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
81     private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
82     private final int refresh = 60; // refresh every minute as default
83
84     private @Nullable PulseaudioBridgeHandler bridgeHandler;
85     private @Nullable String name;
86     private @Nullable ScheduledFuture<?> refreshJob;
87     private @Nullable PulseAudioAudioSink audioSink;
88     private @Nullable PulseAudioAudioSource audioSource;
89     private @Nullable Integer savedVolume;
90
91     private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
92     private final Map<String, ServiceRegistration<AudioSource>> audioSourceRegistrations = new ConcurrentHashMap<>();
93
94     private final BundleContext bundleContext;
95
96     public PulseaudioHandler(Thing thing, BundleContext bundleContext) {
97         super(thing);
98         this.bundleContext = bundleContext;
99     }
100
101     @Override
102     public void initialize() {
103         Configuration config = getThing().getConfiguration();
104         name = (String) config.get(DEVICE_PARAMETER_NAME);
105
106         updateStatus(ThingStatus.UNKNOWN);
107         deviceOnlineWatchdog();
108
109         // if it's a SINK thing, then maybe we have to activate the audio sink
110         if (SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
111             // check the property to see if we it's enabled :
112             Boolean sinkActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
113             if (sinkActivated != null && sinkActivated) {
114                 audioSinkSetup();
115             }
116         }
117         // if it's a SOURCE thing, then maybe we have to activate the audio source
118         if (SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) {
119             // check the property to see if we it's enabled :
120             Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION);
121             if (sourceActivated != null && sourceActivated) {
122                 audioSourceSetup();
123             }
124         }
125     }
126
127     private void audioSinkSetup() {
128         final PulseaudioHandler thisHandler = this;
129         scheduler.submit(new Runnable() {
130             @Override
131             public void run() {
132                 // Register the sink as an audio sink in openhab
133                 logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
134                 PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
135                 setAudioSink(audioSink);
136                 try {
137                     audioSink.connectIfNeeded();
138                 } catch (IOException e) {
139                     logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
140                             getHost(), e.getMessage());
141                 } catch (InterruptedException i) {
142                     logger.info("Interrupted during sink audio connection: {}", i.getMessage());
143                     return;
144                 } finally {
145                     audioSink.scheduleDisconnect();
146                 }
147                 @SuppressWarnings("unchecked")
148                 ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
149                         .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
150                 audioSinkRegistrations.put(thing.getUID().toString(), reg);
151             }
152         });
153     }
154
155     private void audioSourceSetup() {
156         final PulseaudioHandler thisHandler = this;
157         scheduler.submit(new Runnable() {
158             @Override
159             public void run() {
160                 // Register the source as an audio source in openhab
161                 logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
162                 PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
163                 setAudioSource(audioSource);
164                 try {
165                     audioSource.connectIfNeeded();
166                 } catch (IOException e) {
167                     logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
168                             getHost(), e.getMessage());
169                 } catch (InterruptedException i) {
170                     logger.info("Interrupted during source audio connection: {}", i.getMessage());
171                     return;
172                 } finally {
173                     audioSource.scheduleDisconnect();
174                 }
175                 @SuppressWarnings("unchecked")
176                 ServiceRegistration<AudioSource> reg = (ServiceRegistration<AudioSource>) bundleContext
177                         .registerService(AudioSource.class.getName(), audioSource, new Hashtable<>());
178                 audioSourceRegistrations.put(thing.getUID().toString(), reg);
179             }
180         });
181     }
182
183     @Override
184     public void dispose() {
185         ScheduledFuture<?> job = refreshJob;
186         if (job != null && !job.isCancelled()) {
187             job.cancel(true);
188             refreshJob = null;
189         }
190         PulseaudioBridgeHandler briHandler = bridgeHandler;
191         if (briHandler != null) {
192             briHandler.unregisterDeviceStatusListener(this);
193             bridgeHandler = null;
194         }
195         logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
196         super.dispose();
197         PulseAudioAudioSink sink = audioSink;
198         if (sink != null) {
199             sink.disconnect();
200         }
201         PulseAudioAudioSource source = audioSource;
202         if (source != null) {
203             source.disconnect();
204         }
205         // Unregister the potential pulse audio sink's audio sink
206         ServiceRegistration<AudioSink> sinkReg = audioSinkRegistrations.remove(getThing().getUID().toString());
207         if (sinkReg != null) {
208             logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
209             sinkReg.unregister();
210         }
211         // Unregister the potential pulse audio source's audio sources
212         ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString());
213         if (sourceReg != null) {
214             logger.trace("Unregistering the audio sync service for pulse audio source thing {}", getThing().getUID());
215             sourceReg.unregister();
216         }
217     }
218
219     @Override
220     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
221         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
222                 && getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
223             // Bridge is now ONLINE, restart the refresh job to get an update of the thing status without waiting
224             // its next planned run
225             ScheduledFuture<?> job = refreshJob;
226             if (job != null && !job.isCancelled()) {
227                 job.cancel(true);
228                 refreshJob = null;
229             }
230             deviceOnlineWatchdog();
231         } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE
232                 || bridgeStatusInfo.getStatus() == ThingStatus.UNKNOWN) {
233             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
234         }
235     }
236
237     private void deviceOnlineWatchdog() {
238         Runnable runnable = () -> {
239             try {
240                 PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
241                 if (bridgeHandler != null) {
242                     if (bridgeHandler.getThing().getStatus() == ThingStatus.ONLINE) {
243                         if (bridgeHandler.getDevice(name) == null) {
244                             updateStatus(ThingStatus.OFFLINE);
245                             this.bridgeHandler = null;
246                         } else {
247                             updateStatus(ThingStatus.ONLINE);
248                         }
249                     } else {
250                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
251                     }
252                 } else {
253                     logger.debug("Bridge for pulseaudio device {} not found.", name);
254                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
255                 }
256             } catch (Exception e) {
257                 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
258                 this.bridgeHandler = null;
259             }
260         };
261
262         refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
263     }
264
265     private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
266         if (this.bridgeHandler == null) {
267             Bridge bridge = getBridge();
268             if (bridge == null) {
269                 logger.debug("Required bridge not defined for device {}.", name);
270                 return null;
271             }
272             ThingHandler handler = bridge.getHandler();
273             if (handler instanceof PulseaudioBridgeHandler) {
274                 this.bridgeHandler = (PulseaudioBridgeHandler) handler;
275                 this.bridgeHandler.registerDeviceStatusListener(this);
276             } else {
277                 logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
278                 return null;
279             }
280         }
281         return this.bridgeHandler;
282     }
283
284     @Override
285     public void handleCommand(ChannelUID channelUID, Command command) {
286         PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
287         if (briHandler == null) {
288             logger.debug("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
289             return;
290         }
291         if (command instanceof RefreshType) {
292             briHandler.handleCommand(channelUID, command);
293             return;
294         }
295
296         AbstractAudioDeviceConfig device = briHandler.getDevice(name);
297         if (device == null) {
298             logger.warn("device {} not found", name);
299             updateStatus(ThingStatus.OFFLINE);
300             bridgeHandler = null;
301             return;
302         } else {
303             State updateState = UnDefType.UNDEF;
304             if (channelUID.getId().equals(VOLUME_CHANNEL)) {
305                 if (command instanceof IncreaseDecreaseType) {
306                     // refresh to get the current volume level
307                     briHandler.getClient().update();
308                     device = briHandler.getDevice(name);
309                     if (device == null) {
310                         logger.warn("missing device info, aborting");
311                         return;
312                     }
313                     int oldVolume = device.getVolume();
314                     int newVolume = oldVolume;
315                     if (command.equals(IncreaseDecreaseType.INCREASE)) {
316                         newVolume = Math.min(100, oldVolume + 5);
317                     }
318                     if (command.equals(IncreaseDecreaseType.DECREASE)) {
319                         newVolume = Math.max(0, oldVolume - 5);
320                     }
321                     briHandler.getClient().setVolumePercent(device, newVolume);
322                     updateState = new PercentType(newVolume);
323                     savedVolume = newVolume;
324                 } else if (command instanceof PercentType) {
325                     DecimalType volume = (DecimalType) command;
326                     briHandler.getClient().setVolumePercent(device, volume.intValue());
327                     updateState = (PercentType) command;
328                     savedVolume = volume.intValue();
329                 } else if (command instanceof DecimalType) {
330                     // set volume
331                     DecimalType volume = (DecimalType) command;
332                     briHandler.getClient().setVolume(device, volume.intValue());
333                     updateState = (DecimalType) command;
334                     savedVolume = volume.intValue();
335                 }
336             } else if (channelUID.getId().equals(MUTE_CHANNEL)) {
337                 if (command instanceof OnOffType) {
338                     briHandler.getClient().setMute(device, OnOffType.ON.equals(command));
339                     updateState = (OnOffType) command;
340                 }
341             } else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
342                 if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
343                     if (command instanceof StringType) {
344                         List<Sink> slaves = new ArrayList<>();
345                         for (String slaveName : command.toString().split(",")) {
346                             Sink slave = briHandler.getClient().getSink(slaveName.trim());
347                             if (slave != null) {
348                                 slaves.add(slave);
349                             }
350                         }
351                         if (!slaves.isEmpty()) {
352                             briHandler.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
353                         }
354                     }
355                 } else {
356                     logger.warn("{} is no combined sink", device);
357                 }
358             } else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
359                 if (device instanceof SinkInput) {
360                     Sink newSink = null;
361                     if (command instanceof DecimalType) {
362                         newSink = briHandler.getClient().getSink(((DecimalType) command).intValue());
363                     } else {
364                         newSink = briHandler.getClient().getSink(command.toString());
365                     }
366                     if (newSink != null) {
367                         logger.debug("rerouting {} to {}", device, newSink);
368                         briHandler.getClient().moveSinkInput(((SinkInput) device), newSink);
369                         updateState = new StringType(newSink.getPaName());
370                     } else {
371                         logger.warn("no sink {} found", command.toString());
372                     }
373                 }
374             }
375             logger.trace("updating {} to {}", channelUID, updateState);
376             if (!updateState.equals(UnDefType.UNDEF)) {
377                 updateState(channelUID, updateState);
378             }
379         }
380     }
381
382     /**
383      * Use last checked volume for faster access
384      *
385      * @return
386      */
387     public int getLastVolume() {
388         if (savedVolume == null) {
389             PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
390             if (briHandler != null) {
391                 // refresh to get the current volume level
392                 briHandler.getClient().update();
393                 AbstractAudioDeviceConfig device = briHandler.getDevice(name);
394                 if (device != null) {
395                     savedVolume = device.getVolume();
396                 }
397             }
398         }
399         return savedVolume == null ? 50 : savedVolume;
400     }
401
402     public void setVolume(int volume) {
403         PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
404         if (briHandler == null) {
405             logger.warn("bridge is not ready");
406             return;
407         }
408         AbstractAudioDeviceConfig device = briHandler.getDevice(name);
409         if (device == null) {
410             logger.warn("missing device info, aborting");
411             return;
412         }
413         briHandler.getClient().setVolumePercent(device, volume);
414         updateState(VOLUME_CHANNEL, new PercentType(volume));
415         savedVolume = volume;
416     }
417
418     @Override
419     public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
420         if (device.getPaName().equals(name)) {
421             updateStatus(ThingStatus.ONLINE);
422             logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
423             savedVolume = device.getVolume();
424             updateState(VOLUME_CHANNEL, new PercentType(savedVolume));
425             updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
426             updateState(STATE_CHANNEL,
427                     device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
428             if (device instanceof SinkInput) {
429                 updateState(ROUTE_TO_SINK_CHANNEL,
430                         ((SinkInput) device).getSink() != null
431                                 ? new StringType(((SinkInput) device).getSink().getPaName())
432                                 : new StringType("-"));
433             }
434             if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
435                 updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames())));
436             }
437         }
438     }
439
440     public String getHost() {
441         Bridge bridge = getBridge();
442         if (bridge != null) {
443             return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST);
444         } else {
445             logger.warn("A bridge must be configured for this pulseaudio thing");
446             return "null";
447         }
448     }
449
450     /**
451      * This method will scan the pulseaudio server to find the port on which the module/sink is listening
452      * If no module is listening, then it will command the module to load on the pulse audio server,
453      *
454      * @return the port on which the pulseaudio server is listening for this sink
455      * @throws IOException when device info is not available
456      * @throws InterruptedException when interrupted during the loading module wait
457      */
458     public int getSimpleTcpPort() throws IOException, InterruptedException {
459         var briHandler = getPulseaudioBridgeHandler();
460         if (briHandler == null) {
461             throw new IOException("bridge is not ready");
462         }
463         AbstractAudioDeviceConfig device = briHandler.getDevice(name);
464         if (device == null) {
465             throw new IOException("missing device info, device appears to be offline");
466         }
467         String simpleTcpPortPrefName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_PORT
468                 : DEVICE_PARAMETER_AUDIO_SINK_PORT;
469         BigDecimal simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration().get(simpleTcpPortPrefName));
470         int simpleTcpPort = simpleTcpPortPref != null ? simpleTcpPortPref.intValue()
471                 : MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT;
472         String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
473         BigDecimal simpleRate = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE);
474         BigDecimal simpleChannels = (BigDecimal) getThing().getConfiguration()
475                 .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS);
476         return briHandler.getClient()
477                 .loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPort, simpleFormat, simpleRate, simpleChannels)
478                 .orElse(simpleTcpPort);
479     }
480
481     public @Nullable AudioFormat getSourceAudioFormat() {
482         String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
483         BigDecimal simpleRate = ((BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE));
484         BigDecimal simpleChannels = ((BigDecimal) getThing().getConfiguration()
485                 .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS));
486         if (simpleFormat == null || simpleRate == null || simpleChannels == null) {
487             return null;
488         }
489         switch (simpleFormat) {
490             case "u8":
491                 return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, 8, 1,
492                         simpleRate.longValue(), simpleChannels.intValue());
493             case "s16le":
494                 return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 1,
495                         simpleRate.longValue(), simpleChannels.intValue());
496             case "s16be":
497                 return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, true, 16, 1,
498                         simpleRate.longValue(), simpleChannels.intValue());
499             case "s24le":
500                 return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 24, 1,
501                         simpleRate.longValue(), simpleChannels.intValue());
502             case "s24be":
503                 return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 24, 1,
504                         simpleRate.longValue(), simpleChannels.intValue());
505             case "s32le":
506                 return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 32, 1,
507                         simpleRate.longValue(), simpleChannels.intValue());
508             case "s32be":
509                 return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 32, 1,
510                         simpleRate.longValue(), simpleChannels.intValue());
511             default:
512                 logger.warn("unsupported format {}", simpleFormat);
513                 return null;
514         }
515     }
516
517     public int getIdleTimeout() {
518         var handler = getPulseaudioBridgeHandler();
519         if (handler == null) {
520             return 30000;
521         }
522         AbstractAudioDeviceConfig device = handler.getDevice(name);
523         String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
524                 : DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
525         var idleTimeout = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
526         return idleTimeout != null ? idleTimeout.intValue() : 30000;
527     }
528
529     public int getBasicProtocolSOTimeout() {
530         var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT);
531         return soTimeout != null ? soTimeout.intValue() : 500;
532     }
533
534     @Override
535     public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
536         if (device.getPaName().equals(name)) {
537             bridgeHandler.unregisterDeviceStatusListener(this);
538             bridgeHandler = null;
539             audioSink.disconnect();
540             audioSink = null;
541             updateStatus(ThingStatus.OFFLINE);
542         }
543     }
544
545     @Override
546     public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
547         logger.trace("new device discovered {} by {}", device, bridge);
548     }
549
550     public void setAudioSink(PulseAudioAudioSink audioSink) {
551         this.audioSink = audioSink;
552     }
553
554     public void setAudioSource(PulseAudioAudioSource audioSource) {
555         this.audioSource = audioSource;
556     }
557 }