added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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("-", "_"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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' };
|
||||
}
|
||||
@@ -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'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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() + ">";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user