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 VolumioService volumioLocal = volumio;
71 if (volumioLocal == null) {
72 if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
73 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
74 "Volumio service was not yet initialized, cannot handle command.");
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 volumioLocal.replacePlay(uri, "Radio", VolumioServiceTypes.WEBRADIO);
102 case VolumioBindingConstants.CHANNEL_PLAY_URI:
103 if (command instanceof StringType) {
104 final String uri = command.toFullString();
105 volumioLocal.replacePlay(uri, "URI", VolumioServiceTypes.WEBRADIO);
110 case VolumioBindingConstants.CHANNEL_PLAY_FILE:
111 if (command instanceof StringType) {
112 final String uri = command.toFullString();
113 volumioLocal.replacePlay(uri, "", VolumioServiceTypes.MPD);
118 case VolumioBindingConstants.CHANNEL_PLAY_PLAYLIST:
119 if (command instanceof StringType) {
120 final String playlistName = command.toFullString();
121 volumioLocal.playPlaylist(playlistName);
125 case VolumioBindingConstants.CHANNEL_CLEAR_QUEUE:
126 if ((command instanceof OnOffType) && (command == OnOffType.ON)) {
127 volumioLocal.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 volumioLocal.setRandom(enableRandom);
138 case VolumioBindingConstants.CHANNEL_PLAY_REPEAT:
139 if (command instanceof OnOffType) {
140 boolean enableRepeat = command == OnOffType.ON;
141 volumioLocal.setRepeat(enableRepeat);
145 logger.debug("Called Refresh");
146 volumioLocal.getState();
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 VolumioService volumioLocal = volumio;
175 if (volumioLocal == null) {
176 if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
177 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
178 "Volumio service was not yet initialized, cannot handle send system command.");
183 if (command instanceof StringType) {
184 volumioLocal.sendSystemCommand(command.toString());
185 updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
186 } else if (command.equals(RefreshType.REFRESH)) {
187 updateState(VolumioBindingConstants.CHANNEL_SYSTEM_COMMAND, UnDefType.UNDEF);
192 * Set all channel of thing to UNDEF during connection.
194 private void clearChannels() {
195 for (Channel channel : getThing().getChannels()) {
196 updateState(channel.getUID(), UnDefType.UNDEF);
200 private void handleVolumeCommand(Command command) {
201 VolumioService volumioLocal = volumio;
203 if (volumioLocal == null) {
204 if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
206 "Volumio service was not yet initialized, cannot handle volume command.");
211 if (command instanceof PercentType commandAsPercentType) {
212 volumioLocal.setVolume(commandAsPercentType);
213 } else if (command instanceof RefreshType) {
214 volumioLocal.getState();
216 logger.error("Command is not handled");
220 private void handleStopCommand(Command command) {
221 VolumioService volumioLocal = volumio;
223 if (volumioLocal == null) {
224 if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
225 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
226 "Volumio service was not yet initialized, cannot handle stop command.");
231 if (command instanceof StringType) {
233 updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
234 } else if (command.equals(RefreshType.REFRESH)) {
235 updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
239 private void handlePlaybackCommands(Command command) {
240 VolumioService volumioLocal = volumio;
242 if (volumioLocal == null) {
243 if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
244 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
245 "Volumio service was not yet initialized, cannot handle playback command.");
249 if (command instanceof PlayPauseType playPauseCmd) {
250 switch (playPauseCmd) {
255 volumioLocal.pause();
258 } else if (command instanceof NextPreviousType nextPreviousType) {
259 switch (nextPreviousType) {
261 volumioLocal.previous();
267 } else if (command instanceof RewindFastforwardType fastForwardType) {
268 switch (fastForwardType) {
271 logger.warn("Not implemented yet");
274 } else if (command instanceof RefreshType) {
275 volumioLocal.getState();
277 logger.error("Command is not handled: {}", command);
282 * Bind default listeners to volumio session.
283 * - EVENT_CONNECT - Connection to volumio was established
284 * - EVENT_DISCONNECT - Connection was disconnected
287 private void bindDefaultListener() {
288 VolumioService volumioLocal = volumio;
290 if (volumioLocal == null) {
291 if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
293 "Volumio service was not yet initialized.");
298 volumioLocal.on(Socket.EVENT_CONNECT, connectListener());
299 volumioLocal.on(Socket.EVENT_DISCONNECT, disconnectListener());
300 volumioLocal.on(VolumioEvents.PUSH_STATE, pushStateListener());
304 * Read the configuration and connect to volumio device. The Volumio impl. is
305 * async so it should not block the process in any way.
308 public void initialize() {
309 String hostname = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_HOSTNAME);
310 int port = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PORT))
312 String protocol = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PROTOCOL);
313 int timeout = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_TIMEOUT))
316 if (hostname == null) {
317 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
318 "Configuration incomplete, missing hostname");
319 } else if (protocol == null) {
320 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
321 "Configuration incomplete, missing protocol");
323 logger.debug("Trying to connect to Volumio on {}://{}:{}", protocol, hostname, port);
325 VolumioService volumioLocal = new VolumioService(protocol, hostname, port, timeout);
326 volumio = volumioLocal;
328 bindDefaultListener();
329 updateStatus(ThingStatus.OFFLINE);
330 volumioLocal.connect();
331 } catch (Exception e) {
332 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
338 public void dispose() {
339 VolumioService volumioLocal = volumio;
340 if (volumioLocal != null) {
341 scheduler.schedule(() -> {
342 if (volumioLocal.isConnected()) {
343 logger.warn("Timeout during disconnect event");
345 volumioLocal.close();
348 }, 30, TimeUnit.SECONDS);
350 volumioLocal.disconnect();
357 * As soon as the Connect Listener is executed
358 * the ThingStatus is set to ONLINE.
360 private Emitter.Listener connectListener() {
361 return arg -> updateStatus(ThingStatus.ONLINE);
365 * As soon as the Disconnect Listener is executed
366 * the ThingStatus is set to OFFLINE.
368 private Emitter.Listener disconnectListener() {
369 return arg0 -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
373 * On received a pushState Event, the ThingChannels are
374 * updated if there is a change and they are linked.
376 private Emitter.Listener pushStateListener() {
379 JSONObject jsonObject = (JSONObject) data[0];
380 logger.debug("{}", jsonObject.toString());
381 state.update(jsonObject);
382 if (isLinked(VolumioBindingConstants.CHANNEL_TITLE) && state.isTitleDirty()) {
383 updateState(VolumioBindingConstants.CHANNEL_TITLE, state.getTitle());
385 if (isLinked(VolumioBindingConstants.CHANNEL_ARTIST) && state.isArtistDirty()) {
386 updateState(VolumioBindingConstants.CHANNEL_ARTIST, state.getArtist());
388 if (isLinked(VolumioBindingConstants.CHANNEL_ALBUM) && state.isAlbumDirty()) {
389 updateState(VolumioBindingConstants.CHANNEL_ALBUM, state.getAlbum());
391 if (isLinked(VolumioBindingConstants.CHANNEL_VOLUME) && state.isVolumeDirty()) {
392 updateState(VolumioBindingConstants.CHANNEL_VOLUME, state.getVolume());
394 if (isLinked(VolumioBindingConstants.CHANNEL_PLAYER) && state.isStateDirty()) {
395 updateState(VolumioBindingConstants.CHANNEL_PLAYER, state.getState());
397 if (isLinked(VolumioBindingConstants.CHANNEL_TRACK_TYPE) && state.isTrackTypeDirty()) {
398 updateState(VolumioBindingConstants.CHANNEL_TRACK_TYPE, state.getTrackType());
401 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_RANDOM) && state.isRandomDirty()) {
402 updateState(VolumioBindingConstants.CHANNEL_PLAY_RANDOM, state.getRandom());
404 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_REPEAT) && state.isRepeatDirty()) {
405 updateState(VolumioBindingConstants.CHANNEL_PLAY_REPEAT, state.getRepeat());
408 * if (isLinked(CHANNEL_COVER_ART) && state.isCoverArtDirty()) {
409 * updateState(CHANNEL_COVER_ART, state.getCoverArt());
412 } catch (JSONException e) {
413 logger.error("Could not refresh channel: {}", e.getMessage());