2 * Copyright (c) 2010-2023 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.volumio.internal;
15 import java.math.BigDecimal;
16 import java.util.concurrent.TimeUnit;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.json.JSONException;
21 import org.json.JSONObject;
22 import org.openhab.binding.volumio.internal.mapping.VolumioData;
23 import org.openhab.binding.volumio.internal.mapping.VolumioEvents;
24 import org.openhab.binding.volumio.internal.mapping.VolumioServiceTypes;
25 import org.openhab.core.library.types.NextPreviousType;
26 import org.openhab.core.library.types.OnOffType;
27 import org.openhab.core.library.types.PercentType;
28 import org.openhab.core.library.types.PlayPauseType;
29 import org.openhab.core.library.types.RewindFastforwardType;
30 import org.openhab.core.library.types.StringType;
31 import org.openhab.core.thing.Channel;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.RefreshType;
39 import org.openhab.core.types.UnDefType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import io.socket.client.Socket;
44 import io.socket.emitter.Emitter;
47 * The {@link VolumioHandler} is responsible for handling commands, which are
48 * sent to one of the channels.
50 * @author Patrick Sernetz - Initial Contribution
51 * @author Chris Wohlbrecht - Adaption for openHAB 3
52 * @author Michael Loercher - Adaption for openHAB 3
55 public class VolumioHandler extends BaseThingHandler {
57 private final Logger logger = LoggerFactory.getLogger(VolumioHandler.class);
59 private @Nullable VolumioService volumio;
61 private final VolumioData state = new VolumioData();
63 public VolumioHandler(Thing thing) {
68 public void handleCommand(ChannelUID channelUID, Command command) {
69 logger.debug("channelUID: {}", channelUID);
71 if (volumio == null) {
72 logger.debug("Ignoring command {} = {} because device is offline.", channelUID.getId(), command);
73 if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
74 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "device is offline");
80 switch (channelUID.getId()) {
81 case VolumioBindingConstants.CHANNEL_PLAYER:
82 handlePlaybackCommands(command);
84 case VolumioBindingConstants.CHANNEL_VOLUME:
85 handleVolumeCommand(command);
88 case VolumioBindingConstants.CHANNEL_ARTIST:
89 case VolumioBindingConstants.CHANNEL_ALBUM:
90 case VolumioBindingConstants.CHANNEL_TRACK_TYPE:
91 case VolumioBindingConstants.CHANNEL_TITLE:
94 case VolumioBindingConstants.CHANNEL_PLAY_RADIO_STREAM:
95 if (command instanceof StringType) {
96 final String uri = command.toFullString();
97 volumio.replacePlay(uri, "Radio", VolumioServiceTypes.WEBRADIO);
102 case VolumioBindingConstants.CHANNEL_PLAY_URI:
103 if (command instanceof StringType) {
104 final String uri = command.toFullString();
105 volumio.replacePlay(uri, "URI", VolumioServiceTypes.WEBRADIO);
110 case VolumioBindingConstants.CHANNEL_PLAY_FILE:
111 if (command instanceof StringType) {
112 final String uri = command.toFullString();
113 volumio.replacePlay(uri, "", VolumioServiceTypes.MPD);
118 case VolumioBindingConstants.CHANNEL_PLAY_PLAYLIST:
119 if (command instanceof StringType) {
120 final String playlistName = command.toFullString();
121 volumio.playPlaylist(playlistName);
125 case VolumioBindingConstants.CHANNEL_CLEAR_QUEUE:
126 if ((command instanceof OnOffType) && (command == OnOffType.ON)) {
127 volumio.clearQueue();
128 // Make it feel like a toggle button ...
129 updateState(channelUID, OnOffType.OFF);
132 case VolumioBindingConstants.CHANNEL_PLAY_RANDOM:
133 if (command instanceof OnOffType) {
134 boolean enableRandom = command == OnOffType.ON;
135 volumio.setRandom(enableRandom);
138 case VolumioBindingConstants.CHANNEL_PLAY_REPEAT:
139 if (command instanceof OnOffType) {
140 boolean enableRepeat = command == OnOffType.ON;
141 volumio.setRepeat(enableRepeat);
145 logger.debug("Called Refresh");
148 case VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND:
149 if (command instanceof StringType) {
150 sendSystemCommand(command);
151 updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
152 } else if (RefreshType.REFRESH == command) {
153 updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
156 case VolumioBindingConstants.CHANNEL_STOP:
157 if (command instanceof StringType) {
158 handleStopCommand(command);
159 updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
160 } else if (RefreshType.REFRESH == command) {
161 updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
165 logger.error("Unknown channel: {}", channelUID.getId());
167 } catch (Exception e) {
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
172 private void sendSystemCommand(Command command) {
173 if (command instanceof StringType) {
174 volumio.sendSystemCommand(command.toString());
175 updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
176 } else if (command.equals(RefreshType.REFRESH)) {
177 updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
182 * Set all channel of thing to UNDEF during connection.
184 private void clearChannels() {
185 for (Channel channel : getThing().getChannels()) {
186 updateState(channel.getUID(), UnDefType.UNDEF);
190 private void handleVolumeCommand(Command command) {
191 if (command instanceof PercentType) {
192 volumio.setVolume((PercentType) command);
193 } else if (command instanceof RefreshType) {
196 logger.error("Command is not handled");
200 private void handleStopCommand(Command command) {
201 if (command instanceof StringType) {
203 updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
204 } else if (command.equals(RefreshType.REFRESH)) {
205 updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
209 private void handlePlaybackCommands(Command command) {
210 if (command instanceof PlayPauseType playPauseCmd) {
211 switch (playPauseCmd) {
219 } else if (command instanceof NextPreviousType nextPreviousType) {
220 switch (nextPreviousType) {
228 } else if (command instanceof RewindFastforwardType fastForwardType) {
229 switch (fastForwardType) {
232 logger.warn("Not implemented yet");
235 } else if (command instanceof RefreshType) {
238 logger.error("Command is not handled: {}", command);
243 * Bind default listeners to volumio session.
244 * - EVENT_CONNECT - Connection to volumio was established
245 * - EVENT_DISCONNECT - Connection was disconnected
248 private void bindDefaultListener() {
249 volumio.on(Socket.EVENT_CONNECT, connectListener());
250 volumio.on(Socket.EVENT_DISCONNECT, disconnectListener());
251 volumio.on(VolumioEvents.PUSH_STATE, pushStateListener());
255 * Read the configuration and connect to volumio device. The Volumio impl. is
256 * async so it should not block the process in any way.
259 public void initialize() {
260 String hostname = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_HOSTNAME);
261 int port = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PORT))
263 String protocol = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PROTOCOL);
264 int timeout = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_TIMEOUT))
267 if (hostname == null) {
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
269 "Configuration incomplete, missing hostname");
270 } else if (protocol == null) {
271 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
272 "Configuration incomplete, missing protocol");
274 logger.debug("Trying to connect to Volumio on {}://{}:{}", protocol, hostname, port);
276 volumio = new VolumioService(protocol, hostname, port, timeout);
278 bindDefaultListener();
279 updateStatus(ThingStatus.OFFLINE);
281 } catch (Exception e) {
282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
288 public void dispose() {
289 if (volumio != null) {
290 scheduler.schedule(() -> {
291 if (volumio.isConnected()) {
292 logger.warn("Timeout during disconnect event");
297 }, 30, TimeUnit.SECONDS);
299 volumio.disconnect();
306 * As soon as the Connect Listener is executed
307 * the ThingStatus is set to ONLINE.
309 private Emitter.Listener connectListener() {
310 return arg -> updateStatus(ThingStatus.ONLINE);
314 * As soon as the Disconnect Listener is executed
315 * the ThingStatus is set to OFFLINE.
317 private Emitter.Listener disconnectListener() {
318 return arg0 -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
322 * On received a pushState Event, the ThingChannels are
323 * updated if there is a change and they are linked.
325 private Emitter.Listener pushStateListener() {
328 JSONObject jsonObject = (JSONObject) data[0];
329 logger.debug("{}", jsonObject.toString());
330 state.update(jsonObject);
331 if (isLinked(VolumioBindingConstants.CHANNEL_TITLE) && state.isTitleDirty()) {
332 updateState(VolumioBindingConstants.CHANNEL_TITLE, state.getTitle());
334 if (isLinked(VolumioBindingConstants.CHANNEL_ARTIST) && state.isArtistDirty()) {
335 updateState(VolumioBindingConstants.CHANNEL_ARTIST, state.getArtist());
337 if (isLinked(VolumioBindingConstants.CHANNEL_ALBUM) && state.isAlbumDirty()) {
338 updateState(VolumioBindingConstants.CHANNEL_ALBUM, state.getAlbum());
340 if (isLinked(VolumioBindingConstants.CHANNEL_VOLUME) && state.isVolumeDirty()) {
341 updateState(VolumioBindingConstants.CHANNEL_VOLUME, state.getVolume());
343 if (isLinked(VolumioBindingConstants.CHANNEL_PLAYER) && state.isStateDirty()) {
344 updateState(VolumioBindingConstants.CHANNEL_PLAYER, state.getState());
346 if (isLinked(VolumioBindingConstants.CHANNEL_TRACK_TYPE) && state.isTrackTypeDirty()) {
347 updateState(VolumioBindingConstants.CHANNEL_TRACK_TYPE, state.getTrackType());
350 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_RANDOM) && state.isRandomDirty()) {
351 updateState(VolumioBindingConstants.CHANNEL_PLAY_RANDOM, state.getRandom());
353 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_REPEAT) && state.isRepeatDirty()) {
354 updateState(VolumioBindingConstants.CHANNEL_PLAY_REPEAT, state.getRepeat());
357 * if (isLinked(CHANNEL_COVER_ART) && state.isCoverArtDirty()) {
358 * updateState(CHANNEL_COVER_ART, state.getCoverArt());
361 } catch (JSONException e) {
362 logger.error("Could not refresh channel: {}", e.getMessage());