2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pulseaudio.internal.handler;
15 import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
24 import java.util.stream.Stream;
26 import org.apache.commons.lang.StringUtils;
27 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
28 import org.openhab.binding.pulseaudio.internal.items.Sink;
29 import org.openhab.binding.pulseaudio.internal.items.SinkInput;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.IncreaseDecreaseType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingTypeUID;
41 import org.openhab.core.thing.ThingUID;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.binding.ThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link PulseaudioHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Tobias Bräutigam - Initial contribution
57 public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
59 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
60 .unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
61 SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
63 private int refresh = 60; // refresh every minute as default
64 private ScheduledFuture<?> refreshJob;
66 private PulseaudioBridgeHandler bridgeHandler;
68 private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
72 public PulseaudioHandler(Thing thing) {
77 public void initialize() {
78 Configuration config = getThing().getConfiguration();
79 name = (String) config.get(DEVICE_PARAMETER_NAME);
81 // until we get an update put the Thing offline
82 updateStatus(ThingStatus.OFFLINE);
83 deviceOnlineWatchdog();
87 public void dispose() {
88 if (refreshJob != null && !refreshJob.isCancelled()) {
89 refreshJob.cancel(true);
92 updateStatus(ThingStatus.OFFLINE);
94 logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
98 private void deviceOnlineWatchdog() {
99 Runnable runnable = () -> {
101 PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
102 if (bridgeHandler != null) {
103 if (bridgeHandler.getDevice(name) == null) {
104 updateStatus(ThingStatus.OFFLINE);
105 bridgeHandler = null;
107 updateStatus(ThingStatus.ONLINE);
110 logger.debug("Bridge for pulseaudio device {} not found.", name);
111 updateStatus(ThingStatus.OFFLINE);
113 } catch (Exception e) {
114 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
115 bridgeHandler = null;
119 refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
122 private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
123 if (this.bridgeHandler == null) {
124 Bridge bridge = getBridge();
125 if (bridge == null) {
126 logger.debug("Required bridge not defined for device {}.", name);
129 ThingHandler handler = bridge.getHandler();
130 if (handler instanceof PulseaudioBridgeHandler) {
131 this.bridgeHandler = (PulseaudioBridgeHandler) handler;
132 this.bridgeHandler.registerDeviceStatusListener(this);
134 logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
138 return this.bridgeHandler;
142 public void handleCommand(ChannelUID channelUID, Command command) {
143 PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
144 if (bridge == null) {
145 logger.warn("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
148 if (command instanceof RefreshType) {
149 bridge.handleCommand(channelUID, command);
153 AbstractAudioDeviceConfig device = bridge.getDevice(name);
154 if (device == null) {
155 logger.warn("device {} not found", name);
156 updateStatus(ThingStatus.OFFLINE);
157 bridgeHandler = null;
160 State updateState = UnDefType.UNDEF;
161 if (channelUID.getId().equals(VOLUME_CHANNEL)) {
162 if (command instanceof IncreaseDecreaseType) {
163 // refresh to get the current volume level
164 bridge.getClient().update();
165 device = bridge.getDevice(name);
166 int volume = device.getVolume();
167 if (command.equals(IncreaseDecreaseType.INCREASE)) {
168 volume = Math.min(100, volume + 5);
170 if (command.equals(IncreaseDecreaseType.DECREASE)) {
171 volume = Math.max(0, volume - 5);
173 bridge.getClient().setVolumePercent(device, volume);
174 updateState = new PercentType(volume);
175 } else if (command instanceof PercentType) {
176 DecimalType volume = (DecimalType) command;
177 bridge.getClient().setVolumePercent(device, volume.intValue());
178 updateState = (PercentType) command;
179 } else if (command instanceof DecimalType) {
181 DecimalType volume = (DecimalType) command;
182 bridge.getClient().setVolume(device, volume.intValue());
183 updateState = (DecimalType) command;
185 } else if (channelUID.getId().equals(MUTE_CHANNEL)) {
186 if (command instanceof OnOffType) {
187 bridge.getClient().setMute(device, OnOffType.ON.equals(command));
188 updateState = (OnOffType) command;
190 } else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
191 if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
192 if (command instanceof StringType) {
193 List<Sink> slaves = new ArrayList<>();
194 for (String slaveName : command.toString().split(",")) {
195 Sink slave = bridge.getClient().getSink(slaveName.trim());
200 if (!slaves.isEmpty()) {
201 bridge.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
205 logger.error("{} is no combined sink", device);
207 } else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
208 if (device instanceof SinkInput) {
210 if (command instanceof DecimalType) {
211 newSink = bridge.getClient().getSink(((DecimalType) command).intValue());
213 newSink = bridge.getClient().getSink(command.toString());
215 if (newSink != null) {
216 logger.debug("rerouting {} to {}", device, newSink);
217 bridge.getClient().moveSinkInput(((SinkInput) device), newSink);
218 updateState = new StringType(newSink.getPaName());
220 logger.error("no sink {} found", command.toString());
224 logger.trace("updating {} to {}", channelUID, updateState);
225 if (!updateState.equals(UnDefType.UNDEF)) {
226 updateState(channelUID, updateState);
232 public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
233 if (device.getPaName().equals(name)) {
234 updateStatus(ThingStatus.ONLINE);
235 logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
236 updateState(VOLUME_CHANNEL, new PercentType(device.getVolume()));
237 updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
238 updateState(STATE_CHANNEL,
239 device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
240 if (device instanceof SinkInput) {
241 updateState(ROUTE_TO_SINK_CHANNEL,
242 ((SinkInput) device).getSink() != null
243 ? new StringType(((SinkInput) device).getSink().getPaName())
244 : new StringType("-"));
246 if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
247 updateState(SLAVES_CHANNEL,
248 new StringType(StringUtils.join(((Sink) device).getCombinedSinkNames(), ",")));
254 public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
255 if (device.getPaName().equals(name)) {
256 bridgeHandler.unregisterDeviceStatusListener(this);
257 bridgeHandler = null;
258 updateStatus(ThingStatus.OFFLINE);
263 public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
264 logger.trace("new device discovered {} by {}", device, bridge);