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.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
27 import org.openhab.binding.pulseaudio.internal.items.Sink;
28 import org.openhab.binding.pulseaudio.internal.items.SinkInput;
29 import org.openhab.core.config.core.Configuration;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.IncreaseDecreaseType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.PercentType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.ThingUID;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.thing.binding.ThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link PulseaudioHandler} is responsible for handling commands, which are
52 * sent to one of the channels.
54 * @author Tobias Bräutigam - Initial contribution
56 public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
58 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
59 .unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
60 SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
62 private int refresh = 60; // refresh every minute as default
63 private ScheduledFuture<?> refreshJob;
65 private PulseaudioBridgeHandler bridgeHandler;
67 private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
71 public PulseaudioHandler(Thing thing) {
76 public void initialize() {
77 Configuration config = getThing().getConfiguration();
78 name = (String) config.get(DEVICE_PARAMETER_NAME);
80 // until we get an update put the Thing offline
81 updateStatus(ThingStatus.OFFLINE);
82 deviceOnlineWatchdog();
86 public void dispose() {
87 if (refreshJob != null && !refreshJob.isCancelled()) {
88 refreshJob.cancel(true);
91 updateStatus(ThingStatus.OFFLINE);
93 logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
97 private void deviceOnlineWatchdog() {
98 Runnable runnable = () -> {
100 PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
101 if (bridgeHandler != null) {
102 if (bridgeHandler.getDevice(name) == null) {
103 updateStatus(ThingStatus.OFFLINE);
104 bridgeHandler = null;
106 updateStatus(ThingStatus.ONLINE);
109 logger.debug("Bridge for pulseaudio device {} not found.", name);
110 updateStatus(ThingStatus.OFFLINE);
112 } catch (Exception e) {
113 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
114 bridgeHandler = null;
118 refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
121 private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
122 if (this.bridgeHandler == null) {
123 Bridge bridge = getBridge();
124 if (bridge == null) {
125 logger.debug("Required bridge not defined for device {}.", name);
128 ThingHandler handler = bridge.getHandler();
129 if (handler instanceof PulseaudioBridgeHandler) {
130 this.bridgeHandler = (PulseaudioBridgeHandler) handler;
131 this.bridgeHandler.registerDeviceStatusListener(this);
133 logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
137 return this.bridgeHandler;
141 public void handleCommand(ChannelUID channelUID, Command command) {
142 PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
143 if (bridge == null) {
144 logger.warn("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
147 if (command instanceof RefreshType) {
148 bridge.handleCommand(channelUID, command);
152 AbstractAudioDeviceConfig device = bridge.getDevice(name);
153 if (device == null) {
154 logger.warn("device {} not found", name);
155 updateStatus(ThingStatus.OFFLINE);
156 bridgeHandler = null;
159 State updateState = UnDefType.UNDEF;
160 if (channelUID.getId().equals(VOLUME_CHANNEL)) {
161 if (command instanceof IncreaseDecreaseType) {
162 // refresh to get the current volume level
163 bridge.getClient().update();
164 device = bridge.getDevice(name);
165 int volume = device.getVolume();
166 if (command.equals(IncreaseDecreaseType.INCREASE)) {
167 volume = Math.min(100, volume + 5);
169 if (command.equals(IncreaseDecreaseType.DECREASE)) {
170 volume = Math.max(0, volume - 5);
172 bridge.getClient().setVolumePercent(device, volume);
173 updateState = new PercentType(volume);
174 } else if (command instanceof PercentType) {
175 DecimalType volume = (DecimalType) command;
176 bridge.getClient().setVolumePercent(device, volume.intValue());
177 updateState = (PercentType) command;
178 } else if (command instanceof DecimalType) {
180 DecimalType volume = (DecimalType) command;
181 bridge.getClient().setVolume(device, volume.intValue());
182 updateState = (DecimalType) command;
184 } else if (channelUID.getId().equals(MUTE_CHANNEL)) {
185 if (command instanceof OnOffType) {
186 bridge.getClient().setMute(device, OnOffType.ON.equals(command));
187 updateState = (OnOffType) command;
189 } else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
190 if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
191 if (command instanceof StringType) {
192 List<Sink> slaves = new ArrayList<>();
193 for (String slaveName : command.toString().split(",")) {
194 Sink slave = bridge.getClient().getSink(slaveName.trim());
199 if (!slaves.isEmpty()) {
200 bridge.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
204 logger.error("{} is no combined sink", device);
206 } else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
207 if (device instanceof SinkInput) {
209 if (command instanceof DecimalType) {
210 newSink = bridge.getClient().getSink(((DecimalType) command).intValue());
212 newSink = bridge.getClient().getSink(command.toString());
214 if (newSink != null) {
215 logger.debug("rerouting {} to {}", device, newSink);
216 bridge.getClient().moveSinkInput(((SinkInput) device), newSink);
217 updateState = new StringType(newSink.getPaName());
219 logger.error("no sink {} found", command.toString());
223 logger.trace("updating {} to {}", channelUID, updateState);
224 if (!updateState.equals(UnDefType.UNDEF)) {
225 updateState(channelUID, updateState);
231 public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
232 if (device.getPaName().equals(name)) {
233 updateStatus(ThingStatus.ONLINE);
234 logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
235 updateState(VOLUME_CHANNEL, new PercentType(device.getVolume()));
236 updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
237 updateState(STATE_CHANNEL,
238 device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
239 if (device instanceof SinkInput) {
240 updateState(ROUTE_TO_SINK_CHANNEL,
241 ((SinkInput) device).getSink() != null
242 ? new StringType(((SinkInput) device).getSink().getPaName())
243 : new StringType("-"));
245 if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
246 updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames())));
252 public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
253 if (device.getPaName().equals(name)) {
254 bridgeHandler.unregisterDeviceStatusListener(this);
255 bridgeHandler = null;
256 updateStatus(ThingStatus.OFFLINE);
261 public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
262 logger.trace("new device discovered {} by {}", device, bridge);