]> git.basschouten.com Git - openhab-addons.git/blob
fdb9a2d8018b5a9ad6c373e3fd8dafb235602405
[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.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.binding.heos.internal.json.dto.HeosEvent.PLAYER_VOLUME_CHANGED;
18
19 import java.io.IOException;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.concurrent.Future;
23 import java.util.concurrent.TimeUnit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.heos.internal.configuration.GroupConfiguration;
28 import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
29 import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
30 import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
31 import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute;
32 import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
33 import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
34 import org.openhab.binding.heos.internal.json.payload.Group;
35 import org.openhab.binding.heos.internal.json.payload.Media;
36 import org.openhab.binding.heos.internal.resources.HeosGroup;
37 import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.PercentType;
40 import org.openhab.core.library.types.PlayPauseType;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 /**
50  * The {@link HeosGroupHandler} handles the actions for a HEOS group.
51  * Channel commands are received and send to the dedicated channels
52  *
53  * @author Johannes Einig - Initial contribution
54  */
55 @NonNullByDefault
56 public class HeosGroupHandler extends HeosThingBaseHandler {
57     private final Logger logger = LoggerFactory.getLogger(HeosGroupHandler.class);
58
59     private @NonNullByDefault({}) GroupConfiguration configuration;
60     private @Nullable String gid;
61
62     private boolean blockInitialization;
63     private @Nullable Future<?> scheduledStartupFuture;
64
65     public HeosGroupHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
66         super(thing, heosDynamicStateDescriptionProvider);
67     }
68
69     @Override
70     public void handleCommand(ChannelUID channelUID, Command command) {
71         // The GID is null if there is no group online with the groupMemberHash
72         // Only commands from the UNGROUP channel are passed through
73         // to activate the group if it is offline
74         if (gid != null || CH_ID_UNGROUP.equals(channelUID.getId())) {
75             @Nullable
76             HeosChannelHandler channelHandler = getHeosChannelHandler(channelUID);
77             if (channelHandler != null) {
78                 try {
79                     @Nullable
80                     String id = getMaybeId(channelUID, command);
81                     channelHandler.handleGroupCommand(command, id, thing.getUID(), this);
82                     handleSuccess();
83                 } catch (IOException | ReadException e) {
84                     handleError(e);
85                 }
86             }
87         }
88     }
89
90     @Nullable
91     private String getMaybeId(ChannelUID channelUID, Command command) throws HeosNotFoundException {
92         if (isCreateGroupRequest(channelUID, command)) {
93             return null;
94         } else {
95             return getId();
96         }
97     }
98
99     private boolean isCreateGroupRequest(ChannelUID channelUID, Command command) {
100         return CH_ID_UNGROUP.equals(channelUID.getId()) && OnOffType.ON == command;
101     }
102
103     /**
104      * Initialize the HEOS group. Starts an extra thread to avoid blocking
105      * during start up phase. Gathering all information can take longer
106      * than 5 seconds which can throw an error within the openHAB system.
107      */
108     @Override
109     public synchronized void initialize() {
110         super.initialize();
111
112         configuration = thing.getConfiguration().as(GroupConfiguration.class);
113
114         // Prevents that initialize() is called multiple times if group goes online
115         blockInitialization = true;
116
117         scheduledStartUp();
118     }
119
120     @Override
121     public void dispose() {
122         cancel(scheduledStartupFuture);
123         super.dispose();
124     }
125
126     @Override
127     public String getId() throws HeosNotFoundException {
128         @Nullable
129         String localGroupId = this.gid;
130         if (localGroupId == null) {
131             throw new HeosNotFoundException();
132         }
133         return localGroupId;
134     }
135
136     public String getGroupMemberHash() {
137         return HeosGroup.calculateGroupMemberHash(configuration.members);
138     }
139
140     public String[] getGroupMemberPidList() {
141         return configuration.members.split(";");
142     }
143
144     @Override
145     public void setNotificationSoundVolume(PercentType volume) {
146         super.setNotificationSoundVolume(volume);
147         try {
148             getApiConnection().volumeGroup(volume.toString(), getId());
149         } catch (IOException | ReadException e) {
150             logger.warn("Failed to set notification volume", e);
151         }
152     }
153
154     @Override
155     public void playerStateChangeEvent(HeosEventObject eventObject) {
156         if (ThingStatus.UNINITIALIZED == getThing().getStatus()) {
157             logger.debug("Can't Handle Event. Group {} not initialized. Status is: {}", getConfig().get(PROP_NAME),
158                     getThing().getStatus());
159             return;
160         }
161
162         @Nullable
163         String localGid = this.gid;
164         @Nullable
165         String eventGroupId = eventObject.getAttribute(HeosCommunicationAttribute.GROUP_ID);
166         @Nullable
167         String eventPlayerId = eventObject.getAttribute(HeosCommunicationAttribute.PLAYER_ID);
168         if (localGid == null || !(localGid.equals(eventGroupId) || localGid.equals(eventPlayerId))) {
169             return;
170         }
171
172         if (PLAYER_VOLUME_CHANGED.equals(eventObject.command)) {
173             logger.debug("Ignoring player-volume changes for groups");
174             return;
175         }
176
177         handleThingStateUpdate(eventObject);
178     }
179
180     @Override
181     public void playerStateChangeEvent(HeosResponseObject<?> responseObject) throws HeosFunctionalException {
182         if (ThingStatus.UNINITIALIZED == getThing().getStatus()) {
183             logger.debug("Can't Handle Event. Group {} not initialized. Status is: {}", getConfig().get(PROP_NAME),
184                     getThing().getStatus());
185             return;
186         }
187
188         @Nullable
189         String localGid = this.gid;
190         if (localGid == null || !localGid.equals(responseObject.getAttribute(HeosCommunicationAttribute.GROUP_ID))) {
191             return;
192         }
193
194         handleThingStateUpdate(responseObject);
195     }
196
197     @Override
198     public void playerMediaChangeEvent(String pid, Media media) {
199         if (!pid.equals(gid)) {
200             return;
201         }
202
203         handleThingMediaUpdate(media);
204     }
205
206     /**
207      * Sets the status of the HEOS group to OFFLINE.
208      * Also sets the UNGROUP channel to OFF and the CONTROL
209      * channel to PAUSE
210      */
211     @Override
212     public void setStatusOffline() {
213         logger.debug("Status was set offline");
214         try {
215             getApiConnection().unregisterForChangeEvents(this);
216         } catch (HeosNotConnectedException e) {
217             logger.debug("Not connected, failed to unregister");
218         }
219         updateState(CH_ID_UNGROUP, OnOffType.OFF);
220         updateState(CH_ID_CONTROL, PlayPauseType.PAUSE);
221         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DISABLED, "Group is not available on HEOS system");
222     }
223
224     @Override
225     public void setStatusOnline() {
226         if (!blockInitialization) {
227             initialize();
228         } else {
229             logger.debug("Not initializing from setStatusOnline ({}, {})", thing.getStatus(), blockInitialization);
230         }
231     }
232
233     private void updateConfiguration(String groupId, Group group) {
234         Map<String, String> prop = new HashMap<>();
235         prop.put(PROP_NAME, group.name);
236         prop.put(PROP_GROUP_MEMBERS, group.getGroupMemberIds());
237         prop.put(PROP_GROUP_LEADER, group.getLeaderId());
238         prop.put(PROP_GROUP_HASH, HeosGroup.calculateGroupMemberHash(group));
239         prop.put(PROP_GID, groupId);
240         updateProperties(prop);
241     }
242
243     private void scheduledStartUp() {
244         cancel(scheduledStartupFuture);
245         scheduledStartupFuture = scheduler.submit(this::delayedInitialize);
246     }
247
248     private void delayedInitialize() {
249         @Nullable
250         HeosBridgeHandler bridgeHandler = this.bridgeHandler;
251
252         if (bridgeHandler == null) {
253             logger.debug("Bridge handler not found, rescheduling");
254             scheduledStartUp();
255             return;
256         }
257
258         if (bridgeHandler.isLoggedIn()) {
259             handleDynamicStatesSignedIn();
260         }
261
262         bridgeHandler.addGroupHandlerInformation(this);
263         // Checks if there is a group online with the same group member hash.
264         // If not setting the group offline.
265         @Nullable
266         String groupId = bridgeHandler.getActualGID(HeosGroup.calculateGroupMemberHash(configuration.members));
267         if (groupId == null) {
268             blockInitialization = false;
269             setStatusOffline();
270         } else {
271             try {
272                 refreshPlayState(groupId);
273
274                 HeosResponseObject<Group> response = getApiConnection().getGroupInfo(groupId);
275                 @Nullable
276                 Group group = response.payload;
277                 if (group == null) {
278                     throw new IllegalStateException("Invalid group response received");
279                 }
280
281                 assertSameGroup(group);
282
283                 gid = groupId;
284                 updateConfiguration(groupId, group);
285                 updateStatus(ThingStatus.ONLINE);
286                 updateState(CH_ID_UNGROUP, OnOffType.ON);
287                 blockInitialization = false;
288             } catch (IOException | ReadException | IllegalStateException e) {
289                 logger.debug("Failed initializing, will retry", e);
290                 cancel(scheduledStartupFuture, false);
291                 scheduledStartupFuture = scheduler.schedule(this::delayedInitialize, 30, TimeUnit.SECONDS);
292             }
293         }
294     }
295
296     /**
297      * Make sure the given group is group which this handler represents
298      * 
299      * @param group retrieved from HEOS system
300      */
301     private void assertSameGroup(Group group) {
302         String storedGroupHash = HeosGroup.calculateGroupMemberHash(configuration.members);
303         String retrievedGroupHash = HeosGroup.calculateGroupMemberHash(group);
304
305         if (!retrievedGroupHash.equals(storedGroupHash)) {
306             throw new IllegalStateException("Invalid group received, members / hash do not match.");
307         }
308     }
309
310     @Override
311     void refreshPlayState(String id) throws IOException, ReadException {
312         super.refreshPlayState(id);
313
314         handleThingStateUpdate(getApiConnection().getGroupMuteState(id));
315         handleThingStateUpdate(getApiConnection().getGroupVolume(id));
316     }
317 }