]> git.basschouten.com Git - openhab-addons.git/blob
f88b02407ee6ff6677237557a34e557fd0a45d25
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.Optional;
25 import java.util.Set;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.regex.PatternSyntaxException;
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.audio.utils.AudioSinkUtils;
44 import org.openhab.core.config.core.Configuration;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.IncreaseDecreaseType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PercentType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.ThingStatusInfo;
56 import org.openhab.core.thing.ThingTypeUID;
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 {
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
83     private @Nullable DeviceIdentifier deviceIdentifier;
84     private @Nullable PulseAudioAudioSink audioSink;
85     private @Nullable PulseAudioAudioSource audioSource;
86     private @Nullable Integer savedVolume;
87
88     private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
89     private final Map<String, ServiceRegistration<AudioSource>> audioSourceRegistrations = new ConcurrentHashMap<>();
90
91     private final BundleContext bundleContext;
92
93     private AudioSinkUtils audioSinkUtils;
94
95     public PulseaudioHandler(Thing thing, BundleContext bundleContext, AudioSinkUtils audioSinkUtils) {
96         super(thing);
97         this.bundleContext = bundleContext;
98         this.audioSinkUtils = audioSinkUtils;
99     }
100
101     @Override
102     public void initialize() {
103         Configuration config = getThing().getConfiguration();
104         try {
105             deviceIdentifier = new DeviceIdentifier((String) config.get(DEVICE_PARAMETER_NAME_OR_DESCRIPTION),
106                     (String) config.get(DEVICE_PARAMETER_ADDITIONAL_FILTERS));
107         } catch (PatternSyntaxException p) {
108             deviceIdentifier = null;
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110                     "Incorrect regular expression: " + (String) config.get(DEVICE_PARAMETER_ADDITIONAL_FILTERS));
111             return;
112         }
113         initializeWithTheBridge();
114     }
115
116     public @Nullable DeviceIdentifier getDeviceIdentifier() {
117         return deviceIdentifier;
118     }
119
120     private void audioSinkSetup() {
121         if (audioSink != null) {
122             // Audio sink is already setup
123             return;
124         }
125         if (!SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
126             return;
127         }
128         // check the property to see if it's enabled :
129         Boolean sinkActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
130         if (sinkActivated == null || !sinkActivated.booleanValue()) {
131             return;
132         }
133         final PulseaudioHandler thisHandler = this;
134         PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler, audioSinkUtils);
135         scheduler.submit(new Runnable() {
136             @Override
137             public void run() {
138                 PulseaudioHandler.this.audioSink = audioSink;
139                 try {
140                     audioSink.connectIfNeeded();
141                 } catch (IOException e) {
142                     logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
143                             getHost(), e.getMessage());
144                 } catch (InterruptedException i) {
145                     logger.info("Interrupted during sink audio connection: {}", i.getMessage());
146                     return;
147                 }
148             }
149         });
150         // Register the sink as an audio sink in openhab
151         logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
152         @SuppressWarnings("unchecked")
153         ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
154                 .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
155         audioSinkRegistrations.put(thing.getUID().toString(), reg);
156     }
157
158     private void audioSinkUnsetup() {
159         PulseAudioAudioSink sink = audioSink;
160         if (sink != null) {
161             sink.disconnect();
162             audioSink = null;
163         }
164         // Unregister the potential pulse audio sink's audio sink
165         ServiceRegistration<AudioSink> sinkReg = audioSinkRegistrations.remove(getThing().getUID().toString());
166         if (sinkReg != null) {
167             logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
168             sinkReg.unregister();
169         }
170     }
171
172     private void audioSourceSetup() {
173         if (audioSource != null) {
174             // Audio source is already setup
175             return;
176         }
177         if (!SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) {
178             return;
179         }
180         // check the property to see if it's enabled :
181         Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION);
182         if (sourceActivated == null || !sourceActivated.booleanValue()) {
183             return;
184         }
185         final PulseaudioHandler thisHandler = this;
186         PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
187         scheduler.submit(new Runnable() {
188             @Override
189             public void run() {
190                 PulseaudioHandler.this.audioSource = audioSource;
191                 try {
192                     audioSource.connectIfNeeded();
193                 } catch (IOException e) {
194                     logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
195                             getHost(), e.getMessage());
196                 } catch (InterruptedException i) {
197                     logger.info("Interrupted during source audio connection: {}", i.getMessage());
198                     return;
199                 }
200             }
201         });
202         // Register the source as an audio source in openhab
203         logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
204         @SuppressWarnings("unchecked")
205         ServiceRegistration<AudioSource> reg = (ServiceRegistration<AudioSource>) bundleContext
206                 .registerService(AudioSource.class.getName(), audioSource, new Hashtable<>());
207         audioSourceRegistrations.put(thing.getUID().toString(), reg);
208     }
209
210     private void audioSourceUnsetup() {
211         PulseAudioAudioSource source = audioSource;
212         if (source != null) {
213             source.disconnect();
214             audioSource = null;
215         }
216         // Unregister the potential pulse audio source's audio sources
217         ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString());
218         if (sourceReg != null) {
219             logger.trace("Unregistering the audio sync service for pulse audio source thing {}", getThing().getUID());
220             sourceReg.unregister();
221         }
222     }
223
224     @Override
225     public void dispose() {
226         logger.trace("Thing {} {} disposed.", getThing().getUID(), safeGetDeviceNameOrDescription());
227         super.dispose();
228         audioSinkUnsetup();
229         audioSourceUnsetup();
230     }
231
232     @Override
233     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
234         initializeWithTheBridge();
235     }
236
237     private void initializeWithTheBridge() {
238         PulseaudioBridgeHandler pulseaudioBridgeHandler = getPulseaudioBridgeHandler();
239         if (pulseaudioBridgeHandler == null) {
240             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
241         } else if (pulseaudioBridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
242             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
243         } else {
244             deviceUpdate(pulseaudioBridgeHandler.getDevice(deviceIdentifier));
245         }
246     }
247
248     private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
249         Bridge bridge = getBridge();
250         if (bridge == null) {
251             logger.debug("Required bridge not defined for device {}.", safeGetDeviceNameOrDescription());
252             return null;
253         }
254         ThingHandler handler = bridge.getHandler();
255         if (handler instanceof PulseaudioBridgeHandler pulseaudioBridgeHandler) {
256             return pulseaudioBridgeHandler;
257         } else {
258             logger.debug("No available bridge handler found for device {} bridge {} .",
259                     safeGetDeviceNameOrDescription(), bridge.getUID());
260             return null;
261         }
262     }
263
264     @Override
265     public void handleCommand(ChannelUID channelUID, Command command) {
266         PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
267         if (briHandler == null) {
268             logger.debug("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
269             return;
270         }
271         if (command instanceof RefreshType) {
272             briHandler.handleCommand(channelUID, command);
273             return;
274         }
275
276         AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
277         if (device == null) {
278             logger.warn("device {} not found", safeGetDeviceNameOrDescription());
279             deviceUpdate(null);
280             return;
281         } else {
282             State updateState = UnDefType.UNDEF;
283             if (channelUID.getId().equals(VOLUME_CHANNEL)) {
284                 if (command instanceof IncreaseDecreaseType) {
285                     // refresh to get the current volume level
286                     briHandler.getClient().update();
287                     device = briHandler.getDevice(deviceIdentifier);
288                     if (device == null) {
289                         logger.warn("missing device info, aborting");
290                         return;
291                     }
292                     int oldVolume = device.getVolume();
293                     int newVolume = oldVolume;
294                     if (command.equals(IncreaseDecreaseType.INCREASE)) {
295                         newVolume = Math.min(100, oldVolume + 5);
296                     }
297                     if (command.equals(IncreaseDecreaseType.DECREASE)) {
298                         newVolume = Math.max(0, oldVolume - 5);
299                     }
300                     briHandler.getClient().setVolumePercent(device, newVolume);
301                     updateState = new PercentType(newVolume);
302                     savedVolume = newVolume;
303                 } else if (command instanceof PercentType volume) {
304                     briHandler.getClient().setVolumePercent(device, volume.intValue());
305                     updateState = volume;
306                     savedVolume = volume.intValue();
307                 } else if (command instanceof DecimalType volume) {
308                     briHandler.getClient().setVolume(device, volume.intValue());
309                     updateState = volume;
310                     savedVolume = volume.intValue();
311                 }
312             } else if (channelUID.getId().equals(MUTE_CHANNEL)) {
313                 if (command instanceof OnOffType onOffCommand) {
314                     briHandler.getClient().setMute(device, OnOffType.ON.equals(command));
315                     updateState = onOffCommand;
316                 }
317             } else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
318                 if (device instanceof Sink sink && sink.isCombinedSink()) {
319                     if (command instanceof StringType) {
320                         List<Sink> slaves = new ArrayList<>();
321                         for (String slaveName : command.toString().split(",")) {
322                             Sink slave = briHandler.getClient().getSink(slaveName.trim());
323                             if (slave != null) {
324                                 slaves.add(slave);
325                             }
326                         }
327                         if (!slaves.isEmpty()) {
328                             briHandler.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
329                         }
330                     }
331                 } else {
332                     logger.warn("{} is no combined sink", device);
333                 }
334             } else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
335                 if (device instanceof SinkInput input) {
336                     Sink newSink = null;
337                     if (command instanceof DecimalType decimalCommand) {
338                         newSink = briHandler.getClient().getSink(decimalCommand.intValue());
339                     } else {
340                         newSink = briHandler.getClient().getSink(command.toString());
341                     }
342                     if (newSink != null) {
343                         logger.debug("rerouting {} to {}", device, newSink);
344                         briHandler.getClient().moveSinkInput(input, newSink);
345                         updateState = new StringType(newSink.getPaName());
346                     } else {
347                         logger.warn("no sink {} found", command.toString());
348                     }
349                 }
350             }
351             logger.trace("updating {} to {}", channelUID, updateState);
352             if (!updateState.equals(UnDefType.UNDEF)) {
353                 updateState(channelUID, updateState);
354             }
355         }
356     }
357
358     /**
359      * Use last checked volume for faster access
360      *
361      * @return
362      */
363     public Integer getLastVolume() {
364         Integer savedVolumeFinal = savedVolume;
365         if (savedVolumeFinal == null) {
366             PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
367             if (briHandler != null) {
368                 // refresh to get the current volume level
369                 briHandler.getClient().update();
370                 AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
371                 if (device != null) {
372                     savedVolume = savedVolumeFinal = device.getVolume();
373                 }
374             }
375         }
376         return savedVolumeFinal == null ? 50 : savedVolumeFinal;
377     }
378
379     public void setVolume(int volume) {
380         PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
381         if (briHandler == null) {
382             logger.warn("bridge is not ready");
383             return;
384         }
385         AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
386         if (device == null) {
387             logger.warn("missing device info, aborting");
388             return;
389         }
390         briHandler.getClient().setVolumePercent(device, volume);
391         updateState(VOLUME_CHANNEL, new PercentType(volume));
392         savedVolume = volume;
393     }
394
395     public void deviceUpdate(@Nullable AbstractAudioDeviceConfig device) {
396         if (device != null) {
397             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
398             logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
399             int actualVolume = device.getVolume();
400             savedVolume = actualVolume;
401             updateState(VOLUME_CHANNEL, new PercentType(actualVolume));
402             updateState(MUTE_CHANNEL, OnOffType.from(device.isMuted()));
403             org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State state = device.getState();
404             updateState(STATE_CHANNEL, state != null ? new StringType(state.toString()) : new StringType("-"));
405             if (device instanceof SinkInput input) {
406                 updateState(ROUTE_TO_SINK_CHANNEL,
407                         new StringType(Optional.ofNullable(input.getSink()).map(Sink::getPaName).orElse("-")));
408             }
409             if (device instanceof Sink sink && sink.isCombinedSink()) {
410                 updateState(SLAVES_CHANNEL, new StringType(String.join(",", sink.getCombinedSinkNames())));
411             }
412             audioSinkSetup();
413             audioSourceSetup();
414         } else {
415             updateState(VOLUME_CHANNEL, UnDefType.UNDEF);
416             updateState(MUTE_CHANNEL, UnDefType.UNDEF);
417             updateState(STATE_CHANNEL, UnDefType.UNDEF);
418             if (SINK_INPUT_THING_TYPE.equals(thing.getThingTypeUID())) {
419                 updateState(ROUTE_TO_SINK_CHANNEL, UnDefType.UNDEF);
420             }
421             if (COMBINED_SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
422                 updateState(SLAVES_CHANNEL, UnDefType.UNDEF);
423             }
424             audioSinkUnsetup();
425             audioSourceUnsetup();
426             updateStatus(ThingStatus.OFFLINE);
427         }
428     }
429
430     public String getHost() {
431         Bridge bridge = getBridge();
432         if (bridge != null) {
433             return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST);
434         } else {
435             logger.warn("A bridge must be configured for this pulseaudio thing");
436             return "null";
437         }
438     }
439
440     /**
441      * This method will scan the pulseaudio server to find the port on which the module/sink/source is listening
442      * If no module is listening, then it will command the module to load on the pulse audio server,
443      *
444      * @return the port on which the pulseaudio server is listening for this sink/source
445      * @throws IOException when device info is not available
446      * @throws InterruptedException when interrupted during the loading module wait
447      */
448     public int getSimpleTcpPortAndLoadModuleIfNecessary() throws IOException, InterruptedException {
449         var briHandler = getPulseaudioBridgeHandler();
450         if (briHandler == null) {
451             throw new IOException("bridge is not ready");
452         }
453         AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
454         if (device == null) {
455             throw new IOException(
456                     "missing device info, device " + safeGetDeviceNameOrDescription() + " appears to be offline");
457         }
458         String simpleTcpPortPrefName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_PORT
459                 : DEVICE_PARAMETER_AUDIO_SINK_PORT;
460         BigDecimal simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration().get(simpleTcpPortPrefName));
461         int simpleTcpPort = simpleTcpPortPref != null ? simpleTcpPortPref.intValue()
462                 : MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT;
463         String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
464         BigDecimal simpleRate = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE);
465         BigDecimal simpleChannels = (BigDecimal) getThing().getConfiguration()
466                 .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS);
467         return briHandler.getClient()
468                 .loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPort, simpleFormat, simpleRate, simpleChannels)
469                 .orElse(simpleTcpPort);
470     }
471
472     public AudioFormat getSourceAudioFormat() {
473         String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
474         BigDecimal simpleRate = ((BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE));
475         BigDecimal simpleChannels = ((BigDecimal) getThing().getConfiguration()
476                 .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS));
477         AudioFormat fallback = new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED, false, 16,
478                 16 * 16000, 16000L, 1);
479         if (simpleFormat == null || simpleRate == null || simpleChannels == null) {
480             return fallback;
481         }
482         int sampleRateAllChannels = simpleRate.intValue() * simpleChannels.intValue();
483         switch (simpleFormat) {
484             case "u8" -> {
485                 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_UNSIGNED, null, 8,
486                         8 * sampleRateAllChannels, simpleRate.longValue(), simpleChannels.intValue());
487             }
488             case "s16le" -> {
489                 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED, false, 16,
490                         16 * sampleRateAllChannels, simpleRate.longValue(), simpleChannels.intValue());
491             }
492             case "s16be" -> {
493                 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED, true, 16,
494                         16 * sampleRateAllChannels, simpleRate.longValue(), simpleChannels.intValue());
495             }
496             case "s24le" -> {
497                 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED, false, 24,
498                         24 * sampleRateAllChannels, simpleRate.longValue(), simpleChannels.intValue());
499             }
500             case "s24be" -> {
501                 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED, true, 24,
502                         24 * sampleRateAllChannels, simpleRate.longValue(), simpleChannels.intValue());
503             }
504             case "s32le" -> {
505                 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED, false, 32,
506                         32 * sampleRateAllChannels, simpleRate.longValue(), simpleChannels.intValue());
507             }
508             case "s32be" -> {
509                 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED, true, 32,
510                         32 * sampleRateAllChannels, simpleRate.longValue(), simpleChannels.intValue());
511             }
512             default -> {
513                 logger.warn("unsupported format {}", simpleFormat);
514                 return fallback;
515             }
516         }
517     }
518
519     public int getIdleTimeout() {
520         var idleTimeout = 3000;
521         var handler = getPulseaudioBridgeHandler();
522         if (handler != null) {
523             AbstractAudioDeviceConfig device = handler.getDevice(deviceIdentifier);
524             String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
525                     : DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
526             var idleTimeoutB = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
527             if (idleTimeoutB != null) {
528                 idleTimeout = idleTimeoutB.intValue();
529             }
530         }
531         return idleTimeout;
532     }
533
534     private String safeGetDeviceNameOrDescription() {
535         DeviceIdentifier deviceIdentifierFinal = deviceIdentifier;
536         return deviceIdentifierFinal == null ? "UNKNOWN" : deviceIdentifierFinal.getNameOrDescription();
537     }
538
539     public int getBasicProtocolSOTimeout() {
540         var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT);
541         return soTimeout != null ? soTimeout.intValue() : 500;
542     }
543 }