added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.heos-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-heos" description="HEOS Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<bundle dependency="true">mvn:commons-net/commons-net/3.6</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.heos/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link HeosBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosBindingConstants extends HeosConstants {
public static final String BINDING_ID = "heos";
// List of all Bridge Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_PLAYER = new ThingTypeUID(BINDING_ID, "player");
public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group");
// List off all Channel Types
public static final ChannelTypeUID CH_TYPE_PLAYER = new ChannelTypeUID(BINDING_ID, "chPlayer");
// List of all Channel IDs
public static final String CH_ID_CONTROL = "Control";
public static final String CH_ID_VOLUME = "Volume";
public static final String CH_ID_MUTE = "Mute";
public static final String CH_ID_UNGROUP = "Ungroup";
public static final String CH_ID_SONG = "Title";
public static final String CH_ID_ARTIST = "Artist";
public static final String CH_ID_ALBUM = "Album";
public static final String CH_ID_BUILDGROUP = "BuildGroup";
public static final String CH_ID_REBOOT = "Reboot";
public static final String CH_ID_COVER = "Cover";
public static final String CH_ID_PLAYLISTS = "Playlists";
public static final String CH_ID_FAVORITES = "Favorites";
public static final String CH_ID_QUEUE = "Queue";
public static final String CH_ID_CLEAR_QUEUE = "ClearQueue";
public static final String CH_ID_INPUTS = "Inputs";
public static final String CH_ID_CUR_POS = "CurrentPosition";
public static final String CH_ID_DURATION = "Duration";
public static final String CH_ID_STATION = "Station";
public static final String CH_ID_RAW_COMMAND = "RawCommand";
public static final String CH_ID_TYPE = "Type";
public static final String CH_ID_PLAY_URL = "PlayUrl";
public static final String CH_ID_SHUFFLE_MODE = "Shuffle";
public static final String CH_ID_REPEAT_MODE = "RepeatMode";
// Values for Bridge, Player and Group Properties;
// Using this values to display the correct name
// within the thing properties.
public static final String PROP_PID = "pid";
public static final String PROP_GROUP_MEMBERS = "members";
public static final String PROP_NAME = "Name";
public static final String PROP_GID = "Group ID";
public static final String PROP_IP = "IP Address";
public static final String PROP_NETWORK = "Connection";
public static final String PROP_GROUP_HASH = "Members Hash value";
public static final String PROP_GROUP_LEADER = "Group leader";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String HEARTBEAT = "heartbeat";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(THING_TYPE_BRIDGE, THING_TYPE_GROUP, THING_TYPE_PLAYER).collect(Collectors.toSet()));
public static final int FAILURE_COUNT_LIMIT = 5;
}

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.handler.*;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link HeosChannelHandlerFactory} is responsible for creating and returning
* of the single handler for each channel of the single things.
* It also stores already created handler for further use.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerFactory {
private final HeosBridgeHandler bridge;
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
private final Map<ChannelUID, HeosChannelHandler> handlerStorageMap = new HashMap<>();
public HeosChannelHandlerFactory(HeosBridgeHandler bridge,
HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
this.bridge = bridge;
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
public @Nullable HeosChannelHandler getChannelHandler(ChannelUID channelUID, HeosEventListener eventListener,
@Nullable ChannelTypeUID channelTypeUID) {
if (handlerStorageMap.containsKey(channelUID)) {
return handlerStorageMap.get(channelUID);
} else {
HeosChannelHandler handler = createNewChannelHandler(channelUID, eventListener, channelTypeUID);
if (handler != null) {
handlerStorageMap.put(channelUID, handler);
}
return handler;
}
}
private @Nullable HeosChannelHandler createNewChannelHandler(ChannelUID channelUID, HeosEventListener eventListener,
@Nullable ChannelTypeUID channelTypeUID) {
switch (channelUID.getId()) {
case CH_ID_CONTROL:
return new HeosChannelHandlerControl(eventListener, bridge);
case CH_ID_VOLUME:
return new HeosChannelHandlerVolume(eventListener, bridge);
case CH_ID_MUTE:
return new HeosChannelHandlerMute(eventListener, bridge);
case CH_ID_INPUTS:
return new HeosChannelHandlerInputs(eventListener, bridge);
case CH_ID_REPEAT_MODE:
return new HeosChannelHandlerRepeatMode(eventListener, bridge);
case CH_ID_SHUFFLE_MODE:
return new HeosChannelHandlerShuffleMode(eventListener, bridge);
case CH_ID_ALBUM:
case CH_ID_SONG:
case CH_ID_ARTIST:
case CH_ID_COVER:
case CH_ID_TYPE:
case CH_ID_STATION:
return new HeosChannelHandlerNowPlaying(eventListener, bridge);
case CH_ID_QUEUE:
return new HeosChannelHandlerQueue(heosDynamicStateDescriptionProvider, bridge);
case CH_ID_CLEAR_QUEUE:
return new HeosChannelHandlerClearQueue(bridge);
case CH_ID_PLAY_URL:
return new HeosChannelHandlerPlayURL(bridge);
case CH_ID_UNGROUP:
return new HeosChannelHandlerGrouping(bridge);
case CH_ID_RAW_COMMAND:
return new HeosChannelHandlerRawCommand(eventListener, bridge);
case CH_ID_REBOOT:
return new HeosChannelHandlerReboot(bridge);
case CH_ID_BUILDGROUP:
return new HeosChannelHandlerBuildGroup(channelUID, bridge);
case CH_ID_PLAYLISTS:
return new HeosChannelHandlerPlaylist(heosDynamicStateDescriptionProvider, bridge);
case CH_ID_FAVORITES:
return new HeosChannelHandlerFavorite(heosDynamicStateDescriptionProvider, bridge);
case CH_ID_CUR_POS:
case CH_ID_DURATION:
// nothing to handle, we receive updates automatically
return null;
}
if (channelTypeUID != null) {
if (CH_TYPE_PLAYER.equals(channelTypeUID)) {
return new HeosChannelHandlerPlayerSelect(channelUID, bridge);
}
}
return null;
}
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.ThingHandler;
/**
* The {@link HeosChannelManager} provides the functions to
* add and remove channels from the channel list provided by the thing
* The generation of the individual channels has to be done by the thingHandler
* itself.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelManager {
private final ThingHandler handler;
public HeosChannelManager(ThingHandler handler) {
this.handler = handler;
}
public synchronized List<Channel> addSingleChannel(Channel channel) {
ChannelWrapper channelList = getChannelsFromThing();
channelList.removeChannel(channel.getUID());
channelList.add(channel);
return channelList.get();
}
public synchronized List<Channel> removeSingleChannel(String channelIdentifier) {
ChannelWrapper channelWrapper = getChannelsFromThing();
channelWrapper.removeChannel(generateChannelUID(channelIdentifier));
return channelWrapper.get();
}
/*
* Gets the channels from the Thing and makes the channel
* list editable.
*/
private ChannelWrapper getChannelsFromThing() {
return new ChannelWrapper(handler.getThing().getChannels());
}
private ChannelUID generateChannelUID(String channelIdentifier) {
return new ChannelUID(handler.getThing().getUID(), channelIdentifier);
}
/**
* Wrap a channel list
*
* @author Martin van Wingerden - Initial contribution
*/
private static class ChannelWrapper {
private final List<Channel> channels;
ChannelWrapper(List<Channel> channels) {
this.channels = new ArrayList<>(channels);
}
private void removeChannel(ChannelUID uid) {
List<Channel> itemsToBeRemoved = channels.stream().filter(Objects::nonNull)
.filter(channel -> uid.equals(channel.getUID())).collect(Collectors.toList());
channels.removeAll(itemsToBeRemoved);
}
public void add(Channel channel) {
channels.add(channel);
}
public List<Channel> get() {
return Collections.unmodifiableList(channels);
}
}
}

View File

@@ -0,0 +1,177 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.api.HeosAudioSink;
import org.openhab.binding.heos.internal.discovery.HeosPlayerDiscovery;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
import org.openhab.binding.heos.internal.handler.HeosDynamicStateDescriptionProvider;
import org.openhab.binding.heos.internal.handler.HeosGroupHandler;
import org.openhab.binding.heos.internal.handler.HeosPlayerHandler;
import org.openhab.binding.heos.internal.handler.HeosThingBaseHandler;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Johannes Einig - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.heos")
@NonNullByDefault
public class HeosHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(HeosHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
private @NonNullByDefault({}) AudioHTTPServer audioHTTPServer;
private @NonNullByDefault({}) NetworkAddressService networkAddressService;
private @NonNullByDefault({}) HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
@Activate
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
HeosBridgeHandler bridgeHandler = new HeosBridgeHandler((Bridge) thing,
heosDynamicStateDescriptionProvider);
HeosPlayerDiscovery playerDiscovery = new HeosPlayerDiscovery(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(), bundleContext
.registerService(DiscoveryService.class.getName(), playerDiscovery, new Hashtable<>()));
logger.debug("Register discovery service for HEOS player and HEOS groups by bridge '{}'",
bridgeHandler.getThing().getUID().getId());
return bridgeHandler;
}
if (THING_TYPE_PLAYER.equals(thingTypeUID)) {
HeosPlayerHandler playerHandler = new HeosPlayerHandler(thing, heosDynamicStateDescriptionProvider);
registerAudioSink(thing, playerHandler);
return playerHandler;
}
if (THING_TYPE_GROUP.equals(thingTypeUID)) {
HeosGroupHandler groupHandler = new HeosGroupHandler(thing, heosDynamicStateDescriptionProvider);
registerAudioSink(thing, groupHandler);
return groupHandler;
}
return null;
}
private void registerAudioSink(Thing thing, HeosThingBaseHandler thingBaseHandler) {
HeosAudioSink audioSink = new HeosAudioSink(thingBaseHandler, audioHTTPServer, createCallbackUrl());
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
audioSinkRegistrations.put(thing.getUID().toString(), reg);
}
@Override
public void unregisterHandler(Thing thing) {
if (thing.getThingTypeUID().equals(THING_TYPE_BRIDGE)) {
super.unregisterHandler(thing);
ServiceRegistration<?> serviceRegistration = discoveryServiceRegs.get(thing.getUID());
if (serviceRegistration != null) {
serviceRegistration.unregister();
discoveryServiceRegs.remove(thing.getUID());
logger.debug("Unregister discovery service for HEOS player and HEOS groups by bridge '{}'",
thing.getUID().getId());
}
}
if (THING_TYPE_PLAYER.equals(thing.getThingTypeUID()) || THING_TYPE_GROUP.equals(thing.getThingTypeUID())) {
super.unregisterHandler(thing);
ServiceRegistration<AudioSink> reg = audioSinkRegistrations.get(thing.getUID().toString());
if (reg != null) {
reg.unregister();
}
}
}
@Reference
protected void setAudioHTTPServer(AudioHTTPServer audioHTTPServer) {
this.audioHTTPServer = audioHTTPServer;
}
protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) {
this.audioHTTPServer = null;
}
@Reference
protected void setNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = networkAddressService;
}
protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = null;
}
@Reference
protected void setDynamicStateDescriptionProvider(HeosDynamicStateDescriptionProvider provider) {
this.heosDynamicStateDescriptionProvider = provider;
}
protected void unsetDynamicStateDescriptionProvider(HeosDynamicStateDescriptionProvider provider) {
this.heosDynamicStateDescriptionProvider = null;
}
private @Nullable String createCallbackUrl() {
final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
if (ipAddress == null) {
logger.warn("No network interface could be found.");
return null;
}
// we do not use SSL as it can cause certificate validation issues.
final int port = HttpServiceUtil.getHttpServicePort(bundleContext);
if (port == -1) {
logger.warn("Cannot find port of the http service.");
return null;
}
return "http://" + ipAddress + ":" + port;
}
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.action;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The class is responsible to call corresponding action on HEOS Handler
* <p>
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
* the test <i>actions instanceof HeosActions</i> fails. This test can fail
* due to an issue in openHAB core v2.5.0 where the {@link HeosActions} class
* can be loaded by a different classloader than the <i>actions</i> instance.
*
* @author Martin van Wingerden - Initial contribution
*/
@ThingActionsScope(name = "heos")
@NonNullByDefault
public class HeosActions implements ThingActions, IHeosActions {
private final static Logger logger = LoggerFactory.getLogger(HeosActions.class);
private @Nullable HeosBridgeHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof HeosBridgeHandler) {
this.handler = (HeosBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
private @Nullable HeosFacade getConnection() throws HeosNotConnectedException {
if (handler == null) {
return null;
}
return handler.getApiConnection();
}
@Override
@RuleAction(label = "Play Input", description = "Play an input from another device")
public void playInputFromPlayer(
@ActionInput(name = "source", label = "Source Player", description = "Player used for input") @Nullable Integer sourcePlayer,
@ActionInput(name = "input", label = "Source Input", description = "Input source used") @Nullable String input,
@ActionInput(name = "destination", label = "Destination Player", description = "Device for audio output") @Nullable Integer destinationPlayer) {
if (sourcePlayer == null || input == null || destinationPlayer == null) {
logger.debug(
"Skipping HEOS playInputFromPlayer due to null value: sourcePlayer: {}, input: {}, destination: {}",
sourcePlayer, input, destinationPlayer);
return;
}
try {
HeosFacade connection = getConnection();
if (connection == null) {
logger.debug("Skipping HEOS playInputFromPlayer because no connection was available");
return;
}
connection.playInputSource(destinationPlayer.toString(), sourcePlayer.toString(), input);
} catch (IOException | Telnet.ReadException e) {
logger.warn("Failed to play input source!", e);
}
}
public static void playInputFromPlayer(@Nullable ThingActions actions, @Nullable Integer sourcePlayer,
@Nullable String input, @Nullable Integer destinationPlayer) {
invokeMethodOf(actions).playInputFromPlayer(sourcePlayer, input, destinationPlayer);
}
private static IHeosActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(HeosActions.class.getName())) {
if (actions instanceof IHeosActions) {
return (IHeosActions) actions;
} else {
return (IHeosActions) Proxy.newProxyInstance(IHeosActions.class.getClassLoader(),
new Class[] { IHeosActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of HeosActions");
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link IHeosActions} defines the interface for all thing actions supported by the binding.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface IHeosActions {
public void playInputFromPlayer(@Nullable Integer sourcePlayer, @Nullable String input,
@Nullable Integer destinationPlayer);
}

View File

@@ -0,0 +1,135 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.api;
import java.io.IOException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.handler.HeosThingBaseHandler;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FileAudioStream;
import org.openhab.core.audio.FixedLengthAudioStream;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.utils.AudioStreamUtils;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.util.ThingHandlerHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This makes HEOS to serve as an {@link AudioSink}.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosAudioSink implements AudioSink {
private final Logger logger = LoggerFactory.getLogger(HeosAudioSink.class);
private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = new HashSet<>();
private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = new HashSet<>();
static {
SUPPORTED_AUDIO_FORMATS.add(AudioFormat.WAV);
SUPPORTED_AUDIO_FORMATS.add(AudioFormat.MP3);
SUPPORTED_AUDIO_FORMATS.add(AudioFormat.AAC);
SUPPORTED_AUDIO_STREAMS.add(URLAudioStream.class);
SUPPORTED_AUDIO_STREAMS.add(FixedLengthAudioStream.class);
}
private final HeosThingBaseHandler handler;
private final AudioHTTPServer audioHTTPServer;
private @Nullable final String callbackUrl;
public HeosAudioSink(HeosThingBaseHandler handler, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
this.handler = handler;
this.audioHTTPServer = audioHTTPServer;
this.callbackUrl = callbackUrl;
}
@Override
public String getId() {
return handler.getThing().getUID().toString();
}
@Override
public @Nullable String getLabel(@Nullable Locale locale) {
return handler.getThing().getLabel();
}
@Override
public void process(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException {
try {
if (audioStream instanceof URLAudioStream) {
// it is an external URL, the speaker can access it itself and play it.
URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
handler.playURL(urlAudioStream.getURL());
} else if (audioStream instanceof FixedLengthAudioStream) {
if (callbackUrl != null) {
// we serve it on our own HTTP server for 30 seconds as HEOS requests the stream several times
String relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 30);
String url = callbackUrl + relativeUrl + AudioStreamUtils.EXTENSION_SEPARATOR;
AudioFormat audioFormat = audioStream.getFormat();
if (!ThingHandlerHelper.isHandlerInitialized(handler)) {
logger.debug("HEOS speaker '{}' is not initialized - status is {}", handler.getThing().getUID(),
handler.getThing().getStatus());
} else if (AudioFormat.MP3.isCompatible(audioFormat)) {
handler.playURL(url + FileAudioStream.MP3_EXTENSION);
} else if (AudioFormat.WAV.isCompatible(audioFormat)) {
handler.playURL(url + FileAudioStream.WAV_EXTENSION);
} else if (AudioFormat.AAC.isCompatible(audioFormat)) {
handler.playURL(url + FileAudioStream.AAC_EXTENSION);
} else {
throw new UnsupportedAudioFormatException("HEOS only supports MP3, WAV and AAC.", audioFormat);
}
} else {
logger.warn("We do not have any callback url, so HEOS cannot play the audio stream!");
}
} else {
throw new UnsupportedAudioFormatException(
"HEOS can only handle FixedLengthAudioStreams & URLAudioStream.", null);
}
} catch (IOException | ReadException e) {
logger.warn("Failed to play audio stream: {}", e.getMessage());
}
}
@Override
public Set<AudioFormat> getSupportedFormats() {
return SUPPORTED_AUDIO_FORMATS;
}
@Override
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_AUDIO_STREAMS;
}
@Override
public PercentType getVolume() {
return handler.getNotificationSoundVolume();
}
@Override
public void setVolume(PercentType volume) {
handler.setNotificationSoundVolume(volume);
}
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.api;
import static org.openhab.binding.heos.internal.resources.HeosConstants.*;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosCommands;
import org.openhab.binding.heos.internal.resources.HeosSystemEventListener;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosEventController} is responsible for handling event, which are
* received by the HEOS system.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosEventController extends HeosSystemEventListener {
private final Logger logger = LoggerFactory.getLogger(HeosEventController.class);
private final HeosSystem system;
private long lastEventTime;
public HeosEventController(HeosSystem system) {
this.system = system;
lastEventTime = System.currentTimeMillis();
}
public void handleEvent(HeosEventObject eventObject) {
HeosEvent command = eventObject.command;
lastEventTime = System.currentTimeMillis();
logger.debug("Handling event: {}", eventObject);
if (command == null) {
return;
}
switch (command) {
case PLAYER_NOW_PLAYING_PROGRESS:
case PLAYER_STATE_CHANGED:
case PLAYER_VOLUME_CHANGED:
case SHUFFLE_MODE_CHANGED:
case REPEAT_MODE_CHANGED:
case PLAYER_PLAYBACK_ERROR:
case GROUP_VOLUME_CHANGED:
case PLAYER_QUEUE_CHANGED:
case SOURCES_CHANGED:
fireStateEvent(eventObject);
break;
case USER_CHANGED:
fireBridgeEvent(EVENT_TYPE_SYSTEM, true, command);
break;
case PLAYER_NOW_PLAYING_CHANGED:
String pid = eventObject.getAttribute(HeosCommunicationAttribute.PLAYER_ID);
if (pid == null) {
logger.debug("HEOS did not mention which player changed, unlikely but ignore");
break;
}
try {
HeosResponseObject<Media> mediaResponse = system.send(HeosCommands.getNowPlayingMedia(pid),
Media.class);
Media responseMedia = mediaResponse.payload;
if (responseMedia != null) {
fireMediaEvent(pid, responseMedia);
}
} catch (IOException | Telnet.ReadException e) {
logger.debug("Failed to retrieve current playing media, will try again next time.", e);
}
break;
case GROUPS_CHANGED:
case PLAYERS_CHANGED:
fireBridgeEvent(EVENT_TYPE_EVENT, true, command);
break;
}
}
public void connectionToSystemLost() {
fireBridgeEvent(EVENT_TYPE_EVENT, false, CONNECTION_LOST);
}
public void eventStreamTimeout() {
fireBridgeEvent(EVENT_TYPE_EVENT, false, EVENT_STREAM_TIMEOUT);
}
public void systemReachable() {
fireBridgeEvent(EVENT_TYPE_EVENT, true, CONNECTION_RESTORED);
}
long getLastEventTime() {
return lastEventTime;
}
}

View File

@@ -0,0 +1,556 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.api;
import static org.openhab.binding.heos.internal.resources.HeosConstants.*;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.BrowseResult;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosCommands;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link HeosFacade} is the interface for handling commands, which are
* sent to the HEOS system.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosFacade {
private static final int MAX_QUEUE_PAGES = 25;
private final Logger logger = LoggerFactory.getLogger(HeosFacade.class);
private final HeosSystem heosSystem;
private final HeosEventController eventController;
public HeosFacade(HeosSystem heosSystem, HeosEventController eventController) {
this.heosSystem = heosSystem;
this.eventController = eventController;
}
public synchronized List<BrowseResult> getFavorites() throws IOException, ReadException {
return getBrowseResults(FAVORITE_SID);
}
public List<BrowseResult> getInputs() throws IOException, ReadException {
return getBrowseResults(String.valueOf(INPUT_SID));
}
public List<BrowseResult> getPlaylists() throws IOException, ReadException {
return getBrowseResults(PLAYLISTS_SID);
}
@NotNull
private List<BrowseResult> getBrowseResults(String sourceIdentifier) throws IOException, ReadException {
HeosResponseObject<BrowseResult[]> response = browseSource(sourceIdentifier);
logger.debug("Response: {}", response);
if (response.payload == null) {
return Collections.emptyList();
}
logger.debug("Received results: {}", Arrays.asList(response.payload));
return Arrays.asList(response.payload);
}
public List<Media> getQueue(String pid) throws IOException, ReadException {
List<Media> media = new ArrayList<>();
for (int page = 0; page < MAX_QUEUE_PAGES; page++) {
HeosResponseObject<Media[]> response = fetchQueue(pid, page);
if (!response.result || response.payload == null) {
break;
}
media.addAll(Arrays.asList(response.payload));
if (response.payload.length < 100) {
break;
}
if (page == MAX_QUEUE_PAGES - 1) {
logger.info("Currently only a maximum of {} pages is fetched for every queue", MAX_QUEUE_PAGES);
}
}
return media;
}
HeosResponseObject<Media[]> fetchQueue(String pid, int page) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getQueue(pid, page * 100, (page + 1) * 100), Media[].class);
}
public HeosResponseObject<Player> getPlayerInfo(String pid) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayerInfo(pid), Player.class);
}
public HeosResponseObject<Group> getGroupInfo(String gid) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroupInfo(gid), Group.class);
}
/**
* Pauses the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void pause(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setPlayStatePause(pid));
}
/**
* Starts the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void play(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setPlayStatePlay(pid));
}
/**
* Stops the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void stop(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setPlayStateStop(pid));
}
/**
* Jumps to the next song on the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void next(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playNext(pid));
}
/**
* Jumps to the previous song on the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void previous(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playPrevious(pid));
}
/**
* Toggles the mute state the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void mute(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteToggle(pid));
}
/**
* Mutes the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void muteON(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteOn(pid));
}
/**
* Un-mutes the HEOS player
*
* @param pid The PID of the dedicated player
*/
public void muteOFF(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteOff(pid));
}
/**
* Set the play mode of the player or group
*
* @param pid The PID of the dedicated player or group
* @param mode The shuffle mode: Allowed commands: on; off
*/
public void setShuffleMode(String pid, String mode) throws IOException, ReadException {
heosSystem.send(HeosCommands.setShuffleMode(pid, mode));
}
/**
* Sets the repeat mode of the player or group
*
* @param pid The ID of the dedicated player or group
* @param mode The repeat mode. Allowed commands: on_all; on_one; off
*/
public void setRepeatMode(String pid, String mode) throws IOException, ReadException {
heosSystem.send(HeosCommands.setRepeatMode(pid, mode));
}
/**
* Set the HEOS player to a dedicated volume
*
* @param vol The volume the player shall be set to (value between 0 -100)
* @param pid The ID of the dedicated player or group
*/
public void setVolume(String vol, String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setVolume(vol, pid));
}
/**
* Increases the HEOS player volume 1 Step
*
* @param pid The ID of the dedicated player or group
*/
public void increaseVolume(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.volumeUp(pid));
}
/**
* Decreases the HEOS player volume 1 Step
*
* @param pid The ID of the dedicated player or group
*/
public void decreaseVolume(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.volumeDown(pid));
}
/**
* Toggles mute state of the HEOS group
*
* @param gid The GID of the group
*/
public void muteGroup(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setMuteToggle(gid));
}
/**
* Mutes the HEOS group
*
* @param gid The GID of the group
*/
public void muteGroupON(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupMuteOn(gid));
}
/**
* Un-mutes the HEOS group
*
* @param gid The GID of the group
*/
public void muteGroupOFF(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupMuteOff(gid));
}
/**
* Set the volume of the group to a specific level
*
* @param vol The volume the group shall be set to (value between 0-100)
* @param gid The GID of the group
*/
public void volumeGroup(String vol, String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupVolume(vol, gid));
}
/**
* Increases the HEOS group volume 1 Step
*
* @param gid The ID of the dedicated player or group
*/
public void increaseGroupVolume(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupVolumeUp(gid));
}
/**
* Decreases the HEOS group volume 1 Step
*
* @param gid The ID of the dedicated player or group
*/
public void decreaseGroupVolume(String gid) throws IOException, ReadException {
heosSystem.send(HeosCommands.setGroupVolumeDown(gid));
}
/**
* Un-Group the HEOS group to single player
*
* @param gid The GID of the group
*/
public void ungroupGroup(String gid) throws IOException, ReadException {
String[] pid = new String[] { gid };
heosSystem.send(HeosCommands.setGroup(pid));
}
/**
* Builds a group from single players
*
* @param pids The single player IDs of the player which shall be grouped
* @return
*/
public boolean groupPlayer(String[] pids) throws IOException, ReadException {
return heosSystem.send(HeosCommands.setGroup(pids)).result;
}
/**
* Browses through a HEOS source. Currently no response
*
* @param sid The source sid which shall be browsed
* @return
*/
public HeosResponseObject<BrowseResult[]> browseSource(String sid) throws IOException, ReadException {
return heosSystem.send(HeosCommands.browseSource(sid), BrowseResult[].class);
}
/**
* Adds a media container to the queue and plays the media directly
* Information of the sid and cid has to be obtained via the browse function
*
* @param pid The player ID where the media object shall be played
* @param sid The source ID where the media is located
* @param cid The container ID of the media
*/
public void addContainerToQueuePlayNow(String pid, String sid, String cid) throws IOException, ReadException {
heosSystem.send(HeosCommands.addContainerToQueuePlayNow(pid, sid, cid));
}
/**
* Reboot the bridge to which the connection is established
*/
public void reboot() throws IOException, ReadException {
heosSystem.send(HeosCommands.rebootSystem());
}
/**
* Login in via the bridge to the HEOS account
*
* @param name The username
* @param password The password of the user
* @return
*/
public HeosResponseObject<Void> logIn(String name, String password) throws IOException, ReadException {
return heosSystem.send(HeosCommands.signIn(name, password));
}
/**
* Get all the players known by HEOS
*
* @return
*/
public HeosResponseObject<Player[]> getPlayers() throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayers(), Player[].class);
}
/**
* Get all the groups known by HEOS
*
* @return
*/
public HeosResponseObject<Group[]> getGroups() throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroups(), Group[].class);
}
/**
* Plays a specific station on the HEOS player
*
* @param pid The player ID
* @param sid The source ID where the media is located
* @param cid The container ID of the media
* @param mid The media ID of the media
* @param name Station name returned by 'browse' command.
*/
public void playStream(@Nullable String pid, @Nullable String sid, @Nullable String cid, @Nullable String mid,
@Nullable String name) throws IOException, ReadException {
heosSystem.send(HeosCommands.playStream(pid, sid, cid, mid, name));
}
/**
* Plays a specific station on the HEOS player
*
* @param pid The player ID
* @param sid The source ID where the media is located
* @param mid The media ID of the media
*/
public void playStream(String pid, String sid, String mid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playStream(pid, sid, mid));
}
/**
* Plays a specified local input source on the player.
* Input name as per specified in HEOS CLI Protocol
*
* @param pid
* @param input
*/
public void playInputSource(String pid, String input) throws IOException, ReadException {
heosSystem.send(HeosCommands.playInputSource(pid, pid, input));
}
/**
* Plays a specified input source from another player on the selected player.
* Input name as per specified in HEOS CLI Protocol
*
* @param destinationPid the PID where the source shall be played
* @param sourcePid the PID where the source is located.
* @param input the input name
*/
public void playInputSource(String destinationPid, String sourcePid, String input)
throws IOException, ReadException {
heosSystem.send(HeosCommands.playInputSource(destinationPid, sourcePid, input));
}
/**
* Plays a file from a URL
*
* @param pid the PID where the file shall be played
* @param url the complete URL the file is located
*/
public void playURL(String pid, URL url) throws IOException, ReadException {
heosSystem.send(HeosCommands.playURL(pid, url.toString()));
}
/**
* clear the queue
*
* @param pid The player ID the media is playing on
*/
public void clearQueue(String pid) throws IOException, ReadException {
heosSystem.send(HeosCommands.clearQueue(pid));
}
/**
* Deletes a media from the queue
*
* @param pid The player ID the media is playing on
* @param qid The queue ID of the media. (starts by 1)
*/
public void deleteMediaFromQueue(String pid, String qid) throws IOException, ReadException {
heosSystem.send(HeosCommands.deleteQueueItem(pid, qid));
}
/**
* Plays a specific media file from the queue
*
* @param pid The player ID the media shall be played on
* @param qid The queue ID of the media. (starts by 1)
*/
public void playMediaFromQueue(String pid, String qid) throws IOException, ReadException {
heosSystem.send(HeosCommands.playQueueItem(pid, qid));
}
/**
* Asks for the actual state of the player. The result has
* to be handled by the event controller. The system returns {@link HeosConstants.PLAY},
* {@link HeosConstants.PAUSE} or {@link HeosConstants.STOP}.
*
* @param id The player ID the state shall get for
* @return
*/
public HeosResponseObject<Void> getPlayState(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayState(id));
}
/**
* Ask for the actual mute state of the player. The result has
* to be handled by the event controller. The HEOS system returns {@link HeosConstants.ON}
* or {@link HeosConstants.OFF}.
*
* @param id The player id the mute state shall get for
* @return
*/
public HeosResponseObject<Void> getPlayerMuteState(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getMute(id));
}
/**
* Ask for the actual volume the player. The result has
* to be handled by the event controller. The HEOS system returns
* a value between 0 and 100
*
* @param id The player id the volume shall get for
* @return
*/
public HeosResponseObject<Void> getPlayerVolume(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getVolume(id));
}
/**
* Ask for the actual shuffle mode of the player. The result has
* to be handled by the event controller. The HEOS system returns {@link HeosConstants.ON},
* {@link HeosConstants.HEOS_REPEAT_ALL} or {@link HeosConstants.HEOS_REPEAT_ONE}
*
* @param id The player id the shuffle mode shall get for
* @return
*/
public HeosResponseObject<Void> getPlayMode(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getPlayMode(id));
}
public HeosResponseObject<Void> getGroupMuteState(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroupMute(id));
}
public HeosResponseObject<Void> getGroupVolume(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getGroupVolume(id));
}
public HeosResponseObject<Media> getNowPlayingMedia(String id) throws IOException, ReadException {
return heosSystem.send(HeosCommands.getNowPlayingMedia(id), Media.class);
}
/**
* Sends a RAW command to the HEOS bridge. The command has to be
* in accordance with the HEOS CLI specification
*
* @param command to send
* @return
*/
public HeosResponseObject<JsonElement> sendRawCommand(String command) throws IOException, ReadException {
return heosSystem.send(command, JsonElement.class);
}
/**
* Register an {@link HeosEventListener} to get notification of system events
*
* @param listener The HeosEventListener
*/
public void registerForChangeEvents(HeosEventListener listener) {
eventController.addListener(listener);
}
/**
* Unregister an {@link HeosEventListener} to get notification of system events
*
* @param listener The HeosEventListener
*/
public void unregisterForChangeEvents(HeosEventListener listener) {
eventController.removeListener(listener);
}
public boolean isConnected() {
return heosSystem.isConnected();
}
public void closeConnection() {
heosSystem.closeConnection();
}
}

View File

@@ -0,0 +1,223 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.api;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.HeosJsonParser;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.resources.HeosCommands;
import org.openhab.binding.heos.internal.resources.HeosSendCommand;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* The {@link HeosSystem} is handling the main commands, which are
* sent and received by the HEOS system.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosSystem {
private final Logger logger = LoggerFactory.getLogger(HeosSystem.class);
private static final int START_DELAY_SEC = 30;
private static final long LAST_EVENT_THRESHOLD = TimeUnit.HOURS.toMillis(2);
private final ScheduledExecutorService scheduler;
private @Nullable ExecutorService singleThreadExecutor;
private final HeosEventController eventController = new HeosEventController(this);
private final Telnet eventLine = new Telnet();
private final HeosSendCommand eventSendCommand = new HeosSendCommand(eventLine);
private final Telnet commandLine = new Telnet();
private final HeosSendCommand sendCommand = new HeosSendCommand(commandLine);
private final HeosJsonParser parser = new HeosJsonParser();
private final PropertyChangeListener eventProcessor = evt -> {
String newValue = (String) evt.getNewValue();
ExecutorService executor = singleThreadExecutor;
if (executor == null) {
logger.debug("No executor available ignoring event: {}", newValue);
return;
}
try {
executor.submit(() -> eventController.handleEvent(parser.parseEvent(newValue)));
} catch (JsonSyntaxException e) {
logger.debug("Failed processing event JSON", e);
}
};
private @Nullable ScheduledFuture<?> keepAliveJob;
private @Nullable ScheduledFuture<?> reconnectJob;
public HeosSystem(ScheduledExecutorService scheduler) {
this.scheduler = scheduler;
}
/**
* Establishes the connection to the HEOS-Network if IP and Port is
* set. The caller has to handle the retry to establish the connection
* if the method returns {@code false}.
*
* @param connectionIP
* @param connectionPort
* @param heartbeat
* @return {@code true} if connection is established else returns {@code false}
*/
public HeosFacade establishConnection(String connectionIP, int connectionPort, int heartbeat)
throws IOException, ReadException {
singleThreadExecutor = Executors.newSingleThreadExecutor();
if (commandLine.connect(connectionIP, connectionPort)) {
logger.debug("HEOS command line connected at IP {} @ port {}", connectionIP, connectionPort);
send(HeosCommands.registerChangeEventOff());
}
if (eventLine.connect(connectionIP, connectionPort)) {
logger.debug("HEOS event line connected at IP {} @ port {}", connectionIP, connectionPort);
eventSendCommand.send(HeosCommands.registerChangeEventOff(), Void.class);
}
startHeartBeat(heartbeat);
startEventListener();
return new HeosFacade(this, eventController);
}
boolean isConnected() {
return sendCommand.isConnected() && eventSendCommand.isConnected();
}
/**
* Starts the HEOS Heart Beat. This held the connection open even
* if no data is transmitted. If the connection to the HEOS system
* is lost, the method reconnects to the HEOS system by calling the
* {@code establishConnection()} method. If the connection is lost or
* reconnect the method fires a bridgeEvent via the {@code HeosEvenController.class}
*/
void startHeartBeat(int heartbeatPulse) {
keepAliveJob = scheduler.scheduleWithFixedDelay(new KeepAliveRunnable(), START_DELAY_SEC, heartbeatPulse,
TimeUnit.SECONDS);
}
synchronized void startEventListener() throws IOException, ReadException {
logger.debug("HEOS System Event Listener is starting....");
eventSendCommand.startInputListener(HeosCommands.registerChangeEventOn());
logger.debug("HEOS System Event Listener successfully started");
eventLine.getReadResultListener().addPropertyChangeListener(eventProcessor);
}
void closeConnection() {
logger.debug("Shutting down HEOS Heart Beat");
cancel(keepAliveJob);
cancel(this.reconnectJob, false);
eventLine.getReadResultListener().removePropertyChangeListener(eventProcessor);
eventSendCommand.stopInputListener(HeosCommands.registerChangeEventOff());
eventSendCommand.disconnect();
sendCommand.disconnect();
@Nullable
ExecutorService executor = this.singleThreadExecutor;
if (executor != null && executor.isShutdown()) {
executor.shutdownNow();
}
}
HeosResponseObject<Void> send(String command) throws IOException, ReadException {
return send(command, Void.class);
}
synchronized <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
return sendCommand.send(command, clazz);
}
/**
* A class which provides a runnable for the HEOS Heart Beat
*
* @author Johannes Einig
*/
private class KeepAliveRunnable implements Runnable {
@Override
public void run() {
try {
if (sendCommand.isHostReachable()) {
long timeSinceLastEvent = System.currentTimeMillis() - eventController.getLastEventTime();
logger.debug("Time since latest event: {} s", timeSinceLastEvent / 1000);
if (timeSinceLastEvent > LAST_EVENT_THRESHOLD) {
logger.debug("Events haven't been received for too long");
resetEventStream();
return;
}
logger.debug("Sending HEOS Heart Beat");
HeosResponseObject<Void> response = send(HeosCommands.heartbeat());
if (response.result) {
return;
}
}
logger.debug("Connection to HEOS Network lost!");
// catches a failure during a heart beat send message if connection was
// getting lost between last Heart Beat but Bridge is online again and not
// detected by isHostReachable()
} catch (ReadException | IOException e) {
logger.debug("Failed at {}", System.currentTimeMillis(), e);
logger.debug("Failure during HEOS Heart Beat command with message: {}", e.getMessage());
}
restartConnection();
}
private void restartConnection() {
reset(a -> eventController.connectionToSystemLost());
}
private void resetEventStream() {
reset(a -> eventController.eventStreamTimeout());
}
private void reset(Consumer<@Nullable Void> method) {
closeConnection();
method.accept(null);
cancel(HeosSystem.this.reconnectJob, false);
reconnectJob = scheduler.scheduleWithFixedDelay(this::reconnect, 1, 5, TimeUnit.SECONDS);
}
private void reconnect() {
logger.debug("Trying to reconnect to HEOS Network...");
if (!sendCommand.isHostReachable()) {
return;
}
cancel(HeosSystem.this.reconnectJob, false);
logger.debug("Reconnecting to Bridge");
scheduler.schedule(eventController::systemReachable, 15, TimeUnit.SECONDS);
}
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration wrapper for bridge configuration
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class BridgeConfiguration {
public static final String IP_ADDRESS = "ipAddress";
/**
* Network address of the HEOS bridge
*/
public String ipAddress = "";
/**
* Username for login to the HEOS account.
*/
public @Nullable String username;
/**
* Password for login to the HEOS account
*/
public @Nullable String password;
/**
* The time in seconds for the HEOS Heartbeat (default = 60 s)
*/
public int heartbeat;
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration wrapper for group configuration
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class GroupConfiguration {
/**
*
*/
public String members = "";
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration wrapper for player configuration
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class PlayerConfiguration {
/**
* The Player ID
*/
public String pid = "";
}

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.discovery;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jupnp.model.meta.DeviceDetails;
import org.jupnp.model.meta.ModelDetails;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.binding.heos.internal.configuration.BridgeConfiguration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosDiscoveryParticipant} discovers the HEOS Player of the
* network via an UPnP interface.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
@Component(service = UpnpDiscoveryParticipant.class, immediate = true, configurationPid = "discovery.heos")
public class HeosDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(HeosDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(THING_TYPE_BRIDGE);
}
@Override
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
ThingUID uid = getThingUID(device);
if (uid != null) {
Map<String, Object> properties = new HashMap<>();
properties.put(Thing.PROPERTY_VENDOR, device.getDetails().getManufacturerDetails().getManufacturer());
properties.put(Thing.PROPERTY_MODEL_ID, getModel(device.getDetails().getModelDetails()));
properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getDetails().getSerialNumber());
properties.put(BridgeConfiguration.IP_ADDRESS, device.getIdentity().getDescriptorURL().getHost());
properties.put(PROP_NAME, device.getDetails().getFriendlyName());
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
.withLabel(" Bridge - " + device.getDetails().getFriendlyName())
.withRepresentationProperty(Thing.PROPERTY_VENDOR).build();
logger.debug("Found HEOS device with UID: {}", uid.getAsString());
return result;
}
return null;
}
private String getModel(ModelDetails modelDetails) {
return String.format("%s (%s)", modelDetails.getModelName(), modelDetails.getModelNumber());
}
@Override
public @Nullable ThingUID getThingUID(RemoteDevice device) {
DeviceDetails details = device.getDetails();
String modelName = details.getModelDetails().getModelName();
String modelManufacturer = details.getManufacturerDetails().getManufacturer();
if ("Denon".equals(modelManufacturer) && (modelName.startsWith("HEOS") || modelName.endsWith("H"))) {
String deviceType = device.getType().getType();
if (deviceType.startsWith("ACT") || deviceType.startsWith("Aios")) {
return new ThingUID(THING_TYPE_BRIDGE, device.getIdentity().getUdn().getIdentifierString());
}
}
return null;
}
}

View File

@@ -0,0 +1,230 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.discovery;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
import org.openhab.binding.heos.internal.handler.HeosPlayerHandler;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosGroup;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosPlayerDiscovery} discovers the player and groups within
* the HEOS network and reacts on changed groups or player.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosPlayerDiscovery extends AbstractDiscoveryService implements HeosPlayerDiscoveryListener {
private final Logger logger = LoggerFactory.getLogger(HeosPlayerDiscovery.class);
private static final int SEARCH_TIME = 5;
private static final int INITIAL_DELAY = 5;
private static final int SCAN_INTERVAL = 20;
private final HeosBridgeHandler bridge;
private Map<Integer, Player> players = new HashMap<>();
private Map<String, Group> groups = new HashMap<>();
private @Nullable ScheduledFuture<?> scanningJob;
public HeosPlayerDiscovery(HeosBridgeHandler bridge) throws IllegalArgumentException {
super(SEARCH_TIME);
this.bridge = bridge;
bridge.registerPlayerDiscoverListener(this);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return Stream.of(THING_TYPE_GROUP, THING_TYPE_PLAYER).collect(Collectors.toSet());
}
@Override
protected void startScan() {
if (!bridge.isBridgeConnected()) {
logger.debug("Scan for Players not possible. HEOS Bridge is not connected");
return;
}
scanForPlayers();
scanForGroups();
}
private void scanForPlayers() {
logger.debug("Start scan for HEOS Player");
try {
Map<Integer, Player> currentPlayers = new HashMap<>();
for (Player player : bridge.getPlayers()) {
currentPlayers.put(player.playerId, player);
}
handleRemovedPlayers(findRemovedEntries(currentPlayers, players));
handleDiscoveredPlayers(currentPlayers);
players = currentPlayers;
} catch (IOException | Telnet.ReadException e) {
logger.debug("Failed getting/processing groups", e);
}
}
private void handleDiscoveredPlayers(Map<Integer, Player> currentPlayers) {
logger.debug("Found: {} player", currentPlayers.size());
ThingUID bridgeUID = bridge.getThing().getUID();
for (Player player : currentPlayers.values()) {
ThingUID uid = new ThingUID(THING_TYPE_PLAYER, bridgeUID, String.valueOf(player.playerId));
Map<String, Object> properties = new HashMap<>();
HeosPlayerHandler.propertiesFromPlayer(properties, player);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(player.name)
.withProperties(properties).withBridge(bridgeUID)
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
thingDiscovered(result);
}
}
private void handleRemovedPlayers(Map<Integer, Player> removedPlayers) {
for (Player player : removedPlayers.values()) {
// The same as above!
ThingUID uid = new ThingUID(THING_TYPE_PLAYER, String.valueOf(player.playerId));
logger.debug("Removed HEOS Player: {} ", uid);
thingRemoved(uid);
}
}
private void scanForGroups() {
logger.debug("Start scan for HEOS Groups");
try {
HashMap<String, Group> currentGroups = new HashMap<>();
for (Group group : bridge.getGroups()) {
logger.debug("Found: Group {} with {} Players", group.name, group.players.size());
currentGroups.put(HeosGroup.calculateGroupMemberHash(group), group);
}
handleRemovedGroups(findRemovedEntries(currentGroups, groups));
handleDiscoveredGroups(currentGroups);
groups = currentGroups;
} catch (IOException | Telnet.ReadException e) {
logger.debug("Failed getting/processing groups", e);
}
}
private void handleDiscoveredGroups(HashMap<String, Group> currentGroups) {
if (currentGroups.isEmpty()) {
logger.debug("No HEOS Groups found");
return;
}
logger.debug("Found: {} new Groups", currentGroups.size());
ThingUID bridgeUID = bridge.getThing().getUID();
for (Map.Entry<String, Group> entry : currentGroups.entrySet()) {
Group group = entry.getValue();
String groupMemberHash = entry.getKey();
// Using an unsigned hashCode from the group members to identify
// the group and generates the Thing UID.
// This allows identifying the group even if the sorting within the group has changed
ThingUID uid = new ThingUID(THING_TYPE_GROUP, bridgeUID, groupMemberHash);
Map<String, Object> properties = new HashMap<>();
properties.put(PROP_NAME, group.name);
properties.put(PROP_GID, group.id);
String groupMembers = group.players.stream().map(p -> p.id).collect(Collectors.joining(";"));
properties.put(PROP_GROUP_MEMBERS, groupMembers);
properties.put(PROP_GROUP_LEADER, group.players.get(0).id);
properties.put(PROP_GROUP_HASH, groupMemberHash);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(group.name).withProperties(properties)
.withBridge(bridgeUID).withRepresentationProperty(PROP_GROUP_HASH).build();
thingDiscovered(result);
bridge.setGroupOnline(groupMemberHash, group.id);
}
}
private void handleRemovedGroups(Map<String, Group> removedGroups) {
for (String groupMemberHash : removedGroups.keySet()) {
// The same as above!
ThingUID uid = new ThingUID(THING_TYPE_GROUP, groupMemberHash);
logger.debug("Removed HEOS Group: {}", uid);
thingRemoved(uid);
bridge.setGroupOffline(groupMemberHash);
}
}
private <K, V> Map<K, V> findRemovedEntries(Map<K, V> mapNew, Map<K, V> mapOld) {
Map<K, V> removedItems = new HashMap<>();
for (K key : mapOld.keySet()) {
if (!mapNew.containsKey(key)) {
removedItems.put(key, mapOld.get(key));
}
}
return removedItems;
}
@Override
protected void startBackgroundDiscovery() {
ScheduledFuture<?> runningScanningJob = this.scanningJob;
if (runningScanningJob == null || runningScanningJob.isCancelled()) {
this.scanningJob = scheduler.scheduleWithFixedDelay(this::startScan, INITIAL_DELAY, SCAN_INTERVAL,
TimeUnit.SECONDS);
}
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stop HEOS Player background discovery");
cancel(scanningJob);
}
@Override
protected synchronized void stopScan() {
super.stopScan();
removeOlderResults(getTimestampOfLastScan());
}
private void scanForNewPlayers() {
removeOlderResults(getTimestampOfLastScan());
startScan();
}
@Override
public void playerChanged() {
scanForNewPlayers();
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.discovery;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.handler.HeosBridgeHandler;
/**
* The {@link HeosPlayerDiscoveryListener } is an Event Listener
* for the HEOS network. Handler which wants the get informed
* if the player or groups within the HEOS network have changed has to
* implement this class and register itself at the {@link HeosBridgeHandler}
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public interface HeosPlayerDiscoveryListener {
void playerChanged();
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.exception;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.dto.HeosErrorCode;
/**
* Exception to inform the caller that there is functional error reported by the HEOS system
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosFunctionalException extends IOException {
private final HeosErrorCode code;
public HeosFunctionalException(HeosErrorCode code) {
this.code = code;
}
public HeosErrorCode getCode() {
return code;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.exception;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception to inform the caller that there is no connection to the HEOS system (yet)
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosNotConnectedException extends IOException {
public HeosNotConnectedException() {
super("HEOS not connected");
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.exception;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception to inform the caller that there is no connection to the HEOS system (yet)
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosNotFoundException extends IOException {
public HeosNotFoundException() {
super("HEOS not found");
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
/**
* Base class for the channel handlers
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public abstract class BaseHeosChannelHandler implements HeosChannelHandler {
final HeosBridgeHandler bridge;
public BaseHeosChannelHandler(HeosBridgeHandler bridge) {
this.bridge = bridge;
}
protected HeosFacade getApi() throws HeosNotConnectedException {
return bridge.getApiConnection();
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.util.concurrent.Future;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Instead of continously rewriting the termination of future a small util method was added
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class FutureUtil {
private FutureUtil() {
// this is a util no instances should be created
}
/**
* Cancel the future
*
* - when it is not null
* - and it is not already cancelled
*
* interrupt if still/already running
*
* @param future nullable future to be cancelled
*/
public static void cancel(@Nullable Future<?> future) {
cancel(future, true);
}
/**
* Cancel the future
*
* - when it is not null
* - and it is not already cancelled
*
* @param future nullable future to be cancelled
* @param interruptIfRunning choose whether to interrupt a running future
*/
public static void cancel(@Nullable Future<?> future, boolean interruptIfRunning) {
if (future != null && !future.isCancelled()) {
future.cancel(interruptIfRunning);
}
}
}

View File

@@ -0,0 +1,519 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.core.thing.ThingStatus.OFFLINE;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.HeosChannelHandlerFactory;
import org.openhab.binding.heos.internal.HeosChannelManager;
import org.openhab.binding.heos.internal.action.HeosActions;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.api.HeosSystem;
import org.openhab.binding.heos.internal.configuration.BridgeConfiguration;
import org.openhab.binding.heos.internal.discovery.HeosPlayerDiscoveryListener;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.dto.HeosError;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosBridgeHandler extends BaseBridgeHandler implements HeosEventListener {
private final Logger logger = LoggerFactory.getLogger(HeosBridgeHandler.class);
private static final int HEOS_PORT = 1255;
private final List<HeosPlayerDiscoveryListener> playerDiscoveryList = new CopyOnWriteArrayList<>();
private final HeosChannelManager channelManager = new HeosChannelManager(this);
private final HeosChannelHandlerFactory channelHandlerFactory;
private final Map<String, HeosGroupHandler> groupHandlerMap = new ConcurrentHashMap<>();
private final Map<String, String> hashToGidMap = new ConcurrentHashMap<>();
private List<String[]> selectedPlayerList = new CopyOnWriteArrayList<>();
private @Nullable Future<?> startupFuture;
private final List<Future<?>> childHandlerInitializedFutures = new CopyOnWriteArrayList<>();
private final HeosSystem heosSystem;
private @Nullable HeosFacade apiConnection;
private boolean loggedIn = false;
private boolean bridgeHandlerDisposalOngoing = false;
private @NonNullByDefault({}) BridgeConfiguration configuration;
private int failureCount;
public HeosBridgeHandler(Bridge bridge, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(bridge);
heosSystem = new HeosSystem(scheduler);
channelHandlerFactory = new HeosChannelHandlerFactory(this, heosDynamicStateDescriptionProvider);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
return;
}
@Nullable
Channel channel = this.getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.debug("No valid channel found");
return;
}
@Nullable
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
@Nullable
HeosChannelHandler channelHandler = channelHandlerFactory.getChannelHandler(channelUID, this, channelTypeUID);
if (channelHandler != null) {
try {
channelHandler.handleBridgeCommand(command, thing.getUID());
failureCount = 0;
updateStatus(ONLINE);
} catch (IOException | ReadException e) {
logger.debug("Failed to handle bridge command", e);
failureCount++;
if (failureCount > FAILURE_COUNT_LIMIT) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Failed to handle command: " + e.getMessage());
}
}
}
}
@Override
public synchronized void initialize() {
configuration = thing.getConfiguration().as(BridgeConfiguration.class);
cancel(startupFuture);
startupFuture = scheduler.submit(this::delayedInitialize);
}
private void delayedInitialize() {
@Nullable
HeosFacade connection = null;
try {
logger.debug("Running scheduledStartUp job");
connection = connectBridge();
updateStatus(ThingStatus.ONLINE);
updateState(CH_ID_REBOOT, OnOffType.OFF);
logger.debug("HEOS System heart beat started. Pulse time is {}s", configuration.heartbeat);
// gets all available player and groups to ensure that the system knows
// about the conjunction between the groupMemberHash and the GID
triggerPlayerDiscovery();
@Nullable
String username = configuration.username;
@Nullable
String password = configuration.password;
if (username != null && !"".equals(username) && password != null && !"".equals(password)) {
login(connection, username, password);
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Can't log in. Username or password not set.");
}
fetchPlayersAndGroups();
} catch (Telnet.ReadException | IOException | RuntimeException e) {
logger.debug("Error occurred while connecting", e);
if (connection != null) {
connection.closeConnection();
}
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Errors occurred: " + e.getMessage());
cancel(startupFuture, false);
startupFuture = scheduler.schedule(this::delayedInitialize, 30, TimeUnit.SECONDS);
}
}
private void fetchPlayersAndGroups() {
try {
@Nullable
Player[] onlinePlayers = getApiConnection().getPlayers().payload;
@Nullable
Group[] onlineGroups = getApiConnection().getGroups().payload;
updatePlayerStatus(onlinePlayers, onlineGroups);
} catch (ReadException | IOException e) {
logger.debug("Failed updating online state of groups/players", e);
}
}
private void updatePlayerStatus(@Nullable Player[] onlinePlayers, @Nullable Group[] onlineGroups) {
if (onlinePlayers == null || onlineGroups == null) {
return;
}
Set<String> players = Stream.of(onlinePlayers).map(p -> Objects.toString(p.playerId))
.collect(Collectors.toSet());
Set<String> groups = Stream.of(onlineGroups).map(p -> p.id).collect(Collectors.toSet());
for (Thing thing : getThing().getThings()) {
try {
@Nullable
ThingHandler handler = thing.getHandler();
if (handler instanceof HeosThingBaseHandler) {
Set<String> target = handler instanceof HeosPlayerHandler ? players : groups;
HeosThingBaseHandler heosHandler = (HeosThingBaseHandler) handler;
String id = heosHandler.getId();
if (target.contains(id)) {
heosHandler.setStatusOnline();
} else {
heosHandler.setStatusOffline();
}
}
} catch (HeosNotFoundException e) {
logger.debug("SKipping handler which reported not found", e);
}
}
}
private HeosFacade connectBridge() throws IOException, Telnet.ReadException {
loggedIn = false;
logger.debug("Initialize Bridge '{}' with IP '{}'", thing.getProperties().get(PROP_NAME),
configuration.ipAddress);
bridgeHandlerDisposalOngoing = false;
HeosFacade connection = heosSystem.establishConnection(configuration.ipAddress, HEOS_PORT,
configuration.heartbeat);
connection.registerForChangeEvents(this);
apiConnection = connection;
return connection;
}
private void login(HeosFacade connection, String username, String password) throws IOException, ReadException {
logger.debug("Logging in to HEOS account.");
HeosResponseObject<Void> response = connection.logIn(username, password);
if (response.result) {
logger.debug("successfully logged-in, event is fired to handle post-login behaviour");
return;
}
@Nullable
HeosError error = response.getError();
logger.debug("Failed to login: {}", error);
updateStatus(ONLINE, ThingStatusDetail.CONFIGURATION_ERROR,
error != null ? error.code.toString() : "Failed to login, no error was returned.");
}
@Override
public void dispose() {
bridgeHandlerDisposalOngoing = true; // Flag to prevent the handler from being updated during disposal
cancel(startupFuture);
for (Future<?> future : childHandlerInitializedFutures) {
cancel(future);
}
@Nullable
HeosFacade localApiConnection = apiConnection;
if (localApiConnection == null) {
logger.debug("Not disposing bridge because of missing apiConnection");
return;
}
localApiConnection.unregisterForChangeEvents(this);
logger.debug("HEOS bridge removed from change notifications");
logger.debug("Dispose bridge '{}'", thing.getProperties().get(PROP_NAME));
localApiConnection.closeConnection();
}
/**
* Manages the removal of the player or group channels from the bridge.
*/
@Override
public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
logger.debug("Disposing child handler for: {}.", childThing.getUID().getId());
if (bridgeHandlerDisposalOngoing) { // Checks if bridgeHandler is going to disposed (by stopping the binding or
// openHAB for example) and prevents it from being updated which stops the
// disposal process.
} else if (childHandler instanceof HeosPlayerHandler) {
String channelIdentifier = "P" + childThing.getUID().getId();
updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
} else if (childHandler instanceof HeosGroupHandler) {
String channelIdentifier = "G" + childThing.getUID().getId();
updateThingChannels(channelManager.removeSingleChannel(channelIdentifier));
// removes the handler from the groupMemberMap that handler is no longer called
// if group is getting online
removeGroupHandlerInformation((HeosGroupHandler) childHandler);
}
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
logger.debug("Initialized child handler for: {}.", childThing.getUID().getId());
childHandlerInitializedFutures.add(scheduler.submit(() -> addPlayerChannel(childThing, null)));
}
void resetPlayerList(ChannelUID channelUID) {
selectedPlayerList.forEach(element -> updateState(element[1], OnOffType.OFF));
selectedPlayerList.clear();
updateState(channelUID, OnOffType.OFF);
}
/**
* Sets the HEOS Thing offline
*/
@SuppressWarnings("null")
public void setGroupOffline(String groupMemberHash) {
HeosGroupHandler groupHandler = groupHandlerMap.get(groupMemberHash);
if (groupHandler != null) {
groupHandler.setStatusOffline();
}
hashToGidMap.remove(groupMemberHash);
}
/**
* Sets the HEOS Thing online. Also updates the link between
* the groupMemberHash value with the actual gid of this group
*/
public void setGroupOnline(String groupMemberHash, String groupId) {
hashToGidMap.put(groupMemberHash, groupId);
Optional.ofNullable(groupHandlerMap.get(groupMemberHash)).ifPresent(handler -> {
handler.setStatusOnline();
addPlayerChannel(handler.getThing(), groupId);
});
}
/**
* Create a channel for the childThing. Depending if it is a HEOS Group
* or a player an identification prefix is added
*
* @param childThing the thing the channel is created for
* @param groupId
*/
private void addPlayerChannel(Thing childThing, @Nullable String groupId) {
try {
String channelIdentifier = "";
String pid = "";
@Nullable
ThingHandler handler = childThing.getHandler();
if (handler instanceof HeosPlayerHandler) {
channelIdentifier = "P" + childThing.getUID().getId();
pid = ((HeosPlayerHandler) handler).getId();
} else if (handler instanceof HeosGroupHandler) {
channelIdentifier = "G" + childThing.getUID().getId();
if (groupId == null) {
pid = ((HeosGroupHandler) handler).getId();
} else {
pid = groupId;
}
}
Map<String, String> properties = new HashMap<>();
@Nullable
String playerName = childThing.getLabel();
playerName = playerName == null ? pid : playerName;
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelIdentifier);
properties.put(PROP_NAME, playerName);
properties.put(PID, pid);
Channel channel = ChannelBuilder.create(channelUID, "Switch").withLabel(playerName).withType(CH_TYPE_PLAYER)
.withProperties(properties).build();
updateThingChannels(channelManager.addSingleChannel(channel));
} catch (HeosNotFoundException e) {
logger.debug("Group is not yet initialized fully");
}
}
public void addGroupHandlerInformation(HeosGroupHandler handler) {
groupHandlerMap.put(handler.getGroupMemberHash(), handler);
}
private void removeGroupHandlerInformation(HeosGroupHandler handler) {
groupHandlerMap.remove(handler.getGroupMemberHash());
}
public @Nullable String getActualGID(String groupHash) {
return hashToGidMap.get(groupHash);
}
@Override
public void playerStateChangeEvent(HeosEventObject eventObject) {
// do nothing
}
@Override
public void playerStateChangeEvent(HeosResponseObject<?> responseObject) {
// do nothing
}
@Override
public void playerMediaChangeEvent(String pid, Media media) {
// do nothing
}
@Override
public void bridgeChangeEvent(String event, boolean success, Object command) {
if (EVENT_TYPE_EVENT.equals(event)) {
if (HeosEvent.PLAYERS_CHANGED.equals(command) || HeosEvent.GROUPS_CHANGED.equals(command)) {
fetchPlayersAndGroups();
triggerPlayerDiscovery();
} else if (EVENT_STREAM_TIMEOUT.equals(command)) {
logger.debug("HEOS Bridge events timed-out might be nothing, trying to reconnect");
} else if (CONNECTION_LOST.equals(command)) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
logger.debug("HEOS Bridge OFFLINE");
} else if (CONNECTION_RESTORED.equals(command)) {
initialize();
}
}
if (EVENT_TYPE_SYSTEM.equals(event) && HeosEvent.USER_CHANGED == command) {
if (success && !loggedIn) {
loggedIn = true;
}
}
}
private synchronized void updateThingChannels(List<Channel> channelList) {
ThingBuilder thingBuilder = editThing();
thingBuilder.withChannels(channelList);
updateThing(thingBuilder.build());
}
public Player[] getPlayers() throws IOException, ReadException {
HeosResponseObject<Player[]> response = getApiConnection().getPlayers();
@Nullable
Player[] players = response.payload;
if (players == null) {
throw new IOException("Received no valid payload");
}
return players;
}
public Group[] getGroups() throws IOException, ReadException {
HeosResponseObject<Group[]> response = getApiConnection().getGroups();
@Nullable
Group[] groups = response.payload;
if (groups == null) {
throw new IOException("Received no valid payload");
}
return groups;
}
/**
* The list with the currently selected player
*
* @return a HashMap which the currently selected player
*/
public Map<String, String> getSelectedPlayer() {
return selectedPlayerList.stream().collect(Collectors.toMap(a -> a[0], a -> a[1], (a, b) -> a));
}
public List<String[]> getSelectedPlayerList() {
return selectedPlayerList;
}
public void setSelectedPlayerList(List<String[]> selectedPlayerList) {
this.selectedPlayerList = selectedPlayerList;
}
public HeosChannelHandlerFactory getChannelHandlerFactory() {
return channelHandlerFactory;
}
/**
* Register an {@link HeosPlayerDiscoveryListener} to get informed
* if the amount of groups or players have changed
*
* @param listener the implementing class
*/
public void registerPlayerDiscoverListener(HeosPlayerDiscoveryListener listener) {
playerDiscoveryList.add(listener);
}
private void triggerPlayerDiscovery() {
playerDiscoveryList.forEach(HeosPlayerDiscoveryListener::playerChanged);
}
public boolean isLoggedIn() {
return loggedIn;
}
public boolean isBridgeConnected() {
@Nullable
HeosFacade connection = apiConnection;
return connection != null && connection.isConnected();
}
public HeosFacade getApiConnection() throws HeosNotConnectedException {
@Nullable
HeosFacade localApiConnection = apiConnection;
if (localApiConnection != null) {
return localApiConnection;
} else {
throw new HeosNotConnectedException();
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(HeosActions.class);
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
/**
* The {@link HeosChannelHandler} handles the base class for the different
* channel handler which handles the command from the channels of the things
* to the HEOS system
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public interface HeosChannelHandler {
/**
* Handle a command received from a channel. Requires the class which
* wants to handle the command to decide which subclass has to be used
*
* @param command the command to handle
* @param id of the group or player
* @param uid
*/
void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException;
void handleGroupCommand(Command command, @Nullable String id, ThingUID uid, HeosGroupHandler heosGroupHandler)
throws IOException, ReadException;
/**
* Handles a command for classes without an id. Used
* for BridgeHandler
*
* @param command the command to handle
* @param uid
*/
void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException;
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerBuildGroup} handles the BuidlGroup channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution *
*/
@NonNullByDefault
public class HeosChannelHandlerBuildGroup extends BaseHeosChannelHandler {
private final ChannelUID channelUID;
public HeosChannelHandlerBuildGroup(ChannelUID channelUID, HeosBridgeHandler bridge) {
super(bridge);
this.channelUID = channelUID;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
bridge.resetPlayerList(channelUID);
return;
}
if (command == OnOffType.ON) {
List<String[]> selectedPlayerList = bridge.getSelectedPlayerList();
if (!selectedPlayerList.isEmpty()) {
getApi().groupPlayer(selectedPlayerList.stream().map(a -> a[0]).toArray(String[]::new));
bridge.resetPlayerList(channelUID);
}
}
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerReboot} handles the Reboot channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerClearQueue extends BaseHeosChannelHandler {
public HeosChannelHandlerClearQueue(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// Not used on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
if (command == OnOffType.ON) {
getApi().clearQueue(id);
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerControl} handles the control commands
* coming from the implementing thing
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerControl extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerControl(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel within bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayState(id));
return;
}
switch (command.toString()) {
case "PLAY":
case "ON":
getApi().play(id);
break;
case "PAUSE":
case "OFF":
getApi().pause(id);
break;
case "NEXT":
getApi().next(id);
break;
case "PREVIOUS":
getApi().previous(id);
break;
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.CH_ID_FAVORITES;
import static org.openhab.binding.heos.internal.resources.HeosConstants.FAVORITE_SID;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerFavorite} handles the playlist selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerFavorite extends BaseHeosChannelHandler {
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
public HeosChannelHandlerFavorite(HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider,
HeosBridgeHandler bridge) {
super(bridge);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id, uid);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id, uid);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
ChannelUID channelUID = new ChannelUID(uid, CH_ID_FAVORITES);
if (command instanceof RefreshType) {
heosDynamicStateDescriptionProvider.setFavorites(channelUID, getApi().getFavorites());
return;
}
String idCommand = heosDynamicStateDescriptionProvider.getValueByLabel(channelUID, command.toString());
getApi().playStream(id, FAVORITE_SID, idCommand);
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerGrouping} handles the grouping channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerGrouping extends BaseHeosChannelHandler {
public HeosChannelHandlerGrouping(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// No such channel on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
if (OnOffType.OFF == command) {
if (id == null) {
throw new HeosNotFoundException();
}
getApi().ungroupGroup(id);
} else if (OnOffType.ON == command) {
getApi().groupPlayer(heosGroupHandler.getGroupMemberPidList());
}
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel on Bridge
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosChannelHandlerInputs} handles the Input channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerInputs extends BaseHeosChannelHandler {
protected final Logger logger = LoggerFactory.getLogger(HeosChannelHandlerInputs.class);
private final HeosEventListener eventListener;
public HeosChannelHandlerInputs(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
@Nullable
Media payload = getApi().getNowPlayingMedia(id).payload;
if (payload != null) {
eventListener.playerMediaChangeEvent(id, payload);
}
return;
}
Map<String, String> selectedPlayers = bridge.getSelectedPlayer();
if (selectedPlayers.isEmpty()) {
// no selected player, just play it from the player itself
getApi().playInputSource(id, command.toString());
} else if (selectedPlayers.size() > 1) {
logger.debug("Only one source can be selected for HEOS Input. Selected amount of sources: {} ",
selectedPlayers.size());
} else {
for (String sourcePid : selectedPlayers.keySet()) {
getApi().playInputSource(id, sourcePid, command.toString());
}
}
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerMute} handles the Mute channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerMute extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerMute(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayerMuteState(id));
return;
}
if (command.equals(OnOffType.ON)) {
getApi().muteON(id);
} else if (command.equals(OnOffType.OFF)) {
getApi().muteOFF(id);
}
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getGroupMuteState(id));
return;
}
if (command.equals(OnOffType.ON)) {
getApi().muteGroupON(id);
} else if (command.equals(OnOffType.OFF)) {
getApi().muteGroupOFF(id);
}
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel on bridge
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerNowPlaying} handles the refresh commands
* coming from the implementing thing
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerNowPlaying extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerNowPlaying(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// No such channel on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
// TODO consider caching this somehow, this method is triggered from a lot of channels for the same player
@Nullable
Media payload = getApi().getNowPlayingMedia(id).payload;
if (payload != null) {
eventListener.playerMediaChangeEvent(id, payload);
}
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosChannelHandlerPlayURL} handles the PlayURL channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerPlayURL extends BaseHeosChannelHandler {
protected final Logger logger = LoggerFactory.getLogger(HeosChannelHandlerPlayURL.class);
public HeosChannelHandlerPlayURL(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
try {
URL url = new URL(command.toString());
getApi().playURL(id, url);
} catch (MalformedURLException e) {
logger.debug("Command '{}' is not a proper URL. Error: {}", command.toString(), e.getMessage());
}
}
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.resources.HeosConstants.PID;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosChannelHandlerPlayerSelect} handles the player selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerPlayerSelect extends BaseHeosChannelHandler {
protected final Logger logger = LoggerFactory.getLogger(HeosChannelHandlerPlayerSelect.class);
private final ChannelUID channelUID;
public HeosChannelHandlerPlayerSelect(ChannelUID channelUID, HeosBridgeHandler bridge) {
super(bridge);
this.channelUID = channelUID;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
if (command instanceof RefreshType) {
return;
}
Channel channel = bridge.getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.debug("Channel {} not found", channelUID);
return;
}
List<String[]> selectedPlayerList = bridge.getSelectedPlayerList();
if (command.equals(OnOffType.ON)) {
String[] selectedPlayerInfo = new String[2];
selectedPlayerInfo[0] = channel.getProperties().get(PID);
selectedPlayerInfo[1] = channelUID.getId();
selectedPlayerList.add(selectedPlayerInfo);
} else if (!selectedPlayerList.isEmpty()) {
int indexPlayerChannel = -1;
for (int i = 0; i < selectedPlayerList.size(); i++) {
String localPID = selectedPlayerList.get(i)[0];
if (localPID.equals(channel.getProperties().get(PID))) {
indexPlayerChannel = i;
}
}
selectedPlayerList.remove(indexPlayerChannel);
bridge.setSelectedPlayerList(selectedPlayerList);
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.CH_ID_PLAYLISTS;
import static org.openhab.binding.heos.internal.resources.HeosConstants.PLAYLISTS_SID;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerPlaylist} handles the playlist selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerPlaylist extends BaseHeosChannelHandler {
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
public HeosChannelHandlerPlaylist(HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider,
HeosBridgeHandler bridge) {
super(bridge);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id, uid);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id, uid);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
ChannelUID channelUID = new ChannelUID(uid, CH_ID_PLAYLISTS);
if (command instanceof RefreshType) {
heosDynamicStateDescriptionProvider.setPlaylists(channelUID, getApi().getPlaylists());
return;
}
String idCommand = heosDynamicStateDescriptionProvider.getValueByLabel(channelUID, command.toString());
getApi().addContainerToQueuePlayNow(id, PLAYLISTS_SID, idCommand);
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.CH_ID_QUEUE;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerFavorite} handles the playlist selection channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerQueue extends BaseHeosChannelHandler {
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
public HeosChannelHandlerQueue(HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider,
HeosBridgeHandler bridge) {
super(bridge);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id, uid);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id, uid);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
private void handleCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
ChannelUID channelUID = new ChannelUID(uid, CH_ID_QUEUE);
if (command instanceof RefreshType) {
heosDynamicStateDescriptionProvider.setQueue(channelUID, getApi().getQueue(id));
return;
}
String idCommand = heosDynamicStateDescriptionProvider.getValueByLabel(channelUID, command.toString());
getApi().playMediaFromQueue(id, idCommand);
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
/**
*
* The {@link HeosChannelHandlerRawCommand} handles the RawCommand channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerRawCommand extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerRawCommand(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
HeosResponseObject<JsonElement> response = getApi().sendRawCommand(command.toString());
if (response.result) {
eventListener.playerStateChangeEvent(response);
}
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerReboot} handles the Reboot channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerReboot extends BaseHeosChannelHandler {
public HeosChannelHandlerReboot(HeosBridgeHandler bridge) {
super(bridge);
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) {
// not used on player
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) {
// Not used on group
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
return;
}
if (command == OnOffType.ON) {
getApi().reboot();
}
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerRepeatMode} handles the RepeatMode channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerRepeatMode extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerRepeatMode(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// Do nothing
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayMode(id));
return;
}
if (HeosConstants.HEOS_UI_ALL.equalsIgnoreCase(command.toString())) {
getApi().setRepeatMode(id, HeosConstants.REPEAT_ALL);
} else if (HeosConstants.HEOS_UI_ONE.equalsIgnoreCase(command.toString())) {
getApi().setRepeatMode(id, HeosConstants.REPEAT_ONE);
} else if (HeosConstants.HEOS_UI_OFF.equalsIgnoreCase(command.toString())) {
getApi().setRepeatMode(id, HeosConstants.OFF);
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosConstants;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerShuffleMode} handles the SchuffelModechannel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerShuffleMode extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerShuffleMode(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
handleCommand(command, id);
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
handleCommand(command, id);
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// Do nothing
}
private void handleCommand(Command command, String id) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayMode(id));
return;
}
if (command == OnOffType.ON) {
getApi().setShuffleMode(id, HeosConstants.ON);
} else if (command == OnOffType.OFF) {
getApi().setShuffleMode(id, HeosConstants.OFF);
}
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link HeosChannelHandlerVolume} handles the Volume channel command
* from the implementing thing.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosChannelHandlerVolume extends BaseHeosChannelHandler {
private final HeosEventListener eventListener;
public HeosChannelHandlerVolume(HeosEventListener eventListener, HeosBridgeHandler bridge) {
super(bridge);
this.eventListener = eventListener;
}
@Override
public void handlePlayerCommand(Command command, String id, ThingUID uid) throws IOException, ReadException {
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getPlayerVolume(id));
return;
}
if (command instanceof IncreaseDecreaseType) {
if (IncreaseDecreaseType.INCREASE == command) {
getApi().increaseVolume(id);
} else {
getApi().decreaseVolume(id);
}
} else {
getApi().setVolume(command.toString(), id);
}
}
@Override
public void handleGroupCommand(Command command, @Nullable String id, ThingUID uid,
HeosGroupHandler heosGroupHandler) throws IOException, ReadException {
if (id == null) {
throw new HeosNotFoundException();
}
if (command instanceof RefreshType) {
eventListener.playerStateChangeEvent(getApi().getGroupVolume(id));
return;
}
if (command instanceof IncreaseDecreaseType) {
if (IncreaseDecreaseType.INCREASE == command) {
getApi().increaseGroupVolume(id);
} else {
getApi().decreaseGroupVolume(id);
}
} else {
getApi().volumeGroup(command.toString(), id);
}
}
@Override
public void handleBridgeCommand(Command command, ThingUID uid) {
// not used on bridge
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.payload.BrowseResult;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.YesNoEnum;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Component;
/**
* Dynamically create the users list of favorites and playlists.
*
* @author Martin van Wingerden - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, HeosDynamicStateDescriptionProvider.class })
@NonNullByDefault
public class HeosDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
String getValueByLabel(ChannelUID channelUID, String input) {
Optional<String> optionalValueByLabel = channelOptionsMap.get(channelUID).stream()
.filter(o -> input.equals(o.getLabel())).map(StateOption::getValue).findFirst();
// if no match was found we assume that it already was a value and not a label
return optionalValueByLabel.orElse(input);
}
public void setFavorites(ChannelUID channelUID, List<BrowseResult> favorites) {
setBrowseResultList(channelUID, favorites, d -> d.mediaId);
}
public void setPlaylists(ChannelUID channelUID, List<BrowseResult> playLists) {
setBrowseResultList(channelUID, playLists, d -> d.containerId);
}
private void setBrowseResultList(ChannelUID channelUID, List<BrowseResult> playlists,
Function<BrowseResult, @Nullable String> function) {
setStateOptions(channelUID,
playlists.stream().filter(browseResult -> browseResult.playable == YesNoEnum.YES)
.map(browseResult -> getStateOption(function, browseResult)).filter(Optional::isPresent)
.map(Optional::get).collect(Collectors.toList()));
}
private Optional<StateOption> getStateOption(Function<BrowseResult, @Nullable String> function,
BrowseResult browseResult) {
@Nullable
String identifier = function.apply(browseResult);
if (identifier != null) {
return Optional.of(new StateOption(identifier, browseResult.name));
} else {
return Optional.empty();
}
}
public void setQueue(ChannelUID channelUID, List<Media> queue) {
setStateOptions(channelUID,
queue.stream().map(m -> new StateOption(String.valueOf(m.queueId), m.combinedSongArtist()))
.collect(Collectors.toList()));
}
}

View File

@@ -0,0 +1,317 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.binding.heos.internal.json.dto.HeosEvent.PLAYER_VOLUME_CHANGED;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.configuration.GroupConfiguration;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.resources.HeosGroup;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosGroupHandler} handles the actions for a HEOS group.
* Channel commands are received and send to the dedicated channels
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosGroupHandler extends HeosThingBaseHandler {
private final Logger logger = LoggerFactory.getLogger(HeosGroupHandler.class);
private @NonNullByDefault({}) GroupConfiguration configuration;
private @Nullable String gid;
private boolean blockInitialization;
private @Nullable Future<?> scheduledStartupFuture;
public HeosGroupHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(thing, heosDynamicStateDescriptionProvider);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// The GID is null if there is no group online with the groupMemberHash
// Only commands from the UNGROUP channel are passed through
// to activate the group if it is offline
if (gid != null || CH_ID_UNGROUP.equals(channelUID.getId())) {
@Nullable
HeosChannelHandler channelHandler = getHeosChannelHandler(channelUID);
if (channelHandler != null) {
try {
@Nullable
String id = getMaybeId(channelUID, command);
channelHandler.handleGroupCommand(command, id, thing.getUID(), this);
handleSuccess();
} catch (IOException | ReadException e) {
handleError(e);
}
}
}
}
@Nullable
private String getMaybeId(ChannelUID channelUID, Command command) throws HeosNotFoundException {
if (isCreateGroupRequest(channelUID, command)) {
return null;
} else {
return getId();
}
}
private boolean isCreateGroupRequest(ChannelUID channelUID, Command command) {
return CH_ID_UNGROUP.equals(channelUID.getId()) && OnOffType.ON == command;
}
/**
* Initialize the HEOS group. Starts an extra thread to avoid blocking
* during start up phase. Gathering all information can take longer
* than 5 seconds which can throw an error within the openHAB system.
*/
@Override
public synchronized void initialize() {
super.initialize();
configuration = thing.getConfiguration().as(GroupConfiguration.class);
// Prevents that initialize() is called multiple times if group goes online
blockInitialization = true;
scheduledStartUp();
}
@Override
public void dispose() {
cancel(scheduledStartupFuture);
super.dispose();
}
@Override
public String getId() throws HeosNotFoundException {
@Nullable
String localGroupId = this.gid;
if (localGroupId == null) {
throw new HeosNotFoundException();
}
return localGroupId;
}
public String getGroupMemberHash() {
return HeosGroup.calculateGroupMemberHash(configuration.members);
}
public String[] getGroupMemberPidList() {
return configuration.members.split(";");
}
@Override
public void setNotificationSoundVolume(PercentType volume) {
super.setNotificationSoundVolume(volume);
try {
getApiConnection().volumeGroup(volume.toString(), getId());
} catch (IOException | ReadException e) {
logger.warn("Failed to set notification volume", e);
}
}
@Override
public void playerStateChangeEvent(HeosEventObject eventObject) {
if (ThingStatus.UNINITIALIZED == getThing().getStatus()) {
logger.debug("Can't Handle Event. Group {} not initialized. Status is: {}", getConfig().get(PROP_NAME),
getThing().getStatus());
return;
}
@Nullable
String localGid = this.gid;
@Nullable
String eventGroupId = eventObject.getAttribute(HeosCommunicationAttribute.GROUP_ID);
@Nullable
String eventPlayerId = eventObject.getAttribute(HeosCommunicationAttribute.PLAYER_ID);
if (localGid == null || !(localGid.equals(eventGroupId) || localGid.equals(eventPlayerId))) {
return;
}
if (PLAYER_VOLUME_CHANGED.equals(eventObject.command)) {
logger.debug("Ignoring player-volume changes for groups");
return;
}
handleThingStateUpdate(eventObject);
}
@Override
public void playerStateChangeEvent(HeosResponseObject<?> responseObject) throws HeosFunctionalException {
if (ThingStatus.UNINITIALIZED == getThing().getStatus()) {
logger.debug("Can't Handle Event. Group {} not initialized. Status is: {}", getConfig().get(PROP_NAME),
getThing().getStatus());
return;
}
@Nullable
String localGid = this.gid;
if (localGid == null || !localGid.equals(responseObject.getAttribute(HeosCommunicationAttribute.GROUP_ID))) {
return;
}
handleThingStateUpdate(responseObject);
}
@Override
public void playerMediaChangeEvent(String pid, Media media) {
if (!pid.equals(gid)) {
return;
}
handleThingMediaUpdate(media);
}
/**
* Sets the status of the HEOS group to OFFLINE.
* Also sets the UNGROUP channel to OFF and the CONTROL
* channel to PAUSE
*/
@Override
public void setStatusOffline() {
logger.debug("Status was set offline");
try {
getApiConnection().unregisterForChangeEvents(this);
} catch (HeosNotConnectedException e) {
logger.debug("Not connected, failed to unregister");
}
updateState(CH_ID_UNGROUP, OnOffType.OFF);
updateState(CH_ID_CONTROL, PlayPauseType.PAUSE);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DISABLED, "Group is not available on HEOS system");
}
@Override
public void setStatusOnline() {
if (!blockInitialization) {
initialize();
} else {
logger.debug("Not initializing from setStatusOnline ({}, {})", thing.getStatus(), blockInitialization);
}
}
private void updateConfiguration(String groupId, Group group) {
Map<String, String> prop = new HashMap<>();
prop.put(PROP_NAME, group.name);
prop.put(PROP_GROUP_MEMBERS, group.getGroupMemberIds());
prop.put(PROP_GROUP_LEADER, group.getLeaderId());
prop.put(PROP_GROUP_HASH, HeosGroup.calculateGroupMemberHash(group));
prop.put(PROP_GID, groupId);
updateProperties(prop);
}
private void scheduledStartUp() {
cancel(scheduledStartupFuture);
scheduledStartupFuture = scheduler.submit(this::delayedInitialize);
}
private void delayedInitialize() {
@Nullable
HeosBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
logger.debug("Bridge handler not found, rescheduling");
scheduledStartUp();
return;
}
if (bridgeHandler.isLoggedIn()) {
handleDynamicStatesSignedIn();
}
bridgeHandler.addGroupHandlerInformation(this);
// Checks if there is a group online with the same group member hash.
// If not setting the group offline.
@Nullable
String groupId = bridgeHandler.getActualGID(HeosGroup.calculateGroupMemberHash(configuration.members));
if (groupId == null) {
blockInitialization = false;
setStatusOffline();
} else {
try {
refreshPlayState(groupId);
HeosResponseObject<Group> response = getApiConnection().getGroupInfo(groupId);
@Nullable
Group group = response.payload;
if (group == null) {
throw new IllegalStateException("Invalid group response received");
}
assertSameGroup(group);
gid = groupId;
updateConfiguration(groupId, group);
updateStatus(ThingStatus.ONLINE);
updateState(CH_ID_UNGROUP, OnOffType.ON);
blockInitialization = false;
} catch (IOException | ReadException | IllegalStateException e) {
logger.debug("Failed initializing, will retry", e);
cancel(scheduledStartupFuture, false);
scheduledStartupFuture = scheduler.schedule(this::delayedInitialize, 30, TimeUnit.SECONDS);
}
}
}
/**
* Make sure the given group is group which this handler represents
*
* @param group retrieved from HEOS system
*/
private void assertSameGroup(Group group) {
String storedGroupHash = HeosGroup.calculateGroupMemberHash(configuration.members);
String retrievedGroupHash = HeosGroup.calculateGroupMemberHash(group);
if (!retrievedGroupHash.equals(storedGroupHash)) {
throw new IllegalStateException("Invalid group received, members / hash do not match.");
}
}
@Override
void refreshPlayState(String id) throws IOException, ReadException {
super.refreshPlayState(id);
handleThingStateUpdate(getApiConnection().getGroupMuteState(id));
handleThingStateUpdate(getApiConnection().getGroupVolume(id));
}
}

View File

@@ -0,0 +1,184 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.PLAYER_ID;
import static org.openhab.binding.heos.internal.json.dto.HeosEvent.GROUP_VOLUME_CHANGED;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.configuration.PlayerConfiguration;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.json.dto.HeosErrorCode;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosPlayerHandler} handles the actions for a HEOS player.
* Channel commands are received and send to the dedicated channels
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosPlayerHandler extends HeosThingBaseHandler {
private final Logger logger = LoggerFactory.getLogger(HeosPlayerHandler.class);
private @NonNullByDefault({}) String pid;
private @Nullable Future<?> scheduledFuture;
public HeosPlayerHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(thing, heosDynamicStateDescriptionProvider);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
@Nullable
HeosChannelHandler channelHandler = getHeosChannelHandler(channelUID);
if (channelHandler != null) {
try {
channelHandler.handlePlayerCommand(command, getId(), thing.getUID());
handleSuccess();
} catch (IOException | ReadException e) {
handleError(e);
}
}
}
@Override
public void initialize() {
super.initialize();
PlayerConfiguration configuration = thing.getConfiguration().as(PlayerConfiguration.class);
pid = configuration.pid;
cancel(scheduledFuture);
scheduledFuture = scheduler.submit(this::delayedInitialize);
}
private synchronized void delayedInitialize() {
try {
refreshPlayState(pid);
handleThingStateUpdate(getApiConnection().getPlayerInfo(pid));
updateStatus(ThingStatus.ONLINE);
} catch (HeosFunctionalException e) {
if (e.getCode() == HeosErrorCode.INVALID_ID) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, e.getCode().toString());
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getCode().toString());
}
} catch (IOException | ReadException e) {
logger.debug("Failed to initialize, will try again", e);
cancel(scheduledFuture, false);
scheduledFuture = scheduler.schedule(this::delayedInitialize, 3, TimeUnit.SECONDS);
}
}
@Override
void refreshPlayState(String id) throws IOException, ReadException {
super.refreshPlayState(id);
handleThingStateUpdate(getApiConnection().getPlayerMuteState(id));
handleThingStateUpdate(getApiConnection().getPlayerVolume(id));
}
@Override
public void dispose() {
cancel(scheduledFuture);
super.dispose();
}
@Override
public String getId() {
return pid;
}
@Override
public void setNotificationSoundVolume(PercentType volume) {
}
@Override
public void playerStateChangeEvent(HeosEventObject eventObject) {
if (!pid.equals(eventObject.getAttribute(PLAYER_ID))) {
return;
}
if (GROUP_VOLUME_CHANGED == eventObject.command) {
logger.debug("Ignoring group-volume changes for players");
return;
}
handleThingStateUpdate(eventObject);
}
@Override
public void playerStateChangeEvent(HeosResponseObject<?> responseObject) throws HeosFunctionalException {
if (!pid.equals(responseObject.getAttribute(PLAYER_ID))) {
return;
}
handleThingStateUpdate(responseObject);
}
@Override
public void playerMediaChangeEvent(String eventPid, Media media) {
if (!pid.equals(eventPid)) {
return;
}
handleThingMediaUpdate(media);
}
@Override
public void setStatusOffline() {
updateStatus(ThingStatus.OFFLINE);
}
@Override
public void setStatusOnline() {
updateStatus(ThingStatus.ONLINE);
}
public static void propertiesFromPlayer(Map<String, ? super String> prop, Player player) {
prop.put(PROP_NAME, player.name);
prop.put(PROP_PID, String.valueOf(player.playerId));
prop.put(Thing.PROPERTY_MODEL_ID, player.model);
prop.put(Thing.PROPERTY_FIRMWARE_VERSION, player.version);
prop.put(PROP_NETWORK, player.network);
prop.put(PROP_IP, player.ip);
@Nullable
String serialNumber = player.serial;
if (serialNumber != null) {
prop.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
}
}
}

View File

@@ -0,0 +1,516 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.handler;
import static org.openhab.binding.heos.internal.HeosBindingConstants.*;
import static org.openhab.binding.heos.internal.handler.FutureUtil.cancel;
import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.GROUP;
import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.PLAYER;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.*;
import static org.openhab.core.thing.ThingStatus.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.measure.quantity.Time;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.HeosChannelHandlerFactory;
import org.openhab.binding.heos.internal.api.HeosFacade;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.exception.HeosNotConnectedException;
import org.openhab.binding.heos.internal.exception.HeosNotFoundException;
import org.openhab.binding.heos.internal.json.dto.*;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.resources.HeosEventListener;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.*;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosThingBaseHandler} class is the base Class all HEOS handler have to extend.
* It provides basic command handling and common needed methods.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public abstract class HeosThingBaseHandler extends BaseThingHandler implements HeosEventListener {
private final Logger logger = LoggerFactory.getLogger(HeosThingBaseHandler.class);
private final HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider;
private final ChannelUID favoritesChannelUID;
private final ChannelUID playlistsChannelUID;
private final ChannelUID queueChannelUID;
private @Nullable HeosChannelHandlerFactory channelHandlerFactory;
protected @Nullable HeosBridgeHandler bridgeHandler;
private String notificationVolume = "0";
private int failureCount;
private @Nullable Future<?> scheduleQueueFetchFuture;
private @Nullable Future<?> handleDynamicStatesFuture;
HeosThingBaseHandler(Thing thing, HeosDynamicStateDescriptionProvider heosDynamicStateDescriptionProvider) {
super(thing);
this.heosDynamicStateDescriptionProvider = heosDynamicStateDescriptionProvider;
favoritesChannelUID = new ChannelUID(thing.getUID(), CH_ID_FAVORITES);
playlistsChannelUID = new ChannelUID(thing.getUID(), CH_ID_PLAYLISTS);
queueChannelUID = new ChannelUID(thing.getUID(), CH_ID_QUEUE);
}
@Override
public void initialize() {
@Nullable
Bridge bridge = getBridge();
@Nullable
HeosBridgeHandler localBridgeHandler;
if (bridge != null) {
localBridgeHandler = (HeosBridgeHandler) bridge.getHandler();
if (localBridgeHandler != null) {
bridgeHandler = localBridgeHandler;
channelHandlerFactory = localBridgeHandler.getChannelHandlerFactory();
} else {
updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
} else {
logger.warn("No Bridge set within child handler");
updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
try {
getApiConnection().registerForChangeEvents(this);
cancel(scheduleQueueFetchFuture);
scheduleQueueFetchFuture = scheduler.submit(this::fetchQueueFromPlayer);
if (localBridgeHandler.isLoggedIn()) {
scheduleImmediatelyHandleDynamicStatesSignedIn();
}
} catch (HeosNotConnectedException e) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
void handleSuccess() {
failureCount = 0;
updateStatus(ONLINE);
}
void handleError(Exception e) {
logger.debug("Failed to handle player/group command", e);
failureCount++;
if (failureCount > FAILURE_COUNT_LIMIT) {
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Failed to handle command: " + e.getMessage());
}
}
public HeosFacade getApiConnection() throws HeosNotConnectedException {
@Nullable
HeosBridgeHandler localBridge = bridgeHandler;
if (localBridge != null) {
return localBridge.getApiConnection();
}
throw new HeosNotConnectedException();
}
public abstract String getId() throws HeosNotFoundException;
public abstract void setStatusOffline();
public abstract void setStatusOnline();
public PercentType getNotificationSoundVolume() {
return PercentType.valueOf(notificationVolume);
}
public void setNotificationSoundVolume(PercentType volume) {
notificationVolume = volume.toString();
}
@Nullable
HeosChannelHandler getHeosChannelHandler(ChannelUID channelUID) {
@Nullable
HeosChannelHandlerFactory localChannelHandlerFactory = this.channelHandlerFactory;
return localChannelHandlerFactory != null ? localChannelHandlerFactory.getChannelHandler(channelUID, this, null)
: null;
}
@Override
public void bridgeChangeEvent(String event, boolean success, Object command) {
logger.debug("BridgeChangeEvent: {}", command);
if (HeosEvent.USER_CHANGED == command) {
handleDynamicStatesSignedIn();
}
if (EVENT_TYPE_EVENT.equals(event)) {
if (HeosEvent.GROUPS_CHANGED == command) {
fetchQueueFromPlayer();
} else if (CONNECTION_RESTORED.equals(command)) {
try {
refreshPlayState(getId());
} catch (IOException | ReadException e) {
logger.debug("Failed to refreshPlayState", e);
}
}
}
}
void scheduleImmediatelyHandleDynamicStatesSignedIn() {
cancel(handleDynamicStatesFuture);
handleDynamicStatesFuture = scheduler.submit(this::handleDynamicStatesSignedIn);
}
void handleDynamicStatesSignedIn() {
try {
heosDynamicStateDescriptionProvider.setFavorites(favoritesChannelUID, getApiConnection().getFavorites());
heosDynamicStateDescriptionProvider.setPlaylists(playlistsChannelUID, getApiConnection().getPlaylists());
} catch (IOException | ReadException e) {
logger.debug("Failed to set favorites / playlists, rescheduling", e);
cancel(handleDynamicStatesFuture, false);
handleDynamicStatesFuture = scheduler.schedule(this::handleDynamicStatesSignedIn, 30, TimeUnit.SECONDS);
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (ThingStatus.OFFLINE.equals(bridgeStatusInfo.getStatus())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else if (ThingStatus.ONLINE.equals(bridgeStatusInfo.getStatus())) {
updateStatus(ThingStatus.ONLINE);
} else if (ThingStatus.UNINITIALIZED.equals(bridgeStatusInfo.getStatus())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
/**
* Dispose the handler and unregister the handler
* form Change Events
*/
@Override
public void dispose() {
try {
logger.debug("Disposing this: {}", this);
getApiConnection().unregisterForChangeEvents(this);
} catch (HeosNotConnectedException e) {
logger.trace("No connection available while trying to unregister");
}
cancel(scheduleQueueFetchFuture);
cancel(handleDynamicStatesFuture);
}
/**
* Plays a media file from an external source. Can be
* used for audio sink function
*
* @param urlStr The external URL where the file is located
* @throws ReadException
* @throws IOException
*/
public void playURL(String urlStr) throws IOException, ReadException {
try {
URL url = new URL(urlStr);
getApiConnection().playURL(getId(), url);
} catch (MalformedURLException e) {
logger.debug("Command '{}' is not a proper URL. Error: {}", urlStr, e.getMessage());
}
}
/**
* Handles the updates send from the HEOS system to
* the binding. To receive updates the handler has
* to register itself via {@link HeosFacade} via the method:
* {@link HeosFacade#registerForChangeEvents(HeosEventListener)}
*
* @param eventObject containing information about the even which was sent to us by the HEOS device
*/
protected void handleThingStateUpdate(HeosEventObject eventObject) {
updateStatus(ONLINE, ThingStatusDetail.NONE, "Receiving events");
@Nullable
HeosEvent command = eventObject.command;
if (command == null) {
logger.debug("Ignoring event with null command");
return;
}
switch (command) {
case PLAYER_STATE_CHANGED:
playerStateChanged(eventObject);
break;
case PLAYER_VOLUME_CHANGED:
case GROUP_VOLUME_CHANGED:
@Nullable
String level = eventObject.getAttribute(LEVEL);
if (level != null) {
notificationVolume = level;
updateState(CH_ID_VOLUME, PercentType.valueOf(level));
updateState(CH_ID_MUTE, OnOffType.from(eventObject.getBooleanAttribute(MUTE)));
}
break;
case SHUFFLE_MODE_CHANGED:
handleShuffleMode(eventObject);
break;
case PLAYER_NOW_PLAYING_PROGRESS:
@Nullable
Long position = eventObject.getNumericAttribute(CURRENT_POSITION);
@Nullable
Long duration = eventObject.getNumericAttribute(DURATION);
if (position != null && duration != null) {
updateState(CH_ID_CUR_POS, quantityFromMilliSeconds(position));
updateState(CH_ID_DURATION, quantityFromMilliSeconds(duration));
}
break;
case REPEAT_MODE_CHANGED:
handleRepeatMode(eventObject);
break;
case PLAYER_PLAYBACK_ERROR:
updateStatus(UNKNOWN, ThingStatusDetail.NONE, eventObject.getAttribute(ERROR));
break;
case PLAYER_QUEUE_CHANGED:
fetchQueueFromPlayer();
break;
case SOURCES_CHANGED:
// we are not yet handling the actual sources, although we might want to do that in the future
logger.trace("Ignoring {}, support might be added in the future", command);
break;
case GROUPS_CHANGED:
case PLAYERS_CHANGED:
case PLAYER_NOW_PLAYING_CHANGED:
case USER_CHANGED:
logger.trace("Ignoring {}, will be handled inside HeosEventController", command);
break;
}
}
private QuantityType<Time> quantityFromMilliSeconds(long position) {
return new QuantityType<>(position / 1000, SmartHomeUnits.SECOND);
}
private void handleShuffleMode(HeosObject eventObject) {
updateState(CH_ID_SHUFFLE_MODE,
OnOffType.from(eventObject.getBooleanAttribute(HeosCommunicationAttribute.SHUFFLE)));
}
void refreshPlayState(String id) throws IOException, ReadException {
handleThingStateUpdate(getApiConnection().getPlayMode(id));
handleThingStateUpdate(getApiConnection().getPlayState(id));
handleThingStateUpdate(getApiConnection().getNowPlayingMedia(id));
}
protected <T> void handleThingStateUpdate(HeosResponseObject<T> responseObject) throws HeosFunctionalException {
handleResponseError(responseObject);
@Nullable
HeosCommandTuple cmd = responseObject.heosCommand;
if (cmd == null) {
logger.debug("Ignoring response with null command");
return;
}
if (cmd.commandGroup == PLAYER || cmd.commandGroup == GROUP) {
switch (cmd.command) {
case GET_PLAY_STATE:
playerStateChanged(responseObject);
break;
case GET_MUTE:
updateState(CH_ID_MUTE, OnOffType.from(responseObject.getBooleanAttribute(MUTE)));
break;
case GET_VOLUME:
@Nullable
String level = responseObject.getAttribute(LEVEL);
if (level != null) {
notificationVolume = level;
updateState(CH_ID_VOLUME, PercentType.valueOf(level));
}
break;
case GET_PLAY_MODE:
handleRepeatMode(responseObject);
handleShuffleMode(responseObject);
break;
case GET_NOW_PLAYING_MEDIA:
@Nullable
T mediaPayload = responseObject.payload;
if (mediaPayload instanceof Media) {
handleThingMediaUpdate((Media) mediaPayload);
}
break;
case GET_PLAYER_INFO:
@Nullable
T playerPayload = responseObject.payload;
if (playerPayload instanceof Player) {
handlePlayerInfo((Player) playerPayload);
}
break;
}
}
}
private <T> void handleResponseError(HeosResponseObject<T> responseObject) throws HeosFunctionalException {
@Nullable
HeosError error = responseObject.getError();
if (error != null) {
throw new HeosFunctionalException(error.code);
}
}
private void handleRepeatMode(HeosObject eventObject) {
@Nullable
String repeatMode = eventObject.getAttribute(REPEAT);
if (repeatMode == null) {
updateState(CH_ID_REPEAT_MODE, UnDefType.NULL);
return;
}
switch (repeatMode) {
case REPEAT_ALL:
updateState(CH_ID_REPEAT_MODE, StringType.valueOf(HEOS_UI_ALL));
break;
case REPEAT_ONE:
updateState(CH_ID_REPEAT_MODE, StringType.valueOf(HEOS_UI_ONE));
break;
case OFF:
updateState(CH_ID_REPEAT_MODE, StringType.valueOf(HEOS_UI_OFF));
break;
}
}
private void playerStateChanged(HeosObject eventObject) {
@Nullable
String attribute = eventObject.getAttribute(STATE);
if (attribute == null) {
updateState(CH_ID_CONTROL, UnDefType.NULL);
return;
}
switch (attribute) {
case PLAY:
updateState(CH_ID_CONTROL, PlayPauseType.PLAY);
break;
case PAUSE:
case STOP:
updateState(CH_ID_CONTROL, PlayPauseType.PAUSE);
break;
}
}
private synchronized void fetchQueueFromPlayer() {
try {
List<Media> queue = getApiConnection().getQueue(getId());
heosDynamicStateDescriptionProvider.setQueue(queueChannelUID, queue);
return;
} catch (HeosNotFoundException e) {
logger.debug("HEOS player/group is not found, rescheduling");
} catch (IOException | ReadException e) {
logger.debug("Failed to set queue, rescheduling", e);
}
cancel(scheduleQueueFetchFuture, false);
scheduleQueueFetchFuture = scheduler.schedule(this::fetchQueueFromPlayer, 30, TimeUnit.SECONDS);
}
protected void handleThingMediaUpdate(Media info) {
logger.debug("Received updated media state: {}", info);
updateState(CH_ID_SONG, StringType.valueOf(info.song));
updateState(CH_ID_ARTIST, StringType.valueOf(info.artist));
updateState(CH_ID_ALBUM, StringType.valueOf(info.album));
if (SONG.equals(info.type)) {
updateState(CH_ID_QUEUE, StringType.valueOf(String.valueOf(info.queueId)));
updateState(CH_ID_FAVORITES, UnDefType.UNDEF);
} else if (STATION.equals(info.type)) {
updateState(CH_ID_QUEUE, UnDefType.UNDEF);
updateState(CH_ID_FAVORITES, StringType.valueOf(info.albumId));
} else {
updateState(CH_ID_QUEUE, UnDefType.UNDEF);
updateState(CH_ID_FAVORITES, UnDefType.UNDEF);
}
handleImageUrl(info);
handleStation(info);
handleSourceId(info);
}
private void handleImageUrl(Media info) {
if (StringUtils.isNotBlank(info.imageUrl)) {
try {
URL url = new URL(info.imageUrl); // checks if String is proper URL
RawType cover = HttpUtil.downloadImage(url.toString());
if (cover != null) {
updateState(CH_ID_COVER, cover);
return;
}
} catch (MalformedURLException e) {
logger.debug("Cover can't be loaded. No proper URL: {}", info.imageUrl, e);
}
}
updateState(CH_ID_COVER, UnDefType.NULL);
}
private void handleStation(Media info) {
if (STATION.equals(info.type)) {
updateState(CH_ID_STATION, StringType.valueOf(info.station));
} else {
updateState(CH_ID_STATION, UnDefType.UNDEF);
}
}
private void handleSourceId(Media info) {
if (info.sourceId == INPUT_SID && info.mediaId != null) {
String inputName = info.mediaId.substring(info.mediaId.indexOf("/") + 1);
updateState(CH_ID_INPUTS, StringType.valueOf(inputName));
updateState(CH_ID_TYPE, StringType.valueOf(info.station));
} else {
updateState(CH_ID_TYPE, StringType.valueOf(info.type));
updateState(CH_ID_INPUTS, UnDefType.UNDEF);
}
}
private void handlePlayerInfo(Player player) {
Map<String, String> prop = new HashMap<>();
HeosPlayerHandler.propertiesFromPlayer(prop, player);
updateProperties(prop);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Object used for the initial JSON parsing of the result
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
class HeosJsonObject {
String command = "";
@Nullable
String result;
@Nullable
String message;
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.dto.HeosCommandTuple;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Parser used for parsing the responses of JSON cli
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosJsonParser {
private final Logger logger = LoggerFactory.getLogger(HeosJsonParser.class);
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
public HeosEventObject parseEvent(String jsonBody) {
HeosJsonWrapper wrapper = gson.fromJson(jsonBody, HeosJsonWrapper.class);
return postProcess(wrapper.heos);
}
private HeosEventObject postProcess(HeosJsonObject heos) {
return new HeosEventObject(HeosEvent.valueOfString(heos.command), heos.command, splitQuery(heos.message));
}
public <T> HeosResponseObject<T> parseResponse(String jsonBody, Class<T> clazz) {
HeosJsonWrapper wrapper = gson.fromJson(jsonBody, HeosJsonWrapper.class);
return postProcess(wrapper, clazz);
}
private <T> HeosResponseObject<T> postProcess(HeosJsonWrapper wrapper, Class<T> clazz) {
T payload = gson.fromJson(wrapper.payload, clazz);
return new HeosResponseObject<>(HeosCommandTuple.valueOf(wrapper.heos.command), wrapper.heos.command,
wrapper.heos.result, splitQuery(wrapper.heos.message), payload, wrapper.options);
}
private Map<String, String> splitQuery(@Nullable String url) {
if (url == null) {
return Collections.emptyMap();
}
return Arrays.stream(url.split("&")).map(p -> p.split("=", 2))
.collect(Collectors.toMap(v -> decode(v[0]), v -> v.length == 1 ? "" : decode(v[1]), this::merge));
}
/**
* for duplicates we ignore the first one
*
* @param v1 first occurrence
* @param v2 second occurrence
* @return second occurrence
*/
private String merge(String v1, String v2) {
logger.debug("Ignoring first occurrence '{}' in favor of '{}'", v1, v2);
return v2;
}
private static String decode(String encoded) {
try {
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Impossible: UTF-8 is a required encoding", e);
}
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonElement;
/**
* Wrapper used around HeosJsonObject used for the initial JSON parsing of the result
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
class HeosJsonWrapper {
HeosJsonObject heos = new HeosJsonObject();
@Nullable
JsonElement payload;
@Nullable
List<Map<String, List<HeosOption>>> options;
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Object used for the initial JSON parsing of the result
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosOption {
public int id;
@SerializedName("scid")
public @Nullable Integer criteriaId;
public @Nullable String name;
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum for the HEOS commands
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosCommand {
BROWSE,
CHECK_ACCOUNT,
CHECK_UPDATE,
CLEAR_QUEUE,
DELETE_PLAYLIST,
GET_GROUPS,
GET_GROUP_INFO,
GET_MUSIC_SOURCES,
GET_NOW_PLAYING_MEDIA,
GET_PLAYERS,
GET_PLAYER_INFO,
GET_PLAY_MODE,
GET_PLAY_STATE,
GET_SEARCH_CRITERIA,
GET_SOURCE_INFO,
GET_VOLUME,
HEART_BEAT,
PLAY_INPUT,
PLAY_NEXT,
PLAY_PRESET,
PLAY_PREVIOUS,
ADD_TO_QUEUE,
GET_QUEUE,
MOVE_QUEUE_ITEM,
PLAY_QUEUE,
SAVE_QUEUE,
GET_QUICKSELECTS,
PLAY_QUICKSELECT,
SET_QUICKSELECT,
PLAY_STREAM,
PRETTIFY_JSON_RESPONSE,
REGISTER_FOR_CHANGE_EVENTS,
REMOVE_FROM_QUEUE,
RENAME_PLAYLIST,
RETRIEVE_METADATA,
SEARCH,
SET_GROUP,
SET_MUTE,
GET_MUTE,
TOGGLE_MUTE,
SET_PLAY_MODE,
SET_PLAY_STATE,
SIGN_IN,
SIGN_OUT,
SET_VOLUME,
VOLUME_DOWN,
VOLUME_UP;
@Override
public String toString() {
return name().toLowerCase();
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* enum for the HEOS command groups, they appear as the first parts of the command
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosCommandGroup {
BROWSE,
PLAYER,
GROUP,
SYSTEM;
@Override
public String toString() {
return name().toLowerCase();
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tuple to contain a command group and command enum, this represents the full command send to / received by the HEOS
* cli
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosCommandTuple {
private static final Logger LOGGER = LoggerFactory.getLogger(HeosCommandTuple.class);
public final HeosCommandGroup commandGroup;
public final HeosCommand command;
public HeosCommandTuple(HeosCommandGroup commandGroup, HeosCommand command) {
this.commandGroup = commandGroup;
this.command = command;
}
@Nullable
public static HeosCommandTuple valueOf(String commandString) {
String[] split = commandString.split("/");
if (split.length != 2) {
return null;
}
try {
HeosCommandGroup group = HeosCommandGroup.valueOf(split[0].toUpperCase());
HeosCommand cmd = HeosCommand.valueOf(split[1].toUpperCase());
return new HeosCommandTuple(group, cmd);
} catch (IllegalArgumentException e) {
LOGGER.debug("Unsupported command {}", commandString);
return null;
}
}
@Override
public String toString() {
return commandGroup + "/" + command;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum to reference the attributes of the HEOS response
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosCommunicationAttribute {
COMMAND_UNDER_PROCESS("command under process"),
COUNT("count"),
CURRENT_POSITION("cur_pos"),
DURATION("duration"),
ERROR_ID("eid"),
GROUP_ID("gid"),
LEVEL("level"),
MUTE("mute"),
PLAYER_ID("pid"),
REPEAT("repeat"),
RETURNED("returned"),
SHUFFLE("shuffle"),
SIGNED_IN("signed_in"),
SOURCE_ID("sid"),
STATE("state"),
SYSTEM_ERROR_NUMBER("syserrno"),
USERNAME("un"),
ERROR("error"),
TEXT("text");
private final String label;
HeosCommunicationAttribute(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Error object for containing information about HEOS errors
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosError {
public final HeosErrorCode code;
private final @Nullable Long systemErrorNumber;
HeosError(@Nullable Long errorCode, @Nullable Long systemErrorNumber) {
if (errorCode == null) {
throw new IllegalArgumentException("Error code not given");
}
this.code = HeosErrorCode.of(errorCode);
this.systemErrorNumber = systemErrorNumber;
}
@Override
public String toString() {
return "HeosError{" + "code=" + code + ", systemErrorNumber=" + systemErrorNumber + '}';
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum for the different documented error for HEOS responses
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosErrorCode {
UNRECOGNIZED_COMMAND(1, "Unrecognized Command"),
INVALID_ID(2, "Invalid ID"),
WRONG_NUMBER_OF_COMMAND_ARGUMENTS(3, "Wrong Number of Command Arguments"),
REQUESTED_DATA_NOT_AVAILABLE(4, "Requested data not available"),
RESOURCE_CURRENTLY_NOT_AVAILABLE(5, "Resource currently not available"),
INVALID_CREDENTIALS(6, "Invalid Credentials"),
COMMAND_COULD_NOT_BE_EXECUTED(7, "Command Could Not Be Executed"),
USER_NOT_LOGGED_IN(8, "User not logged In"),
PARAMETER_OUT_OF_RANGE(9, "Parameter out of range"),
USER_NOT_FOUND(10, "User not found"),
INTERNAL_ERROR(11, "Internal Error"),
SYSTEM_ERROR(12, "System Error"),
PROCESSING_PREVIOUS_COMMAND(13, "Processing Previous Command"),
MEDIA_CANT_BE_PLAYED(14, "Media can't be played"),
OPTION_NO_SUPPORTED(15, "Option no supported"),
TOO_MANY_COMMANDS_IN_MESSAGE_QUEUE_TO_PROCESS(16, "Too many commands in message queue to process"),
REACHED_SKIP_LIMIT(17, "Reached skip limit");
private final int errorNumber;
private final String msg;
HeosErrorCode(int errorNumber, String msg) {
this.errorNumber = errorNumber;
this.msg = msg;
}
@Override
public String toString() {
return String.format("#%d: %s", errorNumber, msg);
}
public static HeosErrorCode of(long errorNumber) {
return Stream.of(values()).filter(v -> errorNumber == v.errorNumber).findAny()
.orElseThrow(() -> new IllegalArgumentException("An unknown error " + errorNumber + " occurred"));
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Enum to reference the different HEOS events
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum HeosEvent {
SOURCES_CHANGED,
PLAYERS_CHANGED,
GROUPS_CHANGED,
PLAYER_STATE_CHANGED,
PLAYER_NOW_PLAYING_CHANGED,
PLAYER_NOW_PLAYING_PROGRESS,
PLAYER_PLAYBACK_ERROR,
PLAYER_QUEUE_CHANGED,
PLAYER_VOLUME_CHANGED,
REPEAT_MODE_CHANGED,
SHUFFLE_MODE_CHANGED,
GROUP_VOLUME_CHANGED,
USER_CHANGED;
private static final Logger LOGGER = LoggerFactory.getLogger(HeosEvent.class);
@Nullable
public static HeosEvent valueOfString(@Nullable String eventCommand) {
if (eventCommand == null) {
return null;
}
try {
String command = eventCommand.substring(6);
return HeosEvent.valueOf(command.toUpperCase());
} catch (IllegalArgumentException e) {
LOGGER.debug("Unsupported event {}", eventCommand);
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Class for HEOS event objects
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosEventObject extends HeosObject {
public final @Nullable HeosEvent command;
public HeosEventObject(@Nullable HeosEvent command, String rawCommand, Map<String, String> attributes) {
super(rawCommand, attributes);
this.command = command;
}
@Override
public String toString() {
return "HeosEventObject{" + super.toString() + ", command=" + command + '}';
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract parent class for the HEOS event/response objects
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public abstract class HeosObject {
private final Logger logger = LoggerFactory.getLogger(HeosObject.class);
public final String rawCommand;
private final Map<String, String> attributes;
HeosObject(String rawCommand, Map<String, String> attributes) {
this.rawCommand = rawCommand;
this.attributes = attributes;
}
public boolean getBooleanAttribute(HeosCommunicationAttribute attributeName) {
return "on".equals(attributes.get(attributeName.getLabel()));
}
public @Nullable Long getNumericAttribute(HeosCommunicationAttribute attributeName) {
@Nullable
String attribute = attributes.get(attributeName.getLabel());
if (attribute == null) {
return null;
}
try {
return Long.valueOf(attribute);
} catch (NumberFormatException e) {
logger.debug("Failed to parse number: {}, message: {}", attribute, e.getMessage());
return null;
}
}
public @Nullable String getAttribute(HeosCommunicationAttribute attributeName) {
return attributes.get(attributeName.getLabel());
}
public boolean hasAttribute(HeosCommunicationAttribute attribute) {
return attributes.containsKey(attribute.getLabel());
}
@Override
public String toString() {
return "HeosObject{" + "rawCommand='" + rawCommand + '\'' + ", attributes=" + attributes + '}';
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.heos.internal.json.HeosOption;
/**
* Class for HEOS response objects
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class HeosResponseObject<T> extends HeosObject {
public final @Nullable HeosCommandTuple heosCommand;
public final boolean result;
public final @Nullable T payload;
public final Map<String, HeosOption> options;
public HeosResponseObject(@Nullable HeosCommandTuple heosCommand, String rawCommand, @Nullable String result,
Map<String, String> attributes, @Nullable T payload,
@Nullable List<Map<String, List<HeosOption>>> options) {
super(rawCommand, attributes);
this.heosCommand = heosCommand;
this.result = "success".equals(result);
this.payload = payload;
this.options = processOptions(options);
}
private Map<String, HeosOption> processOptions(@Nullable List<Map<String, List<HeosOption>>> options) {
if (options == null) {
return Collections.emptyMap();
}
return options.stream().map(Map::entrySet).flatMap(Set::stream)
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
}
public boolean isFinished() {
return (result || hasAttribute(ERROR_ID)) && !hasAttribute(COMMAND_UNDER_PROCESS);
}
public @Nullable HeosError getError() {
if (result || !hasAttribute(ERROR_ID)) {
return null;
}
return new HeosError(getNumericAttribute(ERROR_ID), getNumericAttribute(SYSTEM_ERROR_NUMBER));
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads from browse commands
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class BrowseResult {
public @Nullable YesNoEnum container;
@SerializedName("mid")
public @Nullable String mediaId;
public @Nullable YesNoEnum playable;
public @Nullable BrowseResultType type;
@SerializedName("cid")
public @Nullable String containerId;
public @Nullable String name;
@SerializedName("image_url")
public @Nullable String imageUrl;
@Override
public String toString() {
return "BrowseResult{" + "container=" + container + ", mediaId='" + mediaId + '\'' + ", playable=" + playable
+ ", type=" + type + ", containerId='" + containerId + '\'' + ", name='" + name + '\'' + ", imageUrl='"
+ imageUrl + '\'' + '}';
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Enum for browse result types from the HEOS cli
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum BrowseResultType {
@SerializedName("artist")
ARTIST,
@SerializedName("album")
ALBUM,
@SerializedName("song")
SONG,
@SerializedName("container")
CONTAINER,
@SerializedName("station")
STATION,
@SerializedName("playlist")
PLAYLIST
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.payload;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads when retrieving group (information)
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class Group {
@SerializedName("gid")
public String id = "";
public String name = "";
public List<Player> players = Collections.emptyList();
public String getGroupMemberIds() {
return players.stream().map(p -> p.id).collect(Collectors.joining(";"));
}
public String getLeaderId() {
return players.stream().filter(p -> p.role == GroupPlayerRole.LEADER).map(p -> p.id).findFirst()
.orElseThrow(() -> new IllegalStateException("Every group should have a leader"));
}
public static class Player {
@SerializedName("pid")
public String id = "";
public String name = "";
public GroupPlayerRole role = GroupPlayerRole.MEMBER;
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Enum for the roles that players have in a HEOS group
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum GroupPlayerRole {
@SerializedName("member")
MEMBER,
@SerializedName("leader")
LEADER,
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads from now_playing commands
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class Media {
public @Nullable String type;
public @Nullable String song;
public @Nullable String station;
public @Nullable String album;
public @Nullable String artist;
public @Nullable String imageUrl;
public @Nullable String albumId;
@SerializedName("mid")
public @Nullable String mediaId;
@SerializedName("qid")
public int queueId;
@SerializedName("sid")
public int sourceId;
public String combinedSongArtist() {
return String.format("%s - %s", artist, song);
}
@Override
public String toString() {
return "Media{" + "type='" + type + '\'' + ", song='" + song + '\'' + ", station='" + station + '\''
+ ", album='" + album + '\'' + ", artist='" + artist + '\'' + ", imageUrl='" + imageUrl + '\''
+ ", albumId='" + albumId + '\'' + ", mediaId='" + mediaId + '\'' + ", queueId=" + queueId
+ ", sourceId=" + sourceId + '}';
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Data class for response payloads when retrieving players
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public class Player {
public String name = "";
@SerializedName("pid")
public int playerId;
@SerializedName("gid")
public @Nullable Integer playerIdOfGroupLeader;
public String model = "";
public String version = "";
public String ip = "";
public String network = "";
public int lineout;
public @Nullable String serial;
@Override
public String toString() {
return "Player{" + "name='" + name + '\'' + ", playerId=" + playerId + ", playerIdOfGroupLeader="
+ playerIdOfGroupLeader + ", model='" + model + '\'' + ", version='" + version + '\'' + ", ip='" + ip
+ '\'' + ", network='" + network + '\'' + ", lineout=" + lineout + ", serial='" + serial + '\'' + '}';
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.payload;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Enum containing a yes/no values in HEOS responses
*
* @author Martin van Wingerden - Initial contribution
*/
@NonNullByDefault
public enum YesNoEnum {
@SerializedName("yes")
YES,
@SerializedName("no")
NO
}

View File

@@ -0,0 +1,333 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link HeosCommands} provides the available commands for the HEOS network.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosCommands {
// System Commands
private static final String REGISTER_CHANGE_EVENT_ON = "heos://system/register_for_change_events?enable=on";
private static final String REGISTER_CHANGE_EVENT_OFF = "heos://system/register_for_change_events?enable=off";
private static final String HEOS_ACCOUNT_CHECK = "heos://system/check_account";
private static final String prettifyJSONon = "heos://system/prettify_json_response?enable=on";
private static final String prettifyJSONoff = "heos://system/prettify_json_response?enable=off";
private static final String rebootSystem = "heos://system/reboot";
private static final String signOut = "heos://system/sign_out";
private static final String heartbeat = "heos://system/heart_beat";
// Player Commands Control
private static final String setPlayStatePlay = "heos://player/set_play_state?pid=";
private static final String setPlayStatePause = "heos://player/set_play_state?pid=";
private static final String setPlayStateStop = "heos://player/set_play_state?pid=";
private static final String setVolume = "heos://player/set_volume?pid=";
private static final String volumeUp = "heos://player/volume_up?pid=";
private static final String volumeDown = "heos://player/volume_down?pid=";
private static final String setMute = "heos://player/set_mute?pid=";
private static final String setMuteToggle = "heos://player/toggle_mute?pid=";
private static final String playNext = "heos://player/play_next?pid=";
private static final String playPrevious = "heos://player/play_previous?pid=";
private static final String playQueueItem = "heos://player/play_queue?pid=";
private static final String clearQueue = "heos://player/clear_queue?pid=";
private static final String deleteQueueItem = "heos://player/remove_from_queue?pid=";
private static final String setPlayMode = "heos://player/set_play_mode?pid=";
// Group Commands Control
private static final String getGroups = "heos://group/get_groups";
private static final String getGroupsInfo = "heos://group/get_group_info?gid=";
private static final String setGroup = "heos://group/set_group?pid=";
private static final String getGroupVolume = "heos://group/get_volume?gid=";
private static final String setGroupVolume = "heos://group/set_volume?gid=";
private static final String getGroupMute = "heos://group/get_mute?gid=";
private static final String setGroupMute = "heos://group/set_mute?gid=";
private static final String toggleGroupMute = "heos://group/toggle_mute?gid=";
private static final String groupVolumeUp = "heos://group/volume_up?gid=";
private static final String groupVolumeDown = "heos://group/volume_down?gid=";
// Player Commands get Information
private static final String getPlayers = "heos://player/get_players";
private static final String getPlayerInfo = "heos://player/get_player_info?pid=";
private static final String getPlayState = "heos://player/get_play_state?pid=";
private static final String getNowPlayingMedia = "heos://player/get_now_playing_media?pid=";
private static final String playerGetVolume = "heos://player/get_volume?pid=";
private static final String playerGetMute = "heos://player/get_mute?pid=";
private static final String getQueue = "heos://player/get_queue?pid=";
private static final String getPlayMode = "heos://player/get_play_mode?pid=";
// Browse Commands
private static final String getMusicSources = "heos://browse/get_music_sources";
private static final String browseSource = "heos://browse/browse?sid=";
private static final String playStream = "heos://browse/play_stream?pid=";
private static final String addToQueue = "heos://browse/add_to_queue?pid=";
private static final String playInputSource = "heos://browse/play_input?pid=";
private static final String playURL = "heos://browse/play_stream?pid=";
public static String registerChangeEventOn() {
return REGISTER_CHANGE_EVENT_ON;
}
public static String registerChangeEventOff() {
return REGISTER_CHANGE_EVENT_OFF;
}
public static String heosAccountCheck() {
return HEOS_ACCOUNT_CHECK;
}
public static String setPlayStatePlay(String pid) {
return setPlayStatePlay + pid + "&state=play";
}
public static String setPlayStatePause(String pid) {
return setPlayStatePause + pid + "&state=pause";
}
public static String setPlayStateStop(String pid) {
return setPlayStateStop + pid + "&state=stop";
}
public static String volumeUp(String pid) {
return volumeUp + pid + "&step=1";
}
public static String volumeDown(String pid) {
return volumeDown + pid + "&step=1";
}
public static String setMuteOn(String pid) {
return setMute + pid + "&state=on";
}
public static String setMuteOff(String pid) {
return setMute + pid + "&state=off";
}
public static String setMuteToggle(String pid) {
return setMuteToggle + pid + "&state=off";
}
public static String setShuffleMode(String pid, String shuffle) {
return setPlayMode + pid + "&shuffle=" + shuffle;
}
public static String setRepeatMode(String pid, String repeat) {
return setPlayMode + pid + "&repeat=" + repeat;
}
public static String getPlayMode(String pid) {
return getPlayMode + pid;
}
public static String playNext(String pid) {
return playNext + pid;
}
public static String playPrevious(String pid) {
return playPrevious + pid;
}
public static String setVolume(String vol, String pid) {
return setVolume + pid + "&level=" + vol;
}
public static String getPlayers() {
return getPlayers;
}
public static String getPlayerInfo(String pid) {
return getPlayerInfo + pid;
}
public static String getPlayState(String pid) {
return getPlayState + pid;
}
public static String getNowPlayingMedia(String pid) {
return getNowPlayingMedia + pid;
}
public static String getVolume(String pid) {
return playerGetVolume + pid;
}
public static String getMusicSources() {
return getMusicSources;
}
public static String prettifyJSONon() {
return prettifyJSONon;
}
public static String prettifyJSONoff() {
return prettifyJSONoff;
}
public static String getMute(String pid) {
return playerGetMute + pid;
}
public static String getQueue(String pid) {
return getQueue + pid;
}
public static String getQueue(String pid, int start, int end) {
return getQueue(pid) + "&range=" + start + "," + end;
}
public static String playQueueItem(String pid, String qid) {
return playQueueItem + pid + "&qid=" + qid;
}
public static String deleteQueueItem(String pid, String qid) {
return deleteQueueItem + pid + "&qid=" + qid;
}
public static String browseSource(String sid) {
return browseSource + sid;
}
public static String playStream(String pid) {
return playStream + pid;
}
public static String addToQueue(String pid) {
return addToQueue + pid;
}
public static String addContainerToQueuePlayNow(String pid, String sid, String cid) {
return addToQueue + pid + "&sid=" + sid + "&cid=" + cid + "&aid=1";
}
public static String clearQueue(String pid) {
return clearQueue + pid;
}
public static String rebootSystem() {
return rebootSystem;
}
public static String playStream(@Nullable String pid, @Nullable String sid, @Nullable String cid,
@Nullable String mid, @Nullable String name) {
String newCommand = playStream;
if (pid != null) {
newCommand = newCommand + pid;
}
if (sid != null) {
newCommand = newCommand + "&sid=" + sid;
}
if (cid != null) {
newCommand = newCommand + "&cid=" + cid;
}
if (mid != null) {
newCommand = newCommand + "&mid=" + mid;
}
if (name != null) {
newCommand = newCommand + "&name=" + name;
}
return newCommand;
}
public static String playStream(String pid, String sid, String mid) {
return playStream + pid + "&sid=" + sid + "&mid=" + mid;
}
public static String playInputSource(String des_pid, String source_pid, String input) {
return playInputSource + des_pid + "&spid=" + source_pid + "&input=inputs/" + input;
}
public static String playURL(String pid, String url) {
return playURL + pid + "&url=" + url;
}
public static String signIn(String username, String password) {
String encodedUsername = urlEncode(username);
String encodedPassword = urlEncode(password);
return "heos://system/sign_in?un=" + encodedUsername + "&pw=" + encodedPassword;
}
public static String signOut() {
return signOut;
}
public static String heartbeat() {
return heartbeat;
}
public static String getGroups() {
return getGroups;
}
public static String getGroupInfo(String gid) {
return getGroupsInfo + gid;
}
public static String setGroup(String[] gid) {
String players = String.join(",", gid);
return setGroup + players;
}
public static String getGroupVolume(String gid) {
return getGroupVolume + gid;
}
public static String setGroupVolume(String volume, String gid) {
return setGroupVolume + gid + "&level=" + volume;
}
public static String setGroupVolumeUp(String gid) {
return groupVolumeUp + gid + "&step=1";
}
public static String setGroupVolumeDown(String gid) {
return groupVolumeDown + gid + "&step=1";
}
public static String getGroupMute(String gid) {
return getGroupMute + gid;
}
public static String setGroupMuteOn(String gid) {
return setGroupMute + gid + "&state=on";
}
public static String setGroupMuteOff(String gid) {
return setGroupMute + gid + "&state=off";
}
public static String getToggleGroupMute(String gid) {
return toggleGroupMute + gid;
}
private static String urlEncode(String username) {
try {
String encoded = URLEncoder.encode(username, StandardCharsets.UTF_8.toString());
// however it cannot handle escaped @ signs
return encoded.replace("%40", "@");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 is not supported, bailing out");
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HeosConstants} provides the constants used within the HEOS
* network
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosConstants {
public static final String HEOS = "heos";
public static final String CONNECTION_LOST = "connection_lost";
public static final String EVENT_STREAM_TIMEOUT = "event_stream_timeout";
public static final String CONNECTION_RESTORED = "connection_restored";
public static final String PID = "pid";
// Event Results
public static final String ON = "on";
public static final String OFF = "off";
public static final String REPEAT_ALL = "on_all";
public static final String REPEAT_ONE = "on_one";
// Event Types
public static final String EVENT_TYPE_SYSTEM = "system";
public static final String EVENT_TYPE_EVENT = "event";
// Browse Command
public static final String FAVORITE_SID = "1028";
public static final String PLAYLISTS_SID = "1025";
public static final int INPUT_SID = 1027;
public static final String PLAY = "play";
public static final String PAUSE = "pause";
public static final String STOP = "stop";
public static final String STATION = "station";
public static final String SONG = "song";
// UI Commands
public static final String HEOS_UI_ALL = "All";
public static final String HEOS_UI_ONE = "One";
public static final String HEOS_UI_OFF = "Off";
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import java.util.EventListener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.api.HeosEventController;
import org.openhab.binding.heos.internal.exception.HeosFunctionalException;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.Media;
/**
* The {@link HeosEventListener } is an Event Listener
* for the HEOS network. Handler which wants the get informed
* by an HEOS event via the {@link HeosEventController} has to
* implement this class and register itself at the {@link HeosEventController}
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public interface HeosEventListener extends EventListener {
void playerStateChangeEvent(HeosEventObject eventObject);
void playerStateChangeEvent(HeosResponseObject<?> responseObject) throws HeosFunctionalException;
void playerMediaChangeEvent(String pid, Media media);
void bridgeChangeEvent(String event, boolean success, Object command);
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.payload.Group;
/**
* The {@link HeosGroup} represents the group within the
* HEOS network
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosGroup {
public static String calculateGroupMemberHash(Group group) {
List<String> sortedPlayerIds = group.players.stream().map(player -> player.id).sorted()
.collect(Collectors.toList());
return sortedToString(sortedPlayerIds);
}
private static String sortedToString(List<String> sortedPlayerIds) {
return Integer.toUnsignedString(sortedPlayerIds.hashCode());
}
public static String calculateGroupMemberHash(String members) {
List<String> sortedPlayerIds = Arrays.stream(members.split(";")).sorted().collect(Collectors.toList());
return sortedToString(sortedPlayerIds);
}
}

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import java.io.IOException;
import org.openhab.binding.heos.internal.json.HeosJsonParser;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.resources.Telnet.ReadException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HeosSendCommand} is responsible to send a command
* to the HEOS bridge
*
* @author Johannes Einig - Initial contribution
*/
public class HeosSendCommand {
private final Logger logger = LoggerFactory.getLogger(HeosSendCommand.class);
private final Telnet client;
private final HeosJsonParser parser = new HeosJsonParser();
public HeosSendCommand(Telnet client) {
this.client = client;
}
public <T> HeosResponseObject<T> send(String command, Class<T> clazz) throws IOException, ReadException {
HeosResponseObject<T> result;
int attempt = 0;
boolean send = client.send(command);
if (clazz == null) {
return null;
} else if (send) {
String line = client.readLine();
if (line == null) {
throw new IOException("No valid input was received");
}
result = parser.parseResponse(line, clazz);
while (!result.isFinished() && attempt < 3) {
attempt++;
logger.trace("Retrying \"{}\" (attempt {})", command, attempt);
line = client.readLine(15000);
if (line != null) {
result = parser.parseResponse(line, clazz);
}
}
if (attempt >= 3 && !result.isFinished()) {
throw new IOException("No valid input was received after multiple attempts");
}
return result;
} else {
throw new IOException("Not connected");
}
}
public boolean isHostReachable() {
return client.isHostReachable();
}
public boolean isConnected() {
return client.isConnected();
}
public void stopInputListener(String registerChangeEventOFF) {
logger.debug("Stopping HEOS event line listener");
client.stopInputListener();
if (client.isConnected()) {
try {
client.send(registerChangeEventOFF);
} catch (IOException e) {
logger.debug("Failure during closing connection to HEOS with message: {}", e.getMessage());
}
}
}
public void disconnect() {
if (client.isConnected()) {
return;
}
try {
logger.debug("Disconnecting HEOS command line");
client.disconnect();
} catch (IOException e) {
logger.debug("Failure during closing connection to HEOS with message: {}", e.getMessage());
}
logger.debug("Connection to HEOS system closed");
}
public void startInputListener(String command) throws IOException, ReadException {
HeosResponseObject<Void> response = send(command, Void.class);
if (response.result) {
client.startInputListener();
}
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@Link HeosStringPropertyChangeListener} provides the possibility
* to add a listener to an String and get informed about the new value.
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosStringPropertyChangeListener {
private final Logger logger = LoggerFactory.getLogger(HeosStringPropertyChangeListener.class);
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener propertyChangeListener) {
pcs.addPropertyChangeListener(propertyChangeListener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public void setValue(String newValue) {
logger.debug("Firing property change: {} {}", newValue, Thread.currentThread());
pcs.firePropertyChange("value", null, newValue);
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
import org.openhab.binding.heos.internal.json.payload.Media;
/**
* The {@link HeosSystemEventListener } is used for classes which
* wants to inform players or groups about change events
* from the HEOS system. Classes which wants to be informed
* has to implement the {@link HeosEventListener} and register at
* the class which extends this {@link HeosSystemEventListener}
*
* @author Johannes Einig - Initial contribution
*/
@NonNullByDefault
public class HeosSystemEventListener {
private Set<HeosEventListener> listenerList = new CopyOnWriteArraySet<>();
/**
* Register a listener from type {@link HeosEventListener} to be notified by
* a change event
*
* @param listener the lister from type {@link HeosEventListener} for change events
*/
public void addListener(HeosEventListener listener) {
listenerList.add(listener);
}
/**
* Removes the listener from the notification list
*
* @param listener the listener from type {@link HeosEventListener} to be removed
*/
public void removeListener(HeosEventListener listener) {
listenerList.remove(listener);
}
/**
* Notifies the registered listener of a changed state type event
*
* @param eventObject the command of the event
*/
public void fireStateEvent(HeosEventObject eventObject) {
listenerList.forEach(element -> element.playerStateChangeEvent(eventObject));
}
/**
* Notifies the registered listener of a changed media type event
*
* @param pid the ID of the player or group which has changed
* @param media the media information
*/
public void fireMediaEvent(String pid, Media media) {
listenerList.forEach(element -> element.playerMediaChangeEvent(pid, media));
}
/**
* Notifies the registered listener if a change of the bridge state
*
* @param event the event type
* @param success the result (success or fail)
* @param command the command of the event
*/
public void fireBridgeEvent(String event, boolean success, Object command) {
for (HeosEventListener heosEventListener : listenerList) {
heosEventListener.bridgeChangeEvent(event, success, command);
}
}
}

View File

@@ -0,0 +1,287 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.resources;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.net.io.CRLFLineReader;
import org.apache.commons.net.telnet.TelnetClient;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.NamedThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Telnet} is an Telnet Client which handles the connection
* to a network via the Telnet interface
*
* @author Johannes Einig - Initial contribution
*/
public class Telnet {
private final Logger logger = LoggerFactory.getLogger(Telnet.class);
private static final int READ_TIMEOUT = 3000;
private static final int IS_ALIVE_TIMEOUT = 10000;
private final HeosStringPropertyChangeListener eolNotifier = new HeosStringPropertyChangeListener();
private final TelnetClient client = new TelnetClient();
private ExecutorService timedReaderExecutor;
private String ip;
private int port;
private String readResult = "";
private InetAddress address;
private DataOutputStream outStream;
private BufferedInputStream bufferedStream;
/**
* Connects to a host with the specified IP address and port
*
* @param ip IP Address of the host
* @param port where to be connected
* @return True if connection was successful
* @throws SocketException
* @throws IOException
*/
public boolean connect(String ip, int port) throws SocketException, IOException {
this.ip = ip;
this.port = port;
try {
address = InetAddress.getByName(ip);
} catch (UnknownHostException e) {
logger.debug("Unknown Host Exception - Message: {}", e.getMessage());
}
timedReaderExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("heos-telnet-reader", true));
return openConnection();
}
private boolean openConnection() throws IOException {
client.setConnectTimeout(5000);
client.connect(ip, port);
outStream = new DataOutputStream(client.getOutputStream());
bufferedStream = new BufferedInputStream(client.getInputStream());
return client.isConnected();
}
/**
* Appends \r\n to the command.
* For clear send use sendClear
*
* @param command The command to be send
* @return true after the command was send
* @throws IOException
*/
public boolean send(String command) throws IOException {
if (client.isConnected()) {
sendClear(command + "\r\n");
return true;
} else {
return false;
}
}
/**
* Send command without additional commands
*
* @param command The command to be send
* @throws IOException
*/
private void sendClear(String command) throws IOException {
if (!client.isConnected()) {
return;
}
outStream.writeBytes(command);
outStream.flush();
}
/**
* Read all commands till an End Of Line is detected
* I more than one line is read every line is an
* element in the returned {@code ArrayList<>}
* Reading timed out after 3000 milliseconds. For another timing
*
* @return A list with all read commands
* @throws ReadException
* @throws IOException
* @see Telnet.readLine(int timeOut).
*/
public String readLine() throws ReadException, IOException {
return readLine(READ_TIMEOUT);
}
/**
* Read all commands till an End Of Line is detected
* I more than one line is read every line is an
* element in the returned {@code ArrayList<>}
* Reading time out is defined by parameter in
* milliseconds.
*
* @param timeOut the time in millis after reading times out
* @return A list with all read commands
* @throws ReadException
* @throws IOException
*/
public @Nullable String readLine(int timeOut) throws ReadException, IOException {
if (client.isConnected()) {
try {
return timedCallable(() -> {
BufferedReader reader = new CRLFLineReader(
new InputStreamReader(bufferedStream, StandardCharsets.UTF_8));
String lastLine;
do {
lastLine = reader.readLine();
} while (reader.ready());
return lastLine;
}, timeOut);
} catch (InterruptedException | TimeoutException e) {
throw new ReadException(e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
throw new ReadException(cause);
}
}
}
return null;
}
private String timedCallable(Callable<String> callable, int timeOut)
throws InterruptedException, ExecutionException, TimeoutException {
Future<String> future = timedReaderExecutor.submit(callable);
try {
return future.get(timeOut, TimeUnit.MILLISECONDS);
} catch (Exception e) {
future.cancel(true);
throw e;
}
}
/**
* Disconnect Telnet and close all Streams
*
* @throws IOException
*/
public void disconnect() throws IOException {
client.disconnect();
timedReaderExecutor.shutdown();
}
/**
* Input Listener which fires event if input is detected
*/
public void startInputListener() {
logger.debug("Starting input listener");
client.setReaderThread(true);
client.registerInputListener(this::inputAvailableRead);
}
public void stopInputListener() {
logger.debug("Stopping input listener");
client.unregisterInputListener();
}
/**
* Reader for InputListenerOnly which only reads the
* available data without any check
*/
private void inputAvailableRead() {
try {
int i = bufferedStream.available();
byte[] buffer = new byte[i];
bufferedStream.read(buffer);
String str = new String(buffer, StandardCharsets.UTF_8);
concatReadResult(str);
} catch (IOException e) {
logger.debug("IO Exception, message: {}", e.getMessage());
}
}
/**
* Read values until end of line is reached.
* Then fires event for change Listener.
*
* @return -1 to indicate that end of line is reached
* else returns 0
*/
private int concatReadResult(String value) {
readResult = readResult.concat(value);
if (readResult.contains("\r\n")) {
eolNotifier.setValue(readResult.trim());
readResult = "";
return -1;
}
return 0;
}
/**
* Checks if the HEOS system is reachable
* via the network. This does not check if
* a Telnet connection is open.
*
* @return true if HEOS is reachable
*/
public boolean isHostReachable() {
try {
return address != null && address.isReachable(IS_ALIVE_TIMEOUT);
} catch (IOException e) {
logger.debug("IO Exception- Message: {}", e.getMessage());
return false;
}
}
@Override
public String toString() {
return "Telnet{" + "ip='" + ip + '\'' + ", port=" + port + '}';
}
public HeosStringPropertyChangeListener getReadResultListener() {
return eolNotifier;
}
public boolean isConnected() {
return client.isConnected();
}
public static class ReadException extends Exception {
private static final long serialVersionUID = 1L;
public ReadException() {
super("Can not read from client");
}
public ReadException(Throwable cause) {
super("Can not read from client", cause);
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="heos" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>HEOS Binding</name>
<description>Binding for the Denon HEOS system.</description>
<author>Johannes Einig</author>
</binding:binding>

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="ungroup">
<item-type>Switch</item-type>
<label>Group</label>
</channel-type>
<channel-type id="album">
<item-type>String</item-type>
<label>Album</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="cover" advanced="true">
<item-type>Image</item-type>
<label>Cover</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="buildGroup">
<item-type>Switch</item-type>
<label>Make Group</label>
</channel-type>
<channel-type id="playlists" advanced="true">
<item-type>String</item-type>
<label>Playlists</label>
</channel-type>
<channel-type id="favorites" advanced="true">
<item-type>String</item-type>
<label>Favorites</label>
</channel-type>
<channel-type id="queue" advanced="true">
<item-type>String</item-type>
<label>Queue</label>
</channel-type>
<channel-type id="clearQueue" advanced="true">
<item-type>Switch</item-type>
<label>Clear Queue</label>
</channel-type>
<channel-type id="reboot">
<item-type>Switch</item-type>
<label>Reboot</label>
</channel-type>
<channel-type id="input" advanced="true">
<item-type>String</item-type>
<label>External Inputs</label>
</channel-type>
<channel-type id="currentPosition" advanced="true">
<item-type>Number:Time</item-type>
<label>Track Position</label>
<description>The current track position</description>
<category>Time</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="duration" advanced="true">
<item-type>Number:Time</item-type>
<label>Track Duration</label>
<description>The overall duration of the track</description>
<category>Time</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="rawCommand" advanced="true">
<item-type>String</item-type>
<label>Send RAW Command</label>
<description>Sending a HEOS command as specified within the HEOS CLI protocol</description>
</channel-type>
<channel-type id="type" advanced="true">
<item-type>String</item-type>
<label>Type</label>
<description>The media currently played type (station, song, ...)</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="station" advanced="true">
<item-type>String</item-type>
<label>Station</label>
<description>The name of the station currently played</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="playUrl" advanced="true">
<item-type>String</item-type>
<label>Play URL</label>
<description>Plays a media file from URL</description>
</channel-type>
<channel-type id="shuffleMode" advanced="true">
<item-type>Switch</item-type>
<label>Shuffle</label>
<description>Sets the shuffle mode</description>
</channel-type>
<channel-type id="repeatMode" advanced="true">
<item-type>String</item-type>
<label>Repeat Mode</label>
<description>Set the repeat mode</description>
<state readOnly="false">
<options>
<option value="One">One</option>
<option value="All">All</option>
<option value="Off">Off</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="group">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>HEOS Group</label>
<description>A group of HEOS Player</description>
<channels>
<channel id="Control" typeId="system.media-control"/>
<channel id="Volume" typeId="system.volume"/>
<channel id="Mute" typeId="system.mute"/>
<channel id="Inputs" typeId="input"/>
<channel id="Title" typeId="system.media-title"/>
<channel id="Artist" typeId="system.media-artist"/>
<channel id="Album" typeId="album"/>
<channel id="Cover" typeId="cover"/>
<channel id="CurrentPosition" typeId="currentPosition"/>
<channel id="Duration" typeId="duration"/>
<channel id="Type" typeId="type"/>
<channel id="Station" typeId="station"/>
<channel id="PlayUrl" typeId="playUrl"/>
<channel id="Ungroup" typeId="ungroup"/>
<channel id="Shuffle" typeId="shuffleMode"/>
<channel id="RepeatMode" typeId="repeatMode"/>
<channel id="Playlists" typeId="playlists"/>
<channel id="Favorites" typeId="favorites"/>
<channel id="Queue" typeId="queue"/>
<channel id="ClearQueue" typeId="clearQueue"/>
</channels>
<properties>
<property name="vendor">Denon</property>
</properties>
<config-description>
<parameter name="members" type="text" readOnly="false">
<label>The Group Member Player IDs</label>
<description>Shows the player IDs of the members of this group</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Heos Player Thing Type -->
<thing-type id="player">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>HEOS Player</label>
<description>A HEOS Player of the HEOS Network</description>
<channels>
<channel id="Control" typeId="system.media-control"/>
<channel id="Volume" typeId="system.volume"/>
<channel id="Mute" typeId="system.mute"/>
<channel id="Inputs" typeId="input"/>
<channel id="Title" typeId="system.media-title"/>
<channel id="Artist" typeId="system.media-artist"/>
<channel id="Album" typeId="album"/>
<channel id="Cover" typeId="cover"/>
<channel id="CurrentPosition" typeId="currentPosition"/>
<channel id="Duration" typeId="duration"/>
<channel id="Type" typeId="type"/>
<channel id="Station" typeId="station"/>
<channel id="PlayUrl" typeId="playUrl"/>
<channel id="Shuffle" typeId="shuffleMode"/>
<channel id="RepeatMode" typeId="repeatMode"/>
<channel id="Favorites" typeId="favorites"/>
<channel id="Playlists" typeId="playlists"/>
<channel id="Queue" typeId="queue"/>
<channel id="ClearQueue" typeId="clearQueue"/>
</channels>
<properties>
<property name="vendor">Denon</property>
</properties>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter name="pid" type="text" readOnly="false">
<label>Player ID</label>
<description>The internal Player ID</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="heos"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Thing Type -->
<bridge-type id="bridge">
<label>HEOS Bridge</label>
<description>The HEOS System Bridge</description>
<channels>
<channel typeId="reboot" id="Reboot"></channel>
<channel typeId="buildGroup" id="BuildGroup"></channel>
</channels>
<representation-property>vendor</representation-property>
<config-description>
<parameter name="ipAddress" type="text">
<context>network-address</context>
<label>Network Address</label>
<description>Network address of the HEOS bridge.</description>
<required>true</required>
</parameter>
<parameter name="username" type="text">
<label>Username</label>
<description>Username for login to the HEOS account.</description>
<required>false</required>
</parameter>
<parameter name="password" type="text">
<context>password</context>
<label>Password</label>
<description>Password for login to the HEOS account</description>
<required>false</required>
</parameter>
<parameter name="heartbeat" type="integer" min="3" max="3600" unit="s">
<required>false</required>
<unitLabel>seconds</unitLabel>
<label>Heartbeat</label>
<description>The time in seconds for the HEOS Heartbeat (default = 60 s)</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,288 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json;
import static java.lang.Long.valueOf;
import static org.junit.Assert.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute;
import org.openhab.binding.heos.internal.json.dto.HeosEvent;
import org.openhab.binding.heos.internal.json.dto.HeosEventObject;
/**
* Tests to validate the functioning of the HeosJsonParser specifically for event objects
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class HeosJsonParserEventTest {
private final HeosJsonParser subject = new HeosJsonParser();
@Test
public void event_now_playing_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_now_playing_changed\", \"message\": \"pid=1679855527\"}}");
assertEquals(HeosEvent.PLAYER_NOW_PLAYING_CHANGED, event.command);
assertEquals("event/player_now_playing_changed", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
}
@Test
public void event_now_playing_progress() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_now_playing_progress\", \"message\": \"pid=1679855527&cur_pos=224848000&duration=0\"}}");
assertEquals(HeosEvent.PLAYER_NOW_PLAYING_PROGRESS, event.command);
assertEquals("event/player_now_playing_progress", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals(valueOf(224848000), event.getNumericAttribute(HeosCommunicationAttribute.CURRENT_POSITION));
assertEquals(valueOf(0), event.getNumericAttribute(HeosCommunicationAttribute.DURATION));
}
@Test
public void event_state_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_state_changed\", \"message\": \"pid=1679855527&state=play\"}}");
assertEquals(HeosEvent.PLAYER_STATE_CHANGED, event.command);
assertEquals("event/player_state_changed", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals("play", event.getAttribute(HeosCommunicationAttribute.STATE));
}
@Test
public void event_playback_error() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_playback_error\", \"message\": \"pid=1679855527&error=Could Not Download\"}}");
assertEquals(HeosEvent.PLAYER_PLAYBACK_ERROR, event.command);
assertEquals("event/player_playback_error", event.rawCommand);
assertEquals(valueOf(1679855527), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals("Could Not Download", event.getAttribute(HeosCommunicationAttribute.ERROR));
}
@Test
public void event_volume_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/player_volume_changed\", \"message\": \"pid=1958912779&level=23&mute=off\"}}");
assertEquals(HeosEvent.PLAYER_VOLUME_CHANGED, event.command);
assertEquals("event/player_volume_changed", event.rawCommand);
assertEquals(valueOf(1958912779), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertEquals(valueOf(23), event.getNumericAttribute(HeosCommunicationAttribute.LEVEL));
assertFalse(event.getBooleanAttribute(HeosCommunicationAttribute.MUTE));
}
@Test
public void event_shuffle_mode_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/shuffle_mode_changed\", \"message\": \"pid=-831584083&shuffle=on\"}}");
assertEquals(HeosEvent.SHUFFLE_MODE_CHANGED, event.command);
assertEquals("event/shuffle_mode_changed", event.rawCommand);
assertEquals(valueOf(-831584083), event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
assertTrue(event.getBooleanAttribute(HeosCommunicationAttribute.SHUFFLE));
}
@Test
public void event_sources_changed() {
HeosEventObject event = subject.parseEvent("{\"heos\": {\"command\": \"event/sources_changed\"}}");
assertEquals(HeosEvent.SOURCES_CHANGED, event.command);
assertEquals("event/sources_changed", event.rawCommand);
}
@Test
public void event_user_changed() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/user_changed\", \"message\": \"signed_in&un=martinvw@mtin.nl\"}}");
assertEquals(HeosEvent.USER_CHANGED, event.command);
assertEquals("event/user_changed", event.rawCommand);
assertTrue(event.hasAttribute(HeosCommunicationAttribute.SIGNED_IN));
assertEquals("martinvw@mtin.nl", event.getAttribute(HeosCommunicationAttribute.USERNAME));
}
@Test
public void event_unknown_event() {
HeosEventObject event = subject.parseEvent("{\"heos\": {\"command\": \"event/does_not_exist\"}}");
assertNull(event.command);
assertEquals("event/does_not_exist", event.rawCommand);
}
@Test
public void event_duplicate_attributes() {
HeosEventObject event = subject.parseEvent(
"{\"heos\": {\"command\": \"event/does_not_exist\", \"message\": \"signed_in&un=test1&un=test2\"}}");
// the first one is ignored but it does not crash
assertEquals("test2", event.getAttribute(HeosCommunicationAttribute.USERNAME));
}
@Test
public void event_non_numeric() {
HeosEventObject event = subject
.parseEvent("{\"heos\": {\"command\": \"event/does_not_exist\", \"message\": \"pid=test\"}}");
// the first one is ignored but it does not crash
assertNull(event.getNumericAttribute(HeosCommunicationAttribute.PLAYER_ID));
}
@Test
public void event_numeric_missing() {
HeosEventObject event = subject.parseEvent("{\"heos\": {\"command\": \"event/does_not_exist\"}}");
// the first one is ignored but it does not crash
assertNull(event.getAttribute(HeosCommunicationAttribute.PLAYER_ID));
}
/*
*
* {"heos": {"command": "browse/browse", "result": "success", "message": "command under process&sid=1025"}}
* {"heos": {"command": "browse/browse", "result": "success", "message": "command under process&sid=1028"}}
* {"heos": {"command": "browse/browse", "result": "success", "message": "sid=1025&returned=6&count=6"}, "payload":
* [{"container": "yes", "type": "playlist", "cid": "132562", "playable": "yes", "name":
* "Maaike Ouboter - En hoe het dan ook weer dag wordt", "image_url": ""}, {"container": "yes", "type": "playlist",
* "cid": "132563", "playable": "yes", "name": "Maaike Ouboter - Vanaf nu is het van jou", "image_url": ""},
* {"container": "yes", "type": "playlist", "cid": "162887", "playable": "yes", "name": "Easy listening",
* "image_url": ""}, {"container": "yes", "type": "playlist", "cid": "174461", "playable": "yes", "name":
* "Nieuwe muziek 5-2019", "image_url": ""}, {"container": "yes", "type": "playlist", "cid": "194000", "playable":
* "yes", "name": "Nieuwe muziek 2019-05", "image_url": ""}, {"container": "yes", "type": "playlist", "cid":
* "194001", "playable": "yes", "name": "Clean Bandit", "image_url": ""}]}
* {"heos": {"command": "browse/browse", "result": "success", "message": "sid=1028&returned=3&count=3"}, "payload":
* [{"container": "no", "mid": "s6707", "type": "station", "playable": "yes", "name":
* "NPO 3FM 96.8 (Top 40 %26 Pop Music)", "image_url":
* "http://cdn-profiles.tunein.com/s6707/images/logoq.png?t=636268"}, {"container": "no", "mid": "s2967", "type":
* "station", "playable": "yes", "name": "Classic FM Nederland (Classical Music)", "image_url":
* "http://cdn-radiotime-logos.tunein.com/s2967q.png"}, {"container": "no", "mid": "s1993", "type": "station",
* "playable": "yes", "name": "BNR Nieuwsradio", "image_url": "http://cdn-radiotime-logos.tunein.com/s1993q.png"}],
* "options": [{"browse": [{"id": 20, "name": "Remove from HEOS Favorites"}]}]}
* {"heos": {"command": "event/user_changed", "message": "signed_in&un=martinvw@mtin.nl"}}
* {"heos": {"command": "group/get_groups", "result": "success", "message": ""}, "payload": []}
* {"heos": {"command": "player/get_mute", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=1958912779&state=off"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=1958912779&state=on"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=-831584083&state=off"}}
* {"heos": {"command": "player/get_mute", "result": "success", "message": "pid=-831584083&state=on"}}
* {"heos": {"command": "player/get_now_playing_media", "result": "fail", "message":
* "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1679855527"},
* "payload": {"type": "song", "song": "", "album": "", "artist": "", "image_url": "", "album_id": "1", "mid": "1",
* "qid": 1, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1958912779"},
* "payload": {"type": "song", "song": "Solo (feat. Demi Lovato)", "album": "What Is Love? (Deluxe)", "artist":
* "Clean Bandit", "image_url":
* "http://192.168.1.230:8015//m-browsableMediaUri/getImageFromTag/mnt/326C72A3E307501E47DE2B0F47D90EB8/Clean%20Bandit/What%20Is%20Love_%20(Deluxe)/03%20Solo%20(feat.%20Demi%20Lovato).m4a",
* "album_id": "", "mid":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/Clean+Bandit/What+Is+Love_+(Deluxe)/03+Solo+(feat.+Demi+Lovato).m4a",
* "qid": 1, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1958912779"},
* "payload": {"type": "station", "song": "HEOS Bar - HDMI 2", "station": "HEOS Bar - HDMI 2", "album": "",
* "artist": "", "image_url": "", "album_id": "inputs", "mid": "inputs/hdmi_in_2", "qid": 1, "sid": 1027},
* "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=1958912779"},
* "payload": {"type": "station", "song": "HEOS Bar - HDMI 3", "station": "HEOS Bar - HDMI 3", "album": "",
* "artist": "", "image_url": "", "album_id": "inputs", "mid": "inputs/hdmi_in_3", "qid": 1, "sid": 1027},
* "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "Applejack", "album":
* "The Real... Dolly Parton: The Ultimate Dolly Parton Collection", "artist": "Dolly Parton", "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/Dolly%20Parton/The%20Real%20Dolly%20Parton%20%5bDisc%202%5d/2-07%20Applejack.m4a",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-418", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-418/t-4150", "qid": 43, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "Dancing Queen", "album": "ABBA Gold: Greatest Hits", "artist": "ABBA",
* "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/ABBA/ABBA%20Gold_%20Greatest%20Hits/01%20Dancing%20Queen%201.m4a",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-398", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-398/t-4237", "qid": 1, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "D.I.V.O.R.C.E.", "album":
* "The Real... Dolly Parton: The Ultimate Dolly Parton Collection", "artist": "Dolly Parton", "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/Dolly%20Parton/The%20Real%20Dolly%20Parton%20%5bDisc%201%5d/1-03%20D.I.V.O.R.C.E.m4a",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-417", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-417/t-4138", "qid": 22, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_now_playing_media", "result": "success", "message": "pid=-831584083"},
* "payload": {"type": "song", "song": "Homeward Bound", "album": "The Very Best Of Art Garfunkel: Across America",
* "artist": "Art Garfunkel", "image_url":
* "http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/getImageFromTag/Art%20Garfunkel/The%20Very%20Best%20Of%20Art%20Garfunkel_%20Across%20A/06%20-%20Art%20Garfunkel%20-%20Homeward%20Bound.mp3",
* "album_id": "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-127", "mid":
* "m-1c176905-f6c7-d168-dc35-86b4735c5976/alb/a-127/t-1385", "qid": 80, "sid": 1024}, "options": []}
* {"heos": {"command": "player/get_player_info", "result": "success", "message": "pid=1958912779"}, "payload":
* {"name": "HEOS Bar", "pid": 1958912779, "model": "HEOS Bar", "version": "1.520.200", "ip": "192.168.1.195",
* "network": "wired", "lineout": 0, "serial": "ADAG9180917029"}}
* {"heos": {"command": "player/get_player_info", "result": "success", "message": "pid=-831584083"}, "payload":
* {"name": "Kantoor HEOS 3", "pid": -831584083, "model": "HEOS 3", "version": "1.520.200", "ip": "192.168.1.230",
* "network": "wired", "lineout": 0, "serial": "ACNG9180110887"}}
* {"heos": {"command": "player/get_players", "result": "success", "message": ""}, "payload": [{"name": "HEOS Bar",
* "pid": 1958912779, "model": "HEOS Bar", "version": "1.520.200", "ip": "192.168.1.195", "network": "wired",
* "lineout": 0, "serial": "ADAG9180917029"}, {"name": "Kantoor HEOS 3", "pid": -831584083, "model": "HEOS 3",
* "version": "1.520.200", "ip": "192.168.1.230", "network": "wired", "lineout": 0, "serial": "ACNG9180110887"}]}
* {"heos": {"command": "player/get_players", "result": "success", "message": ""}, "payload": [{"name":
* "Kantoor HEOS 3", "pid": -831584083, "model": "HEOS 3", "version": "1.520.200", "ip": "192.168.1.230", "network":
* "wired", "lineout": 0, "serial": "ACNG9180110887"}, {"name": "HEOS Bar", "pid": 1958912779, "model": "HEOS Bar",
* "version": "1.520.200", "ip": "192.168.1.195", "network": "wired", "lineout": 0, "serial": "ADAG9180917029"}]}
* {"heos": {"command": "player/get_play_mode", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_play_mode", "result": "success", "message":
* "pid=1958912779&repeat=off&shuffle=off"}}
* {"heos": {"command": "player/get_play_mode", "result": "success", "message":
* "pid=-831584083&repeat=off&shuffle=on"}}
* {"heos": {"command": "player/get_play_state", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=1958912779&state=stop"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=-831584083&state=pause"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=-831584083&state=play"}}
* {"heos": {"command": "player/get_play_state", "result": "success", "message": "pid=-831584083&state=stop"}}
* {"heos": {"command": "player/get_volume", "result": "fail", "message": "eid=2&text=ID Not Valid&pid=null"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=1958912779&level=14"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=1958912779&level=21"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=1958912779&level=23"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=-831584083&level=12"}}
* {"heos": {"command": "player/get_volume", "result": "success", "message": "pid=-831584083&level=15"}}
* {"heos": {"command": "player/play_next", "result": "success", "message": "pid=-831584083"}}
* {"heos": {"command": "player/play_previous", "result": "success", "message": "pid=-831584083"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=1958912779&state=off"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=1958912779&state=on"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=-831584083&state=off"}}
* {"heos": {"command": "player/set_mute", "result": "success", "message": "pid=-831584083&state=on"}}
* {"heos": {"command": "player/set_play_mode", "result": "success", "message": "pid=-831584083&shuffle=off"}}
* {"heos": {"command": "player/set_play_mode", "result": "success", "message": "pid=-831584083&shuffle=on"}}
* {"heos": {"command": "player/set_play_state", "result": "success", "message": "pid=-831584083&state=pause"}}
* {"heos": {"command": "player/set_play_state", "result": "success", "message": "pid=-831584083&state=play"}}
* {"heos": {"command": "player/set_volume", "result": "fail", "message":
* "eid=9&text=Out of range&pid=-831584083&level=OFF"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=1958912779&level=14"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=1958912779&level=17"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=10"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=12"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=14"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=15"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=16"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=18"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=21"}}
* {"heos": {"command": "player/set_volume", "result": "success", "message": "pid=-831584083&level=4"}}
* {"heos": {"command": "player/volume_down", "result": "success", "message": "pid=-831584083&step=1"}}
* {"heos": {"command": "system/heart_beat", "result": "success", "message": ""}}
* {"heos": {"command": "system/register_for_change_events", "result": "success", "message": "enable=off"}}
* {"heos": {"command": "system/register_for_change_events", "result": "success", "message": "enable=on"}}
* {"heos": {"command": "system/register_for_change_events", "reult": "success", "message": "enable=on"}}
* {"heos": {"command": "system/sign_in", "result": "success", "message":
* "command under process&un=martinvw@mtin.nl&pw=Pl7WUFC61Q7zdQD5"}}
* {"heos": {"command": "system/sign_in", "result": "success", "message": "signed_in&un=martinvw@mtin.nl"}}
*
*/
}

View File

@@ -0,0 +1,311 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json;
import static org.junit.Assert.*;
import static org.openhab.binding.heos.internal.json.dto.HeosCommunicationAttribute.*;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.heos.internal.json.dto.HeosCommand;
import org.openhab.binding.heos.internal.json.dto.HeosCommandGroup;
import org.openhab.binding.heos.internal.json.dto.HeosErrorCode;
import org.openhab.binding.heos.internal.json.dto.HeosResponseObject;
import org.openhab.binding.heos.internal.json.payload.BrowseResult;
import org.openhab.binding.heos.internal.json.payload.BrowseResultType;
import org.openhab.binding.heos.internal.json.payload.Group;
import org.openhab.binding.heos.internal.json.payload.GroupPlayerRole;
import org.openhab.binding.heos.internal.json.payload.Media;
import org.openhab.binding.heos.internal.json.payload.Player;
import org.openhab.binding.heos.internal.json.payload.YesNoEnum;
/**
* Tests to validate the functioning of the HeosJsonParser specifically for response objects
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class HeosJsonParserResponseTest {
private final HeosJsonParser subject = new HeosJsonParser();
@Test
public void sign_in() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"system/sign_in\", \"result\": \"success\", \"message\": \"signed_in&un=test@example.org\"}}",
Void.class);
assertEquals(HeosCommandGroup.SYSTEM, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SIGN_IN, response.heosCommand.command);
assertTrue(response.result);
assertEquals("test@example.org", response.getAttribute(USERNAME));
assertTrue(response.hasAttribute(SIGNED_IN));
}
@Test
public void sign_in_under_process() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"system/sign_in\", \"message\": \"command under process\"}}", Void.class);
assertEquals(HeosCommandGroup.SYSTEM, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SIGN_IN, response.heosCommand.command);
assertFalse(response.result);
assertFalse(response.isFinished());
}
@Test
public void sign_in_failed() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"system/sign_in\", \"message\": \"eid=10&text=User not found\"}}",
Void.class);
assertEquals(HeosCommandGroup.SYSTEM, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SIGN_IN, response.heosCommand.command);
assertFalse(response.result);
assertTrue(response.isFinished());
assertEquals(HeosErrorCode.USER_NOT_FOUND, response.getError().code);
}
@Test
public void get_mute() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_mute\", \"result\": \"success\", \"message\": \"pid=1958912779&state=on\"}}",
Void.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_MUTE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1958912779), response.getNumericAttribute(PLAYER_ID));
assertTrue(response.getBooleanAttribute(STATE));
}
@Test
public void get_mute_error() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_mute\", \"result\": \"fail\", \"message\": \"eid=2&text=ID Not Valid&pid=null\"}}",
Void.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_MUTE, response.heosCommand.command);
assertFalse(response.result);
assertEquals(HeosErrorCode.INVALID_ID, response.getError().code);
}
@Test
public void browse_browse_under_process() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"browse/browse\", \"result\": \"success\", \"message\": \"command under process&sid=1025\"}}",
Void.class);
assertEquals(HeosCommandGroup.BROWSE, response.heosCommand.commandGroup);
assertEquals(HeosCommand.BROWSE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1025), response.getNumericAttribute(SOURCE_ID));
assertFalse(response.isFinished());
}
@Test
public void incorrect_level() {
HeosResponseObject<Void> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/set_volume\", \"result\": \"fail\", \"message\": \"eid=9&text=Parameter out of range&pid=-831584083&level=OFF\"}}",
Void.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.SET_VOLUME, response.heosCommand.command);
assertFalse(response.result);
assertEquals(HeosErrorCode.PARAMETER_OUT_OF_RANGE, response.getError().code);
assertEquals("#9: Parameter out of range", response.getError().code.toString());
}
@Test
public void get_players() {
HeosResponseObject<Player[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_players\", \"result\": \"success\", \"message\": \"\"}, \"payload\": ["
+ "{\"name\": \"Kantoor HEOS 3\", \"pid\": -831584083, \"model\": \"HEOS 3\", \"version\": \"1.520.200\", \"ip\": \"192.168.1.230\", \"network\": \"wired\", \"lineout\": 0, \"serial\": \"ACNG9180110887\"}, "
+ "{\"name\": \"HEOS Bar\", \"pid\": 1958912779, \"model\": \"HEOS Bar\", \"version\": \"1.520.200\", \"ip\": \"192.168.1.195\", \"network\": \"wired\", \"lineout\": 0, \"serial\": \"ADAG9180917029\"}]}",
Player[].class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_PLAYERS, response.heosCommand.command);
assertTrue(response.result);
assertEquals(2, response.payload.length);
Player player0 = response.payload[0];
assertEquals("Kantoor HEOS 3", player0.name);
assertEquals(-831584083, player0.playerId);
assertEquals("HEOS 3", player0.model);
assertEquals("1.520.200", player0.version);
assertEquals("192.168.1.230", player0.ip);
assertEquals("wired", player0.network);
assertEquals(0, player0.lineout);
assertEquals("ACNG9180110887", player0.serial);
Player player1 = response.payload[1];
assertEquals("HEOS Bar", player1.name);
assertEquals(1958912779, player1.playerId);
assertEquals("HEOS Bar", player1.model);
assertEquals("1.520.200", player1.version);
assertEquals("192.168.1.195", player1.ip);
assertEquals("wired", player1.network);
assertEquals(0, player1.lineout);
assertEquals("ADAG9180917029", player1.serial);
}
@Test
public void get_player_info() {
HeosResponseObject<Player> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_player_info\", \"result\": \"success\", \"message\": \"pid=1958912779\"}, \"payload\": {\"name\": \"HEOS Bar\", \"pid\": 1958912779, \"model\": \"HEOS Bar\", \"version\": \"1.520.200\", \"ip\": \"192.168.1.195\", \"network\": \"wired\", \"lineout\": 0, \"serial\": \"ADAG9180917029\"}}",
Player.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_PLAYER_INFO, response.heosCommand.command);
assertTrue(response.result);
assertEquals("HEOS Bar", response.payload.name);
assertEquals(1958912779, response.payload.playerId);
assertEquals("HEOS Bar", response.payload.model);
assertEquals("1.520.200", response.payload.version);
assertEquals("192.168.1.195", response.payload.ip);
assertEquals("wired", response.payload.network);
assertEquals(0, response.payload.lineout);
assertEquals("ADAG9180917029", response.payload.serial);
}
@Test
public void get_now_playing_media() {
HeosResponseObject<Media> response = subject.parseResponse(
"{\"heos\": {\"command\": \"player/get_now_playing_media\", \"result\": \"success\", \"message\": \"pid=1958912779\"}, \"payload\": "
+ "{\"type\": \"song\", \"song\": \"Solo (feat. Demi Lovato)\", \"album\": \"What Is Love? (Deluxe)\", \"artist\": \"Clean Bandit\", \"image_url\": \"http://192.168.1.230:8015//m-browsableMediaUri/getImageFromTag/mnt/326C72A3E307501E47DE2B0F47D90EB8/Clean%20Bandit/What%20Is%20Love_%20(Deluxe)/03%20Solo%20(feat.%20Demi%20Lovato).m4a\", \"album_id\": \"\", \"mid\": \"http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/Clean+Bandit/What+Is+Love_+(Deluxe)/03+Solo+(feat.+Demi+Lovato).m4a\", \"qid\": 1, \"sid\": 1024}, \"options\": []}\n",
Media.class);
assertEquals(HeosCommandGroup.PLAYER, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_NOW_PLAYING_MEDIA, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1958912779), response.getNumericAttribute(PLAYER_ID));
assertEquals("song", response.payload.type);
assertEquals("Solo (feat. Demi Lovato)", response.payload.song);
assertEquals("What Is Love? (Deluxe)", response.payload.album);
assertEquals("Clean Bandit", response.payload.artist);
assertEquals(
"http://192.168.1.230:8015//m-browsableMediaUri/getImageFromTag/mnt/326C72A3E307501E47DE2B0F47D90EB8/Clean%20Bandit/What%20Is%20Love_%20(Deluxe)/03%20Solo%20(feat.%20Demi%20Lovato).m4a",
response.payload.imageUrl);
assertEquals("", response.payload.albumId);
assertEquals(
"http://192.168.1.230:8015/m-1c176905-f6c7-d168-dc35-86b4735c5976/Clean+Bandit/What+Is+Love_+(Deluxe)/03+Solo+(feat.+Demi+Lovato).m4a",
response.payload.mediaId);
assertEquals(1, response.payload.queueId);
assertEquals(1024, response.payload.sourceId);
}
@Test
public void browse_playlist() {
HeosResponseObject<BrowseResult[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"browse/browse\", \"result\": \"success\", \"message\": \"sid=1025&returned=6&count=6\"}, \"payload\": ["
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"132562\", \"playable\": \"yes\", \"name\": \"Maaike Ouboter - En hoe het dan ook weer dag wordt\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"132563\", \"playable\": \"yes\", \"name\": \"Maaike Ouboter - Vanaf nu is het van jou\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"162887\", \"playable\": \"yes\", \"name\": \"Easy listening\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"174461\", \"playable\": \"yes\", \"name\": \"Nieuwe muziek 5-2019\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"194000\", \"playable\": \"yes\", \"name\": \"Nieuwe muziek 2019-05\", \"image_url\": \"\"}, "
+ "{\"container\": \"yes\", \"type\": \"playlist\", \"cid\": \"194001\", \"playable\": \"yes\", \"name\": \"Clean Bandit\", \"image_url\": \"\"}]}",
BrowseResult[].class);
assertEquals(HeosCommandGroup.BROWSE, response.heosCommand.commandGroup);
assertEquals(HeosCommand.BROWSE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1025), response.getNumericAttribute(SOURCE_ID));
assertEquals(Long.valueOf(6), response.getNumericAttribute(RETURNED));
assertEquals(Long.valueOf(6), response.getNumericAttribute(COUNT));
BrowseResult result = response.payload[5];
assertEquals(YesNoEnum.YES, result.container);
assertEquals(BrowseResultType.PLAYLIST, result.type);
assertEquals(YesNoEnum.YES, result.playable);
assertEquals("194001", result.containerId);
assertEquals("Clean Bandit", result.name);
assertEquals("", result.imageUrl);
}
@Test
public void browse_favorites() {
HeosResponseObject<BrowseResult[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"browse/browse\", \"result\": \"success\", \"message\": \"sid=1028&returned=3&count=3\"}, \"payload\": ["
+ "{\"container\": \"no\", \"mid\": \"s6707\", \"type\": \"station\", \"playable\": \"yes\", \"name\": \"NPO 3FM 96.8 (Top 40 %26 Pop Music)\", \"image_url\": \"http://cdn-profiles.tunein.com/s6707/images/logoq.png?t=636268\"}, "
+ "{\"container\": \"no\", \"mid\": \"s2967\", \"type\": \"station\", \"playable\": \"yes\", \"name\": \"Classic FM Nederland (Classical Music)\", \"image_url\": \"http://cdn-radiotime-logos.tunein.com/s2967q.png\"}, "
+ "{\"container\": \"no\", \"mid\": \"s1993\", \"type\": \"station\", \"playable\": \"yes\", \"name\": \"BNR Nieuwsradio\", \"image_url\": \"http://cdn-radiotime-logos.tunein.com/s1993q.png\"}], "
+ "\"options\": [{\"browse\": [{\"id\": 20, \"name\": \"Remove from HEOS Favorites\"}]}]}",
BrowseResult[].class);
assertEquals(HeosCommandGroup.BROWSE, response.heosCommand.commandGroup);
assertEquals(HeosCommand.BROWSE, response.heosCommand.command);
assertTrue(response.result);
assertEquals(Long.valueOf(1028), response.getNumericAttribute(SOURCE_ID));
assertEquals(Long.valueOf(3), response.getNumericAttribute(RETURNED));
assertEquals(Long.valueOf(3), response.getNumericAttribute(COUNT));
BrowseResult result = response.payload[0];
assertEquals(YesNoEnum.NO, result.container);
assertEquals("s6707", result.mediaId);
assertEquals(BrowseResultType.STATION, result.type);
assertEquals(YesNoEnum.YES, result.playable);
assertEquals("NPO 3FM 96.8 (Top 40 %26 Pop Music)", result.name);
assertEquals("http://cdn-profiles.tunein.com/s6707/images/logoq.png?t=636268", result.imageUrl);
// TODO validate options
}
@Test
public void get_groups() {
HeosResponseObject<Group[]> response = subject.parseResponse(
"{\"heos\": {\"command\": \"group/get_groups\", \"result\": \"success\", \"message\": \"\"}, \"payload\": [ "
+ "{\"name\": \"Group 1\", \"gid\": \"214243242\", \"players\": [ {\"name\": \"HEOS 1\", \"pid\": \"2142443242\", \"role\": \"leader\"}, {\"name\": \"HEOS 3\", \"pid\": \"32432423432\", \"role\": \"member\"}, {\"name\": \"HEOS 5\", \"pid\": \"342423564\", \"role\": \"member\"}]}, "
+ "{\"name\": \"Group 2\", \"gid\": \"2142432342\", \"players\": [ {\"name\": \"HEOS 3\", \"pid\": \"32432423432\", \"role\": \"member\"}, {\"name\": \"HEOS 5\", \"pid\": \"342423564\", \"role\": \"member\"}]}]}",
Group[].class);
assertEquals(HeosCommandGroup.GROUP, response.heosCommand.commandGroup);
assertEquals(HeosCommand.GET_GROUPS, response.heosCommand.command);
assertTrue(response.result);
Group group = response.payload[0];
assertEquals("Group 1", group.name);
assertEquals("214243242", group.id);
List<Group.Player> players = group.players;
Group.Player player0 = players.get(0);
assertEquals("HEOS 1", player0.name);
assertEquals("2142443242", player0.id);
assertEquals(GroupPlayerRole.LEADER, player0.role);
Group.Player player1 = players.get(1);
assertEquals("HEOS 3", player1.name);
assertEquals("32432423432", player1.id);
assertEquals(GroupPlayerRole.MEMBER, player1.role);
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.heos.internal.json.dto;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.openhab.binding.heos.internal.json.dto.HeosCommand.*;
import static org.openhab.binding.heos.internal.json.dto.HeosCommandGroup.SYSTEM;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* Tests to validate the functioning of the HeosCommandTuple
*
* @author Martin van Wingerden - Initial Contribution
*/
@NonNullByDefault
public class HeosCommandTupleTest {
@Test
public void system() {
assertMatches("system/check_account", SYSTEM, CHECK_ACCOUNT);
assertMatches("system/sign_in", SYSTEM, SIGN_IN);
assertMatches("system/sign_out", SYSTEM, SIGN_OUT);
assertMatches("system/register_for_change_events", SYSTEM, REGISTER_FOR_CHANGE_EVENTS);
assertMatches("system/heart_beat", SYSTEM, HEART_BEAT);
assertMatches("system/prettify_json_response", SYSTEM, PRETTIFY_JSON_RESPONSE);
}
@Test
public void browse() {
assertMatches("browse/browse", HeosCommandGroup.BROWSE, HeosCommand.BROWSE);
assertMatches("browse/get_music_sources", HeosCommandGroup.BROWSE, HeosCommand.GET_MUSIC_SOURCES);
assertMatches("browse/get_source_info", HeosCommandGroup.BROWSE, HeosCommand.GET_SOURCE_INFO);
assertMatches("browse/get_search_criteria", HeosCommandGroup.BROWSE, HeosCommand.GET_SEARCH_CRITERIA);
assertMatches("browse/search", HeosCommandGroup.BROWSE, HeosCommand.SEARCH);
assertMatches("browse/play_stream", HeosCommandGroup.BROWSE, HeosCommand.PLAY_STREAM);
assertMatches("browse/play_preset", HeosCommandGroup.BROWSE, HeosCommand.PLAY_PRESET);
assertMatches("browse/play_input", HeosCommandGroup.BROWSE, HeosCommand.PLAY_INPUT);
assertMatches("browse/play_stream", HeosCommandGroup.BROWSE, HeosCommand.PLAY_STREAM);
assertMatches("browse/add_to_queue", HeosCommandGroup.BROWSE, HeosCommand.ADD_TO_QUEUE);
assertMatches("browse/rename_playlist", HeosCommandGroup.BROWSE, HeosCommand.RENAME_PLAYLIST);
assertMatches("browse/delete_playlist", HeosCommandGroup.BROWSE, HeosCommand.DELETE_PLAYLIST);
assertMatches("browse/retrieve_metadata", HeosCommandGroup.BROWSE, HeosCommand.RETRIEVE_METADATA);
}
@Test
public void player() {
assertMatches("player/get_players", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAYERS);
assertMatches("player/get_player_info", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAYER_INFO);
assertMatches("player/get_play_state", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAY_STATE);
assertMatches("player/set_play_state", HeosCommandGroup.PLAYER, HeosCommand.SET_PLAY_STATE);
assertMatches("player/get_now_playing_media", HeosCommandGroup.PLAYER, HeosCommand.GET_NOW_PLAYING_MEDIA);
assertMatches("player/get_volume", HeosCommandGroup.PLAYER, HeosCommand.GET_VOLUME);
assertMatches("player/set_volume", HeosCommandGroup.PLAYER, HeosCommand.SET_VOLUME);
assertMatches("player/volume_up", HeosCommandGroup.PLAYER, HeosCommand.VOLUME_UP);
assertMatches("player/volume_down", HeosCommandGroup.PLAYER, HeosCommand.VOLUME_DOWN);
assertMatches("player/get_mute", HeosCommandGroup.PLAYER, HeosCommand.GET_MUTE);
assertMatches("player/set_mute", HeosCommandGroup.PLAYER, HeosCommand.SET_MUTE);
assertMatches("player/toggle_mute", HeosCommandGroup.PLAYER, HeosCommand.TOGGLE_MUTE);
assertMatches("player/get_play_mode", HeosCommandGroup.PLAYER, HeosCommand.GET_PLAY_MODE);
assertMatches("player/set_play_mode", HeosCommandGroup.PLAYER, HeosCommand.SET_PLAY_MODE);
assertMatches("player/get_queue", HeosCommandGroup.PLAYER, HeosCommand.GET_QUEUE);
assertMatches("player/play_queue", HeosCommandGroup.PLAYER, HeosCommand.PLAY_QUEUE);
assertMatches("player/remove_from_queue", HeosCommandGroup.PLAYER, HeosCommand.REMOVE_FROM_QUEUE);
assertMatches("player/save_queue", HeosCommandGroup.PLAYER, HeosCommand.SAVE_QUEUE);
assertMatches("player/clear_queue", HeosCommandGroup.PLAYER, HeosCommand.CLEAR_QUEUE);
assertMatches("player/move_queue_item", HeosCommandGroup.PLAYER, HeosCommand.MOVE_QUEUE_ITEM);
assertMatches("player/play_next", HeosCommandGroup.PLAYER, HeosCommand.PLAY_NEXT);
assertMatches("player/play_previous", HeosCommandGroup.PLAYER, HeosCommand.PLAY_PREVIOUS);
assertMatches("player/set_quickselect", HeosCommandGroup.PLAYER, HeosCommand.SET_QUICKSELECT);
assertMatches("player/play_quickselect", HeosCommandGroup.PLAYER, HeosCommand.PLAY_QUICKSELECT);
assertMatches("player/get_quickselects", HeosCommandGroup.PLAYER, HeosCommand.GET_QUICKSELECTS);
assertMatches("player/check_update", HeosCommandGroup.PLAYER, HeosCommand.CHECK_UPDATE);
}
@Test
public void group() {
assertMatches("group/get_groups", HeosCommandGroup.GROUP, HeosCommand.GET_GROUPS);
assertMatches("group/get_group_info", HeosCommandGroup.GROUP, HeosCommand.GET_GROUP_INFO);
assertMatches("group/set_group", HeosCommandGroup.GROUP, HeosCommand.SET_GROUP);
assertMatches("group/get_volume", HeosCommandGroup.GROUP, HeosCommand.GET_VOLUME);
assertMatches("group/set_volume", HeosCommandGroup.GROUP, HeosCommand.SET_VOLUME);
assertMatches("group/volume_up", HeosCommandGroup.GROUP, HeosCommand.VOLUME_UP);
assertMatches("group/volume_down", HeosCommandGroup.GROUP, HeosCommand.VOLUME_DOWN);
assertMatches("group/get_mute", HeosCommandGroup.GROUP, HeosCommand.GET_MUTE);
assertMatches("group/set_mute", HeosCommandGroup.GROUP, HeosCommand.SET_MUTE);
assertMatches("group/toggle_mute", HeosCommandGroup.GROUP, HeosCommand.TOGGLE_MUTE);
}
private void assertMatches(String command, HeosCommandGroup commandGroup, HeosCommand heosCommand) {
HeosCommandTuple tuple = HeosCommandTuple.valueOf(command);
assertNotNull(tuple);
assertEquals(commandGroup, tuple.commandGroup);
assertEquals(heosCommand, tuple.command);
}
/**
* "browse/browse"
* "group/get_groups"
* "player/get_mute"
* "player/get_now_playing_media"
* "player/get_player_info"
* "player/get_players"
* "player/get_play_mode"
* "player/get_play_state"
* "player/get_volume"
* "player/play_next"
* "player/play_previous"
* "player/set_mute"
* "player/set_play_mode"
* "player/set_play_state"
* "player/set_volume"
* "player/volume_down"
* "system/heart_beat"
* "system/register_for_change_events"
* "system/sign_in"
*/
}