]> git.basschouten.com Git - openhab-addons.git/blob
21227a8f88dd3baf9b0618bd512d616dbeab944b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.volumio.internal;
14
15 import java.math.BigDecimal;
16 import java.util.concurrent.TimeUnit;
17
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;
42
43 import io.socket.client.Socket;
44 import io.socket.emitter.Emitter;
45
46 /**
47  * The {@link VolumioHandler} is responsible for handling commands, which are
48  * sent to one of the channels.
49  *
50  * @author Patrick Sernetz - Initial Contribution
51  * @author Chris Wohlbrecht - Adaption for openHAB 3
52  * @author Michael Loercher - Adaption for openHAB 3
53  */
54 @NonNullByDefault
55 public class VolumioHandler extends BaseThingHandler {
56
57     private final Logger logger = LoggerFactory.getLogger(VolumioHandler.class);
58
59     private @Nullable VolumioService volumio;
60
61     private final VolumioData state = new VolumioData();
62
63     public VolumioHandler(Thing thing) {
64         super(thing);
65     }
66
67     @Override
68     public void handleCommand(ChannelUID channelUID, Command command) {
69         logger.debug("channelUID: {}", channelUID);
70
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");
75             }
76             return;
77         }
78
79         try {
80             switch (channelUID.getId()) {
81                 case VolumioBindingConstants.CHANNEL_PLAYER:
82                     handlePlaybackCommands(command);
83                     break;
84                 case VolumioBindingConstants.CHANNEL_VOLUME:
85                     handleVolumeCommand(command);
86                     break;
87
88                 case VolumioBindingConstants.CHANNEL_ARTIST:
89                 case VolumioBindingConstants.CHANNEL_ALBUM:
90                 case VolumioBindingConstants.CHANNEL_TRACK_TYPE:
91                 case VolumioBindingConstants.CHANNEL_TITLE:
92                     break;
93
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);
98                     }
99
100                     break;
101
102                 case VolumioBindingConstants.CHANNEL_PLAY_URI:
103                     if (command instanceof StringType) {
104                         final String uri = command.toFullString();
105                         volumio.replacePlay(uri, "URI", VolumioServiceTypes.WEBRADIO);
106                     }
107
108                     break;
109
110                 case VolumioBindingConstants.CHANNEL_PLAY_FILE:
111                     if (command instanceof StringType) {
112                         final String uri = command.toFullString();
113                         volumio.replacePlay(uri, "", VolumioServiceTypes.MPD);
114                     }
115
116                     break;
117
118                 case VolumioBindingConstants.CHANNEL_PLAY_PLAYLIST:
119                     if (command instanceof StringType) {
120                         final String playlistName = command.toFullString();
121                         volumio.playPlaylist(playlistName);
122                     }
123
124                     break;
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);
130                     }
131                     break;
132                 case VolumioBindingConstants.CHANNEL_PLAY_RANDOM:
133                     if (command instanceof OnOffType) {
134                         boolean enableRandom = command == OnOffType.ON;
135                         volumio.setRandom(enableRandom);
136                     }
137                     break;
138                 case VolumioBindingConstants.CHANNEL_PLAY_REPEAT:
139                     if (command instanceof OnOffType) {
140                         boolean enableRepeat = command == OnOffType.ON;
141                         volumio.setRepeat(enableRepeat);
142                     }
143                     break;
144                 case "REFRESH":
145                     logger.debug("Called Refresh");
146                     volumio.getState();
147                     break;
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);
154                     }
155                     break;
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);
162                     }
163                     break;
164                 default:
165                     logger.error("Unknown channel: {}", channelUID.getId());
166             }
167         } catch (Exception e) {
168             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
169         }
170     }
171
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);
178         }
179     }
180
181     /**
182      * Set all channel of thing to UNDEF during connection.
183      */
184     private void clearChannels() {
185         for (Channel channel : getThing().getChannels()) {
186             updateState(channel.getUID(), UnDefType.UNDEF);
187         }
188     }
189
190     private void handleVolumeCommand(Command command) {
191         if (command instanceof PercentType) {
192             volumio.setVolume((PercentType) command);
193         } else if (command instanceof RefreshType) {
194             volumio.getState();
195         } else {
196             logger.error("Command is not handled");
197         }
198     }
199
200     private void handleStopCommand(Command command) {
201         if (command instanceof StringType) {
202             volumio.stop();
203             updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
204         } else if (command.equals(RefreshType.REFRESH)) {
205             updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
206         }
207     }
208
209     private void handlePlaybackCommands(Command command) {
210         if (command instanceof PlayPauseType playPauseCmd) {
211             switch (playPauseCmd) {
212                 case PLAY:
213                     volumio.play();
214                     break;
215                 case PAUSE:
216                     volumio.pause();
217                     break;
218             }
219         } else if (command instanceof NextPreviousType nextPreviousType) {
220             switch (nextPreviousType) {
221                 case PREVIOUS:
222                     volumio.previous();
223                     break;
224                 case NEXT:
225                     volumio.next();
226                     break;
227             }
228         } else if (command instanceof RewindFastforwardType fastForwardType) {
229             switch (fastForwardType) {
230                 case FASTFORWARD:
231                 case REWIND:
232                     logger.warn("Not implemented yet");
233                     break;
234             }
235         } else if (command instanceof RefreshType) {
236             volumio.getState();
237         } else {
238             logger.error("Command is not handled: {}", command);
239         }
240     }
241
242     /**
243      * Bind default listeners to volumio session.
244      * - EVENT_CONNECT - Connection to volumio was established
245      * - EVENT_DISCONNECT - Connection was disconnected
246      * - PUSH.STATE -
247      */
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());
252     }
253
254     /**
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.
257      */
258     @Override
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))
262                 .intValueExact();
263         String protocol = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PROTOCOL);
264         int timeout = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_TIMEOUT))
265                 .intValueExact();
266
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");
273         } else {
274             logger.debug("Trying to connect to Volumio on {}://{}:{}", protocol, hostname, port);
275             try {
276                 volumio = new VolumioService(protocol, hostname, port, timeout);
277                 clearChannels();
278                 bindDefaultListener();
279                 updateStatus(ThingStatus.OFFLINE);
280                 volumio.connect();
281             } catch (Exception e) {
282                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
283             }
284         }
285     }
286
287     @Override
288     public void dispose() {
289         if (volumio != null) {
290             scheduler.schedule(() -> {
291                 if (volumio.isConnected()) {
292                     logger.warn("Timeout during disconnect event");
293                 } else {
294                     volumio.close();
295                 }
296                 clearChannels();
297             }, 30, TimeUnit.SECONDS);
298
299             volumio.disconnect();
300         }
301     }
302
303     /** Listener **/
304
305     /**
306      * As soon as the Connect Listener is executed
307      * the ThingStatus is set to ONLINE.
308      */
309     private Emitter.Listener connectListener() {
310         return arg -> updateStatus(ThingStatus.ONLINE);
311     }
312
313     /**
314      * As soon as the Disconnect Listener is executed
315      * the ThingStatus is set to OFFLINE.
316      */
317     private Emitter.Listener disconnectListener() {
318         return arg0 -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
319     }
320
321     /**
322      * On received a pushState Event, the ThingChannels are
323      * updated if there is a change and they are linked.
324      */
325     private Emitter.Listener pushStateListener() {
326         return data -> {
327             try {
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());
333                 }
334                 if (isLinked(VolumioBindingConstants.CHANNEL_ARTIST) && state.isArtistDirty()) {
335                     updateState(VolumioBindingConstants.CHANNEL_ARTIST, state.getArtist());
336                 }
337                 if (isLinked(VolumioBindingConstants.CHANNEL_ALBUM) && state.isAlbumDirty()) {
338                     updateState(VolumioBindingConstants.CHANNEL_ALBUM, state.getAlbum());
339                 }
340                 if (isLinked(VolumioBindingConstants.CHANNEL_VOLUME) && state.isVolumeDirty()) {
341                     updateState(VolumioBindingConstants.CHANNEL_VOLUME, state.getVolume());
342                 }
343                 if (isLinked(VolumioBindingConstants.CHANNEL_PLAYER) && state.isStateDirty()) {
344                     updateState(VolumioBindingConstants.CHANNEL_PLAYER, state.getState());
345                 }
346                 if (isLinked(VolumioBindingConstants.CHANNEL_TRACK_TYPE) && state.isTrackTypeDirty()) {
347                     updateState(VolumioBindingConstants.CHANNEL_TRACK_TYPE, state.getTrackType());
348                 }
349
350                 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_RANDOM) && state.isRandomDirty()) {
351                     updateState(VolumioBindingConstants.CHANNEL_PLAY_RANDOM, state.getRandom());
352                 }
353                 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_REPEAT) && state.isRepeatDirty()) {
354                     updateState(VolumioBindingConstants.CHANNEL_PLAY_REPEAT, state.getRepeat());
355                 }
356                 /**
357                  * if (isLinked(CHANNEL_COVER_ART) && state.isCoverArtDirty()) {
358                  * updateState(CHANNEL_COVER_ART, state.getCoverArt());
359                  * }
360                  */
361             } catch (JSONException e) {
362                 logger.error("Could not refresh channel: {}", e.getMessage());
363             }
364         };
365     }
366 }