2 * Copyright (c) 2010-2020 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.heos.internal.handler;
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;
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;
26 import java.util.Objects;
27 import java.util.Optional;
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;
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;
76 * The {@link HeosBridgeHandler} is responsible for handling commands, which are
77 * sent to one of the channels.
79 * @author Johannes Einig - Initial contribution
82 public class HeosBridgeHandler extends BaseBridgeHandler implements HeosEventListener {
83 private final Logger logger = LoggerFactory.getLogger(HeosBridgeHandler.class);
85 private static final int HEOS_PORT = 1255;
87 private final List<HeosPlayerDiscoveryListener> playerDiscoveryList = new CopyOnWriteArrayList<>();
88 private final HeosChannelManager channelManager = new HeosChannelManager(this);
89 private final HeosChannelHandlerFactory channelHandlerFactory;
91 private final Map<String, HeosGroupHandler> groupHandlerMap = new ConcurrentHashMap<>();
92 private final Map<String, String> hashToGidMap = new ConcurrentHashMap<>();
94 private List<String[]> selectedPlayerList = new CopyOnWriteArrayList<>();
96 private @Nullable Future<?> startupFuture;
97 private final List<Future<?>> childHandlerInitializedFutures = new CopyOnWriteArrayList<>();
99 private final HeosSystem heosSystem;
100 private @Nullable HeosFacade apiConnection;
102 private boolean loggedIn = false;
103 private boolean bridgeHandlerDisposalOngoing = false;
105 private @NonNullByDefault({}) BridgeConfiguration configuration;
107 private int failureCount;
109 public HeosBridgeHandler(Bridge bridge, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
111 heosSystem = new HeosSystem(scheduler);
112 channelHandlerFactory = new HeosChannelHandlerFactory(this, heosDynamicStateDescriptionProvider);
116 public void handleCommand(ChannelUID channelUID, Command command) {
117 if (command instanceof RefreshType) {
121 Channel channel = this.getThing().getChannel(channelUID.getId());
122 if (channel == null) {
123 logger.debug("No valid channel found");
128 ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
130 HeosChannelHandler channelHandler = channelHandlerFactory.getChannelHandler(channelUID, this, channelTypeUID);
131 if (channelHandler != null) {
133 channelHandler.handleBridgeCommand(command, thing.getUID());
135 updateStatus(ONLINE);
136 } catch (IOException | ReadException e) {
137 logger.debug("Failed to handle bridge command", e);
140 if (failureCount > FAILURE_COUNT_LIMIT) {
141 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
142 "Failed to handle command: " + e.getMessage());
149 public synchronized void initialize() {
150 configuration = thing.getConfiguration().as(BridgeConfiguration.class);
151 cancel(startupFuture);
152 startupFuture = scheduler.submit(this::delayedInitialize);
155 private void delayedInitialize() {
157 HeosFacade connection = null;
159 logger.debug("Running scheduledStartUp job");
161 connection = connectBridge();
162 updateStatus(ThingStatus.ONLINE);
163 updateState(CH_ID_REBOOT, OnOffType.OFF);
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();
170 String username = configuration.username;
172 String password = configuration.password;
173 if (username != null && !"".equals(username) && password != null && !"".equals(password)) {
174 login(connection, username, password);
176 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
177 "Can't log in. Username or password not set.");
180 fetchPlayersAndGroups();
181 } catch (Telnet.ReadException | IOException | RuntimeException e) {
182 logger.debug("Error occurred while connecting", e);
183 if (connection != null) {
184 connection.closeConnection();
186 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Errors occurred: " + e.getMessage());
187 cancel(startupFuture, false);
188 startupFuture = scheduler.schedule(this::delayedInitialize, 30, TimeUnit.SECONDS);
192 private void fetchPlayersAndGroups() {
195 Player[] onlinePlayers = getApiConnection().getPlayers().payload;
197 Group[] onlineGroups = getApiConnection().getGroups().payload;
199 updatePlayerStatus(onlinePlayers, onlineGroups);
200 } catch (ReadException | IOException e) {
201 logger.debug("Failed updating online state of groups/players", e);
205 private void updatePlayerStatus(@Nullable Player[] onlinePlayers, @Nullable Group[] onlineGroups) {
206 if (onlinePlayers == null || onlineGroups == null) {
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());
213 for (Thing thing : getThing().getThings()) {
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();
222 if (target.contains(id)) {
223 heosHandler.setStatusOnline();
225 heosHandler.setStatusOffline();
228 } catch (HeosNotFoundException e) {
229 logger.debug("SKipping handler which reported not found", e);
234 private HeosFacade connectBridge() throws IOException, Telnet.ReadException {
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);
244 apiConnection = connection;
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);
253 if (response.result) {
254 logger.debug("successfully logged-in, event is fired to handle post-login behaviour");
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.");
266 public void dispose() {
267 bridgeHandlerDisposalOngoing = true; // Flag to prevent the handler from being updated during disposal
269 cancel(startupFuture);
270 for (Future<?> future : childHandlerInitializedFutures) {
275 HeosFacade localApiConnection = apiConnection;
276 if (localApiConnection == null) {
277 logger.debug("Not disposing bridge because of missing apiConnection");
281 localApiConnection.unregisterForChangeEvents(this);
282 logger.debug("HEOS bridge removed from change notifications");
284 logger.debug("Dispose bridge '{}'", thing.getProperties().get(PROP_NAME));
285 localApiConnection.closeConnection();
289 * Manages the removal of the player or group channels from the bridge.
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
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);
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)));
315 void resetPlayerList(ChannelUID channelUID) {
316 selectedPlayerList.forEach(element -> updateState(element[1], OnOffType.OFF));
317 selectedPlayerList.clear();
318 updateState(channelUID, OnOffType.OFF);
322 * Sets the HEOS Thing offline
324 @SuppressWarnings("null")
325 public void setGroupOffline(String groupMemberHash) {
326 HeosGroupHandler groupHandler = groupHandlerMap.get(groupMemberHash);
327 if (groupHandler != null) {
328 groupHandler.setStatusOffline();
330 hashToGidMap.remove(groupMemberHash);
334 * Sets the HEOS Thing online. Also updates the link between
335 * the groupMemberHash value with the actual gid of this group
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);
346 * Create a channel for the childThing. Depending if it is a HEOS Group
347 * or a player an identification prefix is added
349 * @param childThing the thing the channel is created for
352 private void addPlayerChannel(Thing childThing, @Nullable String groupId) {
354 String channelIdentifier = "";
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();
369 Map<String, String> properties = new HashMap<>();
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);
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");
385 public void addGroupHandlerInformation(HeosGroupHandler handler) {
386 groupHandlerMap.put(handler.getGroupMemberHash(), handler);
389 private void removeGroupHandlerInformation(HeosGroupHandler handler) {
390 groupHandlerMap.remove(handler.getGroupMemberHash());
393 public @Nullable String getActualGID(String groupHash) {
394 return hashToGidMap.get(groupHash);
398 public void playerStateChangeEvent(HeosEventObject eventObject) {
403 public void playerStateChangeEvent(HeosResponseObject<?> responseObject) {
408 public void playerMediaChangeEvent(String pid, Media media) {
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)) {
427 if (EVENT_TYPE_SYSTEM.equals(event) && HeosEvent.USER_CHANGED == command) {
428 if (success && !loggedIn) {
434 private synchronized void updateThingChannels(List<Channel> channelList) {
435 ThingBuilder thingBuilder = editThing();
436 thingBuilder.withChannels(channelList);
437 updateThing(thingBuilder.build());
440 public Player[] getPlayers() throws IOException, ReadException {
441 HeosResponseObject<Player[]> response = getApiConnection().getPlayers();
443 Player[] players = response.payload;
444 if (players == null) {
445 throw new IOException("Received no valid payload");
450 public Group[] getGroups() throws IOException, ReadException {
451 HeosResponseObject<Group[]> response = getApiConnection().getGroups();
453 Group[] groups = response.payload;
454 if (groups == null) {
455 throw new IOException("Received no valid payload");
461 * The list with the currently selected player
463 * @return a HashMap which the currently selected player
465 public Map<String, String> getSelectedPlayer() {
466 return selectedPlayerList.stream().collect(Collectors.toMap(a -> a[0], a -> a[1], (a, b) -> a));
469 public List<String[]> getSelectedPlayerList() {
470 return selectedPlayerList;
473 public void setSelectedPlayerList(List<String[]> selectedPlayerList) {
474 this.selectedPlayerList = selectedPlayerList;
477 public HeosChannelHandlerFactory getChannelHandlerFactory() {
478 return channelHandlerFactory;
482 * Register an {@link HeosPlayerDiscoveryListener} to get informed
483 * if the amount of groups or players have changed
485 * @param listener the implementing class
487 public void registerPlayerDiscoverListener(HeosPlayerDiscoveryListener listener) {
488 playerDiscoveryList.add(listener);
491 private void triggerPlayerDiscovery() {
492 playerDiscoveryList.forEach(HeosPlayerDiscoveryListener::playerChanged);
495 public boolean isLoggedIn() {
499 public boolean isBridgeConnected() {
501 HeosFacade connection = apiConnection;
502 return connection != null && connection.isConnected();
505 public HeosFacade getApiConnection() throws HeosNotConnectedException {
507 HeosFacade localApiConnection = apiConnection;
508 if (localApiConnection != null) {
509 return localApiConnection;
511 throw new HeosNotConnectedException();
516 public Collection<Class<? extends ThingHandlerService>> getServices() {
517 return Collections.singletonList(HeosActions.class);