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 if (onlinePlayers != null && onlineGroups != null) {
200 updatePlayerStatus(onlinePlayers, onlineGroups);
202 } catch (ReadException | IOException e) {
203 logger.debug("Failed updating online state of groups/players", e);
207 private void updatePlayerStatus(@Nullable Player[] onlinePlayers, @Nullable Group[] onlineGroups) {
208 if (onlinePlayers == null || onlineGroups == null) {
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());
215 for (Thing thing : getThing().getThings()) {
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();
224 if (target.contains(id)) {
225 heosHandler.setStatusOnline();
227 heosHandler.setStatusOffline();
230 } catch (HeosNotFoundException e) {
231 logger.debug("SKipping handler which reported not found", e);
236 private HeosFacade connectBridge() throws IOException, Telnet.ReadException {
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);
246 apiConnection = connection;
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);
255 if (response.result) {
256 logger.debug("successfully logged-in, event is fired to handle post-login behaviour");
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.");
268 public void dispose() {
269 bridgeHandlerDisposalOngoing = true; // Flag to prevent the handler from being updated during disposal
271 cancel(startupFuture);
272 for (Future<?> future : childHandlerInitializedFutures) {
277 HeosFacade localApiConnection = apiConnection;
278 if (localApiConnection == null) {
279 logger.debug("Not disposing bridge because of missing apiConnection");
283 localApiConnection.unregisterForChangeEvents(this);
284 logger.debug("HEOS bridge removed from change notifications");
286 logger.debug("Dispose bridge '{}'", thing.getProperties().get(PROP_NAME));
287 localApiConnection.closeConnection();
291 * Manages the removal of the player or group channels from the bridge.
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
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);
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)));
317 void resetPlayerList(ChannelUID channelUID) {
318 selectedPlayerList.forEach(element -> updateState(element[1], OnOffType.OFF));
319 selectedPlayerList.clear();
320 updateState(channelUID, OnOffType.OFF);
324 * Sets the HEOS Thing offline
326 @SuppressWarnings("null")
327 public void setGroupOffline(String groupMemberHash) {
328 HeosGroupHandler groupHandler = groupHandlerMap.get(groupMemberHash);
329 if (groupHandler != null) {
330 groupHandler.setStatusOffline();
332 hashToGidMap.remove(groupMemberHash);
336 * Sets the HEOS Thing online. Also updates the link between
337 * the groupMemberHash value with the actual gid of this group
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);
348 * Create a channel for the childThing. Depending if it is a HEOS Group
349 * or a player an identification prefix is added
351 * @param childThing the thing the channel is created for
354 private void addPlayerChannel(Thing childThing, @Nullable String groupId) {
356 String channelIdentifier = "";
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();
371 Map<String, String> properties = new HashMap<>();
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);
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");
387 public void addGroupHandlerInformation(HeosGroupHandler handler) {
388 groupHandlerMap.put(handler.getGroupMemberHash(), handler);
391 private void removeGroupHandlerInformation(HeosGroupHandler handler) {
392 groupHandlerMap.remove(handler.getGroupMemberHash());
395 public @Nullable String getActualGID(String groupHash) {
396 return hashToGidMap.get(groupHash);
400 public void playerStateChangeEvent(HeosEventObject eventObject) {
405 public void playerStateChangeEvent(HeosResponseObject<?> responseObject) {
410 public void playerMediaChangeEvent(String pid, Media media) {
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)) {
429 if (EVENT_TYPE_SYSTEM.equals(event) && HeosEvent.USER_CHANGED == command) {
430 if (success && !loggedIn) {
436 private synchronized void updateThingChannels(List<Channel> channelList) {
437 ThingBuilder thingBuilder = editThing();
438 thingBuilder.withChannels(channelList);
439 updateThing(thingBuilder.build());
442 public Player[] getPlayers() throws IOException, ReadException {
443 HeosResponseObject<Player[]> response = getApiConnection().getPlayers();
445 Player[] players = response.payload;
446 if (players == null) {
447 throw new IOException("Received no valid payload");
452 public Group[] getGroups() throws IOException, ReadException {
453 HeosResponseObject<Group[]> response = getApiConnection().getGroups();
455 Group[] groups = response.payload;
456 if (groups == null) {
457 throw new IOException("Received no valid payload");
463 * The list with the currently selected player
465 * @return a HashMap which the currently selected player
467 public Map<String, String> getSelectedPlayer() {
468 return selectedPlayerList.stream().collect(Collectors.toMap(a -> a[0], a -> a[1], (a, b) -> a));
471 public List<String[]> getSelectedPlayerList() {
472 return selectedPlayerList;
475 public void setSelectedPlayerList(List<String[]> selectedPlayerList) {
476 this.selectedPlayerList = selectedPlayerList;
479 public HeosChannelHandlerFactory getChannelHandlerFactory() {
480 return channelHandlerFactory;
484 * Register an {@link HeosPlayerDiscoveryListener} to get informed
485 * if the amount of groups or players have changed
487 * @param listener the implementing class
489 public void registerPlayerDiscoverListener(HeosPlayerDiscoveryListener listener) {
490 playerDiscoveryList.add(listener);
493 private void triggerPlayerDiscovery() {
494 playerDiscoveryList.forEach(HeosPlayerDiscoveryListener::playerChanged);
497 public boolean isLoggedIn() {
501 public boolean isBridgeConnected() {
503 HeosFacade connection = apiConnection;
504 return connection != null && connection.isConnected();
507 public HeosFacade getApiConnection() throws HeosNotConnectedException {
509 HeosFacade localApiConnection = apiConnection;
510 if (localApiConnection != null) {
511 return localApiConnection;
513 throw new HeosNotConnectedException();
518 public Collection<Class<? extends ThingHandlerService>> getServices() {
519 return Collections.singletonList(HeosActions.class);