]> git.basschouten.com Git - openhab-addons.git/blob
03b1a90869a495501316d1a55868765af9a10757
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.heos.internal.handler;
14
15 import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
16 import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
17 import static org.openhab.core.thing.ThingStatus.OFFLINE;
18 import static org.openhab.core.thing.ThingStatus.ONLINE;
19
20 import java.io.IOException;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.Optional;
28 import java.util.Set;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.CopyOnWriteArrayList;
31 import java.util.concurrent.Future;
32 import java.util.concurrent.TimeUnit;
33 import java.util.stream.Collectors;
34 import java.util.stream.Stream;
35
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.binding.heos.internal.HeosChannelHandlerFactory;
39 import org.openhab.binding.heos.internal.HeosChannelManager;
40 import org.openhab.binding.heos.internal.action.HeosActions;
41 import org.openhab.binding.heos.internal.api.HeosFacade;
42 import org.openhab.binding.heos.internal.api.HeosSystem;
43 import org.openhab.binding.heos.internal.configuration.BridgeConfiguration;
44 import org.openhab.binding.heos.internal.discovery.HeosPlayerDiscoveryListener;
45 import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
46 import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
47 import org.openhab.binding.heos.internal.json.dto.HeosError;
48 import org.openhab.binding.heos.internal.json.dto.HeosEvent;
49 import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
50 import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
51 import org.openhab.binding.heos.internal.json.payload.Group;
52 import org.openhab.binding.heos.internal.json.payload.Media;
53 import org.openhab.binding.heos.internal.json.payload.Player;
54 import org.openhab.binding.heos.internal.resources.HeosEventListener;
55 import org.openhab.binding.heos.internal.resources.Telnet;
56 import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
57 import org.openhab.core.library.types.OnOffType;
58 import org.openhab.core.thing.Bridge;
59 import org.openhab.core.thing.Channel;
60 import org.openhab.core.thing.ChannelUID;
61 import org.openhab.core.thing.Thing;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.binding.BaseBridgeHandler;
65 import org.openhab.core.thing.binding.ThingHandler;
66 import org.openhab.core.thing.binding.ThingHandlerService;
67 import org.openhab.core.thing.binding.builder.ChannelBuilder;
68 import org.openhab.core.thing.binding.builder.ThingBuilder;
69 import org.openhab.core.thing.type.ChannelTypeUID;
70 import org.openhab.core.types.Command;
71 import org.openhab.core.types.RefreshType;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * The {@link HeosBridgeHandler} is responsible for handling commands, which are
77  * sent to one of the channels.
78  *
79  * @author Johannes Einig - Initial contribution
80  */
81 @NonNullByDefault
82 public class HeosBridgeHandler extends BaseBridgeHandler implements HeosEventListener {
83     private final Logger logger = LoggerFactory.getLogger(HeosBridgeHandler.class);
84
85     private static final int HEOS_PORT = 1255;
86
87     private final List<HeosPlayerDiscoveryListener> playerDiscoveryList = new CopyOnWriteArrayList<>();
88     private final HeosChannelManager channelManager = new HeosChannelManager(this);
89     private final HeosChannelHandlerFactory channelHandlerFactory;
90
91     private final Map<String, HeosGroupHandler> groupHandlerMap = new ConcurrentHashMap<>();
92     private final Map<String, String> hashToGidMap = new ConcurrentHashMap<>();
93
94     private List<String[]> selectedPlayerList = new CopyOnWriteArrayList<>();
95
96     private @Nullable Future<?> startupFuture;
97     private final List<Future<?>> childHandlerInitializedFutures = new CopyOnWriteArrayList<>();
98
99     private final HeosSystem heosSystem;
100     private @Nullable HeosFacade apiConnection;
101
102     private boolean loggedIn = false;
103     private boolean bridgeHandlerDisposalOngoing = false;
104
105     private @NonNullByDefault({}) BridgeConfiguration configuration;
106
107     private int failureCount;
108
109     public HeosBridgeHandler(Bridge bridge, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
110         super(bridge);
111         heosSystem = new HeosSystem(scheduler);
112         channelHandlerFactory = new HeosChannelHandlerFactory(this, heosDynamicStateDescriptionProvider);
113     }
114
115     @Override
116     public void handleCommand(ChannelUID channelUID, Command command) {
117         if (command instanceof RefreshType) {
118             return;
119         }
120         @Nullable
121         Channel channel = this.getThing().getChannel(channelUID.getId());
122         if (channel == null) {
123             logger.debug("No valid channel found");
124             return;
125         }
126
127         @Nullable
128         ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
129         @Nullable
130         HeosChannelHandler channelHandler = channelHandlerFactory.getChannelHandler(channelUID, this, channelTypeUID);
131         if (channelHandler != null) {
132             try {
133                 channelHandler.handleBridgeCommand(command, thing.getUID());
134                 failureCount = 0;
135                 updateStatus(ONLINE);
136             } catch (IOException | ReadException e) {
137                 logger.debug("Failed to handle bridge command", e);
138                 failureCount++;
139
140                 if (failureCount > FAILURE_COUNT_LIMIT) {
141                     updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
142                             "Failed to handle command: " + e.getMessage());
143                 }
144             }
145         }
146     }
147
148     @Override
149     public synchronized void initialize() {
150         configuration = thing.getConfiguration().as(BridgeConfiguration.class);
151         cancel(startupFuture);
152         startupFuture = scheduler.submit(this::delayedInitialize);
153     }
154
155     private void delayedInitialize() {
156         @Nullable
157         HeosFacade connection = null;
158         try {
159             logger.debug("Running scheduledStartUp job");
160
161             connection = connectBridge();
162             updateStatus(ThingStatus.ONLINE);
163             updateState(CH_ID_REBOOT, OnOffType.OFF);
164
165             logger.debug("HEOS System heart beat started. Pulse time is {}s", configuration.heartbeat);
166             // gets all available player and groups to ensure that the system knows
167             // about the conjunction between the groupMemberHash and the GID
168             triggerPlayerDiscovery();
169             @Nullable
170             String username = configuration.username;
171             @Nullable
172             String password = configuration.password;
173             if (username != null && !"".equals(username) && password != null && !"".equals(password)) {
174                 login(connection, username, password);
175             } else {
176                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
177                         "Can't log in. Username or password not set.");
178             }
179
180             fetchPlayersAndGroups();
181         } catch (Telnet.ReadException | IOException | RuntimeException e) {
182             logger.debug("Error occurred while connecting", e);
183             if (connection != null) {
184                 connection.closeConnection();
185             }
186             updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Errors occurred: " + e.getMessage());
187             cancel(startupFuture, false);
188             startupFuture = scheduler.schedule(this::delayedInitialize, 30, TimeUnit.SECONDS);
189         }
190     }
191
192     private void fetchPlayersAndGroups() {
193         try {
194             @Nullable
195             Player[] onlinePlayers = getApiConnection().getPlayers().payload;
196             @Nullable
197             Group[] onlineGroups = getApiConnection().getGroups().payload;
198
199             if (onlinePlayers != null && onlineGroups != null) {
200                 updatePlayerStatus(onlinePlayers, onlineGroups);
201             }
202         } catch (ReadException | IOException e) {
203             logger.debug("Failed updating online state of groups/players", e);
204         }
205     }
206
207     private void updatePlayerStatus(@Nullable Player[] onlinePlayers, @Nullable Group[] onlineGroups) {
208         if (onlinePlayers == null || onlineGroups == null) {
209             return;
210         }
211         Set<String> players = Stream.of(onlinePlayers).map(p -> Objects.toString(p.playerId))
212                 .collect(Collectors.toSet());
213         Set<String> groups = Stream.of(onlineGroups).map(p -> p.id).collect(Collectors.toSet());
214
215         for (Thing thing : getThing().getThings()) {
216             try {
217                 @Nullable
218                 ThingHandler handler = thing.getHandler();
219                 if (handler instanceof HeosThingBaseHandler) {
220                     Set<String> target = handler instanceof HeosPlayerHandler ? players : groups;
221                     HeosThingBaseHandler heosHandler = (HeosThingBaseHandler) handler;
222                     String id = heosHandler.getId();
223
224                     if (target.contains(id)) {
225                         heosHandler.setStatusOnline();
226                     } else {
227                         heosHandler.setStatusOffline();
228                     }
229                 }
230             } catch (HeosNotFoundException e) {
231                 logger.debug("SKipping handler which reported not found", e);
232             }
233         }
234     }
235
236     private HeosFacade connectBridge() throws IOException, Telnet.ReadException {
237         loggedIn = false;
238
239         logger.debug("Initialize Bridge '{}' with IP '{}'", thing.getProperties().get(PROP_NAME),
240                 configuration.ipAddress);
241         bridgeHandlerDisposalOngoing = false;
242         HeosFacade connection = heosSystem.establishConnection(configuration.ipAddress, HEOS_PORT,
243                 configuration.heartbeat);
244         connection.registerForChangeEvents(this);
245
246         apiConnection = connection;
247
248         return connection;
249     }
250
251     private void login(HeosFacade connection, String username, String password) throws IOException, ReadException {
252         logger.debug("Logging in to HEOS account.");
253         HeosResponseObject<Void> response = connection.logIn(username, password);
254
255         if (response.result) {
256             logger.debug("successfully logged-in, event is fired to handle post-login behaviour");
257             return;
258         }
259
260         @Nullable
261         HeosError error = response.getError();
262         logger.debug("Failed to login: {}", error);
263         updateStatus(ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
264                 error != null ? error.code.toString() : "Failed to login, no error was returned.");
265     }
266
267     @Override
268     public void dispose() {
269         bridgeHandlerDisposalOngoing = true; // Flag to prevent the handler from being updated during disposal
270
271         cancel(startupFuture);
272         for (Future<?> future : childHandlerInitializedFutures) {
273             cancel(future);
274         }
275
276         @Nullable
277         HeosFacade localApiConnection = apiConnection;
278         if (localApiConnection == null) {
279             logger.debug("Not disposing bridge because of missing apiConnection");
280             return;
281         }
282
283         localApiConnection.unregisterForChangeEvents(this);
284         logger.debug("HEOS bridge removed from change notifications");
285
286         logger.debug("Dispose bridge '{}'", thing.getProperties().get(PROP_NAME));
287         localApiConnection.closeConnection();
288     }
289
290     /**
291      * Manages the removal of the player or group channels from the bridge.
292      */
293     @Override
294     public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
295         logger.debug("Disposing child handler for: {}.", childThing.getUID().getId());
296         if (bridgeHandlerDisposalOngoing) { // Checks if bridgeHandler is going to disposed (by stopping the binding or
297             // openHAB for example) and prevents it from being updated which stops the
298             // disposal process.
299         } else if (childHandler instanceof HeosPlayerHandler) {
300             String channelIdentifier = "P" + childThing.getUID().getId();
301             updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
302         } else if (childHandler instanceof HeosGroupHandler) {
303             String channelIdentifier = "G" + childThing.getUID().getId();
304             updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
305             // removes the handler from the groupMemberMap that handler is no longer called
306             // if group is getting online
307             removeGroupHandlerInformation((HeosGroupHandler) childHandler);
308         }
309     }
310
311     @Override
312     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
313         logger.debug("Initialized child handler for: {}.", childThing.getUID().getId());
314         childHandlerInitializedFutures.add(scheduler.submit(() -> addPlayerChannel(childThing, null)));
315     }
316
317     void resetPlayerList(ChannelUID channelUID) {
318         selectedPlayerList.forEach(element -> updateState(element[1], OnOffType.OFF));
319         selectedPlayerList.clear();
320         updateState(channelUID, OnOffType.OFF);
321     }
322
323     /**
324      * Sets the HEOS Thing offline
325      */
326     @SuppressWarnings("null")
327     public void setGroupOffline(String groupMemberHash) {
328         HeosGroupHandler groupHandler = groupHandlerMap.get(groupMemberHash);
329         if (groupHandler != null) {
330             groupHandler.setStatusOffline();
331         }
332         hashToGidMap.remove(groupMemberHash);
333     }
334
335     /**
336      * Sets the HEOS Thing online. Also updates the link between
337      * the groupMemberHash value with the actual gid of this group
338      */
339     public void setGroupOnline(String groupMemberHash, String groupId) {
340         hashToGidMap.put(groupMemberHash, groupId);
341         Optional.ofNullable(groupHandlerMap.get(groupMemberHash)).ifPresent(handler -> {
342             handler.setStatusOnline();
343             addPlayerChannel(handler.getThing(), groupId);
344         });
345     }
346
347     /**
348      * Create a channel for the childThing. Depending if it is a HEOS Group
349      * or a player an identification prefix is added
350      *
351      * @param childThing the thing the channel is created for
352      * @param groupId
353      */
354     private void addPlayerChannel(Thing childThing, @Nullable String groupId) {
355         try {
356             String channelIdentifier = "";
357             String pid = "";
358             @Nullable
359             ThingHandler handler = childThing.getHandler();
360             if (handler instanceof HeosPlayerHandler) {
361                 channelIdentifier = "P" + childThing.getUID().getId();
362                 pid = ((HeosPlayerHandler) handler).getId();
363             } else if (handler instanceof HeosGroupHandler) {
364                 channelIdentifier = "G" + childThing.getUID().getId();
365                 if (groupId == null) {
366                     pid = ((HeosGroupHandler) handler).getId();
367                 } else {
368                     pid = groupId;
369                 }
370             }
371             Map<String, String> properties = new HashMap<>();
372             @Nullable
373             String playerName = childThing.getLabel();
374             playerName = playerName == null ? pid : playerName;
375             ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelIdentifier);
376             properties.put(PROP_NAME, playerName);
377             properties.put(PID, pid);
378
379             Channel channel = ChannelBuilder.create(channelUID, "Switch").withLabel(playerName).withType(CH_TYPE_PLAYER)
380                     .withProperties(properties).build();
381             updateThingChannels(channelManager.addSingleChannel(channel));
382         } catch (HeosNotFoundException e) {
383             logger.debug("Group is not yet initialized fully");
384         }
385     }
386
387     public void addGroupHandlerInformation(HeosGroupHandler handler) {
388         groupHandlerMap.put(handler.getGroupMemberHash(), handler);
389     }
390
391     private void removeGroupHandlerInformation(HeosGroupHandler handler) {
392         groupHandlerMap.remove(handler.getGroupMemberHash());
393     }
394
395     public @Nullable String getActualGID(String groupHash) {
396         return hashToGidMap.get(groupHash);
397     }
398
399     @Override
400     public void playerStateChangeEvent(HeosEventObject eventObject) {
401         // do nothing
402     }
403
404     @Override
405     public void playerStateChangeEvent(HeosResponseObject<?> responseObject) {
406         // do nothing
407     }
408
409     @Override
410     public void playerMediaChangeEvent(String pid, Media media) {
411         // do nothing
412     }
413
414     @Override
415     public void bridgeChangeEvent(String event, boolean success, Object command) {
416         if (EVENT_TYPE_EVENT.equals(event)) {
417             if (HeosEvent.PLAYERS_CHANGED.equals(command) || HeosEvent.GROUPS_CHANGED.equals(command)) {
418                 fetchPlayersAndGroups();
419                 triggerPlayerDiscovery();
420             } else if (EVENT_STREAM_TIMEOUT.equals(command)) {
421                 logger.debug("HEOS Bridge events timed-out might be nothing, trying to reconnect");
422             } else if (CONNECTION_LOST.equals(command)) {
423                 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
424                 logger.debug("HEOS Bridge OFFLINE");
425             } else if (CONNECTION_RESTORED.equals(command)) {
426                 initialize();
427             }
428         }
429         if (EVENT_TYPE_SYSTEM.equals(event) && HeosEvent.USER_CHANGED == command) {
430             if (success && !loggedIn) {
431                 loggedIn = true;
432             }
433         }
434     }
435
436     private synchronized void updateThingChannels(List<Channel> channelList) {
437         ThingBuilder thingBuilder = editThing();
438         thingBuilder.withChannels(channelList);
439         updateThing(thingBuilder.build());
440     }
441
442     public Player[] getPlayers() throws IOException, ReadException {
443         HeosResponseObject<Player[]> response = getApiConnection().getPlayers();
444         @Nullable
445         Player[] players = response.payload;
446         if (players == null) {
447             throw new IOException("Received no valid payload");
448         }
449         return players;
450     }
451
452     public Group[] getGroups() throws IOException, ReadException {
453         HeosResponseObject<Group[]> response = getApiConnection().getGroups();
454         @Nullable
455         Group[] groups = response.payload;
456         if (groups == null) {
457             throw new IOException("Received no valid payload");
458         }
459         return groups;
460     }
461
462     /**
463      * The list with the currently selected player
464      *
465      * @return a HashMap which the currently selected player
466      */
467     public Map<String, String> getSelectedPlayer() {
468         return selectedPlayerList.stream().collect(Collectors.toMap(a -> a[0], a -> a[1], (a, b) -> a));
469     }
470
471     public List<String[]> getSelectedPlayerList() {
472         return selectedPlayerList;
473     }
474
475     public void setSelectedPlayerList(List<String[]> selectedPlayerList) {
476         this.selectedPlayerList = selectedPlayerList;
477     }
478
479     public HeosChannelHandlerFactory getChannelHandlerFactory() {
480         return channelHandlerFactory;
481     }
482
483     /**
484      * Register an {@link HeosPlayerDiscoveryListener} to get informed
485      * if the amount of groups or players have changed
486      *
487      * @param listener the implementing class
488      */
489     public void registerPlayerDiscoverListener(HeosPlayerDiscoveryListener listener) {
490         playerDiscoveryList.add(listener);
491     }
492
493     private void triggerPlayerDiscovery() {
494         playerDiscoveryList.forEach(HeosPlayerDiscoveryListener::playerChanged);
495     }
496
497     public boolean isLoggedIn() {
498         return loggedIn;
499     }
500
501     public boolean isBridgeConnected() {
502         @Nullable
503         HeosFacade connection = apiConnection;
504         return connection != null && connection.isConnected();
505     }
506
507     public HeosFacade getApiConnection() throws HeosNotConnectedException {
508         @Nullable
509         HeosFacade localApiConnection = apiConnection;
510         if (localApiConnection != null) {
511             return localApiConnection;
512         } else {
513             throw new HeosNotConnectedException();
514         }
515     }
516
517     @Override
518     public Collection<Class<? extends ThingHandlerService>> getServices() {
519         return Collections.singletonList(HeosActions.class);
520     }
521 }