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