]> git.basschouten.com Git - openhab-addons.git/blob
88690998ac819c164be260c95a2021a7b089493e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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);
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                 }
132                 @SuppressWarnings("unchecked")
133                 ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
134                         .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
135                 audioSinkRegistrations.put(thing.getUID().toString(), reg);
136             }
137         });
138     }
139
140     @Override
141     public void dispose() {
142         if (refreshJob != null && !refreshJob.isCancelled()) {
143             refreshJob.cancel(true);
144             refreshJob = null;
145         }
146         updateStatus(ThingStatus.OFFLINE);
147         bridgeHandler.unregisterDeviceStatusListener(this);
148         bridgeHandler = null;
149         logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
150         super.dispose();
151
152         if (audioSink != null) {
153             audioSink.disconnect();
154         }
155
156         // Unregister the potential pulse audio sink's audio sink
157         ServiceRegistration<AudioSink> reg = audioSinkRegistrations.remove(getThing().getUID().toString());
158         if (reg != null) {
159             logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
160             reg.unregister();
161         }
162     }
163
164     private void deviceOnlineWatchdog() {
165         Runnable runnable = () -> {
166             try {
167                 PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
168                 if (bridgeHandler != null) {
169                     if (bridgeHandler.getDevice(name) == null) {
170                         updateStatus(ThingStatus.OFFLINE);
171                         bridgeHandler = null;
172                     } else {
173                         updateStatus(ThingStatus.ONLINE);
174                     }
175                 } else {
176                     logger.debug("Bridge for pulseaudio device {} not found.", name);
177                     updateStatus(ThingStatus.OFFLINE);
178                 }
179             } catch (Exception e) {
180                 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
181                 bridgeHandler = null;
182             }
183         };
184
185         refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
186     }
187
188     private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
189         if (this.bridgeHandler == null) {
190             Bridge bridge = getBridge();
191             if (bridge == null) {
192                 logger.debug("Required bridge not defined for device {}.", name);
193                 return null;
194             }
195             ThingHandler handler = bridge.getHandler();
196             if (handler instanceof PulseaudioBridgeHandler) {
197                 this.bridgeHandler = (PulseaudioBridgeHandler) handler;
198                 this.bridgeHandler.registerDeviceStatusListener(this);
199             } else {
200                 logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
201                 return null;
202             }
203         }
204         return this.bridgeHandler;
205     }
206
207     @Override
208     public void handleCommand(ChannelUID channelUID, Command command) {
209         PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
210         if (bridge == null) {
211             logger.warn("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
212             return;
213         }
214         if (command instanceof RefreshType) {
215             bridge.handleCommand(channelUID, command);
216             return;
217         }
218
219         AbstractAudioDeviceConfig device = bridge.getDevice(name);
220         if (device == null) {
221             logger.warn("device {} not found", name);
222             updateStatus(ThingStatus.OFFLINE);
223             bridgeHandler = null;
224             return;
225         } else {
226             State updateState = UnDefType.UNDEF;
227             if (channelUID.getId().equals(VOLUME_CHANNEL)) {
228                 if (command instanceof IncreaseDecreaseType) {
229                     // refresh to get the current volume level
230                     bridge.getClient().update();
231                     device = bridge.getDevice(name);
232                     savedVolume = device.getVolume();
233                     if (command.equals(IncreaseDecreaseType.INCREASE)) {
234                         savedVolume = Math.min(100, savedVolume + 5);
235                     }
236                     if (command.equals(IncreaseDecreaseType.DECREASE)) {
237                         savedVolume = Math.max(0, savedVolume - 5);
238                     }
239                     bridge.getClient().setVolumePercent(device, savedVolume);
240                     updateState = new PercentType(savedVolume);
241                 } else if (command instanceof PercentType) {
242                     DecimalType volume = (DecimalType) command;
243                     bridge.getClient().setVolumePercent(device, volume.intValue());
244                     updateState = (PercentType) command;
245                 } else if (command instanceof DecimalType) {
246                     // set volume
247                     DecimalType volume = (DecimalType) command;
248                     bridge.getClient().setVolume(device, volume.intValue());
249                     updateState = (DecimalType) command;
250                 }
251             } else if (channelUID.getId().equals(MUTE_CHANNEL)) {
252                 if (command instanceof OnOffType) {
253                     bridge.getClient().setMute(device, OnOffType.ON.equals(command));
254                     updateState = (OnOffType) command;
255                 }
256             } else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
257                 if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
258                     if (command instanceof StringType) {
259                         List<Sink> slaves = new ArrayList<>();
260                         for (String slaveName : command.toString().split(",")) {
261                             Sink slave = bridge.getClient().getSink(slaveName.trim());
262                             if (slave != null) {
263                                 slaves.add(slave);
264                             }
265                         }
266                         if (!slaves.isEmpty()) {
267                             bridge.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
268                         }
269                     }
270                 } else {
271                     logger.error("{} is no combined sink", device);
272                 }
273             } else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
274                 if (device instanceof SinkInput) {
275                     Sink newSink = null;
276                     if (command instanceof DecimalType) {
277                         newSink = bridge.getClient().getSink(((DecimalType) command).intValue());
278                     } else {
279                         newSink = bridge.getClient().getSink(command.toString());
280                     }
281                     if (newSink != null) {
282                         logger.debug("rerouting {} to {}", device, newSink);
283                         bridge.getClient().moveSinkInput(((SinkInput) device), newSink);
284                         updateState = new StringType(newSink.getPaName());
285                     } else {
286                         logger.error("no sink {} found", command.toString());
287                     }
288                 }
289             }
290             logger.trace("updating {} to {}", channelUID, updateState);
291             if (!updateState.equals(UnDefType.UNDEF)) {
292                 updateState(channelUID, updateState);
293             }
294         }
295     }
296
297     /**
298      * Use last checked volume for faster access
299      *
300      * @return
301      */
302     public int getLastVolume() {
303         if (savedVolume == null) {
304             PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
305             AbstractAudioDeviceConfig device = bridge.getDevice(name);
306             // refresh to get the current volume level
307             bridge.getClient().update();
308             device = bridge.getDevice(name);
309             savedVolume = device.getVolume();
310         }
311         return savedVolume == null ? 50 : savedVolume;
312     }
313
314     public void setVolume(int volume) {
315         PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
316         AbstractAudioDeviceConfig device = bridge.getDevice(name);
317         bridge.getClient().setVolumePercent(device, volume);
318         updateState(VOLUME_CHANNEL, new PercentType(volume));
319     }
320
321     @Override
322     public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
323         if (device.getPaName().equals(name)) {
324             updateStatus(ThingStatus.ONLINE);
325             logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
326             savedVolume = device.getVolume();
327             updateState(VOLUME_CHANNEL, new PercentType(savedVolume));
328             updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
329             updateState(STATE_CHANNEL,
330                     device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
331             if (device instanceof SinkInput) {
332                 updateState(ROUTE_TO_SINK_CHANNEL,
333                         ((SinkInput) device).getSink() != null
334                                 ? new StringType(((SinkInput) device).getSink().getPaName())
335                                 : new StringType("-"));
336             }
337             if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
338                 updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames())));
339             }
340         }
341     }
342
343     public String getHost() {
344         Bridge bridge = getBridge();
345         if (bridge != null) {
346             return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST);
347         } else {
348             logger.error("A bridge must be configured for this pulseaudio thing");
349             return "null";
350         }
351     }
352
353     /**
354      * This method will scan the pulseaudio server to find the port on which the module/sink is listening
355      * If no module is listening, then it will command the module to load on the pulse audio server,
356      *
357      * @return the port on which the pulseaudio server is listening for this sink
358      * @throws InterruptedException when interrupted during the loading module wait
359      */
360     public int getSimpleTcpPort() throws InterruptedException {
361         Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration()
362                 .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue();
363
364         PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
365         AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name);
366         return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref)
367                 .orElse(simpleTcpPortPref);
368     }
369
370     @Override
371     public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
372         if (device.getPaName().equals(name)) {
373             bridgeHandler.unregisterDeviceStatusListener(this);
374             bridgeHandler = null;
375             audioSink.disconnect();
376             audioSink = null;
377             updateStatus(ThingStatus.OFFLINE);
378         }
379     }
380
381     @Override
382     public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
383         logger.trace("new device discovered {} by {}", device, bridge);
384     }
385
386     public void setAudioSink(PulseAudioAudioSink audioSink) {
387         this.audioSink = audioSink;
388     }
389 }