diff --git a/CODEOWNERS b/CODEOWNERS
index a3f827da5..4be0fa8c6 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -341,6 +341,7 @@
/bundles/org.openhab.binding.wolfsmartset/ @BoBiene
/bundles/org.openhab.binding.xmltv/ @clinique
/bundles/org.openhab.binding.xmppclient/ @pavel-gololobov
+/bundles/org.openhab.binding.yamahamusiccast/ @coop-git
/bundles/org.openhab.binding.yamahareceiver/ @davidgraeff @zarusz
/bundles/org.openhab.binding.yeelight/ @claell
/bundles/org.openhab.binding.yioremote/ @miloit
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 2a71e5b30..caa29bac5 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1696,6 +1696,11 @@
org.openhab.binding.xmppclient
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.yamahamusiccast
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.yamahareceiver
diff --git a/bundles/org.openhab.binding.yamahamusiccast/README.md b/bundles/org.openhab.binding.yamahamusiccast/README.md
new file mode 100644
index 000000000..874f4a9b7
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/README.md
@@ -0,0 +1,168 @@
+# Yamaha MusicCast Binding
+
+Binding to control Yamaha models via their MusicCast protocol (aka Yamaha Extended Control).
+With support for 4 zones : main, zone2, zone3, zone4. Main is always present. Zone2, Zone3, Zone4 are read from the model.
+
+UDP events are captured to reflect changes in the binding for
+
+- Power
+- Mute
+- Volume
+- Input
+- Presets
+- Sleep
+- Artist
+- Track
+- Album
+- Album Art
+- Repeat
+- Shuffle
+- Play Time
+- Total Time
+- Musiccast Link
+
+## Supported Things
+
+Each model (AV Receiver, ...) is a Thing (Thing Type ID: yamahamusiccast:device). Things are linked to a Bridge (Thing Type ID: yamahamusiccast:bridge) for receiving UDP events.
+
+## Discovery
+
+No auto discovery
+
+## Thing Configuration
+
+| Parameter | Type | Description | Advanced | Required |
+|--------------------|---------|---------------------------------------------------------|----------|---------------|
+| host | String | IP address of the Yamaha model (AVR, ...) | false | true |
+| syncVolume | Boolean | Sync volume across linked models (default=false) | false | false |
+| defaultAfterMCLink | String | Default Input value for client when MC Link is broken | false | false |
+
+Default value for *defaultAfterMCLink* is *NET RADIO* as most of the models have this on board.
+
+## Channels
+
+| channel | type | description |
+|----------------|--------|---------------------------------------------------------------------|
+| power | Switch | Power ON/OFF |
+| mute | Switch | Mute ON/OFF |
+| volume | Dimmer | Volume as % (recalculated based on Max Volume Model) |
+| volumeAbs | Number | Volume as absolute value |
+| input | String | See below for list |
+| soundProgram | String | See below for list |
+| selectPreset | String | Select Netradio/USB preset (fetched from Model) |
+| sleep | Number | Fixed values for Sleep : 0/30/60/90/120 in minutes |
+| recallScene | Number | Select a scene (8 defaults scenes are foreseen) |
+| player | Player | PLAY/PAUSE/NEXT/PREVIOUS/REWIND/FASTFORWARD |
+| artist | String | Artist |
+| track | String | Track |
+| album | String | Album |
+| albumArt | Image | Album Art |
+| repeat | String | Toggle Repeat. Available values: Off, One, All |
+| shuffle | String | Toggle Shuffle. Available values: Off, On, Songs, Album |
+| playTime | String | Play time of current selection: radio, song, track, ... |
+| totalTime | String | Total time of current selection: radio, song, track, ... |
+| mclinkStatus | String | Select your Musiccast Server or set to Standalone, Server or Client |
+
+
+| Zones | description |
+|----------------------|------------------------------------------------------|
+| zone1-4 | Zone 1 to 4 to control Power, Volume, ... |
+| playerControls | Separate zone for Play, Pause, ... |
+
+## Input List
+
+Firmware v1
+
+cd / tuner / multi_ch / phono / hdmi1 / hdmi2 / hdmi3 / hdmi4 / hdmi5 / hdmi6 / hdmi7 /
+hdmi8 / hdmi / av1 / av2 / av3 / av4 / av5 / av6 / av7 / v_aux / aux1 / aux2 / aux / audio1 /
+audio2 / audio3 / audio4 / audio_cd / audio / optical1 / optical2 / optical / coaxial1 / coaxial2 /
+coaxial / digital1 / digital2 / digital / line1 / line2 / line3 / line_cd / analog / tv / bd_dvd /
+usb_dac / usb / bluetooth / server / net_radio / rhapsody / napster / pandora / siriusxm /
+spotify / juke / airplay / radiko / qobuz / mc_link / main_sync / none
+
+Firmware v2
+
+cd / tuner / multi_ch / phono / hdmi1 / hdmi2 / hdmi3 / hdmi4 / hdmi5 / hdmi6 / hdmi7 /
+hdmi8 / hdmi / av1 / av2 / av3 / av4 / av5 / av6 / av7 / v_aux / aux1 / aux2 / aux / audio1 /
+audio2 / audio3 / audio4 / **audio5** / audio_cd / audio / optical1 / optical2 / optical / coaxial1 / coaxial2 /
+coaxial / digital1 / digital2 / digital / line1 / line2 / line3 / line_cd / analog / tv / bd_dvd /
+usb_dac / usb / bluetooth / server / net_radio / ~~rhapsody~~ /napster / pandora / siriusxm /
+spotify / juke / airplay / radiko / qobuz / **tidal** / **deezer** / mc_link / main_sync / none
+
+## Sound Program
+
+munich_a / munich_b / munich / frankfurt / stuttgart / vienna / amsterdam / usa_a / usa_b /
+tokyo / freiburg / royaumont / chamber / concert / village_gate / village_vanguard /
+warehouse_loft / cellar_club / jazz_club / roxy_theatre / bottom_line / arena / sports /
+action_game / roleplaying_game / game / music_video / music / recital_opera / pavilion /
+disco / standard / spectacle / sci-fi / adventure / drama / talk_show / tv_program /
+mono_movie / movie / enhanced / 2ch_stereo / 5ch_stereo / 7ch_stereo / 9ch_stereo /
+11ch_stereo / stereo / surr_decoder / my_surround / target / straight / off
+
+## Full Example
+
+### Bridge & Thing(s)
+
+```
+Bridge yamahamusiccast:bridge:virtual "YXC Bridge" {
+Thing yamahamusiccast:device:Living "YXC Living" [host="1.2.3.4"]
+}
+```
+
+### Basic setup
+
+```
+Switch YamahaPower "" {channel="yamahamusiccast:device:Living:main#power"}
+Switch YamahaMute "" {channel="yamahamusiccast:device:Living:main#mute"}
+Dimmer YamahaVolume "" {channel="yamahamusiccast:device:Living:main#volume"}
+Number YamahaVolumeAbs "" {channel="yamahamusiccast:device:Living:main#volumeAbs"}
+String YamahaInput "" {channel="yamahamusiccast:device:Living:main#input"}
+String YamahaSelectPreset "" {channel="yamahamusiccast:device:Living:main#selectPreset"}
+String YamahaSoundProgram "" {channel="yamahamusiccast:device:Living:main#soundProgram"}
+```
+
+### Player controls
+
+```
+Player YamahaPlayer "" {channel="yamahamusiccast:device:Living:playerControls#player"}
+String YamahaArt "" {channel="yamahamusiccast:device:Living:playerControls#albumArt"}
+String YamahaArtist "" {channel="yamahamusiccast:device:Living:playerControls#artist"}
+String YamahaTrack "" {channel="yamahamusiccast:device:Living:playerControls#track"}
+String YamahaAlbum "" {channel="yamahamusiccast:device:Living:playerControls#album"}
+```
+
+### MusicCast setup
+
+The idea here is to select what device/model will be the master. This needs to be done per device/model which will then be the slave.
+If you want the *Living* to be the master for the *Kitchen*, select *Living - zone (IP)* from the thing *Kitchen*.
+The binding will check if there is already a group active for which *Living* is the master. If yes, this group will be used and *Kitchen* will be added.
+If not, a new group will be created.
+
+*Device A*: Living with IP 192.168.1.1
+*Device B*: Kitchen with IP 192.168.1.2
+
+Set **mclinkStatus** to *Standalone* to remove the device/model from the current active group. The group will keep on exist with other devices/models.
+If the device/model is the server, the group will be disbanded.
+
+```
+String YamahaMCLinkStatus "" {channel="yamahamusiccast:device:Living:main#mclinkStatus"}
+```
+
+During testing with the Yamaha Musiccast app, when removing a slave from the group, the status of the client remained *client* and **input** stayed on *mclink*. Only when changing input, the slave was set to *standalone*. Therefor you can set the parameter **defaultAfterMCLink** to an input value supported by your device to break the whole Musiccast Link in OH.
+
+#### How to use this in a rule?
+
+The label uses the format _Thinglabel - zone (IP)_.
+The value which is sent to OH uses the format _IP***zone_.
+
+```
+sendCommand(Kitchen_YamahaMCServer, "192.168.1.1***main")
+sendCommand(Kitchen_YamahaMCServer, "")
+sendCommand(Kitchen_YamahaMCServer, "server")
+sendCommand(Kitchen_YamahaMCServer, "client")
+```
+
+## Tested Models
+
+RX-D485 / WX-010 / WX-030 / ISX-80 / YSP-1600 / RX-A860 / R-N303D / EX-A1080 / WXA-050 / HTR-4068 (RX-V479)
+MusicCast 20 / WCX-50 / RX-V6A / YAS-306 / ISX-18D / WX-021 / YAS-408
diff --git a/bundles/org.openhab.binding.yamahamusiccast/pom.xml b/bundles/org.openhab.binding.yamahamusiccast/pom.xml
new file mode 100644
index 000000000..d9c412be0
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.3.0-SNAPSHOT
+
+
+ org.openhab.binding.yamahamusiccast
+
+ openHAB Add-ons :: Bundles :: Yamaha Musiccast Binding
+
+
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/feature/feature.xml b/bundles/org.openhab.binding.yamahamusiccast/src/main/feature/feature.xml
new file mode 100644
index 000000000..0486c448d
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-upnp
+ mvn:org.openhab.addons.bundles/org.openhab.binding.yamahamusiccast/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBindingConstants.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBindingConstants.java
new file mode 100644
index 000000000..ea002a354
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBindingConstants.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+/**
+ * The {@link YamahaMusiccastBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+@NonNullByDefault
+public class YamahaMusiccastBindingConstants {
+
+ private static final String BINDING_ID = "yamahamusiccast";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_DEVICE = new ThingTypeUID(BINDING_ID, "device");
+ public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
+
+ // List of all Channel Type UIDs
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_POWER = new ChannelTypeUID("system:power");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_MUTE = new ChannelTypeUID("system:mute");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_VOLUME = new ChannelTypeUID("system:volume");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_VOLUMEABS = new ChannelTypeUID(BINDING_ID, "volumeAbs");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_INPUT = new ChannelTypeUID(BINDING_ID, "input");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_SOUNDPROGRAM = new ChannelTypeUID(BINDING_ID, "soundProgram");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_SELECTPRESET = new ChannelTypeUID(BINDING_ID, "selectPreset");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_SLEEP = new ChannelTypeUID(BINDING_ID, "sleep");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_RECALLSCENE = new ChannelTypeUID(BINDING_ID, "recallScene");
+ public static final ChannelTypeUID CHANNEL_TYPE_UID_MCLINKSTATUS = new ChannelTypeUID(BINDING_ID, "mclinkStatus");
+
+ // List of all Channel ids
+ public static final String CHANNEL_POWER = "power";
+ public static final String CHANNEL_MUTE = "mute";
+ public static final String CHANNEL_VOLUME = "volume";
+ public static final String CHANNEL_VOLUMEABS = "volumeAbs";
+ public static final String CHANNEL_INPUT = "input";
+ public static final String CHANNEL_SOUNDPROGRAM = "soundProgram";
+ public static final String CHANNEL_SELECTPRESET = "selectPreset";
+ public static final String CHANNEL_PLAYER = "player";
+ public static final String CHANNEL_SLEEP = "sleep";
+ public static final String CHANNEL_RECALLSCENE = "recallScene";
+ public static final String CHANNEL_ARTIST = "artist";
+ public static final String CHANNEL_TRACK = "track";
+ public static final String CHANNEL_ALBUM = "album";
+ public static final String CHANNEL_ALBUMART = "albumArt";
+ public static final String CHANNEL_REPEAT = "repeat";
+ public static final String CHANNEL_SHUFFLE = "shuffle";
+ public static final String CHANNEL_MCLINKSTATUS = "mclinkStatus";
+ public static final String CHANNEL_PLAYTIME = "playTime";
+ public static final String CHANNEL_TOTALTIME = "totalTime";
+
+ public static final int CONNECTION_TIMEOUT_MILLISEC = 5000;
+ public static final int LONG_CONNECTION_TIMEOUT_MILLISEC = 60000;
+ public static final String HTTP = "http://";
+ public static final String YAMAHA_EXTENDED_CONTROL = "/YamahaExtendedControl/v1/";
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBridgeHandler.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBridgeHandler.java
new file mode 100644
index 000000000..54e80506c
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBridgeHandler.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.yamahamusiccast.internal.dto.UdpMessage;
+import org.openhab.core.common.NamedThreadFactory;
+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.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link YamahaMusiccastBridgeHandler} is responsible for dispatching UDP events to linked Things.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+@NonNullByDefault
+public class YamahaMusiccastBridgeHandler extends BaseBridgeHandler {
+ private Gson gson = new Gson();
+ private final Logger logger = LoggerFactory.getLogger(YamahaMusiccastBridgeHandler.class);
+ private String threadname = getThing().getUID().getAsString();
+ private @Nullable ExecutorService executor;
+ private @Nullable Future> eventListenerJob;
+ private static final int UDP_PORT = 41100;
+ private static final int SOCKET_TIMEOUT_MILLISECONDS = 3000;
+ private static final int BUFFER_SIZE = 5120;
+ private @Nullable DatagramSocket socket;
+
+ private void receivePackets() {
+ try {
+ DatagramSocket s = new DatagramSocket(null);
+ s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
+ s.setReuseAddress(true);
+ InetSocketAddress address = new InetSocketAddress(UDP_PORT);
+ s.bind(address);
+ socket = s;
+ logger.trace("UDP Listener got socket on port {} with timeout {}", UDP_PORT, SOCKET_TIMEOUT_MILLISECONDS);
+ } catch (SocketException e) {
+ logger.trace("UDP Listener got SocketException: {}", e.getMessage(), e);
+ socket = null;
+ return;
+ }
+
+ DatagramPacket packet = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
+ DatagramSocket localSocket = socket;
+ while (localSocket != null) {
+ try {
+ localSocket.receive(packet);
+ String received = new String(packet.getData(), 0, packet.getLength());
+ String trackingID = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
+ logger.trace("Received packet: {} (Tracking: {})", received, trackingID);
+ handleUDPEvent(received, trackingID);
+ } catch (SocketTimeoutException e) {
+ // Nothing to do on socket timeout
+ } catch (IOException e) {
+ logger.trace("UDP Listener got IOException waiting for datagram: {}", e.getMessage());
+ localSocket = null;
+ }
+ }
+ logger.trace("UDP Listener exiting");
+ }
+
+ public YamahaMusiccastBridgeHandler(Bridge bridge) {
+ super(bridge);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ @Override
+ public void initialize() {
+ updateStatus(ThingStatus.ONLINE);
+ executor = Executors.newSingleThreadExecutor(new NamedThreadFactory(threadname));
+ Future> localEventListenerJob = eventListenerJob;
+ ExecutorService localExecutor = executor;
+ if (localEventListenerJob == null || localEventListenerJob.isCancelled()) {
+ if (localExecutor != null) {
+ localEventListenerJob = localExecutor.submit(this::receivePackets);
+ }
+ }
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ Future> localEventListenerJob = eventListenerJob;
+ ExecutorService localExecutor = executor;
+ if (localEventListenerJob != null) {
+ localEventListenerJob.cancel(true);
+ localEventListenerJob = null;
+ }
+ if (localExecutor != null) {
+ localExecutor.shutdownNow();
+ localExecutor = null;
+ }
+ }
+
+ public void handleUDPEvent(String json, String trackingID) {
+ String udpDeviceId = "";
+ Bridge bridge = (Bridge) thing;
+ for (Thing thing : bridge.getThings()) {
+ ThingStatusInfo statusInfo = thing.getStatusInfo();
+ switch (statusInfo.getStatus()) {
+ case ONLINE:
+ logger.trace("Thing Status: ONLINE - {}", thing.getLabel());
+ YamahaMusiccastHandler handler = (YamahaMusiccastHandler) thing.getHandler();
+ if (handler != null) {
+ logger.trace("UDP: {} - {} ({} - Tracking: {})", json, handler.getDeviceId(), thing.getLabel(),
+ trackingID);
+
+ @Nullable
+ UdpMessage targetObject = gson.fromJson(json, UdpMessage.class);
+ if (targetObject != null) {
+ udpDeviceId = targetObject.getDeviceId();
+ if (udpDeviceId.equals(handler.getDeviceId())) {
+ handler.processUDPEvent(json, trackingID);
+ }
+ }
+ }
+ break;
+ default:
+ logger.trace("Thing Status: NOT ONLINE - {} (Tracking: {})", thing.getLabel(), trackingID);
+ break;
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastConfiguration.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastConfiguration.java
new file mode 100644
index 000000000..79f63279a
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastConfiguration.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link YamahaMusiccastConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+@NonNullByDefault
+public class YamahaMusiccastConfiguration {
+
+ public @Nullable String host;
+ public @Nullable Boolean syncVolume;
+ public @Nullable String defaultAfterMCLink;
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandler.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandler.java
new file mode 100644
index 000000000..c5feae1e3
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandler.java
@@ -0,0 +1,1244 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal;
+
+import static org.openhab.binding.yamahamusiccast.internal.YamahaMusiccastBindingConstants.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.yamahamusiccast.internal.dto.DeviceInfo;
+import org.openhab.binding.yamahamusiccast.internal.dto.DistributionInfo;
+import org.openhab.binding.yamahamusiccast.internal.dto.Features;
+import org.openhab.binding.yamahamusiccast.internal.dto.PlayInfo;
+import org.openhab.binding.yamahamusiccast.internal.dto.PresetInfo;
+import org.openhab.binding.yamahamusiccast.internal.dto.RecentInfo;
+import org.openhab.binding.yamahamusiccast.internal.dto.Response;
+import org.openhab.binding.yamahamusiccast.internal.dto.Status;
+import org.openhab.binding.yamahamusiccast.internal.dto.UdpMessage;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.openhab.core.library.types.DecimalType;
+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.RewindFastforwardType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.StateOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link YamahaMusiccastHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+@NonNullByDefault
+public class YamahaMusiccastHandler extends BaseThingHandler {
+ private Gson gson = new Gson();
+ private Logger logger = LoggerFactory.getLogger(YamahaMusiccastHandler.class);
+ private @Nullable ScheduledFuture> generalHousekeepingTask;
+ private @Nullable String httpResponse;
+ private @Nullable String tmpString = "";
+ private int volumePercent = 0;
+ private int volumeAbsValue = 0;
+ private @Nullable String responseCode = "";
+ private int volumeState = 0;
+ private int maxVolumeState = 0;
+ private @Nullable String inputState = "";
+ private @Nullable String soundProgramState = "";
+ private int sleepState = 0;
+ private @Nullable String artistState = "";
+ private @Nullable String trackState = "";
+ private @Nullable String albumState = "";
+ private @Nullable String repeatState = "";
+ private @Nullable String shuffleState = "";
+ private int playTimeState = 0;
+ private int totalTimeState = 0;
+ private @Nullable String zone = "main";
+ private String channelWithoutGroup = "";
+ private @Nullable String thingLabel = "";
+ private @Nullable String mclinkSetupServer = "";
+ private @Nullable String mclinkSetupZone = "";
+ private String url = "";
+ private String json = "";
+ private String action = "";
+ private int zoneNum = 0;
+ private @Nullable String groupId = "";
+ private @Nullable String host;
+ public @Nullable String deviceId = "";
+
+ private YamahaMusiccastStateDescriptionProvider stateDescriptionProvider;
+
+ public YamahaMusiccastHandler(Thing thing, YamahaMusiccastStateDescriptionProvider stateDescriptionProvider) {
+ super(thing);
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ String localValueToCheck = "";
+ String localRole = "";
+ boolean localSyncVolume;
+ String localDefaultAfterMCLink = "";
+ String localRoleSelectedThing = "";
+ if (command != RefreshType.REFRESH) {
+ logger.trace("Handling command {} for channel {}", command, channelUID);
+ channelWithoutGroup = channelUID.getIdWithoutGroup();
+ zone = channelUID.getGroupId();
+ DistributionInfo distributioninfo = new DistributionInfo();
+ Response response = new Response();
+ switch (channelWithoutGroup) {
+ case CHANNEL_POWER:
+ if (command == OnOffType.ON) {
+ httpResponse = setPower("on", zone, this.host);
+ response = gson.fromJson(httpResponse, Response.class);
+ if (response != null) {
+ localValueToCheck = response.getResponseCode();
+ if (!"0".equals(localValueToCheck)) {
+ updateState(channelUID, OnOffType.OFF);
+ }
+ }
+ // check on scheduler task for UDP events
+ ScheduledFuture> localGeneralHousekeepingTask = generalHousekeepingTask;
+ if (localGeneralHousekeepingTask == null) {
+ logger.trace("YXC - No scheduler task found!");
+ generalHousekeepingTask = scheduler.scheduleWithFixedDelay(this::generalHousekeeping, 5,
+ 300, TimeUnit.SECONDS);
+
+ } else {
+ logger.trace("Scheduler task found!");
+ }
+
+ } else if (command == OnOffType.OFF) {
+ httpResponse = setPower("standby", zone, this.host);
+ response = gson.fromJson(httpResponse, Response.class);
+ powerOffCleanup();
+ if (response != null) {
+ localValueToCheck = response.getResponseCode();
+ if (!"0".equals(localValueToCheck)) {
+ updateState(channelUID, OnOffType.ON);
+ }
+ }
+ }
+ break;
+ case CHANNEL_MUTE:
+ if (command == OnOffType.ON) {
+ httpResponse = setMute("true", zone, this.host);
+ response = gson.fromJson(httpResponse, Response.class);
+ if (response != null) {
+ localValueToCheck = response.getResponseCode();
+ if (!"0".equals(localValueToCheck)) {
+ updateState(channelUID, OnOffType.OFF);
+ }
+ }
+ } else if (command == OnOffType.OFF) {
+ httpResponse = setMute("false", zone, this.host);
+ response = gson.fromJson(httpResponse, Response.class);
+ if (response != null) {
+ localValueToCheck = response.getResponseCode();
+ if (!"0".equals(localValueToCheck)) {
+ updateState(channelUID, OnOffType.ON);
+ }
+ }
+ }
+ break;
+ case CHANNEL_VOLUME:
+ volumePercent = Integer.parseInt(command.toString().replace(".0", ""));
+ volumeAbsValue = (maxVolumeState * volumePercent) / 100;
+ setVolume(volumeAbsValue, zone, this.host);
+ localSyncVolume = Boolean.parseBoolean(getThing().getConfiguration().get("syncVolume").toString());
+ if (localSyncVolume == Boolean.TRUE) {
+ tmpString = getDistributionInfo(this.host);
+ distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
+ if (distributioninfo != null) {
+ localRole = distributioninfo.getRole();
+ if ("server".equals(localRole)) {
+ for (JsonElement ip : distributioninfo.getClientList()) {
+ JsonObject clientObject = ip.getAsJsonObject();
+ setVolumeLinkedDevice(volumePercent, zone,
+ clientObject.get("ip_address").getAsString());
+ }
+ }
+ }
+ } // END config.syncVolume
+ break;
+ case CHANNEL_VOLUMEABS:
+ volumeAbsValue = Integer.parseInt(command.toString().replace(".0", ""));
+ volumePercent = (volumeAbsValue / maxVolumeState) * 100;
+ setVolume(volumeAbsValue, zone, this.host);
+ localSyncVolume = Boolean.parseBoolean(getThing().getConfiguration().get("syncVolume").toString());
+ if (localSyncVolume == Boolean.TRUE) {
+ tmpString = getDistributionInfo(this.host);
+ distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
+ if (distributioninfo != null) {
+ localRole = distributioninfo.getRole();
+ if ("server".equals(localRole)) {
+ for (JsonElement ip : distributioninfo.getClientList()) {
+ JsonObject clientObject = ip.getAsJsonObject();
+ setVolumeLinkedDevice(volumePercent, zone,
+ clientObject.get("ip_address").getAsString());
+ }
+ }
+ }
+ }
+ break;
+ case CHANNEL_INPUT:
+ // if it is a client, disconnect it first.
+ tmpString = getDistributionInfo(this.host);
+ distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
+ if (distributioninfo != null) {
+ localRole = distributioninfo.getRole();
+ if ("client".equals(localRole)) {
+ json = "{\"group_id\":\"\"}";
+ httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
+ }
+ }
+ setInput(command.toString(), zone, this.host);
+ break;
+ case CHANNEL_SOUNDPROGRAM:
+ setSoundProgram(command.toString(), zone, this.host);
+ break;
+ case CHANNEL_SELECTPRESET:
+ setPreset(command.toString(), zone, this.host);
+ break;
+ case CHANNEL_PLAYER:
+ if (command.equals(PlayPauseType.PLAY)) {
+ setPlayback("play", this.host);
+ } else if (command.equals(PlayPauseType.PAUSE)) {
+ setPlayback("pause", this.host);
+ } else if (command.equals(NextPreviousType.NEXT)) {
+ setPlayback("next", this.host);
+ } else if (command.equals(NextPreviousType.PREVIOUS)) {
+ setPlayback("previous", this.host);
+ } else if (command.equals(RewindFastforwardType.REWIND)) {
+ setPlayback("fast_reverse_start", this.host);
+ } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
+ setPlayback("fast_forward_end", this.host);
+ }
+ break;
+ case CHANNEL_SLEEP:
+ setSleep(command.toString(), zone, this.host);
+ break;
+ case CHANNEL_MCLINKSTATUS:
+ action = "";
+ json = "";
+ tmpString = getDistributionInfo(this.host);
+ distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
+ if (distributioninfo != null) {
+ responseCode = distributioninfo.getResponseCode();
+ localRole = distributioninfo.getRole();
+ if (command.toString().equals("")) {
+ action = "unlink";
+ groupId = distributioninfo.getGroupId();
+ } else if (command.toString().contains("***")) {
+ action = "link";
+ String[] parts = command.toString().split("\\*\\*\\*");
+ if (parts.length > 1) {
+ mclinkSetupServer = parts[0];
+ mclinkSetupZone = parts[1];
+ tmpString = getDistributionInfo(mclinkSetupServer);
+ distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
+ if (distributioninfo != null) {
+ responseCode = distributioninfo.getResponseCode();
+ localRoleSelectedThing = distributioninfo.getRole();
+ groupId = distributioninfo.getGroupId();
+ if (localRoleSelectedThing != null) {
+ if ("server".equals(localRoleSelectedThing)) {
+ groupId = distributioninfo.getGroupId();
+ } else if ("client".equals(localRoleSelectedThing)) {
+ groupId = "";
+ } else if ("none".equals(localRoleSelectedThing)) {
+ groupId = generateGroupId();
+ }
+ }
+ }
+ }
+ }
+
+ if ("unlink".equals(action)) {
+ json = "{\"group_id\":\"\"}";
+ if (localRole != null) {
+ if ("server".equals(localRole)) {
+ httpResponse = setClientServerInfo(this.host, json, "setServerInfo");
+ // Set GroupId = "" for linked clients
+ if (distributioninfo != null) {
+ for (JsonElement ip : distributioninfo.getClientList()) {
+ JsonObject clientObject = ip.getAsJsonObject();
+ setClientServerInfo(clientObject.get("ip_address").getAsString(), json,
+ "setClientInfo");
+ }
+ }
+ } else if ("client".equals(localRole)) {
+ mclinkSetupServer = connectedServer();
+ // Step 1. empty group on client
+ httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
+ // empty zone to respect defaults
+ if (!"".equals(mclinkSetupServer)) {
+ // Step 2. remove client from server
+ json = "{\"group_id\":\"" + groupId
+ + "\", \"type\":\"remove\", \"client_list\":[\"" + this.host + "\"]}";
+ httpResponse = setClientServerInfo(mclinkSetupServer, json, "setServerInfo");
+ // Step 3. reflect changes to master
+ httpResponse = startDistribution(mclinkSetupServer);
+ localDefaultAfterMCLink = getThing().getConfiguration()
+ .get("defaultAfterMCLink").toString();
+ httpResponse = setInput(localDefaultAfterMCLink.toString(), zone, this.host);
+ } else if ("".equals(mclinkSetupServer)) {
+ // fallback in case client is removed from group by ending group on server side
+ localDefaultAfterMCLink = getThing().getConfiguration()
+ .get("defaultAfterMCLink").toString();
+ httpResponse = setInput(localDefaultAfterMCLink.toString(), zone, this.host);
+ }
+ }
+ }
+ } else if ("link".equals(action)) {
+ if (localRole != null) {
+ if ("none".equals(localRole)) {
+ json = "{\"group_id\":\"" + groupId + "\", \"zone\":\"" + mclinkSetupZone
+ + "\", \"type\":\"add\", \"client_list\":[\"" + this.host + "\"]}";
+ logger.trace("setServerInfo json: {}", json);
+ httpResponse = setClientServerInfo(mclinkSetupServer, json, "setServerInfo");
+ // All zones of Model are required for MC Link
+ tmpString = "";
+ for (int i = 1; i <= zoneNum; i++) {
+ switch (i) {
+ case 1:
+ tmpString = "\"main\"";
+ break;
+ case 2:
+ tmpString = tmpString + ", \"zone2\"";
+ break;
+ case 3:
+ tmpString = tmpString + ", \"zone3\"";
+ break;
+ case 4:
+ tmpString = tmpString + ", \"zone4\"";
+ break;
+ }
+ }
+ json = "{\"group_id\":\"" + groupId + "\", \"zone\":[" + tmpString + "]}";
+ logger.trace("setClientInfo json: {}", json);
+ httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
+ httpResponse = startDistribution(mclinkSetupServer);
+ }
+ }
+ }
+ }
+ updateMCLinkStatus();
+ break;
+ case CHANNEL_RECALLSCENE:
+ recallScene(command.toString(), zone, this.host);
+ break;
+ case CHANNEL_REPEAT:
+ setRepeat(command.toString(), this.host);
+ break;
+ case CHANNEL_SHUFFLE:
+ setShuffle(command.toString(), this.host);
+ break;
+ } // END Switch Channel
+ }
+ }
+
+ @Override
+ public void initialize() {
+ String localHost = "";
+ thingLabel = thing.getLabel();
+ updateStatus(ThingStatus.UNKNOWN);
+ localHost = getThing().getConfiguration().get("host").toString();
+ this.host = localHost;
+ if (!"".equals(this.host)) {
+ zoneNum = getNumberOfZones(this.host);
+ logger.trace("Zones found: {} - {}", zoneNum, thingLabel);
+
+ if (zoneNum > 0) {
+ refreshOnStartup();
+ generalHousekeepingTask = scheduler.scheduleWithFixedDelay(this::generalHousekeeping, 5, 300,
+ TimeUnit.SECONDS);
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No host found");
+ }
+ }
+ }
+
+ private void generalHousekeeping() {
+ thingLabel = thing.getLabel();
+ logger.trace("YXC - Start Keep Alive UDP events (5 minutes - {}) ", thingLabel);
+ keepUdpEventsAlive(this.host);
+ fillOptionsForMCLink();
+ updateMCLinkStatus();
+ }
+
+ private void refreshOnStartup() {
+ for (int i = 1; i <= zoneNum; i++) {
+ switch (i) {
+ case 1:
+ createChannels("main");
+ updateStatusZone("main");
+ break;
+ case 2:
+ createChannels("zone2");
+ updateStatusZone("zone2");
+ break;
+ case 3:
+ createChannels("zone3");
+ updateStatusZone("zone3");
+ break;
+ case 4:
+ createChannels("zone4");
+ updateStatusZone("zone4");
+ break;
+ }
+ }
+ updatePresets(0);
+ updateNetUSBPlayer();
+ fillOptionsForMCLink();
+ updateMCLinkStatus();
+ }
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> localGeneralHousekeepingTask = generalHousekeepingTask;
+ if (localGeneralHousekeepingTask != null) {
+ localGeneralHousekeepingTask.cancel(true);
+ }
+ }
+
+ // Various functions
+
+ private void createChannels(String zone) {
+ createChannel(zone, CHANNEL_POWER, CHANNEL_TYPE_UID_POWER, "Switch");
+ createChannel(zone, CHANNEL_MUTE, CHANNEL_TYPE_UID_MUTE, "Switch");
+ createChannel(zone, CHANNEL_VOLUME, CHANNEL_TYPE_UID_VOLUME, "Dimmer");
+ createChannel(zone, CHANNEL_VOLUMEABS, CHANNEL_TYPE_UID_VOLUMEABS, "Number");
+ createChannel(zone, CHANNEL_INPUT, CHANNEL_TYPE_UID_INPUT, "String");
+ createChannel(zone, CHANNEL_SOUNDPROGRAM, CHANNEL_TYPE_UID_SOUNDPROGRAM, "String");
+ createChannel(zone, CHANNEL_SLEEP, CHANNEL_TYPE_UID_SLEEP, "Number");
+ createChannel(zone, CHANNEL_SELECTPRESET, CHANNEL_TYPE_UID_SELECTPRESET, "String");
+ createChannel(zone, CHANNEL_RECALLSCENE, CHANNEL_TYPE_UID_RECALLSCENE, "Number");
+ createChannel(zone, CHANNEL_MCLINKSTATUS, CHANNEL_TYPE_UID_MCLINKSTATUS, "String");
+ }
+
+ private void createChannel(String zone, String channel, ChannelTypeUID channelTypeUID, String itemType) {
+ ChannelUID channelToCheck = new ChannelUID(thing.getUID(), zone, channel);
+ if (thing.getChannel(channelToCheck) == null) {
+ ThingBuilder thingBuilder = editThing();
+ Channel testchannel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), zone, channel), itemType)
+ .withType(channelTypeUID).build();
+ thingBuilder.withChannel(testchannel);
+ updateThing(thingBuilder.build());
+ }
+ }
+
+ private void powerOffCleanup() {
+ ChannelUID channel;
+ channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ARTIST);
+ updateState(channel, StringType.valueOf("-"));
+ channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TRACK);
+ updateState(channel, StringType.valueOf("-"));
+ channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUM);
+ updateState(channel, StringType.valueOf("-"));
+ }
+
+ public void processUDPEvent(String json, String trackingID) {
+ logger.trace("UDP package: {} (Tracking: {})", json, trackingID);
+ @Nullable
+ UdpMessage targetObject = gson.fromJson(json, UdpMessage.class);
+ if (targetObject != null) {
+ if (Objects.nonNull(targetObject.getMain())) {
+ updateStateFromUDPEvent("main", targetObject);
+ }
+ if (Objects.nonNull(targetObject.getZone2())) {
+ updateStateFromUDPEvent("zone2", targetObject);
+ }
+ if (Objects.nonNull(targetObject.getZone3())) {
+ updateStateFromUDPEvent("zone3", targetObject);
+ }
+ if (Objects.nonNull(targetObject.getZone4())) {
+ updateStateFromUDPEvent("zone4", targetObject);
+ }
+ if (Objects.nonNull(targetObject.getNetUSB())) {
+ updateStateFromUDPEvent("netusb", targetObject);
+ }
+ if (Objects.nonNull(targetObject.getDist())) {
+ updateStateFromUDPEvent("dist", targetObject);
+ }
+ }
+ }
+
+ private void updateStateFromUDPEvent(String zoneToUpdate, UdpMessage targetObject) {
+ ChannelUID channel;
+ String playInfoUpdated = "";
+ String statusUpdated = "";
+ String powerState = "";
+ String muteState = "";
+ String inputState = "";
+ int volumeState = 0;
+ int presetNumber = 0;
+ int playTime = 0;
+ String distInfoUpdated = "";
+ logger.trace("Handling UDP for {}", zoneToUpdate);
+ switch (zoneToUpdate) {
+ case "main":
+ powerState = targetObject.getMain().getPower();
+ muteState = targetObject.getMain().getMute();
+ inputState = targetObject.getMain().getInput();
+ volumeState = targetObject.getMain().getVolume();
+ statusUpdated = targetObject.getMain().getstatusUpdated();
+ break;
+ case "zone2":
+ powerState = targetObject.getZone2().getPower();
+ muteState = targetObject.getZone2().getMute();
+ inputState = targetObject.getZone2().getInput();
+ volumeState = targetObject.getZone2().getVolume();
+ statusUpdated = targetObject.getZone2().getstatusUpdated();
+ break;
+ case "zone3":
+ powerState = targetObject.getZone3().getPower();
+ muteState = targetObject.getZone3().getMute();
+ inputState = targetObject.getZone3().getInput();
+ volumeState = targetObject.getZone3().getVolume();
+ statusUpdated = targetObject.getZone3().getstatusUpdated();
+ break;
+ case "zone4":
+ powerState = targetObject.getZone4().getPower();
+ muteState = targetObject.getZone4().getMute();
+ inputState = targetObject.getZone4().getInput();
+ volumeState = targetObject.getZone4().getVolume();
+ statusUpdated = targetObject.getZone4().getstatusUpdated();
+ break;
+ case "netusb":
+ if (Objects.isNull(targetObject.getNetUSB().getPresetControl())) {
+ presetNumber = 0;
+ } else {
+ presetNumber = targetObject.getNetUSB().getPresetControl().getNum();
+ }
+ playInfoUpdated = targetObject.getNetUSB().getPlayInfoUpdated();
+ playTime = targetObject.getNetUSB().getPlayTime();
+ // totalTime is not in UDP event
+ break;
+ case "dist":
+ distInfoUpdated = targetObject.getDist().getDistInfoUpdated();
+ break;
+ }
+
+ if (!powerState.isEmpty()) {
+ channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_POWER);
+ if ("on".equals(powerState)) {
+ updateState(channel, OnOffType.ON);
+ } else if ("standby".equals(powerState)) {
+ updateState(channel, OnOffType.OFF);
+ powerOffCleanup();
+ }
+ }
+
+ if (!muteState.isEmpty()) {
+ channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_MUTE);
+ if ("true".equals(muteState)) {
+ updateState(channel, OnOffType.ON);
+ } else if ("false".equals(muteState)) {
+ updateState(channel, OnOffType.OFF);
+ }
+ }
+
+ if (!inputState.isEmpty()) {
+ channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_INPUT);
+ updateState(channel, StringType.valueOf(inputState));
+ }
+
+ if (volumeState != 0) {
+ channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_VOLUME);
+ updateState(channel, new PercentType((volumeState * 100) / maxVolumeState));
+ channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_VOLUMEABS);
+ updateState(channel, new DecimalType(volumeState));
+ }
+
+ if (presetNumber != 0) {
+ logger.trace("Preset detected: {}", presetNumber);
+ updatePresets(presetNumber);
+ }
+
+ if ("true".equals(playInfoUpdated)) {
+ updateNetUSBPlayer();
+ }
+
+ if (!statusUpdated.isEmpty()) {
+ updateStatusZone(zoneToUpdate);
+ }
+ if (playTime != 0) {
+ channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYTIME);
+ updateState(channel, StringType.valueOf(String.valueOf(playTime)));
+ }
+ if ("true".equals(distInfoUpdated)) {
+ updateMCLinkStatus();
+ }
+ }
+
+ private void updateStatusZone(String zoneToUpdate) {
+ String localZone = "";
+ tmpString = getStatus(this.host, zoneToUpdate);
+ @Nullable
+ Status targetObject = gson.fromJson(tmpString, Status.class);
+ if (targetObject != null) {
+ String responseCode = targetObject.getResponseCode();
+ String powerState = targetObject.getPower();
+ String muteState = targetObject.getMute();
+ volumeState = targetObject.getVolume();
+ maxVolumeState = targetObject.getMaxVolume();
+ inputState = targetObject.getInput();
+ soundProgramState = targetObject.getSoundProgram();
+ sleepState = targetObject.getSleep();
+
+ logger.trace("{} - Response: {}", zoneToUpdate, responseCode);
+ logger.trace("{} - Power: {}", zoneToUpdate, powerState);
+ logger.trace("{} - Mute: {}", zoneToUpdate, muteState);
+ logger.trace("{} - Volume: {}", zoneToUpdate, volumeState);
+ logger.trace("{} - Max Volume: {}", zoneToUpdate, maxVolumeState);
+ logger.trace("{} - Input: {}", zoneToUpdate, inputState);
+ logger.trace("{} - Soundprogram: {}", zoneToUpdate, soundProgramState);
+ logger.trace("{} - Sleep: {}", zoneToUpdate, sleepState);
+
+ switch (responseCode) {
+ case "0":
+ for (Channel channel : getThing().getChannels()) {
+ ChannelUID channelUID = channel.getUID();
+ channelWithoutGroup = channelUID.getIdWithoutGroup();
+ localZone = channelUID.getGroupId();
+ if (localZone != null) {
+ if (isLinked(channelUID)) {
+ switch (channelWithoutGroup) {
+ case CHANNEL_POWER:
+ if ("on".equals(powerState)) {
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, OnOffType.ON);
+ }
+ } else if ("standby".equals(powerState)) {
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, OnOffType.OFF);
+ }
+ }
+ break;
+ case CHANNEL_MUTE:
+ if ("true".equals(muteState)) {
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, OnOffType.ON);
+ }
+ } else if ("false".equals(muteState)) {
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, OnOffType.OFF);
+ }
+ }
+ break;
+ case CHANNEL_VOLUME:
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID,
+ new PercentType((volumeState * 100) / maxVolumeState));
+ }
+ break;
+ case CHANNEL_VOLUMEABS:
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, new DecimalType(volumeState));
+ }
+ break;
+ case CHANNEL_INPUT:
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, StringType.valueOf(inputState));
+ }
+ break;
+ case CHANNEL_SOUNDPROGRAM:
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, StringType.valueOf(soundProgramState));
+ }
+ break;
+ case CHANNEL_SLEEP:
+ if (localZone.equals(zoneToUpdate)) {
+ updateState(channelUID, new DecimalType(sleepState));
+ }
+ break;
+ } // END switch (channelWithoutGroup)
+ } // END IsLinked
+ }
+ }
+ break;
+ case "999":
+ logger.trace("Nothing to do! - {} ({})", thingLabel, zoneToUpdate);
+ break;
+ }
+ }
+ }
+
+ private void updatePresets(int value) {
+ String inputText = "";
+ int presetCounter = 0;
+ int currentPreset = 0;
+ tmpString = getPresetInfo(this.host);
+
+ PresetInfo presetinfo = gson.fromJson(tmpString, PresetInfo.class);
+ if (presetinfo != null) {
+ String responseCode = presetinfo.getResponseCode();
+ if ("0".equals(responseCode)) {
+ List optionsPresets = new ArrayList<>();
+ inputText = getLastInput();
+ if (inputText != null) {
+ for (JsonElement pr : presetinfo.getPresetInfo()) {
+ presetCounter = presetCounter + 1;
+ JsonObject presetObject = pr.getAsJsonObject();
+ String text = presetObject.get("text").getAsString();
+ if (!"".equals(text)) {
+ optionsPresets.add(new StateOption(String.valueOf(presetCounter),
+ "#" + String.valueOf(presetCounter) + " " + text));
+ if (inputText.equals(text)) {
+ currentPreset = presetCounter;
+ }
+ }
+ }
+ }
+ if (value != 0) {
+ currentPreset = value;
+ }
+ for (Channel channel : getThing().getChannels()) {
+ ChannelUID channelUID = channel.getUID();
+ channelWithoutGroup = channelUID.getIdWithoutGroup();
+ if (isLinked(channelUID)) {
+ switch (channelWithoutGroup) {
+ case CHANNEL_SELECTPRESET:
+ stateDescriptionProvider.setStateOptions(channelUID, optionsPresets);
+ updateState(channelUID, StringType.valueOf(String.valueOf(currentPreset)));
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void updateNetUSBPlayer() {
+ tmpString = getPlayInfo(this.host);
+
+ @Nullable
+ PlayInfo targetObject = gson.fromJson(tmpString, PlayInfo.class);
+ if (targetObject != null) {
+ String responseCode = targetObject.getResponseCode();
+ String playbackState = targetObject.getPlayback();
+ artistState = targetObject.getArtist();
+ trackState = targetObject.getTrack();
+ albumState = targetObject.getAlbum();
+ String albumArtUrlState = targetObject.getAlbumArtUrl();
+ repeatState = targetObject.getRepeat();
+ shuffleState = targetObject.getShuffle();
+ playTimeState = targetObject.getPlayTime();
+ totalTimeState = targetObject.getTotalTime();
+
+ if ("0".equals(responseCode)) {
+ ChannelUID testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYER);
+ switch (playbackState) {
+ case "play":
+ updateState(testchannel, PlayPauseType.PLAY);
+ break;
+ case "stop":
+ updateState(testchannel, PlayPauseType.PAUSE);
+ break;
+ case "pause":
+ updateState(testchannel, PlayPauseType.PAUSE);
+ break;
+ case "fast_reverse":
+ updateState(testchannel, RewindFastforwardType.REWIND);
+ break;
+ case "fast_forward":
+ updateState(testchannel, RewindFastforwardType.FASTFORWARD);
+ break;
+ }
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ARTIST);
+ updateState(testchannel, StringType.valueOf(artistState));
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TRACK);
+ updateState(testchannel, StringType.valueOf(trackState));
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUM);
+ updateState(testchannel, StringType.valueOf(albumState));
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUMART);
+ if (!"".equals(albumArtUrlState)) {
+ albumArtUrlState = HTTP + this.host + albumArtUrlState;
+ }
+ updateState(testchannel, StringType.valueOf(albumArtUrlState));
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_REPEAT);
+ updateState(testchannel, StringType.valueOf(repeatState));
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_SHUFFLE);
+ updateState(testchannel, StringType.valueOf(shuffleState));
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYTIME);
+ updateState(testchannel, StringType.valueOf(String.valueOf(playTimeState)));
+ testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TOTALTIME);
+ updateState(testchannel, StringType.valueOf(String.valueOf(totalTimeState)));
+ }
+ }
+ }
+
+ private @Nullable String getLastInput() {
+ String text = "";
+ tmpString = getRecentInfo(this.host);
+ RecentInfo recentinfo = gson.fromJson(tmpString, RecentInfo.class);
+ if (recentinfo != null) {
+ String responseCode = recentinfo.getResponseCode();
+ if ("0".equals(responseCode)) {
+ for (JsonElement ri : recentinfo.getRecentInfo()) {
+ JsonObject recentObject = ri.getAsJsonObject();
+ text = recentObject.get("text").getAsString();
+ break;
+ }
+ }
+ }
+ return text;
+ }
+
+ private String connectedServer() {
+ DistributionInfo distributioninfo = new DistributionInfo();
+ Bridge bridge = getBridge();
+ String remotehost = "";
+ String result = "";
+ String localHost = "";
+ if (bridge != null) {
+ for (Thing thing : bridge.getThings()) {
+ remotehost = thing.getConfiguration().get("host").toString();
+ tmpString = getDistributionInfo(remotehost);
+ distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
+ if (distributioninfo != null) {
+ String localRole = distributioninfo.getRole();
+ if ("server".equals(localRole)) {
+ for (JsonElement ip : distributioninfo.getClientList()) {
+ JsonObject clientObject = ip.getAsJsonObject();
+ localHost = getThing().getConfiguration().get("host").toString();
+ if (localHost.equals(clientObject.get("ip_address").getAsString())) {
+ result = remotehost;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ private void fillOptionsForMCLink() {
+ Bridge bridge = getBridge();
+ String host = "";
+ String label = "";
+ int zonesPerHost = 1;
+ int clients = 0;
+ tmpString = getDistributionInfo(this.host);
+ DistributionInfo targetObject = gson.fromJson(tmpString, DistributionInfo.class);
+ if (targetObject != null) {
+ clients = targetObject.getClientList().size();
+ }
+
+ List options = new ArrayList<>();
+ // first add 3 options for MC Link
+ options.add(new StateOption("", "Standalone"));
+ options.add(new StateOption("server", "Server: " + clients + " clients"));
+ options.add(new StateOption("client", "Client"));
+
+ if (bridge != null) {
+ for (Thing thing : bridge.getThings()) {
+ label = thing.getLabel();
+ host = thing.getConfiguration().get("host").toString();
+ logger.trace("Thing found on Bridge: {} - {}", label, host);
+ zonesPerHost = getNumberOfZones(host);
+ for (int i = 1; i <= zonesPerHost; i++) {
+ switch (i) {
+ case 1:
+ options.add(new StateOption(host + "***main", label + " - main (" + host + ")"));
+ break;
+ case 2:
+ options.add(new StateOption(host + "***zone2", label + " - zone2 (" + host + ")"));
+ break;
+ case 3:
+ options.add(new StateOption(host + "***zone3", label + " - zone3 (" + host + ")"));
+ break;
+ case 4:
+ options.add(new StateOption(host + "***zone4", label + " - zone4 (" + host + ")"));
+ break;
+ }
+ }
+
+ }
+ }
+ // for each zone of the device, set all the possible combinations
+ ChannelUID testchannel;
+ for (int i = 1; i <= zoneNum; i++) {
+ switch (i) {
+ case 1:
+ testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
+ if (isLinked(testchannel)) {
+ stateDescriptionProvider.setStateOptions(testchannel, options);
+ }
+ break;
+ case 2:
+ testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
+ if (isLinked(testchannel)) {
+ stateDescriptionProvider.setStateOptions(testchannel, options);
+ }
+ break;
+ case 3:
+ testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
+ if (isLinked(testchannel)) {
+ stateDescriptionProvider.setStateOptions(testchannel, options);
+ }
+ break;
+ case 4:
+ testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
+ if (isLinked(testchannel)) {
+ stateDescriptionProvider.setStateOptions(testchannel, options);
+ }
+ break;
+ }
+ }
+ }
+
+ private String generateGroupId() {
+ return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
+ }
+
+ private int getNumberOfZones(@Nullable String host) {
+ int numberOfZones = 0;
+ tmpString = getFeatures(host);
+ @Nullable
+ Features targetObject = gson.fromJson(tmpString, Features.class);
+ if (targetObject != null) {
+ responseCode = targetObject.getResponseCode();
+ if ("0".equals(responseCode)) {
+ numberOfZones = targetObject.getSystem().getZoneNum();
+ }
+ }
+ return numberOfZones;
+ }
+
+ public @Nullable String getDeviceId() {
+ tmpString = getDeviceInfo(this.host);
+ String localValueToCheck = "";
+ @Nullable
+ DeviceInfo targetObject = gson.fromJson(tmpString, DeviceInfo.class);
+ if (targetObject != null) {
+ localValueToCheck = targetObject.getDeviceId();
+ }
+ return localValueToCheck;
+ }
+
+ private void setVolumeLinkedDevice(int value, @Nullable String zone, String host) {
+ logger.trace("setVolumeLinkedDevice: {}", host);
+ int zoneNumLinkedDevice = getNumberOfZones(host);
+ int maxVolumeLinkedDevice = 0;
+ @Nullable
+ Status targetObject = new Status();
+ int newVolume = 0;
+ for (int i = 1; i <= zoneNumLinkedDevice; i++) {
+ switch (i) {
+ case 1:
+ tmpString = getStatus(host, "main");
+ targetObject = gson.fromJson(tmpString, Status.class);
+ if (targetObject != null) {
+ responseCode = targetObject.getResponseCode();
+ maxVolumeLinkedDevice = targetObject.getMaxVolume();
+ newVolume = maxVolumeLinkedDevice * value / 100;
+ setVolume(newVolume, "main", host);
+ }
+ break;
+ case 2:
+ tmpString = getStatus(host, "zone2");
+ targetObject = gson.fromJson(tmpString, Status.class);
+ if (targetObject != null) {
+ responseCode = targetObject.getResponseCode();
+ maxVolumeLinkedDevice = targetObject.getMaxVolume();
+ newVolume = maxVolumeLinkedDevice * value / 100;
+ setVolume(newVolume, "zone2", host);
+ }
+ break;
+ case 3:
+ tmpString = getStatus(host, "zone3");
+ targetObject = gson.fromJson(tmpString, Status.class);
+ if (targetObject != null) {
+ responseCode = targetObject.getResponseCode();
+ maxVolumeLinkedDevice = targetObject.getMaxVolume();
+ newVolume = maxVolumeLinkedDevice * value / 100;
+ setVolume(newVolume, "zone3", host);
+ }
+ break;
+ case 4:
+ tmpString = getStatus(host, "zone4");
+ targetObject = gson.fromJson(tmpString, Status.class);
+ if (targetObject != null) {
+ responseCode = targetObject.getResponseCode();
+ maxVolumeLinkedDevice = targetObject.getMaxVolume();
+ newVolume = maxVolumeLinkedDevice * value / 100;
+ setVolume(newVolume, "zone4", host);
+ }
+ break;
+ }
+ }
+ }
+
+ public void updateMCLinkStatus() {
+ tmpString = getDistributionInfo(this.host);
+ @Nullable
+ DistributionInfo targetObject = gson.fromJson(tmpString, DistributionInfo.class);
+ if (targetObject != null) {
+ String localRole = targetObject.getRole();
+ groupId = targetObject.getGroupId();
+ switch (localRole) {
+ case "none":
+ setMCLinkToStandalone();
+ break;
+ case "server":
+ setMCLinkToServer();
+ break;
+ case "client":
+ setMCLinkToClient();
+ break;
+ }
+ }
+ }
+
+ private void setMCLinkToStandalone() {
+ ChannelUID testchannel;
+ for (int i = 1; i <= zoneNum; i++) {
+ switch (i) {
+ case 1:
+ testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf(""));
+ break;
+ case 2:
+ testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf(""));
+ break;
+ case 3:
+ testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf(""));
+ break;
+ case 4:
+ testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf(""));
+ break;
+ }
+ }
+ }
+
+ private void setMCLinkToClient() {
+ ChannelUID testchannel;
+ for (int i = 1; i <= zoneNum; i++) {
+ switch (i) {
+ case 1:
+ testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("client"));
+ break;
+ case 2:
+ testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("client"));
+ break;
+ case 3:
+ testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("client"));
+ break;
+ case 4:
+ testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("client"));
+ break;
+ }
+ }
+ }
+
+ private void setMCLinkToServer() {
+ ChannelUID testchannel;
+ for (int i = 1; i <= zoneNum; i++) {
+ switch (i) {
+ case 1:
+ testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("server"));
+ break;
+ case 2:
+ testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("server"));
+ break;
+ case 3:
+ testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("server"));
+ break;
+ case 4:
+ testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
+ updateState(testchannel, StringType.valueOf("server"));
+ break;
+ }
+ }
+ }
+
+ private String makeRequest(@Nullable String topicAVR, String url) {
+ String response = "";
+ try {
+ response = HttpUtil.executeUrl("GET", HTTP + url, LONG_CONNECTION_TIMEOUT_MILLISEC);
+ logger.trace("{} - {}", topicAVR, response);
+ return response;
+ } catch (IOException e) {
+ logger.trace("IO Exception - {} - {}", topicAVR, e.getMessage());
+ return "{\"response_code\":\"999\"}";
+ }
+ }
+ // End Various functions
+
+ // API calls to AVR
+
+ // Start Zone Related
+
+ private @Nullable String getStatus(@Nullable String host, String zone) {
+ return makeRequest("Status", host + YAMAHA_EXTENDED_CONTROL + zone + "/getStatus");
+ }
+
+ private @Nullable String setPower(String value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("Power", host + YAMAHA_EXTENDED_CONTROL + zone + "/setPower?power=" + value);
+ }
+
+ private @Nullable String setMute(String value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("Mute", host + YAMAHA_EXTENDED_CONTROL + zone + "/setMute?enable=" + value);
+ }
+
+ private @Nullable String setVolume(int value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("Volume", host + YAMAHA_EXTENDED_CONTROL + zone + "/setVolume?volume=" + value);
+ }
+
+ private @Nullable String setInput(String value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("setInput", host + YAMAHA_EXTENDED_CONTROL + zone + "/setInput?input=" + value);
+ }
+
+ private @Nullable String setSoundProgram(String value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("setSoundProgram",
+ host + YAMAHA_EXTENDED_CONTROL + zone + "/setSoundProgram?program=" + value);
+ }
+
+ private @Nullable String setPreset(String value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("setPreset",
+ host + YAMAHA_EXTENDED_CONTROL + "netusb/recallPreset?zone=" + zone + "&num=" + value);
+ }
+
+ private @Nullable String setSleep(String value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("setSleep", host + YAMAHA_EXTENDED_CONTROL + zone + "/setSleep?sleep=" + value);
+ }
+
+ private @Nullable String recallScene(String value, @Nullable String zone, @Nullable String host) {
+ return makeRequest("recallScene", host + YAMAHA_EXTENDED_CONTROL + zone + "/recallScene?num=" + value);
+ }
+ // End Zone Related
+
+ // Start Net Radio/USB Related
+
+ private @Nullable String getPresetInfo(@Nullable String host) {
+ return makeRequest("PresetInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getPresetInfo");
+ }
+
+ private @Nullable String getRecentInfo(@Nullable String host) {
+ return makeRequest("RecentInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getRecentInfo");
+ }
+
+ private @Nullable String getPlayInfo(@Nullable String host) {
+ return makeRequest("PlayInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getPlayInfo");
+ }
+
+ private @Nullable String setPlayback(String value, @Nullable String host) {
+ return makeRequest("Playback", host + YAMAHA_EXTENDED_CONTROL + "netusb/setPlayback?playback=" + value);
+ }
+
+ private @Nullable String setRepeat(String value, @Nullable String host) {
+ return makeRequest("Repeat", host + YAMAHA_EXTENDED_CONTROL + "netusb/setRepeat?mode=" + value);
+ }
+
+ private @Nullable String setShuffle(String value, @Nullable String host) {
+ return makeRequest("Shuffle", host + YAMAHA_EXTENDED_CONTROL + "netusb/setShuffle?mode=" + value);
+ }
+
+ // End Net Radio/USB Related
+
+ // Start Music Cast API calls
+ private @Nullable String getDistributionInfo(@Nullable String host) {
+ return makeRequest("DistributionInfo", host + YAMAHA_EXTENDED_CONTROL + "dist/getDistributionInfo");
+ }
+
+ private @Nullable String setClientServerInfo(@Nullable String host, String json, String type) {
+ InputStream is = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
+ try {
+ url = "http://" + host + YAMAHA_EXTENDED_CONTROL + "dist/" + type;
+ httpResponse = HttpUtil.executeUrl("POST", url, is, "", LONG_CONNECTION_TIMEOUT_MILLISEC);
+ logger.trace("MC Link/Unlink Client {}", httpResponse);
+ return httpResponse;
+ } catch (IOException e) {
+ logger.trace("IO Exception - {} - {}", type, e.getMessage());
+ return "{\"response_code\":\"999\"}";
+ }
+ }
+
+ private @Nullable String startDistribution(@Nullable String host) {
+ Random ran = new Random();
+ int nxt = ran.nextInt(200000);
+ return makeRequest("StartDistribution", host + YAMAHA_EXTENDED_CONTROL + "dist/startDistribution?num=" + nxt);
+ }
+
+ // End Music Cast API calls
+
+ // Start General/System API calls
+
+ private @Nullable String getFeatures(@Nullable String host) {
+ return makeRequest("Features", host + YAMAHA_EXTENDED_CONTROL + "system/getFeatures");
+ }
+
+ private @Nullable String getDeviceInfo(@Nullable String host) {
+ return makeRequest("DeviceInfo", host + YAMAHA_EXTENDED_CONTROL + "system/getDeviceInfo");
+ }
+
+ private void keepUdpEventsAlive(@Nullable String host) {
+ Properties appProps = new Properties();
+ appProps.setProperty("X-AppName", "MusicCast/1");
+ appProps.setProperty("X-AppPort", "41100");
+ try {
+ httpResponse = HttpUtil.executeUrl("GET", HTTP + host + YAMAHA_EXTENDED_CONTROL + "netusb/getPlayInfo",
+ appProps, null, "", LONG_CONNECTION_TIMEOUT_MILLISEC);
+ // logger.trace("{}", httpResponse);
+ logger.trace("{} - {}", "UDP task", httpResponse);
+ } catch (IOException e) {
+ logger.trace("UDP refresh failed - {}", e.getMessage());
+ }
+ }
+ // End General/System API calls
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandlerFactory.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandlerFactory.java
new file mode 100644
index 000000000..1dcd8caf1
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandlerFactory.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal;
+
+import static org.openhab.binding.yamahamusiccast.internal.YamahaMusiccastBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+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.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link YamahamusiccastHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.yamahamusiccast", service = ThingHandlerFactory.class)
+public class YamahaMusiccastHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set
+ .of(YamahaMusiccastBindingConstants.THING_DEVICE, YamahaMusiccastBindingConstants.THING_TYPE_BRIDGE);
+
+ private final YamahaMusiccastStateDescriptionProvider stateDescriptionProvider;
+
+ @Activate
+ public YamahaMusiccastHandlerFactory(@Reference YamahaMusiccastStateDescriptionProvider stateDescriptionProvider) {
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ }
+
+ @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(THING_TYPE_BRIDGE)) {
+ return new YamahaMusiccastBridgeHandler((Bridge) thing);
+ } else if (THING_DEVICE.equals(thingTypeUID)) {
+ return new YamahaMusiccastHandler(thing, stateDescriptionProvider);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastStateDescriptionProvider.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastStateDescriptionProvider.java
new file mode 100644
index 000000000..a173bd4d7
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastStateDescriptionProvider.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link YamahaMusiccastStateDescriptionProvider} is responsible for handling the state options of a channel.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, YamahaMusiccastStateDescriptionProvider.class })
+@NonNullByDefault
+public class YamahaMusiccastStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
+
+ @Reference
+ protected void setChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+
+ protected void unsetChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = null;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DeviceInfo.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DeviceInfo.java
new file mode 100644
index 000000000..4a3f24a42
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DeviceInfo.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the DeviceInfo request requested from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+public class DeviceInfo {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ @SerializedName("model_name")
+ private String modelName;
+
+ @SerializedName("device_id")
+ private String deviceId;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+
+ public String getModelName() {
+ if (modelName == null) {
+ modelName = "";
+ }
+ return modelName;
+ }
+
+ public String getDeviceId() {
+ if (deviceId == null) {
+ deviceId = "";
+ }
+ return deviceId;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DistributionInfo.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DistributionInfo.java
new file mode 100644
index 000000000..e808360c1
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DistributionInfo.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.JsonArray;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the DistributionInfo request requested from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+public class DistributionInfo {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ @SerializedName("group_id")
+ private String groupId;
+
+ @SerializedName("role")
+ private String role;
+
+ @SerializedName("server_zone")
+ private String serverZone;
+
+ @SerializedName("client_list")
+ private JsonArray clientList;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+
+ public String getGroupId() {
+ if (groupId == null) {
+ groupId = "";
+ }
+ return groupId;
+ }
+
+ public String getRole() {
+ if (role == null) {
+ role = "";
+ }
+ return role;
+ }
+
+ public String getServerZone() {
+ if (serverZone == null) {
+ serverZone = "";
+ }
+ return serverZone;
+ }
+
+ public JsonArray getClientList() {
+ return clientList;
+ }
+
+ public class ClientList {
+ @SerializedName("ip_address")
+ private String ipaddress;
+
+ public String getIpaddress() {
+ if (ipaddress == null) {
+ ipaddress = "";
+ }
+ return ipaddress;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Features.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Features.java
new file mode 100644
index 000000000..f8644e481
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Features.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the Features request requested from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+
+public class Features {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+
+ @SerializedName("system")
+ private System system;
+
+ public System getSystem() {
+ return system;
+ }
+
+ public class System {
+ @SerializedName("zone_num")
+ private int zoneNum = 0;
+
+ public int getZoneNum() {
+ return zoneNum;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PlayInfo.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PlayInfo.java
new file mode 100644
index 000000000..2abed1164
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PlayInfo.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the PlayInfo request requested from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+
+public class PlayInfo {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ @SerializedName("playback")
+ private String playback;
+
+ @SerializedName("artist")
+ private String artist;
+
+ @SerializedName("track")
+ private String track;
+
+ @SerializedName("album")
+ private String album;
+
+ @SerializedName("albumart_url")
+ private String albumarturl;
+
+ @SerializedName("repeat")
+ private String repeat;
+
+ @SerializedName("shuffle")
+ private String shuffle;
+
+ @SerializedName("play_time")
+ private int playTime = 0;
+
+ @SerializedName("total_time")
+ private int totalTime = 0;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+
+ public String getPlayback() {
+ if (playback == null) {
+ playback = "";
+ }
+ return playback;
+ }
+
+ public String getArtist() {
+ if (artist == null) {
+ artist = "";
+ }
+ return artist;
+ }
+
+ public String getTrack() {
+ if (track == null) {
+ track = "";
+ }
+ return track;
+ }
+
+ public String getAlbum() {
+ if (album == null) {
+ album = "";
+ }
+ return album;
+ }
+
+ public String getAlbumArtUrl() {
+ if (albumarturl == null) {
+ albumarturl = "";
+ }
+ return albumarturl;
+ }
+
+ public String getRepeat() {
+ if (repeat == null) {
+ repeat = "";
+ }
+ return repeat;
+ }
+
+ public String getShuffle() {
+ if (shuffle == null) {
+ shuffle = "";
+ }
+ return shuffle;
+ }
+
+ public int getPlayTime() {
+ return playTime;
+ }
+
+ public int getTotalTime() {
+ return totalTime;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PresetInfo.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PresetInfo.java
new file mode 100644
index 000000000..e0609a392
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PresetInfo.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.JsonArray;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the PresetInfo request requested from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+public class PresetInfo {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ @SerializedName("preset_info")
+ private JsonArray presetInfo;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+
+ public JsonArray getPresetInfo() {
+ return presetInfo;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/RecentInfo.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/RecentInfo.java
new file mode 100644
index 000000000..197b2ba51
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/RecentInfo.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.JsonArray;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the RecentInfo request requested from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+public class RecentInfo {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ @SerializedName("recent_info")
+ private JsonArray recentInfo;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+
+ public JsonArray getRecentInfo() {
+ return recentInfo;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Response.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Response.java
new file mode 100644
index 000000000..141245b37
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Response.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the response received from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+
+public class Response {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Status.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Status.java
new file mode 100644
index 000000000..d7dcad79b
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Status.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the Status request requested from the Yamaha model/device via the API.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+
+public class Status {
+
+ @SerializedName("response_code")
+ private String responseCode;
+
+ @SerializedName("power")
+ private String power;
+
+ @SerializedName("mute")
+ private String mute;
+
+ @SerializedName("volume")
+ private int volume;
+
+ @SerializedName("max_volume")
+ private int maxVolume = 1;
+
+ @SerializedName("input")
+ private String input;
+
+ @SerializedName("sound_program")
+ private String soundProgram;
+
+ @SerializedName("sleep")
+ private int sleep = 0;
+
+ public String getResponseCode() {
+ if (responseCode == null) {
+ responseCode = "";
+ }
+ return responseCode;
+ }
+
+ public String getPower() {
+ if (power == null) {
+ power = "";
+ }
+ return power;
+ }
+
+ public String getMute() {
+ if (mute == null) {
+ mute = "";
+ }
+ return mute;
+ }
+
+ public int getVolume() {
+ return volume;
+ }
+
+ public int getMaxVolume() {
+ // if no value is returned, set to 1 to avoid division by zero
+ if (maxVolume == 0) {
+ maxVolume = 1;
+ }
+ return maxVolume;
+ }
+
+ public String getInput() {
+ if (input == null) {
+ input = "";
+ }
+ return input;
+ }
+
+ public String getSoundProgram() {
+ if (soundProgram == null) {
+ soundProgram = "";
+ }
+ return soundProgram;
+ }
+
+ public int getSleep() {
+ return sleep;
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/UdpMessage.java b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/UdpMessage.java
new file mode 100644
index 000000000..844cdf414
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/UdpMessage.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright (c) 2010-2021 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.yamahamusiccast.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the UDP event received from the Yamaha model/device.
+ *
+ * @author Lennert Coopman - Initial contribution
+ */
+
+public class UdpMessage {
+
+ @SerializedName("device_id")
+ private String deviceId;
+
+ public String getDeviceId() {
+ if (deviceId == null) {
+ deviceId = "";
+ }
+ return deviceId;
+ }
+
+ @SerializedName("main")
+ private Zone main;
+ @SerializedName("zone2")
+ private Zone zone2;
+ @SerializedName("zone3")
+ private Zone zone3;
+ @SerializedName("zone4")
+ private Zone zone4;
+ @SerializedName("netusb")
+ private NetUSB netusb;
+ @SerializedName("dist")
+ private Dist dist;
+
+ public Zone getMain() {
+ return main;
+ }
+
+ public Zone getZone2() {
+ return zone2;
+ }
+
+ public Zone getZone3() {
+ return zone3;
+ }
+
+ public Zone getZone4() {
+ return zone4;
+ }
+
+ public NetUSB getNetUSB() {
+ return netusb;
+ }
+
+ public Dist getDist() {
+ return dist;
+ }
+
+ public class Zone {
+ @SerializedName("power")
+ private String power;
+ @SerializedName("volume")
+ private int volume = 0;
+ @SerializedName("mute")
+ private String mute;
+ @SerializedName("input")
+ private String input;
+ @SerializedName("status_updated")
+ private String statusUpdated;
+
+ public String getPower() {
+ if (power == null) {
+ power = "";
+ }
+ return power;
+ }
+
+ public String getMute() {
+ if (mute == null) {
+ mute = "";
+ }
+ return mute;
+ }
+
+ public String getInput() {
+ if (input == null) {
+ input = "";
+ }
+ return input;
+ }
+
+ public int getVolume() {
+ return volume;
+ }
+
+ public String getstatusUpdated() {
+ if (statusUpdated == null) {
+ statusUpdated = "";
+ }
+ return statusUpdated;
+ }
+ }
+
+ public class NetUSB {
+ @SerializedName("preset_control")
+ private PresetControl presetControl;
+ @SerializedName("play_info_updated")
+ private String playInfoUpdated;
+ @SerializedName("play_time")
+ private int playTime;
+
+ public PresetControl getPresetControl() {
+ return presetControl;
+ }
+
+ public String getPlayInfoUpdated() {
+ if (playInfoUpdated == null) {
+ playInfoUpdated = "";
+ }
+ return playInfoUpdated;
+ }
+
+ public int getPlayTime() {
+ return playTime;
+ }
+ }
+
+ public class PresetControl {
+ @SerializedName("type")
+ private String type;
+ @SerializedName("num")
+ private int num = 1;
+ @SerializedName("result")
+ private String result;
+
+ public String getType() {
+ if (type == null) {
+ type = "";
+ }
+ return type;
+ }
+
+ public String getResult() {
+ if (result == null) {
+ result = "";
+ }
+ return result;
+ }
+
+ public int getNum() {
+ return num;
+ }
+ }
+
+ public class Dist {
+ @SerializedName("dist_info_updated")
+ private String distInfoUpdated;
+
+ public String getDistInfoUpdated() {
+ if (distInfoUpdated == null) {
+ distInfoUpdated = "";
+ }
+ return distInfoUpdated;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 000000000..5e8a56893
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Yamaha Musiccast Binding
+ This is the binding for Yamaha Musiccast
+
+
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644
index 000000000..7a7608e5e
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/bridge.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Virtual Bridge to receive updates
+
+
+
diff --git a/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 000000000..02a8b6372
--- /dev/null
+++ b/bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+ Your Yamaha model with MusicCast functionality
+
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+ The IP address of the AVR to control.
+
+
+
+ Sync Volume across linked Music Cast models
+ true
+
+
+
+ Default value for client when MC Link is broken
+ net_radio
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ Volume channel - Absolute value
+
+
+ String
+
+ Input channel
+
+
+ String
+
+ SoundProgram channel
+
+
+ String
+
+ Select Net Radio/USB Preset channel
+
+
+ Player
+
+ Player for Net Radio/USB channel
+
+
+ Number
+
+ Sleep Time in minutes
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ Scene selection (if available)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ Artist
+
+
+ String
+
+ Track
+
+
+ String
+
+ Album
+
+
+ Image
+
+ Album Art
+
+
+ String
+
+ Repeat mode
+
+
+
+
+
+
+
+
+
+ String
+
+ Shuffle mode
+
+
+
+
+
+
+
+
+
+
+ String
+
+ MusicCast Status
+
+
+ Number:Time
+
+ Play Time
+
+
+ String
+
+ Total Time
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index a633b89ee..e3469531f 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -373,6 +373,7 @@
org.openhab.binding.wolfsmartset
org.openhab.binding.xmltv
org.openhab.binding.xmppclient
+ org.openhab.binding.yamahamusiccast
org.openhab.binding.yamahareceiver
org.openhab.binding.yioremote
org.openhab.binding.yeelight