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