]> git.basschouten.com Git - openhab-addons.git/blob
56c2240f475d34fa18a598bfeb2566710fd2b4fb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.discovery;
14
15 import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
16 import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
17
18 import java.io.IOException;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
26
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;
43
44 /**
45  * The {@link HeosPlayerDiscovery} discovers the player and groups within
46  * the HEOS network and reacts on changed groups or player.
47  *
48  * @author Johannes Einig - Initial contribution
49  */
50 @NonNullByDefault
51 public class HeosPlayerDiscovery extends AbstractDiscoveryService implements HeosPlayerDiscoveryListener {
52     private final Logger logger = LoggerFactory.getLogger(HeosPlayerDiscovery.class);
53
54     private static final int SEARCH_TIME = 5;
55     private static final int INITIAL_DELAY = 5;
56     private static final int SCAN_INTERVAL = 20;
57
58     private final HeosBridgeHandler bridge;
59
60     private Map<Integer, Player> players = new HashMap<>();
61     private Map<String, Group> groups = new HashMap<>();
62
63     private @Nullable ScheduledFuture<?> scanningJob;
64
65     public HeosPlayerDiscovery(HeosBridgeHandler bridge) throws IllegalArgumentException {
66         super(SEARCH_TIME);
67         this.bridge = bridge;
68         bridge.registerPlayerDiscoverListener(this);
69     }
70
71     @Override
72     public Set<ThingTypeUID> getSupportedThingTypes() {
73         return Stream.of(THING_TYPE_GROUP, THING_TYPE_PLAYER).collect(Collectors.toSet());
74     }
75
76     @Override
77     protected void startScan() {
78         if (!bridge.isBridgeConnected()) {
79             logger.debug("Scan for Players not possible. HEOS Bridge is not connected");
80             return;
81         }
82
83         scanForPlayers();
84         scanForGroups();
85     }
86
87     private void scanForPlayers() {
88         logger.debug("Start scan for HEOS Player");
89
90         try {
91             Map<Integer, Player> currentPlayers = new HashMap<>();
92
93             for (Player player : bridge.getPlayers()) {
94                 currentPlayers.put(player.playerId, player);
95             }
96
97             handleRemovedPlayers(findRemovedEntries(currentPlayers, players));
98             handleDiscoveredPlayers(currentPlayers);
99
100             players = currentPlayers;
101         } catch (IOException | Telnet.ReadException e) {
102             logger.debug("Failed getting/processing groups", e);
103         }
104     }
105
106     private void handleDiscoveredPlayers(Map<Integer, Player> currentPlayers) {
107         logger.debug("Found: {} player", currentPlayers.size());
108         ThingUID bridgeUID = bridge.getThing().getUID();
109
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);
114
115             DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(player.name)
116                     .withProperties(properties).withBridge(bridgeUID)
117                     .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
118             thingDiscovered(result);
119         }
120     }
121
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);
127             thingRemoved(uid);
128         }
129     }
130
131     private void scanForGroups() {
132         logger.debug("Start scan for HEOS Groups");
133
134         try {
135             HashMap<String, Group> currentGroups = new HashMap<>();
136
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);
140             }
141
142             handleRemovedGroups(findRemovedEntries(currentGroups, groups));
143             handleDiscoveredGroups(currentGroups);
144
145             groups = currentGroups;
146         } catch (IOException | Telnet.ReadException e) {
147             logger.debug("Failed getting/processing groups", e);
148         }
149     }
150
151     private void handleDiscoveredGroups(HashMap<String, Group> currentGroups) {
152         if (currentGroups.isEmpty()) {
153             logger.debug("No HEOS Groups found");
154             return;
155         }
156         logger.debug("Found: {} new Groups", currentGroups.size());
157         ThingUID bridgeUID = bridge.getThing().getUID();
158
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);
177         }
178     }
179
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);
185             thingRemoved(uid);
186             bridge.setGroupOffline(groupMemberHash);
187         }
188     }
189
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));
195             }
196         }
197         return removedItems;
198     }
199
200     @Override
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,
205                     TimeUnit.SECONDS);
206         }
207     }
208
209     @Override
210     protected void stopBackgroundDiscovery() {
211         logger.debug("Stop HEOS Player background discovery");
212         cancel(scanningJob);
213     }
214
215     @Override
216     protected synchronized void stopScan() {
217         super.stopScan();
218         removeOlderResults(getTimestampOfLastScan());
219     }
220
221     private void scanForNewPlayers() {
222         removeOlderResults(getTimestampOfLastScan());
223         startScan();
224     }
225
226     @Override
227     public void playerChanged() {
228         scanForNewPlayers();
229     }
230 }