]> git.basschouten.com Git - openhab-addons.git/commitdiff
[yamahamusiccast] Initial contribution (#11880)
authorcoop-git <65073745+coop-git@users.noreply.github.com>
Thu, 6 Jan 2022 19:04:25 +0000 (20:04 +0100)
committerGitHub <noreply@github.com>
Thu, 6 Jan 2022 19:04:25 +0000 (20:04 +0100)
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
* Pull Request OH3

Signed-off-by: Lennert Coopman <github@coopman.org>
24 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.yamahamusiccast/README.md [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/YamahaMusiccastStateDescriptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DeviceInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/DistributionInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Features.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PlayInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/PresetInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/RecentInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Response.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/Status.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/java/org/openhab/binding/yamahamusiccast/internal/dto/UdpMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/bridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.yamahamusiccast/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index a3f827da53b3ed90a2e3448a79f1bb93d5b2fe59..4be0fa8c60d70c3ffde6f6530bb517ec7951c16c 100644 (file)
 /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
index 2a71e5b30ca63b3323bfb53d5b0d7d0aaa9d6108..caa29bac5ecee23b296b8774955efd54dfa8a924 100644 (file)
       <artifactId>org.openhab.binding.xmppclient</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.yamahamusiccast</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.yamahareceiver</artifactId>
diff --git a/bundles/org.openhab.binding.yamahamusiccast/README.md b/bundles/org.openhab.binding.yamahamusiccast/README.md
new file mode 100644 (file)
index 0000000..874f4a9
--- /dev/null
@@ -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 (file)
index 0000000..d9c412b
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.yamahamusiccast</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Yamaha Musiccast Binding</name>
+
+</project>
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 (file)
index 0000000..0486c44
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.yamahamusiccast-${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-yamahamusiccast" description="Yamaha Musiccast 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.yamahamusiccast/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..ea002a3
--- /dev/null
@@ -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 (file)
index 0000000..54e8050
--- /dev/null
@@ -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 (file)
index 0000000..79f6327
--- /dev/null
@@ -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 (file)
index 0000000..c5feae1
--- /dev/null
@@ -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<StateOption> 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<StateOption> 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 (file)
index 0000000..1dcd8ca
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..a173bd4
--- /dev/null
@@ -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 (file)
index 0000000..4a3f24a
--- /dev/null
@@ -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 (file)
index 0000000..e808360
--- /dev/null
@@ -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 (file)
index 0000000..f8644e4
--- /dev/null
@@ -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 (file)
index 0000000..2abed11
--- /dev/null
@@ -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 (file)
index 0000000..e0609a3
--- /dev/null
@@ -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 (file)
index 0000000..197b2ba
--- /dev/null
@@ -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 (file)
index 0000000..141245b
--- /dev/null
@@ -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 (file)
index 0000000..d7dcad7
--- /dev/null
@@ -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 (file)
index 0000000..844cdf4
--- /dev/null
@@ -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 (file)
index 0000000..5e8a568
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="yamahamusiccast" 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>Yamaha Musiccast Binding</name>
+       <description>This is the binding for Yamaha Musiccast</description>
+
+</binding:binding>
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 (file)
index 0000000..7a7608e
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="yamahamusiccast"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Bridge -->
+       <bridge-type id="bridge">
+               <label>Virtual Bridge</label>
+               <description>Virtual Bridge to receive updates</description>
+       </bridge-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..02a8b63
--- /dev/null
@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="yamahamusiccast"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+
+       <thing-type id="device">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+               <label>Yamaha MusicCast Model</label>
+               <description>Your Yamaha model with MusicCast functionality</description>
+
+               <channel-groups>
+                       <channel-group id="main" typeId="mainControls"/>
+                       <channel-group id="zone2" typeId="zone2Controls"/>
+                       <channel-group id="zone3" typeId="zone3Controls"/>
+                       <channel-group id="zone4" typeId="zone4Controls"/>
+                       <channel-group id="playerControls" typeId="playerControls"/>
+               </channel-groups>
+
+               <config-description>
+                       <parameter name="host" type="text" required="true">
+                               <label>Address</label>
+                               <context>network-address</context>
+                               <description>The IP address of the AVR to control.</description>
+                       </parameter>
+                       <parameter name="syncVolume" type="boolean">
+                               <label>Sync Volume</label>
+                               <description>Sync Volume across linked Music Cast models</description>
+                               <default>true</default>
+                       </parameter>
+                       <parameter name="defaultAfterMCLink" type="text">
+                               <label>MC Link Default Value</label>
+                               <description>Default value for client when MC Link is broken</description>
+                               <default>net_radio</default>
+                               <options>
+                                       <option value="none">None</option>
+                                       <option value="net_radio">Net Radio</option>
+                               </options>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-group-type id="mainControls">
+               <label>Main Zone (default)</label>
+       </channel-group-type>
+
+       <channel-group-type id="zone2Controls">
+               <label>Zone 2</label>
+       </channel-group-type>
+
+       <channel-group-type id="zone3Controls">
+               <label>Zone 3</label>
+       </channel-group-type>
+
+       <channel-group-type id="zone4Controls">
+               <label>Zone 4</label>
+       </channel-group-type>
+
+       <channel-group-type id="playerControls">
+               <label>Player Controls</label>
+               <channels>
+                       <channel id="player" typeId="player"/>
+                       <channel id="artist" typeId="artist"/>
+                       <channel id="track" typeId="track"/>
+                       <channel id="album" typeId="album"/>
+                       <channel id="albumArt" typeId="albumArt"/>
+                       <channel id="repeat" typeId="repeat"/>
+                       <channel id="shuffle" typeId="shuffle"/>
+                       <channel id="playTime" typeId="playTime"/>
+                       <channel id="totalTime" typeId="totalTime"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-type id="volumeAbs">
+               <item-type>Number</item-type>
+               <label>Volume</label>
+               <description>Volume channel - Absolute value</description>
+       </channel-type>
+       <channel-type id="input">
+               <item-type>String</item-type>
+               <label>Input</label>
+               <description>Input channel</description>
+       </channel-type>
+       <channel-type id="soundProgram">
+               <item-type>String</item-type>
+               <label>Sound Program</label>
+               <description>SoundProgram channel</description>
+       </channel-type>
+       <channel-type id="selectPreset">
+               <item-type>String</item-type>
+               <label>NetRadio/USB Preset</label>
+               <description>Select Net Radio/USB Preset channel</description>
+       </channel-type>
+       <channel-type id="player">
+               <item-type>Player</item-type>
+               <label>NetRadio/USB Player</label>
+               <description>Player for Net Radio/USB channel</description>
+       </channel-type>
+       <channel-type id="sleep">
+               <item-type>Number</item-type>
+               <label>Sleep</label>
+               <description>Sleep Time in minutes</description>
+               <state>
+                       <options>
+                               <option value="0">No Sleep</option>
+                               <option value="30">30 min</option>
+                               <option value="60">60 min</option>
+                               <option value="90">90 min</option>
+                               <option value="120">120 min</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="recallScene">
+               <item-type>Number</item-type>
+               <label>Scene Selection</label>
+               <description>Scene selection (if available)</description>
+               <state>
+                       <options>
+                               <option value="1">Scene 1</option>
+                               <option value="2">Scene 2</option>
+                               <option value="3">Scene 3</option>
+                               <option value="4">Scene 4</option>
+                               <option value="5">Scene 5</option>
+                               <option value="6">Scene 6</option>
+                               <option value="7">Scene 7</option>
+                               <option value="8">Scene 8</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="artist">
+               <item-type>String</item-type>
+               <label>Artist</label>
+               <description>Artist</description>
+       </channel-type>
+       <channel-type id="track">
+               <item-type>String</item-type>
+               <label>Track</label>
+               <description>Track</description>
+       </channel-type>
+       <channel-type id="album">
+               <item-type>String</item-type>
+               <label>Album</label>
+               <description>Album</description>
+       </channel-type>
+       <channel-type id="albumArt">
+               <item-type>Image</item-type>
+               <label>Album Art</label>
+               <description>Album Art</description>
+       </channel-type>
+       <channel-type id="repeat">
+               <item-type>String</item-type>
+               <label>Repeat</label>
+               <description>Repeat mode</description>
+               <state>
+                       <options>
+                               <option value="off">Off</option>
+                               <option value="one">One</option>
+                               <option value="all">All</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="shuffle">
+               <item-type>String</item-type>
+               <label>Shuffle</label>
+               <description>Shuffle mode</description>
+               <state>
+                       <options>
+                               <option value="off">Off</option>
+                               <option value="on">On</option>
+                               <option value="songs">Songs</option>
+                               <option value="albums">Albums</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="mclinkStatus">
+               <item-type>String</item-type>
+               <label>Status MusicCast</label>
+               <description>MusicCast Status</description>
+       </channel-type>
+       <channel-type id="playTime">
+               <item-type>Number:Time</item-type>
+               <label>Play Time</label>
+               <description>Play Time</description>
+       </channel-type>
+       <channel-type id="totalTime">
+               <item-type>String</item-type>
+               <label>Total Time</label>
+               <description>Total Time</description>
+       </channel-type>
+</thing:thing-descriptions>
index a633b89ee6ee48f31b98773a0d6c25cf7412a39c..e3469531f409ff7d819277e9a2705976197d4f0e 100644 (file)
     <module>org.openhab.binding.wolfsmartset</module>
     <module>org.openhab.binding.xmltv</module>
     <module>org.openhab.binding.xmppclient</module>
+    <module>org.openhab.binding.yamahamusiccast</module>
     <module>org.openhab.binding.yamahareceiver</module>
     <module>org.openhab.binding.yioremote</module>
     <module>org.openhab.binding.yeelight</module>