]> git.basschouten.com Git - openhab-addons.git/blob
7dc62237c4bf763362fb79bb72829013497a7e16
[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             updatePlayerStatus(onlinePlayers, onlineGroups);
200         } catch (ReadException | IOException e) {
201             logger.debug("Failed updating online state of groups/players", e);
202         }
203     }
204
205     private void updatePlayerStatus(@Nullable Player[] onlinePlayers, @Nullable Group[] onlineGroups) {
206         if (onlinePlayers == null || onlineGroups == null) {
207             return;
208         }
209         Set<String> players = Stream.of(onlinePlayers).map(p -> Objects.toString(p.playerId))
210                 .collect(Collectors.toSet());
211         Set<String> groups = Stream.of(onlineGroups).map(p -> p.id).collect(Collectors.toSet());
212
213         for (Thing thing : getThing().getThings()) {
214             try {
215                 @Nullable
216                 ThingHandler handler = thing.getHandler();
217                 if (handler instanceof HeosThingBaseHandler) {
218                     Set<String> target = handler instanceof HeosPlayerHandler ? players : groups;
219                     HeosThingBaseHandler heosHandler = (HeosThingBaseHandler) handler;
220                     String id = heosHandler.getId();
221
222                     if (target.contains(id)) {
223                         heosHandler.setStatusOnline();
224                     } else {
225                         heosHandler.setStatusOffline();
226                     }
227                 }
228             } catch (HeosNotFoundException e) {
229                 logger.debug("SKipping handler which reported not found", e);
230             }
231         }
232     }
233
234     private HeosFacade connectBridge() throws IOException, Telnet.ReadException {
235         loggedIn = false;
236
237         logger.debug("Initialize Bridge '{}' with IP '{}'", thing.getProperties().get(PROP_NAME),
238                 configuration.ipAddress);
239         bridgeHandlerDisposalOngoing = false;
240         HeosFacade connection = heosSystem.establishConnection(configuration.ipAddress, HEOS_PORT,
241                 configuration.heartbeat);
242         connection.registerForChangeEvents(this);
243
244         apiConnection = connection;
245
246         return connection;
247     }
248
249     private void login(HeosFacade connection, String username, String password) throws IOException, ReadException {
250         logger.debug("Logging in to HEOS account.");
251         HeosResponseObject<Void> response = connection.logIn(username, password);
252
253         if (response.result) {
254             logger.debug("successfully logged-in, event is fired to handle post-login behaviour");
255             return;
256         }
257
258         @Nullable
259         HeosError error = response.getError();
260         logger.debug("Failed to login: {}", error);
261         updateStatus(ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
262                 error != null ? error.code.toString() : "Failed to login, no error was returned.");
263     }
264
265     @Override
266     public void dispose() {
267         bridgeHandlerDisposalOngoing = true; // Flag to prevent the handler from being updated during disposal
268
269         cancel(startupFuture);
270         for (Future<?> future : childHandlerInitializedFutures) {
271             cancel(future);
272         }
273
274         @Nullable
275         HeosFacade localApiConnection = apiConnection;
276         if (localApiConnection == null) {
277             logger.debug("Not disposing bridge because of missing apiConnection");
278             return;
279         }
280
281         localApiConnection.unregisterForChangeEvents(this);
282         logger.debug("HEOS bridge removed from change notifications");
283
284         logger.debug("Dispose bridge '{}'", thing.getProperties().get(PROP_NAME));
285         localApiConnection.closeConnection();
286     }
287
288     /**
289      * Manages the removal of the player or group channels from the bridge.
290      */
291     @Override
292     public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
293         logger.debug("Disposing child handler for: {}.", childThing.getUID().getId());
294         if (bridgeHandlerDisposalOngoing) { // Checks if bridgeHandler is going to disposed (by stopping the binding or
295             // openHAB for example) and prevents it from being updated which stops the
296             // disposal process.
297         } else if (childHandler instanceof HeosPlayerHandler) {
298             String channelIdentifier = "P" + childThing.getUID().getId();
299             updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
300         } else if (childHandler instanceof HeosGroupHandler) {
301             String channelIdentifier = "G" + childThing.getUID().getId();
302             updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
303             // removes the handler from the groupMemberMap that handler is no longer called
304             // if group is getting online
305             removeGroupHandlerInformation((HeosGroupHandler) childHandler);
306         }
307     }
308
309     @Override
310     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
311         logger.debug("Initialized child handler for: {}.", childThing.getUID().getId());
312         childHandlerInitializedFutures.add(scheduler.submit(() -> addPlayerChannel(childThing, null)));
313     }
314
315     void resetPlayerList(ChannelUID channelUID) {
316         selectedPlayerList.forEach(element -> updateState(element[1], OnOffType.OFF));
317         selectedPlayerList.clear();
318         updateState(channelUID, OnOffType.OFF);
319     }
320
321     /**
322      * Sets the HEOS Thing offline
323      */
324     @SuppressWarnings("null")
325     public void setGroupOffline(String groupMemberHash) {
326         HeosGroupHandler groupHandler = groupHandlerMap.get(groupMemberHash);
327         if (groupHandler != null) {
328             groupHandler.setStatusOffline();
329         }
330         hashToGidMap.remove(groupMemberHash);
331     }
332
333     /**
334      * Sets the HEOS Thing online. Also updates the link between
335      * the groupMemberHash value with the actual gid of this group
336      */
337     public void setGroupOnline(String groupMemberHash, String groupId) {
338         hashToGidMap.put(groupMemberHash, groupId);
339         Optional.ofNullable(groupHandlerMap.get(groupMemberHash)).ifPresent(handler -> {
340             handler.setStatusOnline();
341             addPlayerChannel(handler.getThing(), groupId);
342         });
343     }
344
345     /**
346      * Create a channel for the childThing. Depending if it is a HEOS Group
347      * or a player an identification prefix is added
348      *
349      * @param childThing the thing the channel is created for
350      * @param groupId
351      */
352     private void addPlayerChannel(Thing childThing, @Nullable String groupId) {
353         try {
354             String channelIdentifier = "";
355             String pid = "";
356             @Nullable
357             ThingHandler handler = childThing.getHandler();
358             if (handler instanceof HeosPlayerHandler) {
359                 channelIdentifier = "P" + childThing.getUID().getId();
360                 pid = ((HeosPlayerHandler) handler).getId();
361             } else if (handler instanceof HeosGroupHandler) {
362                 channelIdentifier = "G" + childThing.getUID().getId();
363                 if (groupId == null) {
364                     pid = ((HeosGroupHandler) handler).getId();
365                 } else {
366                     pid = groupId;
367                 }
368             }
369             Map<String, String> properties = new HashMap<>();
370             @Nullable
371             String playerName = childThing.getLabel();
372             playerName = playerName == null ? pid : playerName;
373             ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelIdentifier);
374             properties.put(PROP_NAME, playerName);
375             properties.put(PID, pid);
376
377             Channel channel = ChannelBuilder.create(channelUID, "Switch").withLabel(playerName).withType(CH_TYPE_PLAYER)
378                     .withProperties(properties).build();
379             updateThingChannels(channelManager.addSingleChannel(channel));
380         } catch (HeosNotFoundException e) {
381             logger.debug("Group is not yet initialized fully");
382         }
383     }
384
385     public void addGroupHandlerInformation(HeosGroupHandler handler) {
386         groupHandlerMap.put(handler.getGroupMemberHash(), handler);
387     }
388
389     private void removeGroupHandlerInformation(HeosGroupHandler handler) {
390         groupHandlerMap.remove(handler.getGroupMemberHash());
391     }
392
393     public @Nullable String getActualGID(String groupHash) {
394         return hashToGidMap.get(groupHash);
395     }
396
397     @Override
398     public void playerStateChangeEvent(HeosEventObject eventObject) {
399         // do nothing
400     }
401
402     @Override
403     public void playerStateChangeEvent(HeosResponseObject<?> responseObject) {
404         // do nothing
405     }
406
407     @Override
408     public void playerMediaChangeEvent(String pid, Media media) {
409         // do nothing
410     }
411
412     @Override
413     public void bridgeChangeEvent(String event, boolean success, Object command) {
414         if (EVENT_TYPE_EVENT.equals(event)) {
415             if (HeosEvent.PLAYERS_CHANGED.equals(command) || HeosEvent.GROUPS_CHANGED.equals(command)) {
416                 fetchPlayersAndGroups();
417                 triggerPlayerDiscovery();
418             } else if (EVENT_STREAM_TIMEOUT.equals(command)) {
419                 logger.debug("HEOS Bridge events timed-out might be nothing, trying to reconnect");
420             } else if (CONNECTION_LOST.equals(command)) {
421                 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
422                 logger.debug("HEOS Bridge OFFLINE");
423             } else if (CONNECTION_RESTORED.equals(command)) {
424                 initialize();
425             }
426         }
427         if (EVENT_TYPE_SYSTEM.equals(event) && HeosEvent.USER_CHANGED == command) {
428             if (success && !loggedIn) {
429                 loggedIn = true;
430             }
431         }
432     }
433
434     private synchronized void updateThingChannels(List<Channel> channelList) {
435         ThingBuilder thingBuilder = editThing();
436         thingBuilder.withChannels(channelList);
437         updateThing(thingBuilder.build());
438     }
439
440     public Player[] getPlayers() throws IOException, ReadException {
441         HeosResponseObject<Player[]> response = getApiConnection().getPlayers();
442         @Nullable
443         Player[] players = response.payload;
444         if (players == null) {
445             throw new IOException("Received no valid payload");
446         }
447         return players;
448     }
449
450     public Group[] getGroups() throws IOException, ReadException {
451         HeosResponseObject<Group[]> response = getApiConnection().getGroups();
452         @Nullable
453         Group[] groups = response.payload;
454         if (groups == null) {
455             throw new IOException("Received no valid payload");
456         }
457         return groups;
458     }
459
460     /**
461      * The list with the currently selected player
462      *
463      * @return a HashMap which the currently selected player
464      */
465     public Map<String, String> getSelectedPlayer() {
466         return selectedPlayerList.stream().collect(Collectors.toMap(a -> a[0], a -> a[1], (a, b) -> a));
467     }
468
469     public List<String[]> getSelectedPlayerList() {
470         return selectedPlayerList;
471     }
472
473     public void setSelectedPlayerList(List<String[]> selectedPlayerList) {
474         this.selectedPlayerList = selectedPlayerList;
475     }
476
477     public HeosChannelHandlerFactory getChannelHandlerFactory() {
478         return channelHandlerFactory;
479     }
480
481     /**
482      * Register an {@link HeosPlayerDiscoveryListener} to get informed
483      * if the amount of groups or players have changed
484      *
485      * @param listener the implementing class
486      */
487     public void registerPlayerDiscoverListener(HeosPlayerDiscoveryListener listener) {
488         playerDiscoveryList.add(listener);
489     }
490
491     private void triggerPlayerDiscovery() {
492         playerDiscoveryList.forEach(HeosPlayerDiscoveryListener::playerChanged);
493     }
494
495     public boolean isLoggedIn() {
496         return loggedIn;
497     }
498
499     public boolean isBridgeConnected() {
500         @Nullable
501         HeosFacade connection = apiConnection;
502         return connection != null && connection.isConnected();
503     }
504
505     public HeosFacade getApiConnection() throws HeosNotConnectedException {
506         @Nullable
507         HeosFacade localApiConnection = apiConnection;
508         if (localApiConnection != null) {
509             return localApiConnection;
510         } else {
511             throw new HeosNotConnectedException();
512         }
513     }
514
515     @Override
516     public Collection<Class<? extends ThingHandlerService>> getServices() {
517         return Collections.singletonList(HeosActions.class);
518     }
519 }