]> git.basschouten.com Git - openhab-addons.git/blob
76bae18c378f8f1eb3536435b027a978b606ae58
[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         VolumioService volumioLocal = volumio;
70
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.");
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                         volumioLocal.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                         volumioLocal.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                         volumioLocal.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                         volumioLocal.playPlaylist(playlistName);
122                     }
123
124                     break;
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);
130                     }
131                     break;
132                 case VolumioBindingConstants.CHANNEL_PLAY_RANDOM:
133                     if (command instanceof OnOffType) {
134                         boolean enableRandom = command == OnOffType.ON;
135                         volumioLocal.setRandom(enableRandom);
136                     }
137                     break;
138                 case VolumioBindingConstants.CHANNEL_PLAY_REPEAT:
139                     if (command instanceof OnOffType) {
140                         boolean enableRepeat = command == OnOffType.ON;
141                         volumioLocal.setRepeat(enableRepeat);
142                     }
143                     break;
144                 case "REFRESH":
145                     logger.debug("Called Refresh");
146                     volumioLocal.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         VolumioService volumioLocal = volumio;
174
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.");
179             }
180             return;
181         }
182
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);
188         }
189     }
190
191     /**
192      * Set all channel of thing to UNDEF during connection.
193      */
194     private void clearChannels() {
195         for (Channel channel : getThing().getChannels()) {
196             updateState(channel.getUID(), UnDefType.UNDEF);
197         }
198     }
199
200     private void handleVolumeCommand(Command command) {
201         VolumioService volumioLocal = volumio;
202
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.");
207             }
208             return;
209         }
210
211         if (command instanceof PercentType commandAsPercentType) {
212             volumioLocal.setVolume(commandAsPercentType);
213         } else if (command instanceof RefreshType) {
214             volumioLocal.getState();
215         } else {
216             logger.error("Command is not handled");
217         }
218     }
219
220     private void handleStopCommand(Command command) {
221         VolumioService volumioLocal = volumio;
222
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.");
227             }
228             return;
229         }
230
231         if (command instanceof StringType) {
232             volumioLocal.stop();
233             updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
234         } else if (command.equals(RefreshType.REFRESH)) {
235             updateState(VolumioBindingConstants.CHANNEL_STOP, UnDefType.UNDEF);
236         }
237     }
238
239     private void handlePlaybackCommands(Command command) {
240         VolumioService volumioLocal = volumio;
241
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.");
246             }
247             return;
248         }
249         if (command instanceof PlayPauseType playPauseCmd) {
250             switch (playPauseCmd) {
251                 case PLAY:
252                     volumioLocal.play();
253                     break;
254                 case PAUSE:
255                     volumioLocal.pause();
256                     break;
257             }
258         } else if (command instanceof NextPreviousType nextPreviousType) {
259             switch (nextPreviousType) {
260                 case PREVIOUS:
261                     volumioLocal.previous();
262                     break;
263                 case NEXT:
264                     volumioLocal.next();
265                     break;
266             }
267         } else if (command instanceof RewindFastforwardType fastForwardType) {
268             switch (fastForwardType) {
269                 case FASTFORWARD:
270                 case REWIND:
271                     logger.warn("Not implemented yet");
272                     break;
273             }
274         } else if (command instanceof RefreshType) {
275             volumioLocal.getState();
276         } else {
277             logger.error("Command is not handled: {}", command);
278         }
279     }
280
281     /**
282      * Bind default listeners to volumio session.
283      * - EVENT_CONNECT - Connection to volumio was established
284      * - EVENT_DISCONNECT - Connection was disconnected
285      * - PUSH.STATE -
286      */
287     private void bindDefaultListener() {
288         VolumioService volumioLocal = volumio;
289
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.");
294             }
295             return;
296         }
297
298         volumioLocal.on(Socket.EVENT_CONNECT, connectListener());
299         volumioLocal.on(Socket.EVENT_DISCONNECT, disconnectListener());
300         volumioLocal.on(VolumioEvents.PUSH_STATE, pushStateListener());
301     }
302
303     /**
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.
306      */
307     @Override
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))
311                 .intValueExact();
312         String protocol = (String) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_PROTOCOL);
313         int timeout = ((BigDecimal) getThing().getConfiguration().get(VolumioBindingConstants.CONFIG_PROPERTY_TIMEOUT))
314                 .intValueExact();
315
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");
322         } else {
323             logger.debug("Trying to connect to Volumio on {}://{}:{}", protocol, hostname, port);
324             try {
325                 VolumioService volumioLocal = new VolumioService(protocol, hostname, port, timeout);
326                 volumio = volumioLocal;
327                 clearChannels();
328                 bindDefaultListener();
329                 updateStatus(ThingStatus.OFFLINE);
330                 volumioLocal.connect();
331             } catch (Exception e) {
332                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
333             }
334         }
335     }
336
337     @Override
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");
344                 } else {
345                     volumioLocal.close();
346                 }
347                 clearChannels();
348             }, 30, TimeUnit.SECONDS);
349
350             volumioLocal.disconnect();
351         }
352     }
353
354     /** Listener **/
355
356     /**
357      * As soon as the Connect Listener is executed
358      * the ThingStatus is set to ONLINE.
359      */
360     private Emitter.Listener connectListener() {
361         return arg -> updateStatus(ThingStatus.ONLINE);
362     }
363
364     /**
365      * As soon as the Disconnect Listener is executed
366      * the ThingStatus is set to OFFLINE.
367      */
368     private Emitter.Listener disconnectListener() {
369         return arg0 -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
370     }
371
372     /**
373      * On received a pushState Event, the ThingChannels are
374      * updated if there is a change and they are linked.
375      */
376     private Emitter.Listener pushStateListener() {
377         return data -> {
378             try {
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());
384                 }
385                 if (isLinked(VolumioBindingConstants.CHANNEL_ARTIST) && state.isArtistDirty()) {
386                     updateState(VolumioBindingConstants.CHANNEL_ARTIST, state.getArtist());
387                 }
388                 if (isLinked(VolumioBindingConstants.CHANNEL_ALBUM) && state.isAlbumDirty()) {
389                     updateState(VolumioBindingConstants.CHANNEL_ALBUM, state.getAlbum());
390                 }
391                 if (isLinked(VolumioBindingConstants.CHANNEL_VOLUME) && state.isVolumeDirty()) {
392                     updateState(VolumioBindingConstants.CHANNEL_VOLUME, state.getVolume());
393                 }
394                 if (isLinked(VolumioBindingConstants.CHANNEL_PLAYER) && state.isStateDirty()) {
395                     updateState(VolumioBindingConstants.CHANNEL_PLAYER, state.getState());
396                 }
397                 if (isLinked(VolumioBindingConstants.CHANNEL_TRACK_TYPE) && state.isTrackTypeDirty()) {
398                     updateState(VolumioBindingConstants.CHANNEL_TRACK_TYPE, state.getTrackType());
399                 }
400
401                 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_RANDOM) && state.isRandomDirty()) {
402                     updateState(VolumioBindingConstants.CHANNEL_PLAY_RANDOM, state.getRandom());
403                 }
404                 if (isLinked(VolumioBindingConstants.CHANNEL_PLAY_REPEAT) && state.isRepeatDirty()) {
405                     updateState(VolumioBindingConstants.CHANNEL_PLAY_REPEAT, state.getRepeat());
406                 }
407                 /**
408                  * if (isLinked(CHANNEL_COVER_ART) && state.isCoverArtDirty()) {
409                  * updateState(CHANNEL_COVER_ART, state.getCoverArt());
410                  * }
411                  */
412             } catch (JSONException e) {
413                 logger.error("Could not refresh channel: {}", e.getMessage());
414             }
415         };
416     }
417 }