]> git.basschouten.com Git - openhab-addons.git/blob
c145dcaaefa727fdf1c3e543cc387445baaaef1c
[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.openhab.binding.pulseaudio.internal.PulseAudioAudioSink;
32 import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
33 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
34 import org.openhab.binding.pulseaudio.internal.items.Sink;
35 import org.openhab.binding.pulseaudio.internal.items.SinkInput;
36 import org.openhab.core.audio.AudioSink;
37 import org.openhab.core.config.core.Configuration;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.IncreaseDecreaseType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.PercentType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingTypeUID;
48 import org.openhab.core.thing.ThingUID;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.thing.binding.ThingHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.openhab.core.types.UnDefType;
55 import org.osgi.framework.BundleContext;
56 import org.osgi.framework.ServiceRegistration;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 /**
61  * The {@link PulseaudioHandler} is responsible for handling commands, which are
62  * sent to one of the channels.
63  *
64  * @author Tobias Bräutigam - Initial contribution
65  */
66 public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
67
68     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
69             .unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
70                     SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
71
72     private int refresh = 60; // refresh every minute as default
73     private ScheduledFuture<?> refreshJob;
74
75     private PulseaudioBridgeHandler bridgeHandler;
76
77     private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
78
79     private String name;
80
81     private PulseAudioAudioSink audioSink;
82
83     private Integer savedVolume;
84
85     private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
86
87     private BundleContext bundleContext;
88
89     public PulseaudioHandler(Thing thing, BundleContext bundleContext) {
90         super(thing);
91         this.bundleContext = bundleContext;
92     }
93
94     @Override
95     public void initialize() {
96         Configuration config = getThing().getConfiguration();
97         name = (String) config.get(DEVICE_PARAMETER_NAME);
98
99         // until we get an update put the Thing offline
100         updateStatus(ThingStatus.OFFLINE);
101         deviceOnlineWatchdog();
102
103         // if it's a SINK thing, then maybe we have to activate the audio sink
104         if (PulseaudioBindingConstants.SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
105             // check the property to see if we it's enabled :
106             Boolean sinkActivated = (Boolean) thing.getConfiguration()
107                     .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
108             if (sinkActivated != null && sinkActivated) {
109                 audioSinkSetup();
110             }
111         }
112     }
113
114     private void audioSinkSetup() {
115         final PulseaudioHandler thisHandler = this;
116         scheduler.submit(new Runnable() {
117             @Override
118             public void run() {
119                 // Register the sink as an audio sink in openhab
120                 logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
121                 PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
122                 setAudioSink(audioSink);
123                 try {
124                     audioSink.connectIfNeeded();
125                 } catch (IOException e) {
126                     logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
127                             getHost(), e.getMessage());
128                 } catch (InterruptedException i) {
129                     logger.info("Interrupted during sink audio connection: {}", i.getMessage());
130                     return;
131                 } finally {
132                     audioSink.scheduleDisconnect();
133                 }
134                 @SuppressWarnings("unchecked")
135                 ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
136                         .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
137                 audioSinkRegistrations.put(thing.getUID().toString(), reg);
138             }
139         });
140     }
141
142     @Override
143     public void dispose() {
144         if (refreshJob != null && !refreshJob.isCancelled()) {
145             refreshJob.cancel(true);
146             refreshJob = null;
147         }
148         updateStatus(ThingStatus.OFFLINE);
149         if (bridgeHandler != null) {
150             bridgeHandler.unregisterDeviceStatusListener(this);
151             bridgeHandler = null;
152         }
153         logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
154         super.dispose();
155
156         if (audioSink != null) {
157             audioSink.disconnect();
158         }
159
160         // Unregister the potential pulse audio sink's audio sink
161         ServiceRegistration<AudioSink> reg = audioSinkRegistrations.remove(getThing().getUID().toString());
162         if (reg != null) {
163             logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
164             reg.unregister();
165         }
166     }
167
168     private void deviceOnlineWatchdog() {
169         Runnable runnable = () -> {
170             try {
171                 PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
172                 if (bridgeHandler != null) {
173                     if (bridgeHandler.getDevice(name) == null) {
174                         updateStatus(ThingStatus.OFFLINE);
175                         bridgeHandler = null;
176                     } else {
177                         updateStatus(ThingStatus.ONLINE);
178                     }
179                 } else {
180                     logger.debug("Bridge for pulseaudio device {} not found.", name);
181                     updateStatus(ThingStatus.OFFLINE);
182                 }
183             } catch (Exception e) {
184                 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
185                 bridgeHandler = null;
186             }
187         };
188
189         refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
190     }
191
192     private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
193         if (this.bridgeHandler == null) {
194             Bridge bridge = getBridge();
195             if (bridge == null) {
196                 logger.debug("Required bridge not defined for device {}.", name);
197                 return null;
198             }
199             ThingHandler handler = bridge.getHandler();
200             if (handler instanceof PulseaudioBridgeHandler) {
201                 this.bridgeHandler = (PulseaudioBridgeHandler) handler;
202                 this.bridgeHandler.registerDeviceStatusListener(this);
203             } else {
204                 logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
205                 return null;
206             }
207         }
208         return this.bridgeHandler;
209     }
210
211     @Override
212     public void handleCommand(ChannelUID channelUID, Command command) {
213         PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
214         if (bridge == null) {
215             logger.warn("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
216             return;
217         }
218         if (command instanceof RefreshType) {
219             bridge.handleCommand(channelUID, command);
220             return;
221         }
222
223         AbstractAudioDeviceConfig device = bridge.getDevice(name);
224         if (device == null) {
225             logger.warn("device {} not found", name);
226             updateStatus(ThingStatus.OFFLINE);
227             bridgeHandler = null;
228             return;
229         } else {
230             State updateState = UnDefType.UNDEF;
231             if (channelUID.getId().equals(VOLUME_CHANNEL)) {
232                 if (command instanceof IncreaseDecreaseType) {
233                     // refresh to get the current volume level
234                     bridge.getClient().update();
235                     device = bridge.getDevice(name);
236                     int oldVolume = device.getVolume();
237                     int newVolume = oldVolume;
238                     if (command.equals(IncreaseDecreaseType.INCREASE)) {
239                         newVolume = Math.min(100, oldVolume + 5);
240                     }
241                     if (command.equals(IncreaseDecreaseType.DECREASE)) {
242                         newVolume = Math.max(0, oldVolume - 5);
243                     }
244                     bridge.getClient().setVolumePercent(device, newVolume);
245                     updateState = new PercentType(newVolume);
246                     savedVolume = newVolume;
247                 } else if (command instanceof PercentType) {
248                     DecimalType volume = (DecimalType) command;
249                     bridge.getClient().setVolumePercent(device, volume.intValue());
250                     updateState = (PercentType) command;
251                     savedVolume = volume.intValue();
252                 } else if (command instanceof DecimalType) {
253                     // set volume
254                     DecimalType volume = (DecimalType) command;
255                     bridge.getClient().setVolume(device, volume.intValue());
256                     updateState = (DecimalType) command;
257                     savedVolume = volume.intValue();
258                 }
259             } else if (channelUID.getId().equals(MUTE_CHANNEL)) {
260                 if (command instanceof OnOffType) {
261                     bridge.getClient().setMute(device, OnOffType.ON.equals(command));
262                     updateState = (OnOffType) command;
263                 }
264             } else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
265                 if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
266                     if (command instanceof StringType) {
267                         List<Sink> slaves = new ArrayList<>();
268                         for (String slaveName : command.toString().split(",")) {
269                             Sink slave = bridge.getClient().getSink(slaveName.trim());
270                             if (slave != null) {
271                                 slaves.add(slave);
272                             }
273                         }
274                         if (!slaves.isEmpty()) {
275                             bridge.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
276                         }
277                     }
278                 } else {
279                     logger.error("{} is no combined sink", device);
280                 }
281             } else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
282                 if (device instanceof SinkInput) {
283                     Sink newSink = null;
284                     if (command instanceof DecimalType) {
285                         newSink = bridge.getClient().getSink(((DecimalType) command).intValue());
286                     } else {
287                         newSink = bridge.getClient().getSink(command.toString());
288                     }
289                     if (newSink != null) {
290                         logger.debug("rerouting {} to {}", device, newSink);
291                         bridge.getClient().moveSinkInput(((SinkInput) device), newSink);
292                         updateState = new StringType(newSink.getPaName());
293                     } else {
294                         logger.error("no sink {} found", command.toString());
295                     }
296                 }
297             }
298             logger.trace("updating {} to {}", channelUID, updateState);
299             if (!updateState.equals(UnDefType.UNDEF)) {
300                 updateState(channelUID, updateState);
301             }
302         }
303     }
304
305     /**
306      * Use last checked volume for faster access
307      *
308      * @return
309      */
310     public int getLastVolume() {
311         if (savedVolume == null) {
312             PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
313             AbstractAudioDeviceConfig device = bridge.getDevice(name);
314             // refresh to get the current volume level
315             bridge.getClient().update();
316             device = bridge.getDevice(name);
317             savedVolume = device.getVolume();
318         }
319         return savedVolume == null ? 50 : savedVolume;
320     }
321
322     public void setVolume(int volume) {
323         PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
324         AbstractAudioDeviceConfig device = bridge.getDevice(name);
325         bridge.getClient().setVolumePercent(device, volume);
326         updateState(VOLUME_CHANNEL, new PercentType(volume));
327         savedVolume = volume;
328     }
329
330     @Override
331     public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
332         if (device.getPaName().equals(name)) {
333             updateStatus(ThingStatus.ONLINE);
334             logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
335             savedVolume = device.getVolume();
336             updateState(VOLUME_CHANNEL, new PercentType(savedVolume));
337             updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
338             updateState(STATE_CHANNEL,
339                     device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
340             if (device instanceof SinkInput) {
341                 updateState(ROUTE_TO_SINK_CHANNEL,
342                         ((SinkInput) device).getSink() != null
343                                 ? new StringType(((SinkInput) device).getSink().getPaName())
344                                 : new StringType("-"));
345             }
346             if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
347                 updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames())));
348             }
349         }
350     }
351
352     public String getHost() {
353         Bridge bridge = getBridge();
354         if (bridge != null) {
355             return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST);
356         } else {
357             logger.error("A bridge must be configured for this pulseaudio thing");
358             return "null";
359         }
360     }
361
362     /**
363      * This method will scan the pulseaudio server to find the port on which the module/sink is listening
364      * If no module is listening, then it will command the module to load on the pulse audio server,
365      *
366      * @return the port on which the pulseaudio server is listening for this sink
367      * @throws InterruptedException when interrupted during the loading module wait
368      */
369     public int getSimpleTcpPort() throws InterruptedException {
370         Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration()
371                 .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue();
372
373         PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
374         AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name);
375         return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref)
376                 .orElse(simpleTcpPortPref);
377     }
378
379     public int getIdleTimeout() {
380         return ((BigDecimal) getThing().getConfiguration()
381                 .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT)).intValue();
382     }
383
384     @Override
385     public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
386         if (device.getPaName().equals(name)) {
387             bridgeHandler.unregisterDeviceStatusListener(this);
388             bridgeHandler = null;
389             audioSink.disconnect();
390             audioSink = null;
391             updateStatus(ThingStatus.OFFLINE);
392         }
393     }
394
395     @Override
396     public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
397         logger.trace("new device discovered {} by {}", device, bridge);
398     }
399
400     public void setAudioSink(PulseAudioAudioSink audioSink) {
401         this.audioSink = audioSink;
402     }
403 }