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.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.binding.heos.internal.json.dto.HeosEvent.PLAYER_VOLUME_CHANGED;
19 import java.io.IOException;
20 import java.util.HashMap;
22 import java.util.concurrent.Future;
23 import java.util.concurrent.TimeUnit;
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;
50 * The {@link HeosGroupHandler} handles the actions for a HEOS group.
51 * Channel commands are received and send to the dedicated channels
53 * @author Johannes Einig - Initial contribution
56 public class HeosGroupHandler extends HeosThingBaseHandler {
57 private final Logger logger = LoggerFactory.getLogger(HeosGroupHandler.class);
59 private @NonNullByDefault({}) GroupConfiguration configuration;
60 private @Nullable String gid;
62 private boolean blockInitialization;
63 private @Nullable Future<?> scheduledStartupFuture;
65 public HeosGroupHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
66 super(thing, heosDynamicStateDescriptionProvider);
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())) {
76 HeosChannelHandler channelHandler = getHeosChannelHandler(channelUID);
77 if (channelHandler != null) {
80 String id = getMaybeId(channelUID, command);
81 channelHandler.handleGroupCommand(command, id, thing.getUID(), this);
83 } catch (IOException | ReadException e) {
91 private String getMaybeId(ChannelUID channelUID, Command command) throws HeosNotFoundException {
92 if (isCreateGroupRequest(channelUID, command)) {
99 private boolean isCreateGroupRequest(ChannelUID channelUID, Command command) {
100 return CH_ID_UNGROUP.equals(channelUID.getId()) && OnOffType.ON == command;
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.
109 public synchronized void initialize() {
112 configuration = thing.getConfiguration().as(GroupConfiguration.class);
114 // Prevents that initialize() is called multiple times if group goes online
115 blockInitialization = true;
121 public void dispose() {
122 cancel(scheduledStartupFuture);
127 public String getId() throws HeosNotFoundException {
129 String localGroupId = this.gid;
130 if (localGroupId == null) {
131 throw new HeosNotFoundException();
136 public String getGroupMemberHash() {
137 return HeosGroup.calculateGroupMemberHash(configuration.members);
140 public String[] getGroupMemberPidList() {
141 return configuration.members.split(";");
145 public void setNotificationSoundVolume(PercentType volume) {
146 super.setNotificationSoundVolume(volume);
148 getApiConnection().volumeGroup(volume.toString(), getId());
149 } catch (IOException | ReadException e) {
150 logger.warn("Failed to set notification volume", e);
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());
163 String localGid = this.gid;
165 String eventGroupId = eventObject.getAttribute(HeosCommunicationAttribute.GROUP_ID);
167 String eventPlayerId = eventObject.getAttribute(HeosCommunicationAttribute.PLAYER_ID);
168 if (localGid == null || !(localGid.equals(eventGroupId) || localGid.equals(eventPlayerId))) {
172 if (PLAYER_VOLUME_CHANGED.equals(eventObject.command)) {
173 logger.debug("Ignoring player-volume changes for groups");
177 handleThingStateUpdate(eventObject);
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());
189 String localGid = this.gid;
190 if (localGid == null || !localGid.equals(responseObject.getAttribute(HeosCommunicationAttribute.GROUP_ID))) {
194 handleThingStateUpdate(responseObject);
198 public void playerMediaChangeEvent(String pid, Media media) {
199 if (!pid.equals(gid)) {
203 handleThingMediaUpdate(media);
207 * Sets the status of the HEOS group to OFFLINE.
208 * Also sets the UNGROUP channel to OFF and the CONTROL
212 public void setStatusOffline() {
213 logger.debug("Status was set offline");
215 getApiConnection().unregisterForChangeEvents(this);
216 } catch (HeosNotConnectedException e) {
217 logger.debug("Not connected, failed to unregister");
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");
225 public void setStatusOnline() {
226 if (!blockInitialization) {
229 logger.debug("Not initializing from setStatusOnline ({}, {})", thing.getStatus(), blockInitialization);
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);
243 private void scheduledStartUp() {
244 cancel(scheduledStartupFuture);
245 scheduledStartupFuture = scheduler.submit(this::delayedInitialize);
248 private void delayedInitialize() {
250 HeosBridgeHandler bridgeHandler = this.bridgeHandler;
252 if (bridgeHandler == null) {
253 logger.debug("Bridge handler not found, rescheduling");
258 if (bridgeHandler.isLoggedIn()) {
259 handleDynamicStatesSignedIn();
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.
266 String groupId = bridgeHandler.getActualGID(HeosGroup.calculateGroupMemberHash(configuration.members));
267 if (groupId == null) {
268 blockInitialization = false;
272 refreshPlayState(groupId);
274 HeosResponseObject<Group> response = getApiConnection().getGroupInfo(groupId);
276 Group group = response.payload;
278 throw new IllegalStateException("Invalid group response received");
281 assertSameGroup(group);
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);
297 * Make sure the given group is group which this handler represents
299 * @param group retrieved from HEOS system
301 private void assertSameGroup(Group group) {
302 String storedGroupHash = HeosGroup.calculateGroupMemberHash(configuration.members);
303 String retrievedGroupHash = HeosGroup.calculateGroupMemberHash(group);
305 if (!retrievedGroupHash.equals(storedGroupHash)) {
306 throw new IllegalStateException("Invalid group received, members / hash do not match.");
311 void refreshPlayState(String id) throws IOException, ReadException {
312 super.refreshPlayState(id);
314 handleThingStateUpdate(getApiConnection().getGroupMuteState(id));
315 handleThingStateUpdate(getApiConnection().getGroupVolume(id));