2 * Copyright (c) 2010-2023 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.discovery;
15 import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
16 import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
18 import java.io.IOException;
19 import java.util.HashMap;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
30 import org.openhab.binding.heos.internal.handler.HeosPlayerHandler;
31 import org.openhab.binding.heos.internal.json.payload.Group;
32 import org.openhab.binding.heos.internal.json.payload.Player;
33 import org.openhab.binding.heos.internal.resources.HeosGroup;
34 import org.openhab.binding.heos.internal.resources.Telnet;
35 import org.openhab.core.config.discovery.AbstractDiscoveryService;
36 import org.openhab.core.config.discovery.DiscoveryResult;
37 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.ThingUID;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link HeosPlayerDiscovery} discovers the player and groups within
46 * the HEOS network and reacts on changed groups or player.
48 * @author Johannes Einig - Initial contribution
51 public class HeosPlayerDiscovery extends AbstractDiscoveryService implements HeosPlayerDiscoveryListener {
52 private final Logger logger = LoggerFactory.getLogger(HeosPlayerDiscovery.class);
54 private static final int SEARCH_TIME = 5;
55 private static final int INITIAL_DELAY = 5;
56 private static final int SCAN_INTERVAL = 20;
58 private final HeosBridgeHandler bridge;
60 private Map<Integer, Player> players = new HashMap<>();
61 private Map<String, Group> groups = new HashMap<>();
63 private @Nullable ScheduledFuture<?> scanningJob;
65 public HeosPlayerDiscovery(HeosBridgeHandler bridge) throws IllegalArgumentException {
68 bridge.registerPlayerDiscoverListener(this);
72 public Set<ThingTypeUID> getSupportedThingTypes() {
73 return Stream.of(THING_TYPE_GROUP, THING_TYPE_PLAYER).collect(Collectors.toSet());
77 protected void startScan() {
78 if (!bridge.isBridgeConnected()) {
79 logger.debug("Scan for Players not possible. HEOS Bridge is not connected");
87 private void scanForPlayers() {
88 logger.debug("Start scan for HEOS Player");
91 Map<Integer, Player> currentPlayers = new HashMap<>();
93 for (Player player : bridge.getPlayers()) {
94 currentPlayers.put(player.playerId, player);
97 handleRemovedPlayers(findRemovedEntries(currentPlayers, players));
98 handleDiscoveredPlayers(currentPlayers);
100 players = currentPlayers;
101 } catch (IOException | Telnet.ReadException e) {
102 logger.debug("Failed getting/processing groups", e);
106 private void handleDiscoveredPlayers(Map<Integer, Player> currentPlayers) {
107 logger.debug("Found: {} player", currentPlayers.size());
108 ThingUID bridgeUID = bridge.getThing().getUID();
110 for (Player player : currentPlayers.values()) {
111 ThingUID uid = new ThingUID(THING_TYPE_PLAYER, bridgeUID, String.valueOf(player.playerId));
112 Map<String, Object> properties = new HashMap<>();
113 HeosPlayerHandler.propertiesFromPlayer(properties, player);
115 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(player.name)
116 .withProperties(properties).withBridge(bridgeUID)
117 .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
118 thingDiscovered(result);
122 private void handleRemovedPlayers(Map<Integer, Player> removedPlayers) {
123 for (Player player : removedPlayers.values()) {
124 // The same as above!
125 ThingUID uid = new ThingUID(THING_TYPE_PLAYER, String.valueOf(player.playerId));
126 logger.debug("Removed HEOS Player: {} ", uid);
131 private void scanForGroups() {
132 logger.debug("Start scan for HEOS Groups");
135 HashMap<String, Group> currentGroups = new HashMap<>();
137 for (Group group : bridge.getGroups()) {
138 logger.debug("Found: Group {} with {} Players", group.name, group.players.size());
139 currentGroups.put(HeosGroup.calculateGroupMemberHash(group), group);
142 handleRemovedGroups(findRemovedEntries(currentGroups, groups));
143 handleDiscoveredGroups(currentGroups);
145 groups = currentGroups;
146 } catch (IOException | Telnet.ReadException e) {
147 logger.debug("Failed getting/processing groups", e);
151 private void handleDiscoveredGroups(HashMap<String, Group> currentGroups) {
152 if (currentGroups.isEmpty()) {
153 logger.debug("No HEOS Groups found");
156 logger.debug("Found: {} new Groups", currentGroups.size());
157 ThingUID bridgeUID = bridge.getThing().getUID();
159 for (Map.Entry<String, Group> entry : currentGroups.entrySet()) {
160 Group group = entry.getValue();
161 String groupMemberHash = entry.getKey();
162 // Using an unsigned hashCode from the group members to identify
163 // the group and generates the Thing UID.
164 // This allows identifying the group even if the sorting within the group has changed
165 ThingUID uid = new ThingUID(THING_TYPE_GROUP, bridgeUID, groupMemberHash);
166 Map<String, Object> properties = new HashMap<>();
167 properties.put(PROP_NAME, group.name);
168 properties.put(PROP_GID, group.id);
169 String groupMembers = group.players.stream().map(p -> p.id).collect(Collectors.joining(";"));
170 properties.put(PROP_GROUP_MEMBERS, groupMembers);
171 properties.put(PROP_GROUP_LEADER, group.players.get(0).id);
172 properties.put(PROP_GROUP_HASH, groupMemberHash);
173 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(group.name).withProperties(properties)
174 .withBridge(bridgeUID).withRepresentationProperty(PROP_GROUP_HASH).build();
175 thingDiscovered(result);
176 bridge.setGroupOnline(groupMemberHash, group.id);
180 private void handleRemovedGroups(Map<String, Group> removedGroups) {
181 for (String groupMemberHash : removedGroups.keySet()) {
182 // The same as above!
183 ThingUID uid = new ThingUID(THING_TYPE_GROUP, groupMemberHash);
184 logger.debug("Removed HEOS Group: {}", uid);
186 bridge.setGroupOffline(groupMemberHash);
190 private <K, V> Map<K, V> findRemovedEntries(Map<K, V> mapNew, Map<K, V> mapOld) {
191 Map<K, V> removedItems = new HashMap<>();
192 for (K key : mapOld.keySet()) {
193 if (!mapNew.containsKey(key)) {
194 removedItems.put(key, mapOld.get(key));
201 protected void startBackgroundDiscovery() {
202 ScheduledFuture<?> runningScanningJob = this.scanningJob;
203 if (runningScanningJob == null || runningScanningJob.isCancelled()) {
204 this.scanningJob = scheduler.scheduleWithFixedDelay(this::startScan, INITIAL_DELAY, SCAN_INTERVAL,
210 protected void stopBackgroundDiscovery() {
211 logger.debug("Stop HEOS Player background discovery");
216 protected synchronized void stopScan() {
218 removeOlderResults(getTimestampOfLastScan());
221 private void scanForNewPlayers() {
222 removeOlderResults(getTimestampOfLastScan());
227 public void playerChanged() {