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,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.yamahareceiver-${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-yamahareceiver" description="Yamaha Receiver Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.yamahareceiver/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,139 @@
/**
* 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.yamahareceiver.internal;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.yamahareceiver.internal.handler.YamahaZoneThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
/**
* Provide a custom channel type for available inputs
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Refactoring the input source names.
*/
@NonNullByDefault
public class ChannelsTypeProviderAvailableInputs implements ChannelTypeProvider, ThingHandlerService {
private @NonNullByDefault({}) ChannelType channelType;
private @NonNullByDefault({}) ChannelTypeUID channelTypeUID;
private @NonNullByDefault({}) YamahaZoneThingHandler handler;
@Override
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return Collections.singleton(channelType);
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
if (this.channelTypeUID.equals(channelTypeUID)) {
return channelType;
} else {
return null;
}
}
public ChannelTypeUID getChannelTypeUID() {
return channelTypeUID;
}
private void createChannelType(StateDescription state) {
channelType = ChannelTypeBuilder.state(channelTypeUID, "Input source", "String")
.withDescription("Select the input source of the AVR").withStateDescription(state).build();
}
private StateDescription getDefaultStateDescription() {
List<StateOption> options = new ArrayList<>();
options.add(new StateOption(INPUT_NET_RADIO, "Net Radio"));
options.add(new StateOption(INPUT_PC, "PC"));
options.add(new StateOption(INPUT_USB, "USB"));
options.add(new StateOption(INPUT_TUNER, "Tuner"));
options.add(new StateOption("MULTI_CH", "Multi Channel"));
// Note: this might need review in the future, it should be 'HDMI 1', the 'HDMI_1' are XML node names, not
// source names.
options.add(new StateOption("HDMI_1", "HDMI 1"));
options.add(new StateOption("HDMI_2", "HDMI 2"));
options.add(new StateOption("HDMI_3", "HDMI 3"));
options.add(new StateOption("HDMI_4", "HDMI 4"));
options.add(new StateOption("HDMI_5", "HDMI 5"));
options.add(new StateOption("HDMI_6", "HDMI 6"));
options.add(new StateOption("HDMI_7", "HDMI 7"));
options.add(new StateOption("AV_1", "AV 1"));
options.add(new StateOption("AV_2", "AV 2"));
options.add(new StateOption("AV_3", "AV 3"));
options.add(new StateOption("AV_4", "AV 4"));
options.add(new StateOption("AV_5", "AV 5"));
options.add(new StateOption("AV_6", "AV 6"));
options.add(new StateOption("AV_7", "AV 7"));
options.add(new StateOption("PHONO", "Phono"));
options.add(new StateOption("V_AUX", "Aux"));
options.add(new StateOption("AUDIO_1", "Audio 1"));
options.add(new StateOption("AUDIO_2", "Audio 2"));
options.add(new StateOption("AUDIO_3", "Audio 3"));
options.add(new StateOption("AUDIO_4", "Audio 4"));
options.add(new StateOption(INPUT_DOCK, "DOCK"));
options.add(new StateOption(INPUT_IPOD, "iPod"));
options.add(new StateOption(INPUT_IPOD_USB, "iPod/USB"));
options.add(new StateOption(INPUT_BLUETOOTH, "Bluetooth"));
options.add(new StateOption("UAW", "UAW"));
options.add(new StateOption("NET", "NET"));
options.add(new StateOption(INPUT_SIRIUS, "Sirius"));
options.add(new StateOption(INPUT_RHAPSODY, "Rhapsody"));
options.add(new StateOption("SIRIUS_IR", "SIRIUS IR"));
options.add(new StateOption(INPUT_PANDORA, "Pandora"));
options.add(new StateOption(INPUT_NAPSTER, "Napster"));
options.add(new StateOption(INPUT_SPOTIFY, "Spotify"));
StateDescription state = new StateDescription(null, null, null, "%s", false, options);
return state;
}
public void changeAvailableInputs(Map<String, String> availableInputs) {
List<StateOption> options = new ArrayList<>();
for (Entry<String, String> inputEntry : availableInputs.entrySet()) {
options.add(new StateOption(inputEntry.getKey(), inputEntry.getValue()));
}
createChannelType(new StateDescription(null, null, null, "%s", false, options));
}
@NonNullByDefault({})
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (YamahaZoneThingHandler) handler;
this.handler.channelsTypeProviderAvailableInputs = this;
channelTypeUID = new ChannelTypeUID(YamahaReceiverBindingConstants.BINDING_ID,
YamahaReceiverBindingConstants.CHANNEL_INPUT_TYPE_AVAILABLE + handler.getThing().getUID().getId());
createChannelType(getDefaultStateDescription());
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}

View File

@@ -0,0 +1,107 @@
/**
* 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.yamahareceiver.internal;
import static java.util.stream.Collectors.toList;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.IntStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.yamahareceiver.internal.handler.YamahaZoneThingHandler;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
/**
* Provide a custom channel type for the preset channel
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - RX-V3900 compatibility improvements
*/
@NonNullByDefault
public class ChannelsTypeProviderPreset implements ChannelTypeProvider, ThingHandlerService {
private @NonNullByDefault({}) ChannelType channelType;
private @NonNullByDefault({}) ChannelTypeUID channelTypeUID;
private @NonNullByDefault({}) YamahaZoneThingHandler handler;
@Override
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return Collections.singleton(channelType);
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
if (this.channelTypeUID.equals(channelTypeUID)) {
return channelType;
} else {
return null;
}
}
public ChannelTypeUID getChannelTypeUID() {
return channelTypeUID;
}
private StateDescription getDefaultStateDescription() {
List<StateOption> options = IntStream.rangeClosed(1, 40)
.mapToObj(i -> new StateOption(Integer.toString(i), "Item_" + i)).collect(toList());
StateDescription state = new StateDescription(null, null, null, "%s", false, options);
return state;
}
public void changePresetNames(List<PresetInfoState.Preset> presets) {
List<StateOption> options = presets.stream()
.map(preset -> new StateOption(String.valueOf(preset.getValue()), preset.getName())).collect(toList());
StateDescription state = new StateDescription(null, null, null, "%s", false, options);
createChannelType(state);
}
private void createChannelType(StateDescription state) {
channelType = ChannelTypeBuilder.state(channelTypeUID, "Preset", "Number")
.withDescription("Select a saved channel by its preset number").withStateDescription(state).build();
}
@NonNullByDefault({})
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (YamahaZoneThingHandler) handler;
this.handler.channelsTypeProviderPreset = this;
/**
* We generate a thing specific channelTypeUID, because presets absolutely depends on the thing.
*/
channelTypeUID = new ChannelTypeUID(BINDING_ID,
CHANNEL_PLAYBACK_PRESET_TYPE_NAMED + handler.getThing().getUID().getId());
StateDescription state = getDefaultStateDescription();
createChannelType(state);
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
}

View File

@@ -0,0 +1,183 @@
/**
* 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.yamahareceiver.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.core.thing.ThingTypeUID;
/**
* The {@link YamahaReceiverBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - DAB support, Spotify support, refactoring
*/
@NonNullByDefault
public class YamahaReceiverBindingConstants {
public static final String BINDING_ID = "yamahareceiver";
// List of all Thing Type UIDs
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "yamahaAV");
public static final ThingTypeUID ZONE_THING_TYPE = new ThingTypeUID(BINDING_ID, "zone");
public static final Set<ThingTypeUID> BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE);
public static final Set<ThingTypeUID> ZONE_THING_TYPES_UIDS = Collections.singleton(ZONE_THING_TYPE);
// List of channel IDs for zone control (except power which is also a non-zone/bridge channel)
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_INPUT = "input";
public static final String CHANNEL_INPUT_TYPE_AVAILABLE = "availableinput";
public static final String CHANNEL_SURROUND = "surroundProgram";
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_VOLUME_DB = "volumeDB";
public static final String CHANNEL_MUTE = "mute";
public static final String CHANNEL_SCENE = "scene";
public static final String CHANNEL_DIALOGUE_LEVEL = "dialogueLevel";
public static final String CHANNEL_PARTY_MODE = "party_mode";
public static final String CHANNEL_PARTY_MODE_MUTE = "party_mode_mute";
public static final String CHANNEL_PARTY_MODE_VOLUME = "party_mode_volume";
// List of channel IDs for navigation control: Read/Write
public static final String CHANNEL_NAVIGATION_MENU = "navigation_menu"; // Navigate either in the current menu
// or to the full menu path if "/" is used.
// List of channel IDs for navigation control: Write only
public static final String CHANNEL_NAVIGATION_UPDOWN = "navigation_updown"; // UpDown; Change current line
public static final String CHANNEL_NAVIGATION_LEFTRIGHT = "navigation_leftright"; // UpDown Type
public static final String CHANNEL_NAVIGATION_SELECT = "navigation_select"; // Switch Type
public static final String CHANNEL_NAVIGATION_BACK = "navigation_back"; // Switch Type
public static final String CHANNEL_NAVIGATION_BACKTOROOT = "navigation_backtoroot"; // Switch Type
// List of channel IDs for navigation control: Read only
public static final String CHANNEL_NAVIGATION_LEVEL = "navigation_level"; // DecType
public static final String CHANNEL_NAVIGATION_CURRENT_ITEM = "navigation_current_item"; // DecType
public static final String CHANNEL_NAVIGATION_TOTAL_ITEMS = "navigation_total_items"; // DecType
public static final Set<String> CHANNELS_NAVIGATION = Collections.unmodifiableSet(Stream
.of(CHANNEL_NAVIGATION_MENU, CHANNEL_NAVIGATION_CURRENT_ITEM, CHANNEL_NAVIGATION_UPDOWN,
CHANNEL_NAVIGATION_LEFTRIGHT, CHANNEL_NAVIGATION_SELECT, CHANNEL_NAVIGATION_BACK,
CHANNEL_NAVIGATION_BACKTOROOT, CHANNEL_NAVIGATION_LEVEL, CHANNEL_NAVIGATION_TOTAL_ITEMS)
.collect(Collectors.toSet()));
// List of channel IDs for Tuner DAB control
public static final String CHANNEL_TUNER_BAND = "tuner_band"; // band name for DAB tuner; RW
// List of channel IDs for playback control
public static final String CHANNEL_PLAYBACK_PRESET = "preset"; // Preset number; RW
public static final String CHANNEL_PLAYBACK_PRESET_TYPE_NAMED = "namedpreset"; // Preset number; RW
public static final String CHANNEL_PLAYBACK = "playback"; // Play,Pause,Stop,FastFW,Rewind,Next,Previous.
// Will show the current state as String.
// List of channel IDs for playback control: Read only
public static final String CHANNEL_PLAYBACK_STATION = "playback_station";
public static final String CHANNEL_PLAYBACK_ARTIST = "playback_artist";
public static final String CHANNEL_PLAYBACK_ALBUM = "playback_album";
public static final String CHANNEL_PLAYBACK_SONG = "playback_song";
public static final String CHANNEL_PLAYBACK_SONG_IMAGE_URL = "playback_song_image_url";
public static final Set<String> CHANNELS_PLAYBACK = Collections.unmodifiableSet(
Stream.of(CHANNEL_PLAYBACK, CHANNEL_PLAYBACK_STATION, CHANNEL_PLAYBACK_ARTIST, CHANNEL_PLAYBACK_ALBUM,
CHANNEL_PLAYBACK_SONG, CHANNEL_PLAYBACK_SONG_IMAGE_URL).collect(Collectors.toSet()));
public static final String UPNP_TYPE = "MediaRenderer";
public static final String UPNP_MANUFACTURER = "YAMAHA";
public static class Configs {
public static final String CONFIG_HOST_NAME = "host";
public static final String CONFIG_ZONE = "zone";
}
public static final String PROPERTY_VERSION = "version";
public static final String PROPERTY_ASSIGNED_NAME = "assigned_name";
public static final String PROPERTY_MENU_ERROR = "menu_error";
public static final String PROPERTY_LAST_PARSE_ERROR = "last_parse_error";
public static final String CHANNEL_GROUP_PLAYBACK = "playback_channels";
public static final String CHANNEL_GROUP_NAVIGATION = "navigation_channels";
public static final String CHANNEL_GROUP_ZONE = "zone_channels";
/**
* The names of this enum are part of the protocols!
* Receivers have different capabilities, some have 2 zones, some up to 4.
* Every receiver has a "Main_Zone".
*/
public enum Zone {
Main_Zone,
Zone_2,
Zone_3,
Zone_4
}
/**
* Flags indicating if a feature is supported
*/
public enum Feature {
DAB,
TUNER,
SPOTIFY,
BLUETOOTH,
AIRPLAY,
NET_RADIO,
USB,
/**
* Model RX-V3900 has this and it represents NET_RADIO and USB
*/
NET_USB,
/**
* Model HTR-xxxx has a Zone_2 concept but realized as an extension to Main_Zone
*/
ZONE_B
}
/** Retry time in ms if no response for menu navigation */
public static final int MENU_RETRY_DELAY = 500;
/** Max menu waiting in ms */
public static final int MENU_MAX_WAITING_TIME = 5000;
// List of known inputs
public static class Inputs {
public static final String INPUT_TUNER = "TUNER";
public static final String INPUT_SPOTIFY = "Spotify";
public static final String INPUT_BLUETOOTH = "Bluetooth";
public static final String INPUT_NET_RADIO = "NET RADIO";
// Note (TM): We should only 'NET RADIO' (as the canonical input name), the NET_RADIO seems to be only used in
// the XML nodes when commands are sent.
public static final String INPUT_NET_RADIO_LEGACY = "NET_RADIO";
public static final String INPUT_MUSIC_CAST_LINK = "MusicCast Link";
public static final String INPUT_SERVER = "SERVER";
public static final String INPUT_USB = "USB";
public static final String INPUT_IPOD_USB = "iPOD_USB";
public static final String INPUT_DOCK = "DOCK";
public static final String INPUT_PC = "PC";
public static final String INPUT_NAPSTER = "Napster";
public static final String INPUT_PANDORA = "Pandora";
public static final String INPUT_SIRIUS = "SIRIUS";
public static final String INPUT_RHAPSODY = "Rhapsody";
public static final String INPUT_IPOD = "iPod";
public static final String INPUT_HD_RADIO = "HD_RADIO";
}
/** Placeholder value that is used when the string channel value is not available */
public static final String VALUE_NA = "N/A";
/** Empty value that is used when the string channel value is not available */
public static final String VALUE_EMPTY = "";
public static class Models {
public static final String RX_A2000 = "RX-A2000";
}
}

View File

@@ -0,0 +1,67 @@
/**
* 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.yamahareceiver.internal;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
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.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.yamahareceiver.internal.handler.YamahaBridgeHandler;
import org.openhab.binding.yamahareceiver.internal.handler.YamahaZoneThingHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link YamahaReceiverHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author David Graeff - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.yamahareceiver")
@NonNullByDefault
public class YamahaReceiverHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
.concat(BRIDGE_THING_TYPES_UIDS.stream(), ZONE_THING_TYPES_UIDS.stream()).collect(Collectors.toSet()));
private Logger logger = LoggerFactory.getLogger(YamahaReceiverHandlerFactory.class);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(BRIDGE_THING_TYPE)) {
return new YamahaBridgeHandler((Bridge) thing);
} else if (thingTypeUID.equals(ZONE_THING_TYPE)) {
return new YamahaZoneThingHandler(thing);
}
logger.error("Unexpected thing encountered in factory: {}", thingTypeUID.getAsString());
return null;
}
}

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.yamahareceiver.internal.config;
import java.util.Optional;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Main settings.
*
* @author Tomasz Maruszak - Initial contribution.
*/
@NonNullByDefault
public class YamahaBridgeConfig {
/**
* The host name of the Yamaha AVR.
*/
private @Nullable String host;
/**
* Port under which the control interface is exposed on the Yamaha AVR.
*/
private int port = 80;
/**
* Interval (in seconds) at which state updates are polled.
*/
private int refreshInterval = 60; // Default: Every 1min
/**
* The default album image placeholder URL used when the source does not provide its own.
*/
private String albumUrl = "";
/**
* Input source mapping for each command. This is a comma separated list of settings.
*/
private String inputMapping = "";
public @Nullable String getHost() {
return host;
}
public int getPort() {
return port;
}
public int getRefreshInterval() {
return refreshInterval;
}
public String getAlbumUrl() {
return albumUrl;
}
public Optional<String> getHostWithPort() {
final String str = host;
if (StringUtils.isEmpty(str)) {
return Optional.empty();
}
return Optional.of(str + ":" + port);
}
public String getInputMapping() {
return inputMapping;
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.yamahareceiver.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Utilities used by the Yamaha binding
*
* @author Tomasz Maruszak - Initial contribution
*/
@NonNullByDefault
public class YamahaUtils {
/**
* Tries to parse a string into Enum, if unsuccessful returns null.
*
* @param c
* @param string
* @param <T>
* @return Enum value or null if unsuccessful
*/
public static @Nullable <T extends Enum<T>> T tryParseEnum(Class<T> c, @Nullable String string) {
if (string != null) {
try {
return Enum.valueOf(c, string);
} catch (IllegalArgumentException ex) {
}
}
return null;
}
}

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.yamahareceiver.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
/**
* Zone settings.
*
* @author Tomasz Maruszak - Initial contribution.
*/
@NonNullByDefault
public class YamahaZoneConfig {
/**
* Zone name, will be one of {@link Zone}.
*/
private String zone = "";
/**
* Volume relative change factor when sending {@link org.openhab.core.library.types.IncreaseDecreaseType}
* commands.
*/
private float volumeRelativeChangeFactor = 0.5f; // Default: 0.5 percent
/**
* Minimum allowed volume in dB.
*/
private float volumeDbMin = -80f; // -80.0 dB
/**
* Maximum allowed volume in dB.
*/
private float volumeDbMax = 12f; // 12.0 dB
public @Nullable Zone getZone() {
return YamahaUtils.tryParseEnum(Zone.class, zone);
}
public String getZoneValue() {
return zone;
}
public float getVolumeRelativeChangeFactor() {
return volumeRelativeChangeFactor;
}
public float getVolumeDbMin() {
return volumeDbMin;
}
public float getVolumeDbMax() {
return volumeDbMax;
}
private float getVolumeDbRange() {
return getVolumeDbMax() - getVolumeDbMin();
}
/**
* Converts from volume percentage to volume dB.
*
* @param volume volume percentage
* @return volume dB
*/
public float getVolumeDb(float volume) {
return volume * getVolumeDbRange() / 100.0f + getVolumeDbMin();
}
/**
* Converts from volume dB to volume percentage.
*
* @param volumeDb volume dB
* @return volume percentage
*/
public float getVolumePercentage(float volumeDb) {
float volume = (volumeDb - getVolumeDbMin()) * 100.0f / getVolumeDbRange();
if (volume < 0 || volume > 100) {
volume = Math.max(0, Math.min(volume, 100));
}
return volume;
}
}

View File

@@ -0,0 +1,112 @@
/**
* 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.yamahareceiver.internal.discovery;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Configs.CONFIG_HOST_NAME;
import java.net.URL;
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.RemoteDevice;
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.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link YamahaDiscoveryParticipant} is responsible for processing the
* results of searched UPnP devices
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Introduced config object, migrated to newer UPnP api
*/
@Component(immediate = true)
@NonNullByDefault
public class YamahaDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(YamahaDiscoveryParticipant.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_THING_TYPE);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES;
}
@Override
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
ThingUID uid = getThingUID(device);
if (uid == null) {
return null;
}
Map<String, Object> properties = new HashMap<>(3);
String label = "Yamaha Receiver";
try {
label += " " + device.getDetails().getModelDetails().getModelName();
} catch (Exception e) {
// ignore and use the default label
}
URL url = device.getIdentity().getDescriptorURL();
properties.put(CONFIG_HOST_NAME, url.getHost());
// The port via UPNP is unreliable, sometimes it is 8080, on some models 49154.
// But so far the API was always reachable via port 80.
// We provide the port config therefore, if the user ever needs to adjust the port.
// Note the port is set in the thing-types.xml to 80 by default.
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withTTL(MIN_MAX_AGE_SECS).withProperties(properties)
.withLabel(label).withRepresentationProperty(CONFIG_HOST_NAME).build();
logger.debug("Discovered a Yamaha Receiver '{}' model '{}' thing with UDN '{}'",
device.getDetails().getFriendlyName(), device.getDetails().getModelDetails().getModelName(),
device.getIdentity().getUdn().getIdentifierString());
return result;
}
public static @Nullable ThingUID getThingUID(@Nullable String manufacturer, @Nullable String deviceType,
String udn) {
if (manufacturer == null || deviceType == null) {
return null;
}
if (manufacturer.toUpperCase().contains(UPNP_MANUFACTURER) && deviceType.equals(UPNP_TYPE)) {
return new ThingUID(BRIDGE_THING_TYPE, udn);
} else {
return null;
}
}
@Override
public @Nullable ThingUID getThingUID(RemoteDevice device) {
String manufacturer = device.getDetails().getManufacturerDetails().getManufacturer();
String deviceType = device.getType().getType();
// UDN shouldn't contain '-' characters.
return getThingUID(manufacturer, deviceType,
device.getIdentity().getUdn().getIdentifierString().replace("-", "_"));
}
}

View File

@@ -0,0 +1,114 @@
/**
* 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.yamahareceiver.internal.discovery;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Configs.CONFIG_ZONE;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.handler.YamahaBridgeHandler;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
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.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
/**
* After the AVR bridge thing has been added and a connection could be established,
* the user is presented with the available zones.
*
* @author David Gräff - Initial contribution
* @author Tomasz Maruszak - Introduced config object
*/
@NonNullByDefault
public class ZoneDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
private @Nullable YamahaBridgeHandler handler;
/**
* Constructs a zone discovery service.
* Registers this zone discovery service programmatically.
* Call {@link ZoneDiscoveryService#destroy()} to unregister the service after use.
*/
public ZoneDiscoveryService() {
super(ZONE_THING_TYPES_UIDS, 0, false);
}
@Override
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected void startScan() {
}
public static ThingUID zoneThing(ThingUID bridgeUid, String zoneName) {
return new ThingUID(ZONE_THING_TYPE, bridgeUid, zoneName);
}
/**
* The available zones are within the {@link DeviceInformationState}. Will will publish those
* as things via this discovery service instance.
*
* @param state The device information state
* @param bridgeUid The bridge UID
*/
public void publishZones(DeviceInformationState state, ThingUID bridgeUid) {
// Create a copy of the list to avoid concurrent modification exceptions, because
// the state update takes place in another thread
List<Zone> zoneCopy = new ArrayList<>(state.zones);
for (Zone zone : zoneCopy) {
String zoneName = zone.name();
ThingUID uid = zoneThing(bridgeUid, zoneName);
Map<String, Object> properties = new HashMap<>();
properties.put(CONFIG_ZONE, zoneName);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withProperties(properties)
.withLabel(state.name + " " + zoneName).withBridge(bridgeUid)
.withRepresentationProperty(CONFIG_ZONE).build();
thingDiscovered(discoveryResult);
}
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof YamahaBridgeHandler) {
this.handler = (YamahaBridgeHandler) handler;
this.handler.setZoneDiscoveryService(this);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}

View File

@@ -0,0 +1,420 @@
/**
* 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.yamahareceiver.internal.handler;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.yamahareceiver.internal.config.YamahaBridgeConfig;
import org.openhab.binding.yamahareceiver.internal.discovery.ZoneDiscoveryService;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.ConnectionStateListener;
import org.openhab.binding.yamahareceiver.internal.protocol.DeviceInformation;
import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
import org.openhab.binding.yamahareceiver.internal.protocol.ProtocolFactory;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.protocol.SystemControl;
import org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolFactory;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.SystemControlState;
import org.openhab.binding.yamahareceiver.internal.state.SystemControlStateListener;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
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.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link YamahaBridgeHandler} is responsible for fetching basic information about the
* found AVR and start the zone detection.
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Input mapping fix, volumeDB fix, better feature detection, added config object
*/
public class YamahaBridgeHandler extends BaseBridgeHandler
implements ConnectionStateListener, SystemControlStateListener {
private final Logger logger = LoggerFactory.getLogger(YamahaBridgeHandler.class);
private YamahaBridgeConfig bridgeConfig;
private InputConverter inputConverter;
private ScheduledFuture<?> refreshTimer;
private ZoneDiscoveryService zoneDiscoveryService;
private final DeviceInformationState deviceInformationState = new DeviceInformationState();
private SystemControl systemControl;
private SystemControlState systemControlState = new SystemControlState();
private final CountDownLatch loadingDone = new CountDownLatch(1);
private boolean disposed = false;
private ProtocolFactory protocolFactory;
private AbstractConnection connection;
public YamahaBridgeHandler(Bridge bridge) {
super(bridge);
protocolFactory = new XMLProtocolFactory();
}
/**
* Return the input mapping converter
*/
public InputConverter getInputConverter() {
return inputConverter;
}
/**
* @return Return the protocol communication object. This may be null
* if the bridge is offline.
*/
public AbstractConnection getConnection() {
return connection;
}
/**
* Gets the current protocol factory.
*
* @return
*/
public ProtocolFactory getProtocolFactory() {
return protocolFactory;
}
/**
* Sets the current protocol factory.
*
* @param protocolFactory
*/
public void setProtocolFactory(ProtocolFactory protocolFactory) {
this.protocolFactory = protocolFactory;
}
@Override
public void dispose() {
cancelRefreshTimer();
super.dispose();
disposed = true;
}
/**
* Returns the device information
*/
public DeviceInformationState getDeviceInformationState() {
return deviceInformationState;
}
/**
* Returns the device configuration
*/
public YamahaBridgeConfig getConfiguration() {
return bridgeConfig;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (connection == null || deviceInformationState.host == null) {
return;
}
if (command instanceof RefreshType) {
refreshFromState(channelUID);
return;
}
try {
// Might be extended in the future, therefore a switch statement
String id = channelUID.getId();
switch (id) {
case CHANNEL_POWER:
systemControl.setPower(((OnOffType) command) == OnOffType.ON);
break;
case CHANNEL_PARTY_MODE:
systemControl.setPartyMode(((OnOffType) command) == OnOffType.ON);
break;
case CHANNEL_PARTY_MODE_MUTE:
systemControl.setPartyModeMute(((OnOffType) command) == OnOffType.ON);
break;
case CHANNEL_PARTY_MODE_VOLUME:
if (command instanceof IncreaseDecreaseType) {
systemControl
.setPartyModeVolume(((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE);
} else {
logger.warn("Only {} and {} commands are supported for {}", IncreaseDecreaseType.DECREASE,
IncreaseDecreaseType.DECREASE, id);
}
break;
default:
logger.warn(
"Channel {} not supported on the yamaha device directly! Try with the zone things instead.",
id);
}
} catch (IOException | ReceivedMessageParseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void refreshFromState(ChannelUID channelUID) {
// Might be extended in the future, therefore a switch statement
switch (channelUID.getId()) {
case CHANNEL_POWER:
updateState(channelUID, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
break;
case CHANNEL_PARTY_MODE:
updateState(channelUID, systemControlState.partyMode ? OnOffType.ON : OnOffType.OFF);
break;
case CHANNEL_PARTY_MODE_MUTE:
case CHANNEL_PARTY_MODE_VOLUME:
// no state updates available
break;
default:
logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
}
}
/**
* Sets up a refresh timer (using the scheduler) with the given interval.
*
* @param initialWaitTime The delay before the first refresh. Maybe 0 to immediately
* initiate a refresh.
*/
private void setupRefreshTimer(int initialWaitTime) {
cancelRefreshTimer();
logger.trace("Setting up refresh timer with fixed delay {} seconds, starting in {} seconds",
bridgeConfig.getRefreshInterval(), initialWaitTime);
refreshTimer = scheduler.scheduleWithFixedDelay(() -> updateAllZoneInformation(), initialWaitTime,
bridgeConfig.getRefreshInterval(), TimeUnit.SECONDS);
}
/**
* Cancels the refresh timer (if one was setup).
*/
private void cancelRefreshTimer() {
if (refreshTimer != null) {
refreshTimer.cancel(false);
refreshTimer = null;
}
}
/**
* Periodically and initially called. This must run in another thread, because all update calls are blocking.
*/
void updateAllZoneInformation() {
if (disposed) {
logger.trace("updateAllZoneInformation will be skipped because the bridge is disposed");
return;
}
if (!ensureConnectionInitialized()) {
// The initialization did not yet happen and the device is still offline (or not reachable)
return;
}
logger.trace("updateAllZoneInformation");
try {
// Set power = true before calling systemControl.update(),
// otherwise the systemControlStateChanged method would call updateAllZoneInformation() again
systemControlState.power = true;
systemControl.update();
updateStatus(ThingStatus.ONLINE);
Bridge bridge = (Bridge) thing;
for (Thing thing : bridge.getThings()) {
YamahaZoneThingHandler handler = (YamahaZoneThingHandler) thing.getHandler();
// Ensure the handler has been already assigned
if (handler != null && handler.isCorrectlyInitialized()) {
handler.updateZoneInformation();
}
}
} catch (IOException e) {
systemControlState.invalidate();
onConnectivityError(e);
return;
} catch (ReceivedMessageParseException e) {
updateProperty(PROPERTY_MENU_ERROR, e.getMessage());
// Some AVRs send unexpected responses. We log parser exceptions therefore.
logger.debug("Parse error!", e);
} finally {
loadingDone.countDown();
}
}
/**
* We handle the update ourselves to avoid a costly dispose/initialize
*/
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
if (!isInitialized()) {
super.handleConfigurationUpdate(configurationParameters);
return;
}
validateConfigurationParameters(configurationParameters);
// can be overridden by subclasses
Configuration configurationObject = editConfiguration();
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
configurationObject.put(configurationParameter.getKey(), configurationParameter.getValue());
}
updateConfiguration(configurationObject);
bridgeConfig = configurationObject.as(YamahaBridgeConfig.class);
logger.trace("Update configuration of {} with host '{}' and port {}", getThing().getLabel(),
bridgeConfig.getHost(), bridgeConfig.getPort());
Optional<String> host = bridgeConfig.getHostWithPort();
if (host.isPresent()) {
connection.setHost(host.get());
onConnectionCreated(connection);
}
inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
setupRefreshTimer(bridgeConfig.getRefreshInterval());
}
/**
* We handle updates of this thing ourself.
*/
@Override
public void thingUpdated(Thing thing) {
this.thing = thing;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ZoneDiscoveryService.class);
}
/**
* Called by the zone discovery service to let this handler have a reference.
*/
public void setZoneDiscoveryService(ZoneDiscoveryService s) {
this.zoneDiscoveryService = s;
}
/**
* Calls createCommunicationObject if the host name is configured correctly.
*/
@Override
public void initialize() {
bridgeConfig = getConfigAs(YamahaBridgeConfig.class);
logger.trace("Initialize of {} with host '{}' and port {}", getThing().getLabel(), bridgeConfig.getHost(),
bridgeConfig.getPort());
Optional<String> host = bridgeConfig.getHostWithPort();
if (!host.isPresent()) {
String msg = "Host or port not set. Double check your thing settings.";
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
logger.warn(msg);
return;
}
if (zoneDiscoveryService == null) {
logger.warn("Zone discovery service not ready!");
return;
}
protocolFactory.createConnection(host.get(), this);
}
@Override
public void onConnectionCreated(AbstractConnection connection) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"Waiting for connection with Yamaha device");
this.connection = connection;
this.systemControl = null;
if (!ensureConnectionInitialized()) {
logger.warn("Communication error. Please review your Yamaha thing configuration.");
}
setupRefreshTimer(0);
}
/**
* Attempts to perform a one-time initialization after a connection is created.
*
* @return true if initialization was successful
*/
private boolean ensureConnectionInitialized() {
if (systemControl != null) {
return true;
}
logger.trace("Initializing connection");
try {
DeviceInformation deviceInformation = protocolFactory.DeviceInformation(connection, deviceInformationState);
deviceInformation.update();
updateProperty(PROPERTY_VERSION, deviceInformationState.version);
updateProperty(PROPERTY_ASSIGNED_NAME, deviceInformationState.name);
zoneDiscoveryService.publishZones(deviceInformationState, thing.getUID());
systemControl = protocolFactory.SystemControl(connection, this, deviceInformationState);
inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
} catch (IOException | ReceivedMessageParseException e) {
deviceInformationState.invalidate();
onConnectivityError(e);
return false;
}
return true;
}
private void onConnectivityError(Exception e) {
String description = e.getMessage();
logger.debug(
"Communication error. Either the Yamaha thing configuration is invalid or the device is offline. Details: {}",
description);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
}
@Override
public void systemControlStateChanged(SystemControlState msg) {
// If the device was off and now turns on, we trigger a refresh of all zone things.
// The user might have renamed some of the inputs etc.
boolean needsCompleteRefresh = msg.power && !systemControlState.power;
systemControlState = msg;
updateState(CHANNEL_POWER, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
if (needsCompleteRefresh) {
updateAllZoneInformation();
}
}
}

View File

@@ -0,0 +1,819 @@
/**
* 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.yamahareceiver.internal.handler;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.yamahareceiver.internal.ChannelsTypeProviderAvailableInputs;
import org.openhab.binding.yamahareceiver.internal.ChannelsTypeProviderPreset;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Feature;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.IStateUpdatable;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithNavigationControl;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPlayControl;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPresetControl;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithTunerBandControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ProtocolFactory;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.protocol.ZoneAvailableInputs;
import org.openhab.binding.yamahareceiver.internal.protocol.ZoneControl;
import org.openhab.binding.yamahareceiver.internal.protocol.xml.InputWithNavigationControlXML;
import org.openhab.binding.yamahareceiver.internal.protocol.xml.InputWithPlayControlXML;
import org.openhab.binding.yamahareceiver.internal.protocol.xml.ZoneControlXML;
import org.openhab.binding.yamahareceiver.internal.state.AvailableInputState;
import org.openhab.binding.yamahareceiver.internal.state.AvailableInputStateListener;
import org.openhab.binding.yamahareceiver.internal.state.DabBandState;
import org.openhab.binding.yamahareceiver.internal.state.DabBandStateListener;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlState;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.NextPreviousType;
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.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
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.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link YamahaZoneThingHandler} is managing one zone of an Yamaha AVR.
* It has a state consisting of the zone, the current input ID, {@link ZoneControlState}
* and some more state objects and uses the zone control protocol
* class {@link ZoneControlXML}, {@link InputWithPlayControlXML} and {@link InputWithNavigationControlXML}
* for communication.
*
* @author David Graeff <david.graeff@web.de>
* @author Tomasz Maruszak - [yamaha] Tuner band selection and preset feature for dual band models (RX-S601D), added
* config object
*/
public class YamahaZoneThingHandler extends BaseThingHandler
implements ZoneControlStateListener, NavigationControlStateListener, PlayInfoStateListener,
AvailableInputStateListener, PresetInfoStateListener, DabBandStateListener {
private final Logger logger = LoggerFactory.getLogger(YamahaZoneThingHandler.class);
private YamahaZoneConfig zoneConfig;
/// ChannelType providers
public @NonNullByDefault({}) ChannelsTypeProviderPreset channelsTypeProviderPreset;
public @NonNullByDefault({}) ChannelsTypeProviderAvailableInputs channelsTypeProviderAvailableInputs;
/// State
protected ZoneControlState zoneState = new ZoneControlState();
protected PresetInfoState presetInfoState = new PresetInfoState();
protected DabBandState dabBandState = new DabBandState();
protected PlayInfoState playInfoState = new PlayInfoState();
protected NavigationControlState navigationInfoState = new NavigationControlState();
/// Control
protected ZoneControl zoneControl;
protected InputWithPlayControl inputWithPlayControl;
protected InputWithNavigationControl inputWithNavigationControl;
protected ZoneAvailableInputs zoneAvailableInputs;
protected InputWithPresetControl inputWithPresetControl;
protected InputWithTunerBandControl inputWithDabBandControl;
public YamahaZoneThingHandler(Thing thing) {
super(thing);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections
.unmodifiableList(Stream.of(ChannelsTypeProviderAvailableInputs.class, ChannelsTypeProviderPreset.class)
.collect(Collectors.toList()));
}
/**
* Sets the {@link DeviceInformationState} for the handler.
*/
public DeviceInformationState getDeviceInformationState() {
return getBridgeHandler().getDeviceInformationState();
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
validateConfigurationParameters(configurationParameters);
Configuration configuration = editConfiguration();
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
}
updateConfiguration(configuration);
zoneConfig = configuration.as(YamahaZoneConfig.class);
logger.trace("Updating configuration of {} with zone '{}'", getThing().getLabel(), zoneConfig.getZoneValue());
}
/**
* We handle updates of this thing ourself.
*/
@Override
public void thingUpdated(Thing thing) {
this.thing = thing;
}
/**
* Calls createCommunicationObject if the host name is configured correctly.
*/
@Override
public void initialize() {
// Determine the zone of this thing
zoneConfig = getConfigAs(YamahaZoneConfig.class);
logger.trace("Initialize {} with zone '{}'", getThing().getLabel(), zoneConfig.getZoneValue());
Bridge bridge = getBridge();
initializeThing(bridge != null ? bridge.getStatus() : null);
}
protected YamahaBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge == null) {
return null;
}
return (YamahaBridgeHandler) bridge.getHandler();
}
protected ProtocolFactory getProtocolFactory() {
return getBridgeHandler().getProtocolFactory();
}
protected AbstractConnection getConnection() {
return getBridgeHandler().getConnection();
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
initializeThing(bridgeStatusInfo.getStatus());
}
private void initializeThing(ThingStatus bridgeStatus) {
YamahaBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null && bridgeStatus != null) {
if (bridgeStatus == ThingStatus.ONLINE) {
if (zoneConfig == null || zoneConfig.getZone() == null) {
String msg = String.format(
"Zone not set or invalid zone name used: '%s'. It needs to be on of: '%s'",
zoneConfig.getZoneValue(), Arrays.toString(Zone.values()));
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
logger.info("{}", msg);
} else {
if (zoneControl == null) {
YamahaBridgeHandler brHandler = getBridgeHandler();
zoneControl = getProtocolFactory().ZoneControl(getConnection(), zoneConfig, this,
brHandler::getInputConverter, getDeviceInformationState());
zoneAvailableInputs = getProtocolFactory().ZoneAvailableInputs(getConnection(), zoneConfig,
this, brHandler::getInputConverter, getDeviceInformationState());
updateZoneInformation();
}
updateStatus(ThingStatus.ONLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
zoneControl = null;
zoneAvailableInputs = null;
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
/**
* Return true if the zone is set, and zoneControl and zoneAvailableInputs objects have been created.
*/
boolean isCorrectlyInitialized() {
return zoneConfig != null && zoneConfig.getZone() != null && zoneAvailableInputs != null && zoneControl != null;
}
/**
* Request new zone and available input information
*/
void updateZoneInformation() {
updateAsyncMakeOfflineIfFail(zoneAvailableInputs);
updateAsyncMakeOfflineIfFail(zoneControl);
if (inputWithPlayControl != null) {
updateAsyncMakeOfflineIfFail(inputWithPlayControl);
}
if (inputWithNavigationControl != null) {
updateAsyncMakeOfflineIfFail(inputWithNavigationControl);
}
if (inputWithPresetControl != null) {
updateAsyncMakeOfflineIfFail(inputWithPresetControl);
}
if (inputWithDabBandControl != null) {
updateAsyncMakeOfflineIfFail(inputWithDabBandControl);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (zoneControl == null) {
return;
}
String id = channelUID.getIdWithoutGroup();
try {
if (command instanceof RefreshType) {
refreshFromState(channelUID);
return;
}
switch (id) {
case CHANNEL_POWER:
zoneControl.setPower(((OnOffType) command) == OnOffType.ON);
break;
case CHANNEL_INPUT:
zoneControl.setInput(((StringType) command).toString());
break;
case CHANNEL_SURROUND:
zoneControl.setSurroundProgram(((StringType) command).toString());
break;
case CHANNEL_VOLUME_DB:
zoneControl.setVolumeDB(((DecimalType) command).floatValue());
break;
case CHANNEL_VOLUME:
if (command instanceof DecimalType) {
zoneControl.setVolume(((DecimalType) command).floatValue());
} else if (command instanceof IncreaseDecreaseType) {
zoneControl.setVolumeRelative(zoneState,
(((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE ? 1 : -1)
* zoneConfig.getVolumeRelativeChangeFactor());
}
break;
case CHANNEL_MUTE:
zoneControl.setMute(((OnOffType) command) == OnOffType.ON);
break;
case CHANNEL_SCENE:
zoneControl.setScene(((StringType) command).toString());
break;
case CHANNEL_DIALOGUE_LEVEL:
zoneControl.setDialogueLevel(((DecimalType) command).intValue());
break;
case CHANNEL_NAVIGATION_MENU:
if (inputWithNavigationControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
String path = ((StringType) command).toFullString();
inputWithNavigationControl.selectItemFullPath(path);
break;
case CHANNEL_NAVIGATION_UPDOWN:
if (inputWithNavigationControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
if (((UpDownType) command) == UpDownType.UP) {
inputWithNavigationControl.goUp();
} else {
inputWithNavigationControl.goDown();
}
break;
case CHANNEL_NAVIGATION_LEFTRIGHT:
if (inputWithNavigationControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
if (((UpDownType) command) == UpDownType.UP) {
inputWithNavigationControl.goLeft();
} else {
inputWithNavigationControl.goRight();
}
break;
case CHANNEL_NAVIGATION_SELECT:
if (inputWithNavigationControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
inputWithNavigationControl.selectCurrentItem();
break;
case CHANNEL_NAVIGATION_BACK:
if (inputWithNavigationControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
inputWithNavigationControl.goBack();
break;
case CHANNEL_NAVIGATION_BACKTOROOT:
if (inputWithNavigationControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
inputWithNavigationControl.goToRoot();
break;
case CHANNEL_PLAYBACK_PRESET:
if (inputWithPresetControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
if (command instanceof DecimalType) {
inputWithPresetControl.selectItemByPresetNumber(((DecimalType) command).intValue());
} else if (command instanceof StringType) {
try {
int v = Integer.valueOf(((StringType) command).toString());
inputWithPresetControl.selectItemByPresetNumber(v);
} catch (NumberFormatException e) {
logger.warn("Provide a number for {}", id);
}
}
break;
case CHANNEL_TUNER_BAND:
if (inputWithDabBandControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
if (command instanceof StringType) {
inputWithDabBandControl.selectBandByName(command.toString());
} else {
logger.warn("Provide a string for {}", id);
}
break;
case CHANNEL_PLAYBACK:
if (inputWithPlayControl == null) {
logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
return;
}
if (command instanceof PlayPauseType) {
PlayPauseType t = ((PlayPauseType) command);
switch (t) {
case PAUSE:
inputWithPlayControl.pause();
break;
case PLAY:
inputWithPlayControl.play();
break;
}
} else if (command instanceof NextPreviousType) {
NextPreviousType t = ((NextPreviousType) command);
switch (t) {
case NEXT:
inputWithPlayControl.nextTrack();
break;
case PREVIOUS:
inputWithPlayControl.previousTrack();
break;
}
} else if (command instanceof DecimalType) {
int v = ((DecimalType) command).intValue();
if (v < 0) {
inputWithPlayControl.skipREV();
} else if (v > 0) {
inputWithPlayControl.skipFF();
}
} else if (command instanceof StringType) {
String v = ((StringType) command).toFullString();
switch (v) {
case "Play":
inputWithPlayControl.play();
break;
case "Pause":
inputWithPlayControl.pause();
break;
case "Stop":
inputWithPlayControl.stop();
break;
case "Rewind":
inputWithPlayControl.skipREV();
break;
case "FastForward":
inputWithPlayControl.skipFF();
break;
case "Next":
inputWithPlayControl.nextTrack();
break;
case "Previous":
inputWithPlayControl.previousTrack();
break;
}
}
break;
default:
logger.warn("Channel {} not supported!", id);
}
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (ReceivedMessageParseException e) {
// Some AVRs send unexpected responses. We log parser exceptions therefore.
logger.debug("Parse error!", e);
}
}
/**
* Called by handleCommand() if a RefreshType command was received. It will update
* the given channel with the correct state.
*
* @param channelUID The channel
*/
private void refreshFromState(ChannelUID channelUID) {
String id = channelUID.getId();
if (id.equals(grpZone(CHANNEL_POWER))) {
updateState(channelUID, zoneState.power ? OnOffType.ON : OnOffType.OFF);
} else if (id.equals(grpZone(CHANNEL_VOLUME_DB))) {
updateState(channelUID, new DecimalType(zoneState.volumeDB));
} else if (id.equals(grpZone(CHANNEL_VOLUME))) {
updateState(channelUID, new PercentType((int) zoneConfig.getVolumePercentage(zoneState.volumeDB)));
} else if (id.equals(grpZone(CHANNEL_MUTE))) {
updateState(channelUID, zoneState.mute ? OnOffType.ON : OnOffType.OFF);
} else if (id.equals(grpZone(CHANNEL_INPUT))) {
updateState(channelUID, new StringType(zoneState.inputID));
} else if (id.equals(grpZone(CHANNEL_SURROUND))) {
updateState(channelUID, new StringType(zoneState.surroundProgram));
} else if (id.equals(grpZone(CHANNEL_SCENE))) {
// no state updates available
} else if (id.equals(grpZone(CHANNEL_DIALOGUE_LEVEL))) {
updateState(channelUID, new DecimalType(zoneState.dialogueLevel));
} else if (id.equals(grpPlayback(CHANNEL_PLAYBACK))) {
updateState(channelUID, new StringType(playInfoState.playbackMode));
} else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_STATION))) {
updateState(channelUID, new StringType(playInfoState.station));
} else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_ARTIST))) {
updateState(channelUID, new StringType(playInfoState.artist));
} else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_ALBUM))) {
updateState(channelUID, new StringType(playInfoState.album));
} else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_SONG))) {
updateState(channelUID, new StringType(playInfoState.song));
} else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_SONG_IMAGE_URL))) {
updateState(channelUID, new StringType(playInfoState.songImageUrl));
} else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_PRESET))) {
updateState(channelUID, new DecimalType(presetInfoState.presetChannel));
} else if (id.equals(grpPlayback(CHANNEL_TUNER_BAND))) {
updateState(channelUID, new StringType(dabBandState.band));
} else if (id.equals(grpNav(CHANNEL_NAVIGATION_MENU))) {
updateState(channelUID, new StringType(navigationInfoState.getCurrentItemName()));
} else if (id.equals(grpNav(CHANNEL_NAVIGATION_LEVEL))) {
updateState(channelUID, new DecimalType(navigationInfoState.menuLayer));
} else if (id.equals(grpNav(CHANNEL_NAVIGATION_CURRENT_ITEM))) {
updateState(channelUID, new DecimalType(navigationInfoState.currentLine));
} else if (id.equals(grpNav(CHANNEL_NAVIGATION_TOTAL_ITEMS))) {
updateState(channelUID, new DecimalType(navigationInfoState.maxLine));
} else {
logger.warn("Channel {} not implemented!", id);
}
}
@Override
public void zoneStateChanged(ZoneControlState msg) {
boolean inputChanged = !msg.inputID.equals(zoneState.inputID);
zoneState = msg;
updateStatus(ThingStatus.ONLINE);
updateState(grpZone(CHANNEL_POWER), zoneState.power ? OnOffType.ON : OnOffType.OFF);
updateState(grpZone(CHANNEL_INPUT), new StringType(zoneState.inputID));
updateState(grpZone(CHANNEL_SURROUND), new StringType(zoneState.surroundProgram));
updateState(grpZone(CHANNEL_VOLUME_DB), new DecimalType(zoneState.volumeDB));
updateState(grpZone(CHANNEL_VOLUME), new PercentType((int) zoneConfig.getVolumePercentage(zoneState.volumeDB)));
updateState(grpZone(CHANNEL_MUTE), zoneState.mute ? OnOffType.ON : OnOffType.OFF);
updateState(grpZone(CHANNEL_DIALOGUE_LEVEL), new DecimalType(zoneState.dialogueLevel));
// If the input changed
if (inputChanged) {
inputChanged();
}
}
/**
* Called by {@link #zoneStateChanged(ZoneControlState)} if the input has changed.
* Will request updates from {@see InputWithNavigationControl} and {@see InputWithPlayControl}.
*/
private void inputChanged() {
logger.debug("Input changed to {}", zoneState.inputID);
if (!isInputSupported(zoneState.inputID)) {
// for now just emit a warning in logs
logger.warn("Input {} is not supported on your AVR model", zoneState.inputID);
}
inputChangedCheckForNavigationControl();
// Note: the DAB band needs to be initialized before preset and playback
inputChangedCheckForDabBand();
inputChangedCheckForPlaybackControl();
inputChangedCheckForPresetControl();
}
/**
* Checks if the specified input is supported given the detected device feature information.
*
* @param inputID - the input name
* @return true when input is supported
*/
private boolean isInputSupported(String inputID) {
switch (inputID) {
case INPUT_SPOTIFY:
return getDeviceInformationState().features.contains(Feature.SPOTIFY);
case INPUT_TUNER:
return getDeviceInformationState().features.contains(Feature.TUNER)
|| getDeviceInformationState().features.contains(Feature.DAB);
// Note: add more inputs here in the future
}
return true;
}
private void inputChangedCheckForNavigationControl() {
boolean includeInputWithNavigationControl = false;
for (String channelName : CHANNELS_NAVIGATION) {
if (isLinked(grpNav(channelName))) {
includeInputWithNavigationControl = true;
break;
}
}
if (includeInputWithNavigationControl) {
includeInputWithNavigationControl = InputWithNavigationControl.SUPPORTED_INPUTS.contains(zoneState.inputID);
if (!includeInputWithNavigationControl) {
logger.debug("Navigation control not supported by {}", zoneState.inputID);
}
}
logger.trace("Navigation control requested by channel");
if (!includeInputWithNavigationControl) {
inputWithNavigationControl = null;
navigationInfoState.invalidate();
navigationUpdated(navigationInfoState);
return;
}
inputWithNavigationControl = getProtocolFactory().InputWithNavigationControl(getConnection(),
navigationInfoState, zoneState.inputID, this, getDeviceInformationState());
updateAsyncMakeOfflineIfFail(inputWithNavigationControl);
}
private void inputChangedCheckForPlaybackControl() {
boolean includeInputWithPlaybackControl = false;
for (String channelName : CHANNELS_PLAYBACK) {
if (isLinked(grpPlayback(channelName))) {
includeInputWithPlaybackControl = true;
break;
}
}
logger.trace("Playback control requested by channel");
if (includeInputWithPlaybackControl) {
includeInputWithPlaybackControl = InputWithPlayControl.SUPPORTED_INPUTS.contains(zoneState.inputID);
if (!includeInputWithPlaybackControl) {
logger.debug("Playback control not supported by {}", zoneState.inputID);
}
}
if (!includeInputWithPlaybackControl) {
inputWithPlayControl = null;
playInfoState.invalidate();
playInfoUpdated(playInfoState);
return;
}
/**
* The {@link inputChangedCheckForDabBand} needs to be called first before this method, in case the AVR Supports
* DAB
*/
if (inputWithDabBandControl != null) {
// When input is Tuner DAB there is no playback control
inputWithPlayControl = null;
} else {
inputWithPlayControl = getProtocolFactory().InputWithPlayControl(getConnection(), zoneState.inputID, this,
getBridgeHandler().getConfiguration(), getDeviceInformationState());
updateAsyncMakeOfflineIfFail(inputWithPlayControl);
}
}
private void inputChangedCheckForPresetControl() {
boolean includeInput = isLinked(grpPlayback(CHANNEL_PLAYBACK_PRESET));
logger.trace("Preset control requested by channel");
if (includeInput) {
includeInput = InputWithPresetControl.SUPPORTED_INPUTS.contains(zoneState.inputID);
if (!includeInput) {
logger.debug("Preset control not supported by {}", zoneState.inputID);
}
}
if (!includeInput) {
inputWithPresetControl = null;
presetInfoState.invalidate();
presetInfoUpdated(presetInfoState);
return;
}
/**
* The {@link inputChangedCheckForDabBand} needs to be called first before this method, in case the AVR Supports
* DAB
*/
if (inputWithDabBandControl != null) {
// When the input is Tuner DAB the control also provides preset functionality
inputWithPresetControl = (InputWithPresetControl) inputWithDabBandControl;
// Note: No need to update state - it will be already called for DabBand control (see
// inputChangedCheckForDabBand)
} else {
inputWithPresetControl = getProtocolFactory().InputWithPresetControl(getConnection(), zoneState.inputID,
this, getDeviceInformationState());
updateAsyncMakeOfflineIfFail(inputWithPresetControl);
}
}
private void inputChangedCheckForDabBand() {
boolean includeInput = isLinked(grpPlayback(CHANNEL_TUNER_BAND));
logger.trace("Band control requested by channel");
if (includeInput) {
// Check if TUNER input is DAB - dual bands radio tuner
includeInput = InputWithTunerBandControl.SUPPORTED_INPUTS.contains(zoneState.inputID)
&& getDeviceInformationState().features.contains(Feature.DAB);
if (!includeInput) {
logger.debug("Band control not supported by {}", zoneState.inputID);
}
}
if (!includeInput) {
inputWithDabBandControl = null;
dabBandState.invalidate();
dabBandUpdated(dabBandState);
return;
}
logger.debug("InputWithTunerBandControl created for {}", zoneState.inputID);
inputWithDabBandControl = getProtocolFactory().InputWithDabBandControl(zoneState.inputID, getConnection(), this,
this, this, getDeviceInformationState());
updateAsyncMakeOfflineIfFail(inputWithDabBandControl);
}
protected void updateAsyncMakeOfflineIfFail(IStateUpdatable stateUpdatable) {
scheduler.submit(() -> {
try {
stateUpdatable.update();
} catch (IOException e) {
logger.debug("State update error. Changing thing to offline", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (ReceivedMessageParseException e) {
updateProperty(PROPERTY_LAST_PARSE_ERROR, e.getMessage());
// Some AVRs send unexpected responses. We log parser exceptions therefore.
logger.debug("Parse error!", e);
}
});
}
/**
* Once this thing is set up and the AVR is connected, the available inputs for this zone are requested.
* The thing is updated with a new CHANNEL_AVAILABLE_INPUT which lists the available inputs for the current zone..
*/
@Override
public void availableInputsChanged(AvailableInputState msg) {
// Update channel type provider with a list of available inputs
channelsTypeProviderAvailableInputs.changeAvailableInputs(msg.availableInputs);
// Remove the old channel and add the new channel. The channel will be requested from the
// yamahaChannelTypeProvider.
ChannelUID inputChannelUID = new ChannelUID(thing.getUID(), CHANNEL_GROUP_ZONE, CHANNEL_INPUT);
Channel channel = ChannelBuilder.create(inputChannelUID, "String")
.withType(channelsTypeProviderAvailableInputs.getChannelTypeUID()).build();
updateThing(editThing().withoutChannel(inputChannelUID).withChannel(channel).build());
}
private String grpPlayback(String channelIDWithoutGroup) {
return new ChannelUID(thing.getUID(), CHANNEL_GROUP_PLAYBACK, channelIDWithoutGroup).getId();
}
private String grpNav(String channelIDWithoutGroup) {
return new ChannelUID(thing.getUID(), CHANNEL_GROUP_NAVIGATION, channelIDWithoutGroup).getId();
}
private String grpZone(String channelIDWithoutGroup) {
return new ChannelUID(thing.getUID(), CHANNEL_GROUP_ZONE, channelIDWithoutGroup).getId();
}
@Override
public void playInfoUpdated(PlayInfoState msg) {
playInfoState = msg;
updateState(grpPlayback(CHANNEL_PLAYBACK), new StringType(msg.playbackMode));
updateState(grpPlayback(CHANNEL_PLAYBACK_STATION), new StringType(msg.station));
updateState(grpPlayback(CHANNEL_PLAYBACK_ARTIST), new StringType(msg.artist));
updateState(grpPlayback(CHANNEL_PLAYBACK_ALBUM), new StringType(msg.album));
updateState(grpPlayback(CHANNEL_PLAYBACK_SONG), new StringType(msg.song));
updateState(grpPlayback(CHANNEL_PLAYBACK_SONG_IMAGE_URL), new StringType(msg.songImageUrl));
}
@Override
public void presetInfoUpdated(PresetInfoState msg) {
presetInfoState = msg;
if (msg.presetChannelNamesChanged) {
msg.presetChannelNamesChanged = false;
channelsTypeProviderPreset.changePresetNames(msg.presetChannelNames);
// Remove the old channel and add the new channel. The channel will be requested from the
// channelsTypeProviderPreset.
ChannelUID inputChannelUID = new ChannelUID(thing.getUID(), CHANNEL_GROUP_PLAYBACK,
CHANNEL_PLAYBACK_PRESET);
Channel channel = ChannelBuilder.create(inputChannelUID, "Number")
.withType(channelsTypeProviderPreset.getChannelTypeUID()).build();
updateThing(editThing().withoutChannel(inputChannelUID).withChannel(channel).build());
}
updateState(grpPlayback(CHANNEL_PLAYBACK_PRESET), new DecimalType(msg.presetChannel));
}
@Override
public void dabBandUpdated(DabBandState msg) {
dabBandState = msg;
updateState(grpPlayback(CHANNEL_TUNER_BAND), new StringType(msg.band));
}
@Override
public void navigationUpdated(NavigationControlState msg) {
navigationInfoState = msg;
updateState(grpNav(CHANNEL_NAVIGATION_MENU), new StringType(msg.menuName));
updateState(grpNav(CHANNEL_NAVIGATION_LEVEL), new DecimalType(msg.menuLayer));
updateState(grpNav(CHANNEL_NAVIGATION_CURRENT_ITEM), new DecimalType(msg.currentLine));
updateState(grpNav(CHANNEL_NAVIGATION_TOTAL_ITEMS), new DecimalType(msg.maxLine));
}
@Override
public void navigationError(String msg) {
updateProperty(PROPERTY_MENU_ERROR, msg);
logger.warn("Navigation error: {}", msg);
}
}

View File

@@ -0,0 +1,112 @@
/**
* 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.yamahareceiver.internal.protocol;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import org.openhab.core.config.core.ConfigConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a connection to the AVR. Is implemented by the XMLConnection and JSONConnection.
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Refactoring
*/
public abstract class AbstractConnection {
private Logger logger = LoggerFactory.getLogger(AbstractConnection.class);
protected String host;
protected boolean protocolSnifferEnabled;
protected FileOutputStream debugOutStream;
/**
* Creates a connection with the given host.
* Enables sniffing of the communication to the AVR if the logger level is set to trace
* when the addon is being loaded.
*
* @param host
*/
public AbstractConnection(String host) {
this.host = host;
setProtocolSnifferEnable(logger.isTraceEnabled());
}
public void setHost(String host) {
this.host = host;
}
public String getHost() {
return host;
}
public void setProtocolSnifferEnable(boolean enable) {
if (enable) {
File pathWithoutFilename = new File(ConfigConstants.getUserDataFolder());
pathWithoutFilename.mkdirs();
File file = new File(pathWithoutFilename, "yamaha_trace.log");
if (file.exists()) {
file.delete();
}
logger.warn("Protocol sniffing for Yamaha Receiver Addon is enabled. Performance may suffer! Writing to {}",
file.getAbsolutePath());
try {
debugOutStream = new FileOutputStream(file);
} catch (FileNotFoundException e) {
debugOutStream = null;
logger.trace("Protocol log file not found!!", e);
}
} else if (protocolSnifferEnabled) {
// Close stream if protocol sniffing will be disabled
try {
debugOutStream.close();
} catch (Exception e) {
}
debugOutStream = null;
}
this.protocolSnifferEnabled = enable;
}
protected void writeTraceFile(String message) {
if (protocolSnifferEnabled && debugOutStream != null) {
try {
debugOutStream.write(message.replace('\n', ' ').getBytes());
debugOutStream.write('\n');
debugOutStream.write('\n');
debugOutStream.flush();
} catch (IOException e) {
logger.trace("Writing trace file failed", e);
}
}
}
/**
* Implement this for a pure send.
*
* @param message The message to send. Must be xml or json already.
* @throws IOException
*/
public abstract void send(String message) throws IOException;
/**
* Implement this for a send/receive.
*
* @param message The message to send. Must be xml or json already.
* @throws IOException
*/
public abstract String sendReceive(String message) throws IOException;
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.protocol;
/**
* Implement this to be notified of the asynchronous result of {@link ProtocolFactory}.createConnection.
*
* @author David Graeff - Initial contribution
*/
public interface ConnectionStateListener {
void onConnectionCreated(AbstractConnection connection);
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.protocol;
/**
* The device information protocol interface.
*
* @author David Graeff - Initial contribution
*/
public interface DeviceInformation extends IStateUpdatable {
}

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.yamahareceiver.internal.protocol;
import java.io.IOException;
/**
* To offer a method to retrieve a specific state, a protocol part would extend this interface.
*
* @author David Graeff - Initial contribution
*/
public interface IStateUpdatable {
/**
* Updates the corresponding state. This method is blocking.
*
* @throws IOException If the device is offline this exception will be thrown
* @throws ReceivedMessageParseException If the response cannot be parsed correctly this exception is thrown
*/
void update() throws IOException, ReceivedMessageParseException;
}

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.yamahareceiver.internal.protocol;
/**
* Performs conversion logic between canonical input names and underlying Yamaha protocol.
*
* For example, AVRs when setting input 'AUDIO_X' (or HDMI_X) need the input to be sent in this form.
* However, what comes back in the status update from the AVR is 'AUDIOX' (and 'HDMIX') respectively.
*
* @author Tomasz Maruszak
*/
public interface InputConverter {
/**
* Converts the canonical input name to name used by the protocol
*
* @param name canonical name
* @return command name
*/
String toCommandName(String name);
/**
* Converts the state name used by the protocol to canonical input name
*
* @param name state name
* @return canonical name
*/
String fromStateName(String name);
}

View File

@@ -0,0 +1,103 @@
/**
* 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.yamahareceiver.internal.protocol;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Stream;
/**
* The navigation control protocol interface
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - refactoring
*/
public interface InputWithNavigationControl extends IStateUpdatable {
/**
* List all inputs that are compatible with this kind of control
*/
Set<String> SUPPORTED_INPUTS = Stream
.of(INPUT_NET_RADIO, INPUT_NET_RADIO_LEGACY, INPUT_USB, INPUT_IPOD_USB, INPUT_DOCK, INPUT_PC, INPUT_NAPSTER,
INPUT_PANDORA, INPUT_SIRIUS, INPUT_RHAPSODY, INPUT_IPOD, INPUT_HD_RADIO)
.collect(toSet());
/**
* Navigate back
*
* @throws ReceivedMessageParseException, IOException
*/
void goBack() throws ReceivedMessageParseException, IOException;
/**
* Navigate up
*
* @throws ReceivedMessageParseException, IOException
*/
void goUp() throws IOException, ReceivedMessageParseException;
/**
* Navigate down
*
* @throws ReceivedMessageParseException, IOException
*/
void goDown() throws IOException, ReceivedMessageParseException;
/**
* Navigate left. Not for all zones or functions available.
*
* @throws ReceivedMessageParseException, IOException
*/
void goLeft() throws IOException, ReceivedMessageParseException;
/**
* Navigate right. Not for all zones or functions available.
*
* @throws ReceivedMessageParseException, IOException
*/
void goRight() throws IOException, ReceivedMessageParseException;
/**
* Select current item. Not for all zones or functions available.
*
* @throws ReceivedMessageParseException, IOException
*/
void selectCurrentItem() throws IOException, ReceivedMessageParseException;
/**
* Navigate to root menu
*
* @throws ReceivedMessageParseException, IOException
*/
boolean goToRoot() throws IOException, ReceivedMessageParseException;
/**
* Navigate to the given page. The Yamaha protocol separates list of items into pages.
*
* @param page The page, starting with 1.
* @throws IOException
* @throws ReceivedMessageParseException
*/
void goToPage(int page) throws IOException, ReceivedMessageParseException;
/**
* Provide a full path to the menu and menu item
*
* @param fullPath
* @throws IOException
* @throws ReceivedMessageParseException
*/
void selectItemFullPath(String fullPath) throws IOException, ReceivedMessageParseException;
}

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.yamahareceiver.internal.protocol;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Stream;
/**
* The play controls protocol interface
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Spotify support, adding Server to supported preset inputs
*/
public interface InputWithPlayControl extends IStateUpdatable {
/**
* List all inputs that are compatible with this kind of control
*/
Set<String> SUPPORTED_INPUTS = Stream.of(INPUT_NET_RADIO, INPUT_NET_RADIO_LEGACY, INPUT_USB, INPUT_IPOD_USB,
INPUT_IPOD, INPUT_DOCK, INPUT_PC, INPUT_NAPSTER, INPUT_PANDORA, INPUT_SIRIUS, INPUT_RHAPSODY,
INPUT_BLUETOOTH, INPUT_SPOTIFY, INPUT_SERVER, INPUT_HD_RADIO).collect(toSet());
/**
* Start the playback of the content which is usually selected by the means of the Navigation control class or
* which has been stopped by stop().
*
* @throws Exception
*/
void play() throws IOException, ReceivedMessageParseException;
/**
* Stop the currently playing content. Use start() to start again.
*
* @throws Exception
*/
void stop() throws IOException, ReceivedMessageParseException;
/**
* Pause the currently playing content. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
void pause() throws IOException, ReceivedMessageParseException;
/**
* Skip forward. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
void skipFF() throws IOException, ReceivedMessageParseException;
/**
* Skip reverse. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
void skipREV() throws IOException, ReceivedMessageParseException;
/**
* Next track. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
void nextTrack() throws IOException, ReceivedMessageParseException;
/**
* Previous track. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
void previousTrack() throws IOException, ReceivedMessageParseException;
}

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.yamahareceiver.internal.protocol;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Stream;
/**
* The preset control protocol interface
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Adding Spotify, Server to supported preset inputs
*/
public interface InputWithPresetControl extends IStateUpdatable {
int PRESET_CHANNELS = 40;
/**
* List all inputs that are compatible with this kind of control
*/
Set<String> SUPPORTED_INPUTS = Stream.of(INPUT_TUNER, INPUT_NET_RADIO, INPUT_NET_RADIO_LEGACY, INPUT_USB,
INPUT_IPOD, INPUT_IPOD_USB, INPUT_DOCK, INPUT_PC, INPUT_NAPSTER, INPUT_PANDORA, INPUT_SIRIUS,
INPUT_RHAPSODY, INPUT_BLUETOOTH, INPUT_SPOTIFY, INPUT_SERVER, INPUT_HD_RADIO).collect(toSet());
/**
* Select a preset channel.
*
* @param presetChannel The preset position [1,40]
* @throws Exception
*/
void selectItemByPresetNumber(int presetChannel) throws IOException, ReceivedMessageParseException;
}

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.yamahareceiver.internal.protocol;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.INPUT_TUNER;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Stream;
/**
* The DAB Band control protocol interface.
*
* @author Tomasz Maruszak - Initial contribution.
*/
public interface InputWithTunerBandControl extends IStateUpdatable {
/**
* List all inputs that are compatible with this kind of control
*/
Set<String> SUPPORTED_INPUTS = Stream.of(INPUT_TUNER).collect(toSet());
/**
* Select a DAB band by name.
*
* @param band The band name (e.g. FM or DAB)
* @throws Exception
*/
void selectBandByName(String band) throws IOException, ReceivedMessageParseException;
}

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.yamahareceiver.internal.protocol;
import java.util.function.Supplier;
import org.openhab.binding.yamahareceiver.internal.config.YamahaBridgeConfig;
import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
import org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConnection;
import org.openhab.binding.yamahareceiver.internal.state.AvailableInputStateListener;
import org.openhab.binding.yamahareceiver.internal.state.DabBandStateListener;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.SystemControlStateListener;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
/**
* Factory to create a {@link AbstractConnection} connection object based on a feature test.
* Also returns implementation objects for all the protocol interfaces.
* <p>
* At the moment only the XML protocol is supported.
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Input mapping fix, refactoring
*/
public interface ProtocolFactory {
/**
* Asynchronous method to create and return a connection object. Depending
* on the feature test it might be either a {@link XMLConnection} or a JsonConnection.
*
* @param host The host name
* @param connectionStateListener
*/
void createConnection(String host, ConnectionStateListener connectionStateListener);
SystemControl SystemControl(AbstractConnection connection, SystemControlStateListener listener,
DeviceInformationState deviceInformationState);
InputWithPlayControl InputWithPlayControl(AbstractConnection connection, String currentInputID,
PlayInfoStateListener listener, YamahaBridgeConfig settings, DeviceInformationState deviceInformationState);
InputWithPresetControl InputWithPresetControl(AbstractConnection connection, String currentInputID,
PresetInfoStateListener listener, DeviceInformationState deviceInformationState);
InputWithTunerBandControl InputWithDabBandControl(String currentInputID, AbstractConnection connection,
DabBandStateListener observerForBand, PresetInfoStateListener observerForPreset,
PlayInfoStateListener observerForPlayInfo, DeviceInformationState deviceInformationState);
InputWithNavigationControl InputWithNavigationControl(AbstractConnection connection, NavigationControlState state,
String inputID, NavigationControlStateListener observer, DeviceInformationState deviceInformationState);
ZoneControl ZoneControl(AbstractConnection connection, YamahaZoneConfig zoneSettings,
ZoneControlStateListener listener, Supplier<InputConverter> inputConverterSupplier,
DeviceInformationState deviceInformationState);
ZoneAvailableInputs ZoneAvailableInputs(AbstractConnection connection, YamahaZoneConfig zoneSettings,
AvailableInputStateListener listener, Supplier<InputConverter> inputConverterSupplier,
DeviceInformationState deviceInformationState);
DeviceInformation DeviceInformation(AbstractConnection connection, DeviceInformationState state);
InputConverter InputConverter(AbstractConnection connection, String setting);
}

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.yamahareceiver.internal.protocol;
/**
* An exception that is thrown if parsing of the received XML or JSON failed or
* if data that was expected could not be found in the response.
*
* @author David Graeff - Initial contribution
*/
public class ReceivedMessageParseException extends Exception {
private static final long serialVersionUID = 2703218443322787635L;
/**
* Constructs a ReceivedMessageParseException with the specified detail message.
* A detail message is a String that describes this particular exception.
*
* @param s the detail message
*/
public ReceivedMessageParseException(String s) {
super(s);
}
public ReceivedMessageParseException(Exception e) {
super(e);
}
}

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.yamahareceiver.internal.protocol;
import java.io.IOException;
/**
* The system control protocol interface. This is basically just power.
*
* @author David Graeff - Initial contribution
*/
public interface SystemControl extends IStateUpdatable {
/**
* Switches the AVR on/off (off equals network standby here).
*
* @param power The new power state
*
* @throws IOException
* @throws ReceivedMessageParseException
*/
void setPower(boolean power) throws IOException, ReceivedMessageParseException;
/**
* Enables party mode.
*
* @param on
* @throws IOException
* @throws ReceivedMessageParseException
*/
void setPartyMode(boolean on) throws IOException, ReceivedMessageParseException;
/**
* Enables mute for party mode.
*
* @param on
* @throws IOException
* @throws ReceivedMessageParseException
*/
void setPartyModeMute(boolean on) throws IOException, ReceivedMessageParseException;
/**
* Increment or decrement the volume for party mode.
*
* @param increment
* @throws IOException
* @throws ReceivedMessageParseException
*/
void setPartyModeVolume(boolean increment) throws IOException, ReceivedMessageParseException;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.yamahareceiver.internal.protocol;
/**
* Implement this interface to get callbacks of this and that
*
* @author David Graeff - Initial contribution
*/
public interface ZoneAvailableInputs extends IStateUpdatable {
}

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.yamahareceiver.internal.protocol;
import java.io.IOException;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlState;
/**
* The zone control protocol interface
*
* @author David Graeff - Initial contribution
*/
public interface ZoneControl extends IStateUpdatable {
/**
* Switches the zone on/off (off equals network standby here).
*
* @param on The new power state
*
* @throws IOException
* @throws ReceivedMessageParseException
*/
void setPower(boolean on) throws IOException, ReceivedMessageParseException;
/**
* Sets the absolute volume in decibel.
*
* @param volume Absolute value in decibel ([-80,+12]).
* @throws IOException
*/
void setVolumeDB(float volume) throws IOException, ReceivedMessageParseException;
/**
* Sets the volume in percent
*
* @param volume
* @throws IOException
*/
void setVolume(float volume) throws IOException, ReceivedMessageParseException;
/**
* Increase or decrease the volume by the given percentage.
*
* @param percent
* @throws IOException
*/
void setVolumeRelative(ZoneControlState state, float percent) throws IOException, ReceivedMessageParseException;
void setMute(boolean mute) throws IOException, ReceivedMessageParseException;
void setInput(String name) throws IOException, ReceivedMessageParseException;
void setSurroundProgram(String name) throws IOException, ReceivedMessageParseException;
void setDialogueLevel(int level) throws IOException, ReceivedMessageParseException;
/**
* Sets the active scene for the zone.
*
* @param scene
* @throws IOException
* @throws ReceivedMessageParseException
*/
void setScene(String scene) throws IOException, ReceivedMessageParseException;
}

View File

@@ -0,0 +1,100 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Feature;
import org.openhab.binding.yamahareceiver.internal.config.YamahaUtils;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.slf4j.Logger;
/**
* Provides basis for all input controls
*
* @author Tomasz Maruszak - Initial contribution
*/
public abstract class AbstractInputControlXML {
protected final Logger logger;
protected final WeakReference<AbstractConnection> comReference;
protected final String inputID;
protected final Map<String, String> inputToElement;
protected final DeviceDescriptorXML deviceDescriptor;
protected String inputElement;
protected DeviceDescriptorXML.FeatureDescriptor inputFeatureDescriptor;
private Map<String, String> loadMapping() {
Map<String, String> map = new HashMap<>();
// ToDo: For maximum compatibility these should be obtained fro the VNC_Tag of the desc.xml
map.put(INPUT_TUNER, "Tuner");
map.put(INPUT_NET_RADIO, "NET_RADIO");
map.put(INPUT_MUSIC_CAST_LINK, "MusicCast_Link");
return map;
}
protected AbstractInputControlXML(Logger logger, String inputID, AbstractConnection con,
DeviceInformationState deviceInformationState) {
this.logger = logger;
this.comReference = new WeakReference<>(con);
this.inputID = inputID;
this.deviceDescriptor = DeviceDescriptorXML.getAttached(deviceInformationState);
this.inputToElement = loadMapping();
this.inputElement = inputToElement.getOrDefault(inputID, inputID);
this.inputFeatureDescriptor = getInputFeatureDescriptor();
}
/**
* Wraps the XML message with the inputID tags. Example with inputID=NET_RADIO:
* <NET_RADIO>message</NET_RADIO>.
*
* @param message XML message
* @return
*/
protected String wrInput(String message) {
return String.format("<%s>%s</%s>", inputElement, message, inputElement);
}
protected DeviceDescriptorXML.FeatureDescriptor getInputFeatureDescriptor() {
if (deviceDescriptor == null) {
logger.trace("Descriptor not available");
return null;
}
Feature inputFeature = YamahaUtils.tryParseEnum(Feature.class, inputElement);
// For RX-V3900 both the inputs 'NET RADIO' and 'USB' need to use the same NET_USB element
if ((INPUT_NET_RADIO.equals(inputID) || INPUT_USB.equals(inputID))
&& deviceDescriptor.features.containsKey(Feature.NET_USB)
&& !deviceDescriptor.features.containsKey(Feature.NET_RADIO)
&& !deviceDescriptor.features.containsKey(Feature.USB)) {
// have to use the NET_USB xml element in this case
inputElement = "NET_USB";
inputFeature = Feature.NET_USB;
}
if (inputFeature != null) {
return deviceDescriptor.features.getOrDefault(inputFeature, null);
}
return null;
}
}

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.yamahareceiver.internal.protocol.xml;
/**
* Template for XML commands
*
* @author Tomasz Maruszak - Initial contribution
*/
class CommandTemplate {
private final String command;
private final String path;
public CommandTemplate(String command, String path) {
this.command = command;
this.path = path;
}
public CommandTemplate(String command) {
this(command, "");
}
public CommandTemplate replace(String oldToken, String newToken) {
return new CommandTemplate(command.replace(oldToken, newToken), path.replace(oldToken, newToken));
}
public String apply(Object... args) {
return String.format(command, args);
}
public String getPath() {
return path;
}
@Override
public String toString() {
return command;
}
}

View File

@@ -0,0 +1,233 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static java.util.stream.Collectors.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.getChildElements;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Feature;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.config.YamahaUtils;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
*
* Represents device descriptor for XML protocol
*
* @author Tomasz Maruszak - Initial contribution
*/
public class DeviceDescriptorXML {
private final Logger logger = LoggerFactory.getLogger(DeviceDescriptorXML.class);
private String unitName;
public SystemDescriptor system = new SystemDescriptor(null);
public Map<Zone, ZoneDescriptor> zones = new HashMap<>();
public Map<Feature, FeatureDescriptor> features = new HashMap<>();
public void attach(DeviceInformationState state) {
state.properties.put("desc", this);
}
public static DeviceDescriptorXML getAttached(DeviceInformationState state) {
return (DeviceDescriptorXML) state.properties.getOrDefault("desc", null);
}
public String getUnitName() {
return unitName;
}
/**
* Checks if the condition is met, on false result calls the runnable.
*
* @param predicate
* @param falseAction
* @return
*/
public boolean hasFeature(Predicate<DeviceDescriptorXML> predicate, Runnable falseAction) {
boolean result = predicate.test(this);
if (!result) {
falseAction.run();
}
return result;
}
public abstract static class HasCommands {
public final Set<String> commands;
public HasCommands(Element element) {
Element cmdList = (Element) XMLUtils.getNode(element, "Cmd_List");
if (cmdList != null) {
commands = XMLUtils.toStream(cmdList.getElementsByTagName("Define")).map(x -> x.getTextContent())
.collect(toSet());
} else {
commands = new HashSet<>();
}
}
public boolean hasCommandEnding(String command) {
return commands.stream().anyMatch(x -> x.endsWith(command));
}
public boolean hasAnyCommandEnding(String... anyCommand) {
return Arrays.stream(anyCommand).anyMatch(x -> hasCommandEnding(x));
}
/**
* Checks if the command is available, on false result calls the runnable.
*
* @param command
* @param falseAction
* @return
*/
public boolean hasCommandEnding(String command, Runnable falseAction) {
boolean result = hasCommandEnding(command);
if (!result) {
falseAction.run();
}
return result;
}
@Override
public String toString() {
return commands.stream().collect(joining(";"));
}
}
public class SystemDescriptor extends HasCommands {
public SystemDescriptor(Element element) {
super(element);
}
}
public class ZoneDescriptor extends HasCommands {
public final Zone zone;
public ZoneDescriptor(Zone zone, Element element) {
super(element);
this.zone = zone;
logger.trace("Zone {} has commands: {}", zone, super.toString());
}
}
public class FeatureDescriptor extends HasCommands {
public final Feature feature;
public FeatureDescriptor(Feature feature, Element element) {
super(element);
this.feature = feature;
logger.trace("Feature {} has commands: {}", feature, super.toString());
}
}
/**
* Get the descriptor XML from the AVR and parse
*
* @param con
*/
public void load(XMLConnection con) {
// Get and store the Yamaha Description XML. This will be used to detect proper element naming in other areas.
Node descNode = tryGetDescriptor(con);
unitName = descNode.getAttributes().getNamedItem("Unit_Name").getTextContent();
system = buildFeatureLookup(descNode, "Unit", tag -> tag, (tag, e) -> new SystemDescriptor(e))
.getOrDefault("System", this.system); // there will be only one System entry
zones = buildFeatureLookup(descNode, "Subunit", tag -> YamahaUtils.tryParseEnum(Zone.class, tag),
(zone, e) -> new ZoneDescriptor(zone, e));
features = buildFeatureLookup(descNode, "Source_Device",
tag -> XMLConstants.FEATURE_BY_YNC_TAG.getOrDefault(tag, null),
(feature, e) -> new FeatureDescriptor(feature, e));
logger.debug("Found system {}, zones {}, features {}", system != null ? 1 : 0, zones.size(), features.size());
}
/**
* Tires to get the XML descriptor for the AVR
*
* @param con
* @return
*/
private Node tryGetDescriptor(XMLConnection con) {
for (String path : Arrays.asList("/YamahaRemoteControl/desc.xml", "/YamahaRemoteControl/UnitDesc.xml")) {
try {
String descXml = con.getResponse(path);
Document doc = XMLUtils.xml(descXml);
Node root = doc.getFirstChild();
if (root != null && "Unit_Description".equals(root.getNodeName())) {
logger.debug("Retrieved descriptor from {}", path);
return root;
}
logger.debug("The {} response was invalid: {}", path, descXml);
} catch (IOException e) {
// The XML document under specified path does not exist for this model
logger.debug("No descriptor at path {}", path);
} catch (Exception e) {
// Note: We were able to get the XML, but likely cannot parse it properly
logger.warn("Could not parse descriptor at path {}", path, e);
break;
}
}
logger.warn("Could not retrieve descriptor");
return null;
}
private <T, V> Map<T, V> buildFeatureLookup(Node descNode, String funcValue, Function<String, T> converter,
BiFunction<T, Element, V> factory) {
Map<T, V> groupedElements = new HashMap<>();
if (descNode != null) {
Stream<Element> elements = getChildElements(descNode)
.filter(x -> "Menu".equals(x.getTagName()) && funcValue.equals(x.getAttribute("Func")));
elements.forEach(e -> {
String tag = e.getAttribute("YNC_Tag");
if (StringUtils.isNotEmpty(tag)) {
T key = converter.apply(tag);
if (key != null) {
V value = factory.apply(key, e);
// a VNC_Tag value might appear more than once (e.g. Zone B has Main_Zone tag)
groupedElements.putIfAbsent(key, value);
}
}
});
}
return groupedElements;
}
}

View File

@@ -0,0 +1,158 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.Commands.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Feature;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.DeviceInformation;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.SystemControlState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
/**
* The system control protocol class is used to control basic non-zone functionality
* of a Yamaha receiver with HTTP/xml.
* No state will be saved in here, but in {@link SystemControlState} instead.
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - DAB support, Spotify support, better feature detection
*/
public class DeviceInformationXML implements DeviceInformation {
private final Logger logger = LoggerFactory.getLogger(DeviceInformationXML.class);
private final WeakReference<AbstractConnection> comReference;
protected DeviceInformationState state;
public DeviceInformationXML(AbstractConnection com, DeviceInformationState state) {
this.comReference = new WeakReference<>(com);
this.state = state;
}
/**
* We need that called only once. Will give us name, id, version and zone information.
*
* Example:
* <Feature_Existence>
* <Main_Zone>1</Main_Zone>
* <Zone_2>1</Zone_2>
* <Zone_3>0</Zone_3>
* <Zone_4>0</Zone_4>
* <Tuner>0</Tuner>
* <DAB>1</DAB>
* <HD_Radio>0</HD_Radio>
* <Rhapsody>0</Rhapsody>
* <Napster>0</Napster>
* <SiriusXM>0</SiriusXM>
* <Spotify>1</Spotify>
* <Pandora>0</Pandora>
* <JUKE>1</JUKE>
* <MusicCast_Link>1</MusicCast_Link>
* <SERVER>1</SERVER>
* <NET_RADIO>1</NET_RADIO>
* <Bluetooth>1</Bluetooth>
* <USB>1</USB>
* <iPod_USB>1</iPod_USB>
* <AirPlay>1</AirPlay>
* </Feature_Existence>
*
* @throws IOException
*/
@Override
public void update() throws IOException, ReceivedMessageParseException {
XMLConnection con = (XMLConnection) comReference.get();
Node systemConfigNode = getResponse(con, SYSTEM_STATUS_CONFIG_CMD, SYSTEM_STATUS_CONFIG_PATH);
state.host = con.getHost();
state.name = getNodeContentOrEmpty(systemConfigNode, "Model_Name");
state.id = getNodeContentOrEmpty(systemConfigNode, "System_ID");
state.version = getNodeContentOrEmpty(systemConfigNode, "Version");
state.zones.clear();
state.features.clear();
state.properties.clear();
// Get and store the Yamaha Description XML. This will be used to detect proper command naming in other areas.
DeviceDescriptorXML descriptor = new DeviceDescriptorXML();
descriptor.load(con);
descriptor.attach(state);
Node featureNode = getNode(systemConfigNode, "Feature_Existence");
if (featureNode != null) {
for (Zone zone : Zone.values()) {
checkFeature(featureNode, zone.toString(), zone, state.zones);
}
XMLConstants.FEATURE_BY_YNC_TAG
.forEach((name, feature) -> checkFeature(featureNode, name, feature, state.features));
} else {
// on older models (RX-V3900) the Feature_Existence element does not exist
descriptor.zones.forEach((zone, x) -> state.zones.add(zone));
descriptor.features.forEach((feature, x) -> state.features.add(feature));
}
detectZoneBSupport(con);
logger.debug("Found zones: {}, features: {}", state.zones, state.features);
}
/**
* Detect if Zone_B is supported (HTR-4069). This will allow Zone_2 to be emulated by the Zone_B feature.
*
* @param con
* @throws IOException
* @throws ReceivedMessageParseException
*/
private void detectZoneBSupport(XMLConnection con) throws IOException, ReceivedMessageParseException {
if (state.zones.contains(Main_Zone) && !state.zones.contains(Zone_2)) {
// Detect if Zone_B is supported (HTR-4069). This will allow Zone_2 to be emulated.
// Retrieve Main_Zone basic status, from which we will know this AVR supports Zone_B feature.
Node basicStatusNode = getZoneResponse(con, Main_Zone, ZONE_BASIC_STATUS_CMD, ZONE_BASIC_STATUS_PATH);
String power = getNodeContentOrEmpty(basicStatusNode, "Power_Control/Zone_B_Power_Info");
if (StringUtils.isNotEmpty(power)) {
logger.debug("Zone_2 emulation enabled via Zone_B");
state.zones.add(Zone_2);
state.features.add(Feature.ZONE_B);
}
}
}
private boolean isFeatureSupported(Node node, String name) {
String value = getNodeContentOrEmpty(node, name);
boolean supported = value.equals("1") || value.equals("Available");
return supported;
}
private <T> void checkFeature(Node node, String name, T value, Set<T> set) {
if (isFeatureSupported(node, name)) {
set.add(value);
}
}
}

View File

@@ -0,0 +1,167 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* XML implementation of {@link InputConverter}.
*
* @author Tomasz Maruszak - Initial contribution.
*
*/
public class InputConverterXML implements InputConverter {
private final Logger logger = LoggerFactory.getLogger(InputConverterXML.class);
private final WeakReference<AbstractConnection> comReference;
/**
* User defined mapping for state to input name.
*/
private final Map<String, String> inputMap;
/**
* Holds all the inputs names that should NOT be transformed by the {@link #convertNameToID(String)} method.
*/
private final Set<String> inputsWithoutMapping;
public InputConverterXML(AbstractConnection con, String inputMapConfig) {
this.comReference = new WeakReference<>(con);
logger.trace("User defined mapping: {}", inputMapConfig);
this.inputMap = createMapFromSetting(inputMapConfig);
try {
this.inputsWithoutMapping = createInputsWithoutMapping();
logger.trace("These inputs will not be mapped: {}", inputsWithoutMapping);
} catch (IOException | ReceivedMessageParseException e) {
throw new RuntimeException("Could not communicate with the device", e);
}
}
/**
* Creates a map from a string representation: "KEY1=VALUE1,KEY2=VALUE2"
*
* @param setting
* @return
*/
private Map<String, String> createMapFromSetting(String setting) {
Map<String, String> map = new HashMap<>();
if (!StringUtils.isEmpty(setting)) {
String[] entries = setting.split(","); // will contain KEY=VALUE entires
for (String entry : entries) {
String[] keyValue = entry.split("="); // split the KEY=VALUE string
if (keyValue.length != 2) {
logger.warn("Invalid setting: {} entry: {} - KEY=VALUE format was expected", setting, entry);
} else {
String key = keyValue[0];
String value = keyValue[1];
if (map.putIfAbsent(key, value) != null) {
logger.warn("Invalid setting: {} entry: {} - key: {} was already provided before", setting,
entry, key);
}
}
}
}
return map;
}
private Set<String> createInputsWithoutMapping() throws IOException, ReceivedMessageParseException {
// Tested on RX-S601D, RX-V479
Set<String> inputsWithoutMapping = Stream.of(INPUT_SPOTIFY, INPUT_BLUETOOTH).collect(toSet());
Set<String> nativeInputNames = XMLProtocolService.getInputs(comReference.get(), Zone.Main_Zone).stream()
.filter(x -> x.isWritable()).map(x -> x.getParam()).collect(toSet());
// When native input returned matches any of 'HDMIx', 'AUDIOx' or 'NET RADIO', ensure no conversion happens.
// Tested on RX-S601D, RX-V479
nativeInputNames.stream()
.filter(x -> startsWithAndLength(x, "HDMI", 1) || startsWithAndLength(x, "AUDIO", 1)
|| x.equals(INPUT_NET_RADIO) || x.equals(INPUT_MUSIC_CAST_LINK))
.forEach(x -> inputsWithoutMapping.add(x));
return inputsWithoutMapping;
}
private static boolean startsWithAndLength(String str, String prefix, int extraLength) {
// Should be faster then regex
return str != null && str.length() == prefix.length() + extraLength && str.startsWith(prefix);
}
@Override
public String toCommandName(String name) {
// Note: conversation of outgoing command might be needed in the future
logger.trace("Converting from {} to command name {}", name, name);
return name;
}
@Override
public String fromStateName(String name) {
String convertedName;
String method;
if (inputMap.containsKey(name)) {
// Step 1: Check if the user defined custom mapping for their AVR
convertedName = inputMap.get(name);
method = "user defined mapping";
} else if (inputsWithoutMapping.contains(name)) {
// Step 2: Check if input should not be mapped at all
convertedName = name;
method = "no conversion rule";
} else {
// Step 3: Fallback to legacy logic
convertedName = convertNameToID(name);
method = "legacy mapping";
}
logger.trace("Converting from state name {} to {} - as per {}", name, convertedName, method);
return convertedName;
}
/**
* The xml protocol expects HDMI_1, NET_RADIO as xml nodes, while the actual input IDs are
* HDMI 1, Net Radio. We offer this conversion method therefore.
**
* @param name The inputID like "Net Radio".
* @return An xml node / xml protocol compatible name like NET_RADIO.
*/
public String convertNameToID(String name) {
// Replace whitespace with an underscore. The ID is what is used for xml tags and the AVR doesn't like
// whitespace in xml tags.
name = name.replace(" ", "_").toUpperCase();
// Workaround if the receiver returns "HDMI2" instead of "HDMI_2". We can't really change the input IDs in the
// thing type description, because we still need to send "HDMI_2" for an input change to the receiver.
if (name.length() >= 5 && name.startsWith("HDMI") && name.charAt(4) != '_') {
// Adds the missing underscore.
name = name.replace("HDMI", "HDMI_");
}
return name;
}
}

View File

@@ -0,0 +1,342 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import java.io.IOException;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithNavigationControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlStateListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
/**
* This class implements the Yamaha Receiver protocol related to navigation functionally. USB, NET_RADIO, IPOD and
* other inputs are using the same way of navigating through menus. A menu on Yamaha AVRs
* is hierarchically organised. Entries are divided into pages with 8 elements per page.
*
* The XML nodes <List_Control> and <List_Info> are used.
*
* In contrast to other protocol classes an object of this type will store state information,
* because it caches the received XML information of the updateNavigationState(). This may change
* in the future.
*
* Example:
*
* NavigationControl menu = new NavigationControl("NET_RADIO", comObject);
* menu.goToPath(menuDir);
* menu.selectItem(stationName);
*
* @author David Graeff - Completely refactored class
* @author Dennis Frommknecht - Initial idea and implementaton
* @author Tomasz Maruszak - Refactor
*/
public class InputWithNavigationControlXML extends AbstractInputControlXML implements InputWithNavigationControl {
private final Logger logger = LoggerFactory.getLogger(InputWithNavigationControlXML.class);
public static final int MAX_PER_PAGE = 8;
private boolean useAlternativeBackToHomeCmd = false;
private NavigationControlState state;
private NavigationControlStateListener observer;
/**
* Create a NavigationControl object for altering menu positions and requesting current menu information.
*
* @param state We need the current navigation state, because most navigation commands are relative commands and we
* offer API with absolute values.
* @param inputID The input ID like USB or NET_RADIO.
* @param con The Yamaha communication object to send http requests.
*/
public InputWithNavigationControlXML(NavigationControlState state, String inputID, AbstractConnection con,
NavigationControlStateListener observer, DeviceInformationState deviceInformationState) {
super(LoggerFactory.getLogger(InputWithNavigationControlXML.class), inputID, con, deviceInformationState);
this.state = state;
this.observer = observer;
}
/**
* Sends a cursor command to Yamaha.
*
* @param command
* @throws IOException
* @throws ReceivedMessageParseException
*/
private void navigateCursor(String command) throws IOException, ReceivedMessageParseException {
comReference.get().send(wrInput("<List_Control><Cursor>" + command + "</Cursor></List_Control>"));
update();
}
/**
* Navigate back
*
* @throws Exception
*/
@Override
public void goBack() throws IOException, ReceivedMessageParseException {
navigateCursor("Back");
}
/**
* Navigate up
*
* @throws Exception
*/
@Override
public void goUp() throws IOException, ReceivedMessageParseException {
navigateCursor("Up");
}
/**
* Navigate down
*
* @throws Exception
*/
@Override
public void goDown() throws IOException, ReceivedMessageParseException {
navigateCursor("Down");
}
/**
* Navigate left. Not for all zones or functions available.
*
* @throws Exception
*/
@Override
public void goLeft() throws IOException, ReceivedMessageParseException {
navigateCursor("Left");
}
/**
* Navigate right. Not for all zones or functions available.
*
* @throws Exception
*/
@Override
public void goRight() throws IOException, ReceivedMessageParseException {
navigateCursor("Right");
}
/**
* Select current item. Not for all zones or functions available.
*
* @throws Exception
*/
@Override
public void selectCurrentItem() throws IOException, ReceivedMessageParseException {
navigateCursor("Select");
}
/**
* Navigate to root menu
*
* @throws Exception
*/
@Override
public boolean goToRoot() throws IOException, ReceivedMessageParseException {
if (useAlternativeBackToHomeCmd) {
navigateCursor("Return to Home");
if (state.menuLayer > 0) {
observer.navigationError("Both going back to root commands failed for your receiver!");
return false;
}
} else {
navigateCursor("Back to Home");
if (state.menuLayer > 0) {
observer.navigationError(
"The going back to root command failed for your receiver. Trying to use a different command.");
useAlternativeBackToHomeCmd = true;
return goToRoot();
}
}
return true;
}
@Override
public void goToPage(int page) throws IOException, ReceivedMessageParseException {
int line = (page - 1) * 8 + 1;
comReference.get().send(wrInput("<List_Control><Jump_Line>" + line + "</Jump_Line></List_Control>"));
update();
}
@Override
public void selectItemFullPath(String fullPath) throws IOException, ReceivedMessageParseException {
update();
if (state.menuName == null) {
return;
}
String[] pathArr = fullPath.split("/");
// Just a relative menu item.
if (pathArr.length < 2) {
if (!selectItem(pathArr[0])) {
observer.navigationError("Item '" + pathArr[0] + "' doesn't exist in menu " + state.menuName);
}
return;
}
// Full path info not available, so guess from last path element and number of path elements
String selectMenuName = pathArr[pathArr.length - 2];
String selectItemName = pathArr[pathArr.length - 1];
int selectMenuLevel = pathArr.length - 1;
boolean sameMenu = state.menuName.equals(selectMenuName) && state.menuLayer == selectMenuLevel;
if (sameMenu) {
if (!selectItem(selectItemName)) {
observer.navigationError("Item '" + selectItemName + "' doesn't exist in menu " + state.menuName
+ " at level " + String.valueOf(state.menuLayer) + ". Available options are: "
+ state.getAllItemLabels());
}
return;
}
if (state.menuLayer > 0) {
if (!goToRoot()) {
return;
}
}
for (String pathElement : pathArr) {
if (!selectItem(pathElement)) {
observer.navigationError("Item '" + pathElement + "' doesn't exist in menu " + state.menuName
+ " at level " + String.valueOf(state.menuLayer) + ". Available options are: "
+ state.getAllItemLabels());
return;
}
}
}
/**
* Finds an item on the current page. A page contains up to 8 items.
* Operates on a cached XML node! Call refreshMenuState for up-to-date information.
*
* @return Return the item index [1,8] or -1 if not found.
*/
private int findItemOnCurrentPage(String itemName) {
for (int i = 0; i < MAX_PER_PAGE; i++) {
if (itemName.equals(state.items[i])) {
return i + 1;
}
}
return -1;
}
private boolean selectItem(String name) throws IOException, ReceivedMessageParseException {
final int pageCount = (int) Math.ceil(state.maxLine / (double) MAX_PER_PAGE);
final int currentPage = (int) Math.floor((state.currentLine - 1) / (double) MAX_PER_PAGE);
AbstractConnection com = comReference.get();
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
// Start with the current page and then go to the end and start at page 1 again
int realPage = (currentPage + pageIndex) % pageCount;
if (currentPage != realPage) {
goToPage(pageIndex);
}
int index = findItemOnCurrentPage(name);
if (index > 0) {
com.send(wrInput(
"<List_Control><Direct_Sel>Line_" + String.valueOf(index) + "</Direct_Sel></List_Control>"));
update();
return true;
}
}
return false;
}
/**
* Refreshes the menu state and caches the List_Info node from the response. This method may take
* some time because it retries the request for up to MENU_MAX_WAITING_TIME or the menu state reports
* "Ready", whatever comes first.
*
* @throws Exception
*/
@Override
public void update() throws IOException, ReceivedMessageParseException {
int totalWaitingTime = 0;
Document doc;
Node currentMenu;
AbstractConnection com = comReference.get();
while (true) {
String response = com.sendReceive(wrInput("<List_Info>GetParam</List_Info>"));
doc = XMLUtils.xml(response);
if (doc.getFirstChild() == null) {
throw new ReceivedMessageParseException("<List_Info>GetParam failed: " + response);
}
currentMenu = XMLUtils.getNodeOrFail(doc.getFirstChild(), "List_Info");
Node nodeMenuState = XMLUtils.getNode(currentMenu, "Menu_Status");
if (nodeMenuState == null || "Ready".equals(nodeMenuState.getTextContent())) {
break;
}
totalWaitingTime += YamahaReceiverBindingConstants.MENU_RETRY_DELAY;
if (totalWaitingTime > YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME) {
logger.info("Menu still not ready after " + YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME
+ "ms. The menu state will be out of sync.");
// ToDo: this needs to redesigned to allow for some sort of async update
// Note: there is not really that much we can do here.
return;
}
try {
Thread.sleep(YamahaReceiverBindingConstants.MENU_RETRY_DELAY);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
state.clearItems();
Node node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Name");
state.menuName = node.getTextContent();
node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Layer");
state.menuLayer = Integer.parseInt(node.getTextContent()) - 1;
node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Current_Line");
int currentLine = Integer.parseInt(node.getTextContent());
state.currentLine = currentLine;
node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Max_Line");
int maxLines = Integer.parseInt(node.getTextContent());
state.maxLine = maxLines;
for (int i = 1; i < 8; ++i) {
state.items[i - 1] = XMLUtils.getNodeContentOrDefault(currentMenu, "Current_List/Line_" + i + "/Txt",
(String) null);
}
if (observer != null) {
observer.navigationUpdated(state);
}
}
}

View File

@@ -0,0 +1,256 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.INPUT_SPOTIFY;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.Commands.PLAYBACK_STATUS_CMD;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getResponse;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.config.YamahaBridgeConfig;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPlayControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
/**
* This class implements the Yamaha Receiver protocol related to navigation functionally. USB, NET_RADIO, IPOD and
* other inputs are using the same way of playback control.
* <p>
* The XML nodes <Play_Info> and <Play_Control> are used.
* <p>
* Example:
* <p>
* InputWithPlayControl menu = new InputWithPlayControl("NET_RADIO", comObject);
* menu.goToPath(menuDir);
* menu.selectItem(stationName);
* <p>
* No state will be saved in here, but in {@link PlayInfoState} and
* {@link PresetInfoState} instead.
*
* @author David Graeff
* @author Tomasz Maruszak - Spotify support, refactoring
*/
public class InputWithPlayControlXML extends AbstractInputControlXML implements InputWithPlayControl {
private final PlayInfoStateListener observer;
private final YamahaBridgeConfig bridgeConfig;
protected CommandTemplate playCmd = new CommandTemplate("<Play_Control><Playback>%s</Playback></Play_Control>",
"Play_Info/Playback_Info");
protected CommandTemplate skipCmd = new CommandTemplate("<Play_Control><Playback>%s</Playback></Play_Control>");
protected String skipForwardValue = "Skip Fwd";
protected String skipBackwardValue = "Skip Rev";
/**
* Create a InputWithPlayControl object for altering menu positions and requesting current menu information as well
* as controlling the playback and choosing a preset item.
*
* @param inputID The input ID like USB or NET_RADIO.
* @param com The Yamaha communication object to send http requests.
*/
public InputWithPlayControlXML(String inputID, AbstractConnection com, PlayInfoStateListener observer,
YamahaBridgeConfig bridgeConfig, DeviceInformationState deviceInformationState) {
super(LoggerFactory.getLogger(InputWithPlayControlXML.class), inputID, com, deviceInformationState);
this.observer = observer;
this.bridgeConfig = bridgeConfig;
this.applyModelVariations();
}
/**
* Apply command changes to ensure compatibility with all supported models
*/
protected void applyModelVariations() {
if (inputFeatureDescriptor != null) {
// For RX-V3900
if (inputFeatureDescriptor.hasCommandEnding("Play_Control,Play")) {
playCmd = new CommandTemplate("<Play_Control><Play>%s</Play></Play_Control>", "Play_Info/Status");
logger.debug("Input {} - adjusting command to: {}", inputElement, playCmd);
}
// For RX-V3900
if (inputFeatureDescriptor.hasCommandEnding("Play_Control,Skip")) {
// For RX-V3900 the command value is also different
skipForwardValue = "Fwd";
skipBackwardValue = "Rev";
skipCmd = new CommandTemplate("<Play_Control><Skip>%s</Skip></Play_Control>");
logger.debug("Input {} - adjusting command to: {}", inputElement, skipCmd);
}
}
}
/**
* Start the playback of the content which is usually selected by the means of the Navigation control class or
* which has been stopped by stop().
*
* @throws Exception
*/
@Override
public void play() throws IOException, ReceivedMessageParseException {
sendCommand(playCmd.apply("Play"));
}
/**
* Stop the currently playing content. Use start() to start again.
*
* @throws Exception
*/
@Override
public void stop() throws IOException, ReceivedMessageParseException {
sendCommand(playCmd.apply("Stop"));
}
/**
* Pause the currently playing content. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
@Override
public void pause() throws IOException, ReceivedMessageParseException {
sendCommand(playCmd.apply("Pause"));
}
/**
* Skip forward. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
@Override
public void skipFF() throws IOException, ReceivedMessageParseException {
if (INPUT_SPOTIFY.equals(inputID)) {
logger.warn("Command skip forward is not supported for input {}", inputID);
return;
}
sendCommand(skipCmd.apply(">>|"));
}
/**
* Skip reverse. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
@Override
public void skipREV() throws IOException, ReceivedMessageParseException {
if (INPUT_SPOTIFY.equals(inputID)) {
logger.warn("Command skip reverse is not supported for input {}", inputID);
return;
}
sendCommand(skipCmd.apply("|<<"));
}
/**
* Next track. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
@Override
public void nextTrack() throws IOException, ReceivedMessageParseException {
sendCommand(skipCmd.apply(skipForwardValue));
}
/**
* Previous track. This is not available for streaming content like on NET_RADIO.
*
* @throws Exception
*/
@Override
public void previousTrack() throws IOException, ReceivedMessageParseException {
sendCommand(skipCmd.apply(skipBackwardValue));
}
/**
* Sends a playback command to the AVR. After command is invoked, the state is also being refreshed.
*
* @param command - the protocol level command name
* @throws IOException
* @throws ReceivedMessageParseException
*/
private void sendCommand(String command) throws IOException, ReceivedMessageParseException {
comReference.get().send(wrInput(command));
update();
}
/**
* Updates the playback information
*
* @throws Exception
*/
@Override
public void update() throws IOException, ReceivedMessageParseException {
if (observer == null) {
return;
}
// <YAMAHA_AV rsp="GET" RC="0">
// <Spotify>
// <Play_Info>
// <Feature_Availability>Ready</Feature_Availability>
// <Playback_Info>Play</Playback_Info>
// <Meta_Info>
// <Artist>Way Out West</Artist>
// <Album>Tuesday Maybe</Album>
// <Track>Tuesday Maybe</Track>
// </Meta_Info>
// <Album_ART>
// <URL>/YamahaRemoteControl/AlbumART/AlbumART3929.jpg</URL>
// <ID>39290</ID>
// <Format>JPEG</Format>
// </Album_ART>
// <Input_Logo>
// <URL_S>/YamahaRemoteControl/Logos/logo005.png</URL_S>
// <URL_M></URL_M>
// <URL_L></URL_L>
// </Input_Logo>
// </Play_Info>
// </Spotify>
// </YAMAHA_AV>
AbstractConnection con = comReference.get();
Node node = getResponse(con, wrInput(PLAYBACK_STATUS_CMD), inputElement);
PlayInfoState msg = new PlayInfoState();
msg.playbackMode = getNodeContentOrDefault(node, playCmd.getPath(), msg.playbackMode);
// elements for these are named differently per model and per input, so we try to match any known element
msg.station = getAnyNodeContentOrDefault(node, msg.station, "Play_Info/Meta_Info/Radio_Text_A",
"Play_Info/Meta_Info/Station", "Play_Info/RDS/Program_Service");
msg.artist = getAnyNodeContentOrDefault(node, msg.artist, "Play_Info/Meta_Info/Artist",
"Play_Info/Title/Artist", "Play_Info/RDS/Radio_Text_A");
msg.album = getAnyNodeContentOrDefault(node, msg.album, "Play_Info/Meta_Info/Album", "Play_Info/Title/Album",
"Play_Info/RDS/Program_Type");
msg.song = getAnyNodeContentOrDefault(node, msg.song, "Play_Info/Meta_Info/Track", "Play_Info/Meta_Info/Song",
"Play_Info/Title/Song", "Play_Info/RDS/Radio_Text_B");
// Spotify and NET RADIO input supports song cover image (at least on RX-S601D)
String songImageUrl = getNodeContentOrEmpty(node, "Play_Info/Album_ART/URL");
msg.songImageUrl = StringUtils.isNotEmpty(songImageUrl)
? String.format("http://%s%s", con.getHost(), songImageUrl)
: bridgeConfig.getAlbumUrl();
logger.trace("Playback: {}, Station: {}, Artist: {}, Album: {}, Song: {}, SongImageUrl: {}", msg.playbackMode,
msg.station, msg.artist, msg.album, msg.song, msg.songImageUrl);
observer.playInfoUpdated(msg);
}
}

View File

@@ -0,0 +1,191 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.GET_PARAM;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getResponse;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
import java.io.IOException;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPresetControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
/**
* This class implements the Yamaha Receiver protocol related to navigation functionally. USB, NET_RADIO, IPOD and
* other inputs are using the same way of playback control.
*
* The XML nodes <Play_Info> and <Play_Control> are used.
*
* Example:
*
* InputWithPlayControl menu = new InputWithPlayControl("NET_RADIO", comObject);
* menu.goToPath(menuDir);
* menu.selectItem(stationName);
*
* No state will be saved in here, but in {@link PlayInfoState} and
* {@link PresetInfoState} instead.
*
* @author David Graeff
* @author Tomasz Maruszak - Compatibility fixes
*/
public class InputWithPresetControlXML extends AbstractInputControlXML implements InputWithPresetControl {
protected CommandTemplate preset = new CommandTemplate(
"<Play_Control><Preset><Preset_Sel>%s</Preset_Sel></Preset></Play_Control>",
"Play_Control/Preset/Preset_Sel");
private final PresetInfoStateListener observer;
/**
* Create a InputWithPlayControl object for altering menu positions and requesting current menu information as well
* as controlling the playback and choosing a preset item.
*
* @param inputID The input ID like USB or NET_RADIO.
* @param con The Yamaha communication object to send http requests.
*/
public InputWithPresetControlXML(String inputID, AbstractConnection con, PresetInfoStateListener observer,
DeviceInformationState deviceInformationState) {
super(LoggerFactory.getLogger(InputWithPresetControlXML.class), inputID, con, deviceInformationState);
this.observer = observer;
this.applyModelVariations();
}
/**
* Apply command changes to ensure compatibility with all supported models
*/
protected void applyModelVariations() {
if (deviceDescriptor == null) {
logger.trace("Descriptor not available");
return;
}
// add compatibility adjustments here (if any)
}
/**
* Updates the preset information
*
* @throws Exception
*/
@Override
public void update() throws IOException, ReceivedMessageParseException {
if (observer == null) {
return;
}
AbstractConnection con = comReference.get();
Node response = getResponse(con,
wrInput("<Play_Control><Preset><Preset_Sel_Item>GetParam</Preset_Sel_Item></Preset></Play_Control>"),
inputElement);
PresetInfoState msg = new PresetInfoState();
// Set preset channel names, obtained from this xpath:
// NET_RADIO/Play_Control/Preset/Preset_Sel_Item/Item_1/Title
Node presetNode = getNode(response, "Play_Control/Preset/Preset_Sel_Item");
if (presetNode != null) {
for (int i = 1; i <= PRESET_CHANNELS; i++) {
Node itemNode = getNode(presetNode, "Item_" + i);
if (itemNode == null) {
break;
}
String title = getNodeContentOrDefault(itemNode, "Title", "Item_" + i);
String value = getNodeContentOrDefault(itemNode, "Param", String.valueOf(i));
// For RX-V3900 when a preset slot is not used, this is how it looks
if (StringUtils.isEmpty(title) && "Not Used".equalsIgnoreCase(value)) {
continue;
}
int presetChannel = convertToPresetNumber(value);
PresetInfoState.Preset preset = new PresetInfoState.Preset(title, presetChannel);
msg.presetChannelNames.add(preset);
}
}
msg.presetChannelNamesChanged = true;
String presetValue = getNodeContentOrEmpty(response, preset.getPath());
// fall back to second method of obtaining current preset (works for Tuner on RX-V3900)
if (StringUtils.isEmpty(presetValue)) {
try {
Node presetResponse = getResponse(con, wrInput(preset.apply(GET_PARAM)), inputElement);
presetValue = getNodeContentOrEmpty(presetResponse, preset.getPath());
} catch (IOException | ReceivedMessageParseException e) {
// this is on purpose, in case the AVR does not support this request and responds with error or nonsense
}
}
// For Tuner input on RX-V3900 this is not a number (e.g. "A1" or "B1").
msg.presetChannel = convertToPresetNumber(presetValue);
observer.presetInfoUpdated(msg);
}
private int convertToPresetNumber(String presetValue) {
if (StringUtils.isNotEmpty(presetValue)) {
if (StringUtils.isNumeric(presetValue)) {
return Integer.parseInt(presetValue);
} else {
// special handling for RX-V3900, where 'A1' becomes 101 and 'B2' becomes 202 preset
if (presetValue.length() >= 2) {
Character presetAlpha = presetValue.charAt(0);
if (Character.isLetter(presetAlpha) && Character.isUpperCase(presetAlpha)) {
int presetNumber = Integer.parseInt(presetValue.substring(1));
return (ArrayUtils.indexOf(LETTERS, presetAlpha) + 1) * 100 + presetNumber;
}
}
}
}
return -1;
}
/**
* Select a preset channel.
*
* @param presetChannel The preset position [1,40]
* @throws Exception
*/
@Override
public void selectItemByPresetNumber(int presetChannel) throws IOException, ReceivedMessageParseException {
String presetValue;
// special handling for RX-V3900, where 'A1' becomes 101 and 'B2' becomes 202 preset
if (presetChannel > 100) {
int presetNumber = presetChannel % 100;
char presetAlpha = LETTERS[presetChannel / 100 - 1];
presetValue = Character.toString(presetAlpha) + presetNumber;
} else {
presetValue = Integer.toString(presetChannel);
}
String cmd = wrInput(preset.apply(presetValue));
comReference.get().send(cmd);
update();
}
private static final Character[] LETTERS = new Character[] { 'A', 'B', 'C', 'D' };
}

View File

@@ -0,0 +1,236 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPresetControl;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithTunerBandControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.state.DabBandState;
import org.openhab.binding.yamahareceiver.internal.state.DabBandStateListener;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
/**
* This class implements the Yamaha Receiver protocol related to DAB tuners which allows to control band and preset.
* This control is specific to dual band tuners only.
*
* Note that yamaha maintains separate presets for each band.
*
* The XML nodes <DAB><Play_Control><Band>FM</Band></Play_Control></DAB> are used.
*
* No state will be saved in here, but in {@link DabBandState}, {@link PresetInfoState} and {@link PlayInfoState}
* instead.
*
* @author Tomasz Maruszak - [yamaha] Tuner band selection and preset feature for dual band models (RX-S601D)
*/
public class InputWithTunerDABControlXML extends AbstractInputControlXML
implements InputWithTunerBandControl, InputWithPresetControl {
private static final String BAND_FM = "FM";
private static final String BAND_DAB = "DAB";
private final DabBandStateListener observerForBand;
private final PresetInfoStateListener observerForPreset;
private final PlayInfoStateListener observerForPlayInfo;
protected CommandTemplate band = new CommandTemplate("<Play_Control><Band>%s</Band></Play_Control>",
"Play_Info/Band");
protected CommandTemplate preset = new CommandTemplate(
"<Play_Control><%s><Preset><Preset_Sel>%d</Preset_Sel></Preset></%s></Play_Control>", "");
/**
* Need to remember last band state to drive the preset
*/
private DabBandState bandState;
/**
* Create a InputWithPlayControl object for altering menu positions and requesting current menu information as well
* as controlling the playback and choosing a preset item.
*
* @param inputID The input ID - TUNER is going to be used here.
* @param con The Yamaha communication object to send http requests.
*/
public InputWithTunerDABControlXML(String inputID, AbstractConnection con, DabBandStateListener observerForBand,
PresetInfoStateListener observerForPreset, PlayInfoStateListener observerForPlayInfo,
DeviceInformationState deviceInformationState) {
super(LoggerFactory.getLogger(InputWithTunerDABControlXML.class), inputID, con, deviceInformationState);
this.inputElement = "DAB";
this.observerForBand = observerForBand;
this.observerForPreset = observerForPreset;
this.observerForPlayInfo = observerForPlayInfo;
if (observerForBand == null && observerForPreset == null && observerForPlayInfo == null) {
throw new IllegalArgumentException("At least one observer has to be provided");
}
}
@Override
public void update() throws IOException, ReceivedMessageParseException {
Node responseNode = XMLProtocolService.getResponse(comReference.get(),
wrInput("<Play_Info>GetParam</Play_Info>"), inputElement);
// @formatter:off
//Sample response:
//<YAMAHA_AV rsp="GET" RC="0">
// <DAB>
// <Play_Info>
// <Feature_Availability>Ready</Feature_Availability>
// <FM>
// <Preset>
// <Preset_Sel>1</Preset_Sel>
// </Preset>
// <Tuning>
// <Freq>
// <Val>9945</Val>
// <Exp>2</Exp>
// <Unit>MHz</Unit>
// </Freq>
// </Tuning>
// <FM_Mode>Auto</FM_Mode>
// <Signal_Info>
// <Tuned>Assert</Tuned>
// <Stereo>Assert</Stereo>
// </Signal_Info>
// <Meta_Info>
// <Program_Type>POP M</Program_Type>
// <Program_Service> 22:59</Program_Service>
// <Radio_Text>tel. 22 333 33 33 * Trojka * e-mail: trojka@polskieradio.pl</Radio_Text>
// <Clock_Time>22:59</Clock_Time>
// </Meta_Info>
// </FM>
// <DAB>
// <Status>Ready</Status>
// <Preset>
// <Preset_Sel>No Preset</Preset_Sel>
// </Preset>
// <ID>2</ID>
// <Signal_Info>
// <Freq>
// <Val>218640</Val>
// <Exp>3</Exp>
// <Unit>MHz</Unit>
// </Freq>
// <Category>Primary</Category>
// <Audio_Mode>Stereo</Audio_Mode>
// <Bit_Rate>
// <Val>128</Val>
// <Exp>0</Exp>
// <Unit>Kbps</Unit>
// </Bit_Rate>
// <Quality>82</Quality>
// <Tune_Aid>45</Tune_Aid>
// <Off_Air>Negate</Off_Air>
// <DAB_PLUS>Assert</DAB_PLUS>
// </Signal_Info>
// <Meta_Info>
// <Ch_Label>11B</Ch_Label>
// <Service_Label>PR Czwórka</Service_Label>
// <DLS>Kluboteka Polskie Radio S.A.</DLS>
// <Ensemble_Label>Polskie Radio</Ensemble_Label>
// <Program_Type>Pop</Program_Type>
// <Date_and_Time>12AUG&apos;17 23:47</Date_and_Time>
// </Meta_Info>
// </DAB>
// <Band>FM</Band>
// </Play_Info>
// </DAB>
//</YAMAHA_AV>
// @formatter:on
DabBandState msgForBand = new DabBandState();
PresetInfoState msgForPreset = new PresetInfoState();
PlayInfoState msgForPlayInfo = new PlayInfoState();
msgForBand.band = getNodeContentOrDefault(responseNode, "Play_Info/Band", msgForBand.band);
logger.debug("Band set to {} for input {}", msgForBand.band, inputID);
// store last state of band
bandState = msgForBand;
if (StringUtils.isEmpty(msgForBand.band)) {
logger.warn("Band is unknown for input {}, therefore preset and playback information will not be available",
inputID);
} else {
Node playInfoNode = getNode(responseNode, "Play_Info/" + msgForBand.band);
msgForPreset.presetChannel = getNodeContentOrDefault(playInfoNode, "Preset/Preset_Sel", -1);
logger.debug("Preset set to {} for input {}", msgForPreset.presetChannel, inputID);
Node metaInfoNode = getNode(playInfoNode, "Meta_Info");
if (metaInfoNode != null) {
msgForPlayInfo.album = getNodeContentOrDefault(metaInfoNode, "Program_Type", msgForPlayInfo.album);
if (BAND_DAB.equals(msgForBand.band)) {
msgForPlayInfo.station = getNodeContentOrDefault(metaInfoNode, "Service_Label",
msgForPlayInfo.station);
msgForPlayInfo.artist = getNodeContentOrDefault(metaInfoNode, "Ensemble_Label",
msgForPlayInfo.artist);
msgForPlayInfo.song = getNodeContentOrDefault(metaInfoNode, "DLS", msgForPlayInfo.song);
} else {
msgForPlayInfo.station = getNodeContentOrDefault(metaInfoNode, "Program_Service",
msgForPlayInfo.station);
msgForPlayInfo.artist = getNodeContentOrDefault(metaInfoNode, "Station", msgForPlayInfo.artist);
msgForPlayInfo.song = getNodeContentOrDefault(metaInfoNode, "Radio_Text", msgForPlayInfo.song);
}
}
}
// DAB does not provide channel names, the channel list will be empty
msgForPreset.presetChannelNamesChanged = true;
if (observerForBand != null) {
observerForBand.dabBandUpdated(msgForBand);
}
if (observerForPreset != null) {
observerForPreset.presetInfoUpdated(msgForPreset);
}
if (observerForPlayInfo != null) {
observerForPlayInfo.playInfoUpdated(msgForPlayInfo);
}
}
@Override
public void selectBandByName(String band) throws IOException, ReceivedMessageParseException {
// Example: <Play_Control><Band>FM</Band></Play_Control>
String cmd = this.band.apply(band);
comReference.get().send(wrInput(cmd));
update();
}
@Override
public void selectItemByPresetNumber(int presetChannel) throws IOException, ReceivedMessageParseException {
if (bandState == null || bandState.band == null || bandState.band.isEmpty()) {
logger.warn("Cannot change preset because the band is unknown for input {}", inputID);
return;
}
// Example: <Play_Control><FM><Preset><Preset_Sel>2</Preset_Sel></Preset></FM></Play_Control>
String cmd = this.preset.apply(bandState.band, presetChannel, bandState.band);
comReference.get().send(wrInput(cmd));
update();
}
}

View File

@@ -0,0 +1,162 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Models.RX_A2000;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getResponse;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.protocol.SystemControl;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.SystemControlState;
import org.openhab.binding.yamahareceiver.internal.state.SystemControlStateListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
/**
* The system control protocol class is used to control basic non-zone functionality
* of a Yamaha receiver with HTTP/xml.
* No state will be saved in here, but in {@link SystemControlState} instead.
*
* @author David Gräff - Initial contribution
* @author Tomasz Maruszak - refactoring, HTR-xxxx Zone_2 compatibility
*/
public class SystemControlXML implements SystemControl {
private final Logger logger = LoggerFactory.getLogger(SystemControlXML.class);
private static final Set<String> MODELS_WITH_PARTY_SUPPORT = new HashSet<>(Arrays.asList(RX_A2000));
private WeakReference<AbstractConnection> comReference;
private SystemControlStateListener observer;
private final DeviceDescriptorXML descriptorXML;
protected CommandTemplate power = new CommandTemplate(
"<System><Power_Control><Power>%s</Power></Power_Control></System>", "System/Power_Control/Power");
protected CommandTemplate partyMode = new CommandTemplate(
"<System><Party_Mode><Mode>%s</Mode></Party_Mode></System>", "System/Party_Mode/Mode");
protected boolean partyModeSupported;
protected CommandTemplate partyModeMute = new CommandTemplate(
"<System><Party_Mode><Volume><Mute>%s</Mute></Volume></Party_Mode></System>");
protected boolean partyModeMuteSupported;
protected CommandTemplate partyModeVolume = new CommandTemplate(
"<System><Party_Mode><Volume><Lvl>%s</Lvl></Volume></Party_Mode></System>");
protected boolean partyModeVolumeSupported;
public SystemControlXML(AbstractConnection xml, SystemControlStateListener observer,
DeviceInformationState deviceInformationState) {
this.comReference = new WeakReference<>(xml);
this.observer = observer;
this.descriptorXML = DeviceDescriptorXML.getAttached(deviceInformationState);
this.applyModelVariations();
}
/**
* Apply command changes to ensure compatibility with all supported models
*/
protected void applyModelVariations() {
if (descriptorXML == null) {
logger.trace("Device descriptor not available");
return;
}
logger.trace("Compatibility detection");
partyModeSupported = descriptorXML.hasFeature(
d -> MODELS_WITH_PARTY_SUPPORT.contains(d.getUnitName())
|| d.system.hasCommandEnding("System,Party_Mode,Mode"),
() -> logger.debug("The {} channel is not supported on your model", CHANNEL_PARTY_MODE));
partyModeMuteSupported = descriptorXML.hasFeature(
d -> MODELS_WITH_PARTY_SUPPORT.contains(d.getUnitName())
|| d.system.hasCommandEnding("System,Party_Mode,Volume,Mute"),
() -> logger.debug("The {} channel is not supported on your model", CHANNEL_PARTY_MODE_MUTE));
partyModeVolumeSupported = descriptorXML.hasFeature(
d -> MODELS_WITH_PARTY_SUPPORT.contains(d.getUnitName())
|| d.system.hasCommandEnding("System,Party_Mode,Volume,Lvl"),
() -> logger.debug("The {} channel is not supported on your model", CHANNEL_PARTY_MODE_VOLUME));
}
@Override
public void setPower(boolean power) throws IOException, ReceivedMessageParseException {
String cmd = this.power.apply(power ? ON : POWER_STANDBY);
comReference.get().send(cmd);
update();
}
@Override
public void setPartyMode(boolean on) throws IOException, ReceivedMessageParseException {
if (!partyModeSupported) {
return;
}
String cmd = this.partyMode.apply(on ? ON : OFF);
comReference.get().send(cmd);
update();
}
@Override
public void setPartyModeMute(boolean on) throws IOException, ReceivedMessageParseException {
if (!partyModeMuteSupported) {
return;
}
String cmd = this.partyModeMute.apply(on ? ON : OFF);
comReference.get().send(cmd);
update();
}
@Override
public void setPartyModeVolume(boolean increment) throws IOException, ReceivedMessageParseException {
if (!partyModeVolumeSupported) {
return;
}
String cmd = this.partyModeVolume.apply(increment ? UP : DOWN);
comReference.get().send(cmd);
update();
}
@Override
public void update() throws IOException, ReceivedMessageParseException {
if (observer == null) {
return;
}
AbstractConnection conn = comReference.get();
SystemControlState state = new SystemControlState();
Node node = getResponse(conn, power.apply(GET_PARAM), power.getPath());
state.power = node != null && ON.equals(node.getTextContent());
if (partyModeSupported) {
// prevent an unnecessary call
node = getResponse(conn, partyMode.apply(GET_PARAM), partyMode.getPath());
state.partyMode = node != null && ON.equals(node.getTextContent());
}
logger.debug("System state - power: {}, partyMode: {}", state.power, state.partyMode);
observer.systemControlStateChanged(state);
}
}

View File

@@ -0,0 +1,217 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.Optional;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* All other protocol classes in this directory use this class for communication. An object
* of HttpXMLSendReceive is always bound to a specific host.
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Minor refactor
*
*/
public class XMLConnection extends AbstractConnection {
private Logger logger = LoggerFactory.getLogger(XMLConnection.class);
private static final String XML_GET = "<?xml version=\"1.0\" encoding=\"utf-8\"?><YAMAHA_AV cmd=\"GET\">";
private static final String XML_PUT = "<?xml version=\"1.0\" encoding=\"utf-8\"?><YAMAHA_AV cmd=\"PUT\">";
private static final String XML_END = "</YAMAHA_AV>";
private static final String HEADER_CHARSET_PART = "charset=";
private static final int CONNECTION_TIMEOUT_MS = 5000;
public XMLConnection(String host) {
super(host);
}
@FunctionalInterface
public interface CheckedConsumer<T, R> {
R apply(T t) throws IOException;
}
private <T> T postMessage(String prefix, String message, String suffix,
CheckedConsumer<HttpURLConnection, T> responseConsumer) throws IOException {
if (message.startsWith("<?xml")) {
throw new IOException("No pre-formatted xml allowed!");
}
message = prefix + message + suffix;
writeTraceFile(message);
URL url = createCrlUrl();
logger.debug("Making POST to {} with payload: {}", url, message);
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Length", Integer.toString(message.length()));
// Set a timeout in case the device is not reachable (went offline)
connection.setConnectTimeout(CONNECTION_TIMEOUT_MS);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
// Send request
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.writeBytes(message);
wr.flush();
}
if (connection.getResponseCode() != 200) {
throw new IOException("Changing a value on the Yamaha AVR failed: " + message);
}
return responseConsumer.apply(connection);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* Post the given xml message
*
* @param message XML formatted message excluding < ?xml > or <YAMAHA_AV> tags.
* @throws IOException
*/
@Override
public void send(String message) throws IOException {
postMessage(XML_PUT, message, XML_END, c -> null);
}
/**
* Post the given xml message and return the response as string.
*
* @param message XML formatted message excluding <?xml> or <YAMAHA_AV> tags.
* @return Return the response as text or throws an exception if the connection failed.
* @throws IOException
*/
@Override
public String sendReceive(final String message) throws IOException {
return postMessage(XML_GET, message, XML_END, c -> consumeResponse(c));
}
private String consumeResponse(HttpURLConnection connection) throws IOException {
// Read response
Charset responseCharset = getResponseCharset(connection, StandardCharsets.UTF_8);
try (BufferedReader rd = new BufferedReader(
new InputStreamReader(connection.getInputStream(), responseCharset))) {
String line;
StringBuilder responseBuffer = new StringBuilder();
while ((line = rd.readLine()) != null) {
responseBuffer.append(line);
responseBuffer.append('\r');
}
String response = responseBuffer.toString();
writeTraceFile(response);
return response;
}
}
public String getResponse(String path) throws IOException {
URL url = createBaseUrl(path);
logger.debug("Making GET to {}", url);
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(false);
if (connection.getResponseCode() != 200) {
throw new IOException("Request failed");
}
return consumeResponse(connection);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private Charset getResponseCharset(HttpURLConnection connection, Charset defaultCharset) {
// See https://stackoverflow.com/a/3934280/1906057
Charset charset = defaultCharset;
String contentType = connection.getContentType();
String[] values = contentType.split(";"); // values.length should be 2
// Example:
// Content-Type:text/xml; charset="utf-8"
Optional<String> charsetName = Arrays.stream(values).map(x -> x.trim())
.filter(x -> x.toLowerCase().startsWith(HEADER_CHARSET_PART))
.map(x -> x.substring(HEADER_CHARSET_PART.length() + 1, x.length() - 1)).findFirst();
if (charsetName.isPresent() && !StringUtils.isEmpty(charsetName.get())) {
try {
charset = Charset.forName(charsetName.get());
} catch (UnsupportedCharsetException | IllegalCharsetNameException e) {
logger.warn("The charset {} provided in the response {} is not supported", charsetName, contentType);
}
}
logger.trace("The charset {} will be used to parse the response", charset);
return charset;
}
/**
* Creates an {@link URL} object to the Yamaha control endpoint
*
* @return
* @throws MalformedURLException
*/
private URL createCrlUrl() throws MalformedURLException {
return createBaseUrl("/YamahaRemoteControl/ctrl");
}
/**
* Creates an {@link URL} object to Yamaha
*
* @return
* @throws MalformedURLException
*/
private URL createBaseUrl(String path) throws MalformedURLException {
return new URL("http://" + host + path);
}
}

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.yamahareceiver.internal.protocol.xml;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Feature;
/**
* XML protocol constants.
*
* @author Tomasz Maruszak - Initial contribution
*/
public class XMLConstants {
public static final String ON = "On";
public static final String OFF = "Off";
public static final String POWER_STANDBY = "Standby";
public static final Map<String, Feature> FEATURE_BY_YNC_TAG;
public static final String GET_PARAM = "GetParam";
public static final String UP = "Up";
public static final String DOWN = "Down";
public static class Commands {
public static final String SYSTEM_STATUS_CONFIG_CMD = "<System><Config>GetParam</Config></System>";
public static final String SYSTEM_STATUS_CONFIG_PATH = "System/Config";
public static final String ZONE_BASIC_STATUS_CMD = "<Basic_Status>GetParam</Basic_Status>";
public static final String ZONE_BASIC_STATUS_PATH = "Basic_Status";
public static final String ZONE_INPUT_QUERY = "<Input><Input_Sel_Item>GetParam</Input_Sel_Item></Input>";
public static final String ZONE_INPUT_PATH = "Input/Input_Sel_Item";
public static final String PLAYBACK_STATUS_CMD = "<Play_Info>GetParam</Play_Info>";
}
static {
FEATURE_BY_YNC_TAG = new HashMap<>();
FEATURE_BY_YNC_TAG.put("Tuner", Feature.TUNER);
FEATURE_BY_YNC_TAG.put("DAB", Feature.DAB);
FEATURE_BY_YNC_TAG.put("Spotify", Feature.SPOTIFY);
FEATURE_BY_YNC_TAG.put("Bluetooth", Feature.BLUETOOTH);
FEATURE_BY_YNC_TAG.put("AirPlay", Feature.AIRPLAY);
FEATURE_BY_YNC_TAG.put("NET_RADIO", Feature.NET_RADIO);
FEATURE_BY_YNC_TAG.put("USB", Feature.USB);
FEATURE_BY_YNC_TAG.put("NET_USB", Feature.NET_USB);
}
}

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.yamahareceiver.internal.protocol.xml;
import java.util.function.Supplier;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants;
import org.openhab.binding.yamahareceiver.internal.config.YamahaBridgeConfig;
import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.ConnectionStateListener;
import org.openhab.binding.yamahareceiver.internal.protocol.DeviceInformation;
import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithNavigationControl;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPlayControl;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPresetControl;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithTunerBandControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ProtocolFactory;
import org.openhab.binding.yamahareceiver.internal.protocol.SystemControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ZoneAvailableInputs;
import org.openhab.binding.yamahareceiver.internal.protocol.ZoneControl;
import org.openhab.binding.yamahareceiver.internal.state.AvailableInputStateListener;
import org.openhab.binding.yamahareceiver.internal.state.DabBandStateListener;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlState;
import org.openhab.binding.yamahareceiver.internal.state.NavigationControlStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
import org.openhab.binding.yamahareceiver.internal.state.SystemControlStateListener;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
/**
* Implementation of {@link ProtocolFactory} for XML protocol.
*
* @author Tomasz Maruszak - Initial contribution.
*/
public class XMLProtocolFactory implements ProtocolFactory {
@Override
public void createConnection(String host, ConnectionStateListener connectionStateListener) {
connectionStateListener.onConnectionCreated(new XMLConnection(host));
}
@Override
public SystemControl SystemControl(AbstractConnection connection, SystemControlStateListener listener,
DeviceInformationState deviceInformationState) {
return new SystemControlXML(connection, listener, deviceInformationState);
}
@Override
public InputWithPlayControl InputWithPlayControl(AbstractConnection connection, String currentInputID,
PlayInfoStateListener listener, YamahaBridgeConfig bridgeConfig,
DeviceInformationState deviceInformationState) {
return new InputWithPlayControlXML(currentInputID, connection, listener, bridgeConfig, deviceInformationState);
}
@Override
public InputWithPresetControl InputWithPresetControl(AbstractConnection connection, String currentInputID,
PresetInfoStateListener listener, DeviceInformationState deviceInformationState) {
return new InputWithPresetControlXML(currentInputID, connection, listener, deviceInformationState);
}
@Override
public InputWithTunerBandControl InputWithDabBandControl(String currentInputID, AbstractConnection connection,
DabBandStateListener observerForBand, PresetInfoStateListener observerForPreset,
PlayInfoStateListener observerForPlayInfo, DeviceInformationState deviceInformationState) {
return new InputWithTunerDABControlXML(currentInputID, connection, observerForBand, observerForPreset,
observerForPlayInfo, deviceInformationState);
}
@Override
public InputWithNavigationControl InputWithNavigationControl(AbstractConnection connection,
NavigationControlState state, String inputID, NavigationControlStateListener observer,
DeviceInformationState deviceInformationState) {
return new InputWithNavigationControlXML(state, inputID, connection, observer, deviceInformationState);
}
@Override
public ZoneControl ZoneControl(AbstractConnection connection, YamahaZoneConfig zoneSettings,
ZoneControlStateListener listener, Supplier<InputConverter> inputConverterSupplier,
DeviceInformationState deviceInformationState) {
if (isZoneB(zoneSettings.getZone(), deviceInformationState)) {
return new ZoneBControlXML(connection, zoneSettings, listener, deviceInformationState,
inputConverterSupplier);
}
return new ZoneControlXML(connection, zoneSettings.getZone(), zoneSettings, listener, deviceInformationState,
inputConverterSupplier);
}
@Override
public ZoneAvailableInputs ZoneAvailableInputs(AbstractConnection connection, YamahaZoneConfig zoneSettings,
AvailableInputStateListener listener, Supplier<InputConverter> inputConverterSupplier,
DeviceInformationState deviceInformationState) {
if (isZoneB(zoneSettings.getZone(), deviceInformationState)) {
return new ZoneBAvailableInputsXML(connection, listener, inputConverterSupplier);
}
return new ZoneAvailableInputsXML(connection, zoneSettings.getZone(), listener, inputConverterSupplier);
}
/**
* Checks if the specified Zone_2 should be emulated using Zone_B feature.
*
* @param zone
* @param deviceInformationState
* @return
*/
private boolean isZoneB(YamahaReceiverBindingConstants.Zone zone, DeviceInformationState deviceInformationState) {
return YamahaReceiverBindingConstants.Zone.Zone_2.equals(zone)
&& deviceInformationState.features.contains(YamahaReceiverBindingConstants.Feature.ZONE_B);
}
@Override
public DeviceInformation DeviceInformation(AbstractConnection connection, DeviceInformationState state) {
return new DeviceInformationXML(connection, state);
}
@Override
public InputConverter InputConverter(AbstractConnection connection, String setting) {
return new InputConverterXML(connection, setting);
}
}

View File

@@ -0,0 +1,144 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static java.util.stream.Collectors.joining;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.Commands.*;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
/**
* Provides services for XML protocol
*
* @author Tomasz Maruszak - Initial contribution
*/
public class XMLProtocolService {
private static final Logger LOGGER = LoggerFactory.getLogger(XMLProtocolService.class);
/**
* Sends a command to the specified zone.
*
* @param con
* @param zone
* @param cmd
* @return The response XML node (specific to the command sent).
* @throws IOException
* @throws ReceivedMessageParseException
*/
public static Node getZoneResponse(AbstractConnection con, Zone zone, String cmd)
throws IOException, ReceivedMessageParseException {
return getResponse(con, XMLUtils.wrZone(zone, cmd), zone.toString());
}
/**
* Sends a command to the specified zone.
*
* @param con
* @param zone
* @param cmd
* @param path XML tree path to extract from the response
* @return The response XML node (specific to the command sent).
* @throws IOException
* @throws ReceivedMessageParseException
*/
public static Node getZoneResponse(AbstractConnection con, Zone zone, String cmd, String path)
throws IOException, ReceivedMessageParseException {
return getResponse(con, XMLUtils.wrZone(zone, cmd), zone + "/" + path);
}
/**
* Send the command and retrieve the node at the specified element path.
*
* @param cmd
* @param path
* @return
* @throws IOException
* @throws ReceivedMessageParseException
*/
public static Node getResponse(AbstractConnection con, String cmd, String path)
throws IOException, ReceivedMessageParseException {
String response = con.sendReceive(cmd);
Document doc = XMLUtils.xml(response);
if (doc.getFirstChild() == null) {
throw new ReceivedMessageParseException("The command '" + cmd + "' failed: " + response);
}
Node content = XMLUtils.getNode(doc.getFirstChild(), path);
return content;
}
/**
* Sends a request to retrieve the input values available for the zone.
*
* @param con
* @param zone
* @return
* @throws IOException
* @throws ReceivedMessageParseException
*/
public static Collection<InputDto> getInputs(AbstractConnection con, Zone zone)
throws IOException, ReceivedMessageParseException {
Node inputSelItem = getZoneResponse(con, zone, ZONE_INPUT_QUERY, ZONE_INPUT_PATH);
List<InputDto> inputs = new LinkedList<>();
XMLUtils.getChildElements(inputSelItem).forEach(item -> {
String param = item.getElementsByTagName("Param").item(0).getTextContent();
boolean writable = item.getElementsByTagName("RW").item(0).getTextContent().contains("W");
inputs.add(new InputDto(param, writable));
});
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Zone {} - inputs: {}", zone, inputs.stream().map(InputDto::toString).collect(joining(", ")));
}
return inputs;
}
/**
* Represents an input source
*/
public static class InputDto {
private final String param;
private final boolean writable;
public InputDto(String param, boolean writable) {
this.param = param;
this.writable = writable;
}
public String getParam() {
return param;
}
public boolean isWritable() {
return writable;
}
@Override
public String toString() {
return String.format("%s:%s", param, writable ? "RW" : "R");
}
}
}

View File

@@ -0,0 +1,192 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import java.io.IOException;
import java.io.StringReader;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Utility methods for XML handling
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - DAB support, Spotify support, refactoring, input name conversion fix, Input mapping fix
*/
public class XMLUtils {
private static final Logger LOG = LoggerFactory.getLogger(XMLUtils.class);
// We need a lot of xml parsing. Create a document builder beforehand.
static final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
static Node getNode(Node parent, String[] nodePath, int offset) {
if (parent == null) {
return null;
}
if (offset < nodePath.length - 1) {
return getNode(((Element) parent).getElementsByTagName(nodePath[offset]).item(0), nodePath, offset + 1);
} else {
return ((Element) parent).getElementsByTagName(nodePath[offset]).item(0);
}
}
static Node getNode(Node root, String nodePath) {
String[] nodePathArr = nodePath.split("/");
return getNode(root, nodePathArr, 0);
}
static Stream<Element> getChildElements(Node node) {
if (node == null) {
return Stream.empty();
}
return toStream(node.getChildNodes()).filter(x -> x.getNodeType() == Node.ELEMENT_NODE).map(x -> (Element) x);
}
static Stream<Node> toStream(NodeList nodeList) {
return IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item);
}
/**
* Retrieves the child node according to the xpath expression.
*
* @param root
* @param nodePath
* @return
* @throws ReceivedMessageParseException when the child node does not exist throws
* {@link ReceivedMessageParseException}.
*/
static Node getNodeOrFail(Node root, String nodePath) throws ReceivedMessageParseException {
Node node = getNode(root, nodePath);
if (node == null) {
throw new ReceivedMessageParseException(nodePath + " child in parent node missing!");
}
return node;
}
/**
* Finds the node starting with the root and following the path. If the node is found it's inner text is returned,
* otherwise the default provided value.
*
* @param root
* @param nodePath
* @param defaultValue
* @return
*/
public static String getNodeContentOrDefault(Node root, String nodePath, String defaultValue) {
Node node = getNode(root, nodePath);
if (node != null) {
return node.getTextContent();
}
return defaultValue;
}
/**
* Finds the node starting with the root and following the path.
* If the node is found it's inner text is returned, otherwise the default provided value.
* The first path that exists is returned.
*
* @param root
* @param nodePaths
* @param defaultValue
* @return
*/
public static String getAnyNodeContentOrDefault(Node root, String defaultValue, String... nodePaths) {
for (String nodePath : nodePaths) {
String value = getNodeContentOrDefault(root, nodePath, (String) null);
if (value != null) {
return value;
}
}
return defaultValue;
}
/**
* Finds the node starting with the root and following the path. If the node is found it's inner text is returned,
* otherwise the default provided value.
*
* @param root
* @param nodePath
* @return
*/
public static String getNodeContentOrEmpty(Node root, String nodePath) {
return getNodeContentOrDefault(root, nodePath, "");
}
/**
* Finds the node starting with the root and following the path. If the node is found it's inner text is returned,
* otherwise the default provided value.
*
* @param root
* @param nodePath
* @param defaultValue
* @return
*/
public static Integer getNodeContentOrDefault(Node root, String nodePath, Integer defaultValue) {
Node node = getNode(root, nodePath);
if (node != null) {
try {
return Integer.valueOf(node.getTextContent());
} catch (NumberFormatException e) {
LOG.trace(
"The value '{}' of node with path {} could not been parsed to an integer. Applying default of {}",
node.getTextContent(), nodePath, defaultValue);
}
}
return defaultValue;
}
/**
* Parse the given xml message into a xml document node.
*
* @param message XML formatted message.
* @return Return the response as xml node or throws an exception if response is not xml.
* @throws IOException
*/
public static Document xml(String message) throws IOException, ReceivedMessageParseException {
// Ensure the message contains XML declaration
String response = message.startsWith("<?xml") ? message
: "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + message;
try {
return XMLUtils.dbf.newDocumentBuilder().parse(new InputSource(new StringReader(response)));
} catch (SAXException | ParserConfigurationException e) {
throw new ReceivedMessageParseException(e);
}
}
/**
* Wraps the XML message with the zone tags. Example with zone=Main_Zone:
* <Main_Zone>message</Main_Zone>.
*
* @param message XML message
* @return
*/
public static String wrZone(Zone zone, String message) {
return "<" + zone.name() + ">" + message + "</" + zone.name() + ">";
}
}

View File

@@ -0,0 +1,87 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static java.util.stream.Collectors.joining;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.function.Supplier;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.protocol.ZoneAvailableInputs;
import org.openhab.binding.yamahareceiver.internal.state.AvailableInputState;
import org.openhab.binding.yamahareceiver.internal.state.AvailableInputStateListener;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The zone protocol class is used to control one zone of a Yamaha receiver with HTTP/xml.
* No state will be saved in here, but in {@link ZoneControlState} instead.
*
* @author David Gräff - Initial contribution
* @author Tomasz Maruszak - Refactoring
* @author Tomasz Maruszak - Input mapping fix
*
*/
public class ZoneAvailableInputsXML implements ZoneAvailableInputs {
protected Logger logger = LoggerFactory.getLogger(ZoneAvailableInputsXML.class);
private final WeakReference<AbstractConnection> conReference;
private final AvailableInputStateListener observer;
private final Supplier<InputConverter> inputConverterSupplier;
private final Zone zone;
public ZoneAvailableInputsXML(AbstractConnection con, Zone zone, AvailableInputStateListener observer,
Supplier<InputConverter> inputConverterSupplier) {
this.conReference = new WeakReference<>(con);
this.zone = zone;
this.observer = observer;
this.inputConverterSupplier = inputConverterSupplier;
}
/**
* Return the zone
*/
public Zone getZone() {
return zone;
}
@Override
public void update() throws IOException, ReceivedMessageParseException {
if (observer == null) {
return;
}
Collection<XMLProtocolService.InputDto> inputs = XMLProtocolService.getInputs(conReference.get(), zone);
AvailableInputState state = new AvailableInputState();
inputs.stream().filter(XMLProtocolService.InputDto::isWritable).forEach(x -> {
String inputName = inputConverterSupplier.get().fromStateName(x.getParam());
state.availableInputs.put(inputName, x.getParam());
});
if (logger.isTraceEnabled()) {
logger.trace("Zone {} - available inputs: {}", getZone(),
state.availableInputs.keySet().stream().collect(joining(", ")));
}
observer.availableInputsChanged(state);
}
}

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.yamahareceiver.internal.protocol.xml;
import java.util.function.Supplier;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
import org.openhab.binding.yamahareceiver.internal.state.AvailableInputStateListener;
import org.slf4j.LoggerFactory;
/**
* Special case of {@link ZoneAvailableInputsXML} that emulates Zone_2 for Yamaha HTR-xxx using Zone_B features.
*
* @author Tomasz Maruszak - Initial contribution.
*/
public class ZoneBAvailableInputsXML extends ZoneAvailableInputsXML {
public ZoneBAvailableInputsXML(AbstractConnection con, AvailableInputStateListener observer,
Supplier<InputConverter> inputConverterSupplier) {
super(con, Zone.Main_Zone, observer, inputConverterSupplier);
this.logger = LoggerFactory.getLogger(ZoneBAvailableInputsXML.class);
}
@Override
public Zone getZone() {
return Zone.Zone_2;
}
}

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.yamahareceiver.internal.protocol.xml;
import java.util.function.Supplier;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
import org.slf4j.LoggerFactory;
/**
* Special case of {@link ZoneControlXML} that emulates Zone_2 for Yamaha HTR-xxx using Zone_B features.
*
* @author Tomasz Maruszak - Initial contribution.
*/
public class ZoneBControlXML extends ZoneControlXML {
public ZoneBControlXML(AbstractConnection con, YamahaZoneConfig zoneSettings, ZoneControlStateListener observer,
DeviceInformationState deviceInformationState, Supplier<InputConverter> inputConverterSupplier) {
// Commands will need to be send to Main_Zone
super(con, Zone.Main_Zone, zoneSettings, observer, deviceInformationState, inputConverterSupplier);
this.logger = LoggerFactory.getLogger(ZoneBControlXML.class);
}
@Override
public Zone getZone() {
return Zone.Zone_2;
}
@Override
protected void applyModelVariations() {
super.applyModelVariations();
// Apply custom templates for HTR-xxx
this.power = new CommandTemplate("<Power_Control><Zone_B_Power>%s</Zone_B_Power></Power_Control>",
"Power_Control/Zone_B_Power_Info");
this.mute = new CommandTemplate("<Volume><Zone_B><Mute>%s</Mute></Zone_B></Volume>", "Volume/Zone_B/Mute");
this.volume = new CommandTemplate(
"<Volume><Zone_B><Lvl><Val>%d</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Zone_B></Volume>",
"Volume/Zone_B/Lvl/Val");
}
}

View File

@@ -0,0 +1,313 @@
/**
* 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.yamahareceiver.internal.protocol.xml;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.Commands.*;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getZoneResponse;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.function.Supplier;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.protocol.ZoneControl;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlState;
import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
/**
* The zone protocol class is used to control one zone of a Yamaha receiver with HTTP/xml.
* No state will be saved in here, but in {@link ZoneControlState} instead.
*
* @author David Gräff - Refactored
* @author Eric Thill
* @author Ben Jones
* @author Tomasz Maruszak - Refactoring, input mapping fix, added Straight surround, volume DB fix and config
* improvement.
*/
public class ZoneControlXML implements ZoneControl {
protected Logger logger = LoggerFactory.getLogger(ZoneControlXML.class);
private static final String SURROUND_PROGRAM_STRAIGHT = "Straight";
private final ZoneControlStateListener observer;
private final Supplier<InputConverter> inputConverterSupplier;
private final WeakReference<AbstractConnection> comReference;
private final Zone zone;
private final YamahaZoneConfig zoneConfig;
private final DeviceDescriptorXML.ZoneDescriptor zoneDescriptor;
protected CommandTemplate power = new CommandTemplate("<Power_Control><Power>%s</Power></Power_Control>",
"Power_Control/Power");
protected CommandTemplate mute = new CommandTemplate("<Volume><Mute>%s</Mute></Volume>", "Volume/Mute");
protected CommandTemplate volume = new CommandTemplate(
"<Volume><Lvl><Val>%d</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Volume>", "Volume/Lvl/Val");
protected CommandTemplate inputSel = new CommandTemplate("<Input><Input_Sel>%s</Input_Sel></Input>",
"Input/Input_Sel");
protected String inputSelNamePath = "Input/Input_Sel_Item_Info/Title";
protected CommandTemplate surroundSelProgram = new CommandTemplate(
"<Surround><Program_Sel><Current><Sound_Program>%s</Sound_Program></Current></Program_Sel></Surround>",
"Surround/Program_Sel/Current/Sound_Program");
protected CommandTemplate surroundSelStraight = new CommandTemplate(
"<Surround><Program_Sel><Current><Straight>On</Straight></Current></Program_Sel></Surround>",
"Surround/Program_Sel/Current/Straight");
protected CommandTemplate sceneSel = new CommandTemplate("<Scene><Scene_Sel>%s</Scene_Sel></Scene>");
protected boolean sceneSelSupported = false;
protected CommandTemplate dialogueLevel = new CommandTemplate(
"<Sound_Video><Dialogue_Adjust><Dialogue_Lvl>%d</Dialogue_Lvl></Dialogue_Adjust></Sound_Video>",
"Sound_Video/Dialogue_Adjust/Dialogue_Lvl");
protected boolean dialogueLevelSupported = false;
public ZoneControlXML(AbstractConnection con, Zone zone, YamahaZoneConfig zoneSettings,
ZoneControlStateListener observer, DeviceInformationState deviceInformationState,
Supplier<InputConverter> inputConverterSupplier) {
this.comReference = new WeakReference<>(con);
this.zone = zone;
this.zoneConfig = zoneSettings;
this.zoneDescriptor = DeviceDescriptorXML.getAttached(deviceInformationState).zones.getOrDefault(zone, null);
this.observer = observer;
this.inputConverterSupplier = inputConverterSupplier;
this.applyModelVariations();
}
/**
* Apply command changes to ensure compatibility with all supported models
*/
protected void applyModelVariations() {
if (zoneDescriptor == null) {
logger.trace("Zone {} - descriptor not available", getZone());
return;
}
logger.trace("Zone {} - compatibility detection", getZone());
// Note: Detection if scene is supported
sceneSelSupported = zoneDescriptor.hasCommandEnding("Scene,Scene_Sel", () -> logger
.debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_SCENE));
// Note: Detection if dialogue level is supported
dialogueLevelSupported = zoneDescriptor.hasAnyCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lvl",
"Sound_Video,Dialogue_Adjust,Dialogue_Lift");
if (zoneDescriptor.hasCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lift")) {
dialogueLevel = dialogueLevel.replace("Dialogue_Lvl", "Dialogue_Lift");
logger.debug("Zone {} - adjusting command to: {}", getZone(), dialogueLevel);
}
if (!dialogueLevelSupported) {
logger.debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_DIALOGUE_LEVEL);
}
// Note: Detection for RX-V3900, which uses <Vol> instead of <Volume>
if (zoneDescriptor.hasCommandEnding("Vol,Lvl")) {
volume = volume.replace("Volume", "Vol");
logger.debug("Zone {} - adjusting command to: {}", getZone(), volume);
}
if (zoneDescriptor.hasCommandEnding("Vol,Mute")) {
mute = mute.replace("Volume", "Vol");
logger.debug("Zone {} - adjusting command to: {}", getZone(), mute);
}
try {
// Note: Detection for RX-V3900, which has a different XML node for surround program
Node basicStatusNode = getZoneResponse(comReference.get(), getZone(), ZONE_BASIC_STATUS_CMD,
ZONE_BASIC_STATUS_PATH);
String surroundProgram = getNodeContentOrEmpty(basicStatusNode, "Surr/Pgm_Sel/Pgm");
if (StringUtils.isNotEmpty(surroundProgram)) {
surroundSelProgram = new CommandTemplate(
"<Surr><Pgm_Sel><Straight>Off</Straight><Pgm>%s</Pgm></Pgm_Sel></Surr>", "Surr/Pgm_Sel/Pgm");
logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelProgram);
surroundSelStraight = new CommandTemplate("<Surr><Pgm_Sel><Straight>On</Straight></Pgm_Sel></Surr>",
"Surr/Pgm_Sel/Straight");
logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelStraight);
}
} catch (ReceivedMessageParseException | IOException e) {
logger.debug("Could not perform feature detection for RX-V3900");
}
}
protected void sendCommand(String message) throws IOException {
comReference.get().send(XMLUtils.wrZone(zone, message));
}
/**
* Return the zone
*/
public Zone getZone() {
return zone;
}
@Override
public void setPower(boolean on) throws IOException, ReceivedMessageParseException {
String cmd = power.apply(on ? ON : POWER_STANDBY);
sendCommand(cmd);
update();
}
@Override
public void setMute(boolean on) throws IOException, ReceivedMessageParseException {
String cmd = this.mute.apply(on ? ON : OFF);
sendCommand(cmd);
update();
}
/**
* Sets the absolute volume in decibel.
*
* @param volume Absolute value in decibel ([-80,+12]).
* @throws IOException
*/
@Override
public void setVolumeDB(float volume) throws IOException, ReceivedMessageParseException {
if (volume < zoneConfig.getVolumeDbMin()) {
volume = zoneConfig.getVolumeDbMin();
}
if (volume > zoneConfig.getVolumeDbMax()) {
volume = zoneConfig.getVolumeDbMax();
}
// Yamaha accepts only integer values with .0 or .5 at the end only (-20.5dB, -20.0dB) - at least on RX-S601D.
// The order matters here. We want to cast to integer first and then scale by 10.
// Effectively we're only allowing dB values with .0 at the end.
int vol = (int) volume * 10;
sendCommand(this.volume.apply(vol));
update();
}
/**
* Sets the volume in percent
*
* @param volume
* @throws IOException
*/
@Override
public void setVolume(float volume) throws IOException, ReceivedMessageParseException {
if (volume < 0) {
volume = 0;
}
if (volume > 100) {
volume = 100;
}
// Compute value in db
setVolumeDB(zoneConfig.getVolumeDb(volume));
}
/**
* Increase or decrease the volume by the given percentage.
*
* @param percent
* @throws IOException
*/
@Override
public void setVolumeRelative(ZoneControlState state, float percent)
throws IOException, ReceivedMessageParseException {
setVolume(zoneConfig.getVolumePercentage(state.volumeDB) + percent);
}
@Override
public void setInput(String name) throws IOException, ReceivedMessageParseException {
name = inputConverterSupplier.get().toCommandName(name);
String cmd = inputSel.apply(name);
sendCommand(cmd);
update();
}
@Override
public void setSurroundProgram(String name) throws IOException, ReceivedMessageParseException {
String cmd = name.equalsIgnoreCase(SURROUND_PROGRAM_STRAIGHT) ? surroundSelStraight.apply()
: surroundSelProgram.apply(name);
sendCommand(cmd);
update();
}
@Override
public void setDialogueLevel(int level) throws IOException, ReceivedMessageParseException {
if (!dialogueLevelSupported) {
return;
}
sendCommand(dialogueLevel.apply(level));
update();
}
@Override
public void setScene(String scene) throws IOException, ReceivedMessageParseException {
if (!sceneSelSupported) {
return;
}
sendCommand(sceneSel.apply(scene));
update();
}
@Override
public void update() throws IOException, ReceivedMessageParseException {
if (observer == null) {
return;
}
Node statusNode = getZoneResponse(comReference.get(), zone, ZONE_BASIC_STATUS_CMD, ZONE_BASIC_STATUS_PATH);
String value;
ZoneControlState state = new ZoneControlState();
value = getNodeContentOrEmpty(statusNode, power.getPath());
state.power = ON.equalsIgnoreCase(value);
value = getNodeContentOrEmpty(statusNode, mute.getPath());
state.mute = ON.equalsIgnoreCase(value);
// The value comes in dB x 10, on AVR it says -30.5dB, the values comes as -305
value = getNodeContentOrDefault(statusNode, volume.getPath(), String.valueOf(zoneConfig.getVolumeDbMin()));
state.volumeDB = Float.parseFloat(value) * .1f; // in dB
value = getNodeContentOrEmpty(statusNode, inputSel.getPath());
state.inputID = inputConverterSupplier.get().fromStateName(value);
if (StringUtils.isBlank(state.inputID)) {
throw new ReceivedMessageParseException("Expected inputID. Failed to read Input/Input_Sel");
}
// Some receivers may use Src_Name instead?
value = getNodeContentOrEmpty(statusNode, inputSelNamePath);
state.inputName = value;
value = getNodeContentOrEmpty(statusNode, surroundSelStraight.getPath());
boolean straightOn = ON.equalsIgnoreCase(value);
value = getNodeContentOrEmpty(statusNode, surroundSelProgram.getPath());
// Surround is either in straight mode or sound program
state.surroundProgram = straightOn ? SURROUND_PROGRAM_STRAIGHT : value;
value = getNodeContentOrDefault(statusNode, dialogueLevel.getPath(), "0");
state.dialogueLevel = Integer.parseInt(value);
logger.debug("Zone {} state - power: {}, mute: {}, volumeDB: {}, input: {}, surroundProgram: {}", getZone(),
state.power, state.mute, state.volumeDB, state.inputID, state.surroundProgram);
observer.zoneStateChanged(state);
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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.yamahareceiver.internal.state;
import java.util.Map;
import java.util.TreeMap;
/**
* List of AVR input channel names with <Input ID, Input Name>
*
* @author David Graeff - Initial contribution
*/
public class AvailableInputState {
// List of inputs with <Input ID, Input Name>
public Map<String, String> availableInputs = new TreeMap<>();
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Listener for changes to {@link AvailableInputState}
*
* @author David Graeff - Initial contribution
*/
public interface AvailableInputStateListener {
void availableInputsChanged(AvailableInputState msg);
}

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.yamahareceiver.internal.state;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.VALUE_EMPTY;
/**
* The band state for DAB tuners.
*
* @author Tomasz Maruszak - [yamaha] Tuner band selection and preset feature for dual band models (RX-S601D)
*/
public class DabBandState implements Invalidateable {
public String band = VALUE_EMPTY; // Used by TUNER
@Override
public void invalidate() {
band = VALUE_EMPTY;
}
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Listener for changes to {@link DabBandState}
*
* @author Tomasz Maruszak - [yamaha] Tuner band selection and preset feature for dual band models (RX-S601D)
*/
public interface DabBandStateListener {
void dabBandUpdated(DabBandState msg);
}

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.yamahareceiver.internal.state;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants;
/**
* Basic AVR state (name, version, available zones, etc)
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - DAB support, Spotify support, better feature detection
*/
public class DeviceInformationState implements Invalidateable {
public String host;
// Some AVR information
public String name;
public String id;
public String version;
public final Set<YamahaReceiverBindingConstants.Zone> zones = new HashSet<>();
public final Set<YamahaReceiverBindingConstants.Feature> features = new HashSet<>();
/**
* Stores additional properties for the device (protocol specific)
*/
public final Map<String, Object> properties = new HashMap<>();
public DeviceInformationState() {
invalidate();
}
// If we lost the connection, invalidate the state.
@Override
public void invalidate() {
host = null;
name = "N/A";
id = "";
version = "0.0";
zones.clear();
features.clear();
properties.clear();
}
}

View File

@@ -0,0 +1,26 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Represent object whose state can be invalidated.
*
* @author Tomasz Maruszak - [yamaha] Tuner band selection and preset feature for dual band models (RX-S601D)
*/
public interface Invalidateable {
/**
* Invalidate the object
*/
void invalidate();
}

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.yamahareceiver.internal.state;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.protocol.xml.InputWithNavigationControlXML;
/**
* The current state of the navigation
*
* @author David Graeff - Initial contribution
*/
public class NavigationControlState implements Invalidateable {
public String menuName = null;
public int menuLayer = -1;
public int currentLine = 0;
public int maxLine = -1;
public String items[] = new String[InputWithNavigationControlXML.MAX_PER_PAGE];
public String getCurrentItemName() {
if (currentLine < 1 || currentLine > items.length) {
return "";
}
return items[currentLine - 1];
}
public String getAllItemLabels() {
StringBuilder sb = new StringBuilder();
for (String item : items) {
if (StringUtils.isNotEmpty(item)) {
sb.append(item);
sb.append(',');
}
}
return sb.toString();
}
public void clearItems() {
for (int i = 0; i < items.length; ++i) {
items[i] = null;
}
}
@Override
public void invalidate() {
this.menuName = "N/A";
this.maxLine = 0;
this.currentLine = 0;
this.menuLayer = 0;
}
}

View File

@@ -0,0 +1,24 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Listener for changes to {@link NavigationControlState}
*
* @author David Graeff - Initial contribution
*/
public interface NavigationControlStateListener {
void navigationUpdated(NavigationControlState msg);
void navigationError(String msg);
}

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.yamahareceiver.internal.state;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
/**
* The play information state with current station, artist, song name
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - Spotify support
*
*/
public class PlayInfoState implements Invalidateable {
public String station = VALUE_NA; // NET_RADIO. Will also be used for TUNER where Radio_Text_A/B will be used
// instead.
public String artist = VALUE_NA; // USB, iPOD, PC
public String album = VALUE_NA; // USB, iPOD, PC
public String song = VALUE_NA; // USB, iPOD, PC
public String songImageUrl = VALUE_EMPTY; // Spotify
public String playbackMode = "Stop"; // All inputs
@Override
public void invalidate() {
this.playbackMode = VALUE_NA;
this.station = VALUE_NA;
this.artist = VALUE_NA;
this.album = VALUE_NA;
this.song = VALUE_NA;
this.songImageUrl = VALUE_EMPTY;
}
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Listener for changes to {@link PlayInfoState}
*
* @author David Graeff - Initial contribution
*/
public interface PlayInfoStateListener {
void playInfoUpdated(PlayInfoState msg);
}

View File

@@ -0,0 +1,70 @@
/**
* 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.yamahareceiver.internal.state;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* The preset state containing the channel names and currently selected channel
*
* @author David Graeff - Initial contribution
* @author Tomasz Maruszak - RX-V3900 compatibility improvements
*/
public class PresetInfoState implements Invalidateable {
public static class Preset {
private final String name;
private final int value;
public Preset(String name, int value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Preset preset = (Preset) o;
return value == preset.value && Objects.equals(name, preset.name);
}
@Override
public int hashCode() {
return Objects.hash(name, value);
}
}
public int presetChannel = 0; // Used by NET_RADIO, RADIO, HD_RADIO, iPOD, USB, PC
public final List<Preset> presetChannelNames = new ArrayList<>();
public boolean presetChannelNamesChanged = false;
@Override
public void invalidate() {
presetChannel = 0;
presetChannelNames.clear();
}
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Listener for changes to {@link PresetInfoState}
*
* @author David Graeff - Initial contribution
*/
public interface PresetInfoStateListener {
void presetInfoUpdated(PresetInfoState msg);
}

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.yamahareceiver.internal.state;
/**
* System AVR state (system power, etc)
*
* @author David Graeff - Initial contribution
*
*/
public class SystemControlState implements Invalidateable {
public boolean power;
public boolean partyMode;
// If we lost the connection, invalidate the state.
@Override
public void invalidate() {
power = false;
partyMode = false;
}
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Listener for changes to {@link SystemControlState}
*
* @author David Graeff - Initial contribution
*/
public interface SystemControlStateListener {
void systemControlStateChanged(SystemControlState msg);
}

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.yamahareceiver.internal.state;
import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.VALUE_EMPTY;
/**
* The state of a specific zone of a Yamaha receiver.
*
* @author David Graeff - Initial contribution
*/
public class ZoneControlState {
public boolean power = false;
// User visible name of the input channel for the current zone
public String inputName = VALUE_EMPTY;
// The ID of the input channel that is used as xml tags (for example NET_RADIO, HDMI_1).
// This may differ from what the AVR returns in Input/Input_Sel ("NET RADIO", "HDMI1")
public String inputID = VALUE_EMPTY;
public String surroundProgram = VALUE_EMPTY;
public float volumeDB = 0.0f; // volume in dB
public boolean mute = false;
public int dialogueLevel = 0;
}

View File

@@ -0,0 +1,22 @@
/**
* 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.yamahareceiver.internal.state;
/**
* Listener for changes to {@link ZoneControlState}
*
* @author David Graeff - Initial contribution
*/
public interface ZoneControlStateListener {
void zoneStateChanged(ZoneControlState msg);
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="yamahareceiver" 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>YamahaReceiver Binding</name>
<description>For all network enabled Yamaha receivers.</description>
<author>David Graeff, Tomasz Maruszak</author>
</binding:binding>

File diff suppressed because one or more lines are too long