]> git.basschouten.com Git - openhab-addons.git/blob
5a6ceeee876ee43048fde2fe9860e944ae96e984
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.yamahamusiccast.internal;
14
15 import static org.openhab.binding.yamahamusiccast.internal.YamahaMusiccastBindingConstants.*;
16
17 import java.io.ByteArrayInputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Objects;
24 import java.util.Properties;
25 import java.util.Random;
26 import java.util.UUID;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.yamahamusiccast.internal.dto.DeviceInfo;
33 import org.openhab.binding.yamahamusiccast.internal.dto.DistributionInfo;
34 import org.openhab.binding.yamahamusiccast.internal.dto.Features;
35 import org.openhab.binding.yamahamusiccast.internal.dto.PlayInfo;
36 import org.openhab.binding.yamahamusiccast.internal.dto.PresetInfo;
37 import org.openhab.binding.yamahamusiccast.internal.dto.RecentInfo;
38 import org.openhab.binding.yamahamusiccast.internal.dto.Response;
39 import org.openhab.binding.yamahamusiccast.internal.dto.Status;
40 import org.openhab.binding.yamahamusiccast.internal.dto.UdpMessage;
41 import org.openhab.core.io.net.http.HttpUtil;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.NextPreviousType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.PercentType;
46 import org.openhab.core.library.types.PlayPauseType;
47 import org.openhab.core.library.types.QuantityType;
48 import org.openhab.core.library.types.RewindFastforwardType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.unit.Units;
51 import org.openhab.core.thing.Bridge;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.BaseThingHandler;
58 import org.openhab.core.thing.binding.builder.ChannelBuilder;
59 import org.openhab.core.thing.binding.builder.ThingBuilder;
60 import org.openhab.core.thing.type.ChannelTypeUID;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.openhab.core.types.StateOption;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66
67 import com.google.gson.Gson;
68 import com.google.gson.JsonElement;
69 import com.google.gson.JsonObject;
70
71 /**
72  * The {@link YamahaMusiccastHandler} is responsible for handling commands, which are
73  * sent to one of the channels.
74  *
75  * @author Lennert Coopman - Initial contribution
76  * @author Florian Hotze - Add volume in decibel
77  */
78 @NonNullByDefault
79 public class YamahaMusiccastHandler extends BaseThingHandler {
80     private Gson gson = new Gson();
81     private Logger logger = LoggerFactory.getLogger(YamahaMusiccastHandler.class);
82     private @Nullable ScheduledFuture<?> generalHousekeepingTask;
83     private @Nullable String httpResponse;
84     private @Nullable String tmpString = "";
85     private int volumePercent = 0;
86     private int volumeAbsValue = 0;
87     private @Nullable String responseCode = "";
88     private int volumeState = 0;
89     private float volumeDbState = -80f; // -80.0 dB
90     private int maxVolumeState = 0;
91     private @Nullable String inputState = "";
92     private @Nullable String soundProgramState = "";
93     private int sleepState = 0;
94     private @Nullable String artistState = "";
95     private @Nullable String trackState = "";
96     private @Nullable String albumState = "";
97     private @Nullable String repeatState = "";
98     private @Nullable String shuffleState = "";
99     private int playTimeState = 0;
100     private int totalTimeState = 0;
101     private @Nullable String zone = "main";
102     private String channelWithoutGroup = "";
103     private @Nullable String thingLabel = "";
104     private @Nullable String mclinkSetupServer = "";
105     private @Nullable String mclinkSetupZone = "";
106     private String url = "";
107     private String json = "";
108     private String action = "";
109     private int zoneNum = 0;
110     private @Nullable String groupId = "";
111     private @Nullable String host;
112     public @Nullable String deviceId = "";
113
114     private YamahaMusiccastStateDescriptionProvider stateDescriptionProvider;
115
116     public YamahaMusiccastHandler(Thing thing, YamahaMusiccastStateDescriptionProvider stateDescriptionProvider) {
117         super(thing);
118         this.stateDescriptionProvider = stateDescriptionProvider;
119     }
120
121     @Override
122     public void handleCommand(ChannelUID channelUID, Command command) {
123         String localValueToCheck = "";
124         String localRole = "";
125         boolean localSyncVolume;
126         String localDefaultAfterMCLink = "";
127         String localRoleSelectedThing = "";
128         if (command != RefreshType.REFRESH) {
129             logger.trace("Handling command {} for channel {}", command, channelUID);
130             channelWithoutGroup = channelUID.getIdWithoutGroup();
131             zone = channelUID.getGroupId();
132             DistributionInfo distributioninfo = new DistributionInfo();
133             Response response = new Response();
134             switch (channelWithoutGroup) {
135                 case CHANNEL_POWER:
136                     if (command == OnOffType.ON) {
137                         httpResponse = setPower("on", zone, this.host);
138                         response = gson.fromJson(httpResponse, Response.class);
139                         if (response != null) {
140                             localValueToCheck = response.getResponseCode();
141                             if (!"0".equals(localValueToCheck)) {
142                                 updateState(channelUID, OnOffType.OFF);
143                             }
144                         }
145                         // check on scheduler task for UDP events
146                         ScheduledFuture<?> localGeneralHousekeepingTask = generalHousekeepingTask;
147                         if (localGeneralHousekeepingTask == null) {
148                             logger.trace("YXC - No scheduler task found!");
149                             generalHousekeepingTask = scheduler.scheduleWithFixedDelay(this::generalHousekeeping, 5,
150                                     300, TimeUnit.SECONDS);
151
152                         } else {
153                             logger.trace("Scheduler task found!");
154                         }
155
156                     } else if (command == OnOffType.OFF) {
157                         httpResponse = setPower("standby", zone, this.host);
158                         response = gson.fromJson(httpResponse, Response.class);
159                         powerOffCleanup();
160                         if (response != null) {
161                             localValueToCheck = response.getResponseCode();
162                             if (!"0".equals(localValueToCheck)) {
163                                 updateState(channelUID, OnOffType.ON);
164                             }
165                         }
166                     }
167                     break;
168                 case CHANNEL_MUTE:
169                     if (command == OnOffType.ON) {
170                         httpResponse = setMute("true", zone, this.host);
171                         response = gson.fromJson(httpResponse, Response.class);
172                         if (response != null) {
173                             localValueToCheck = response.getResponseCode();
174                             if (!"0".equals(localValueToCheck)) {
175                                 updateState(channelUID, OnOffType.OFF);
176                             }
177                         }
178                     } else if (command == OnOffType.OFF) {
179                         httpResponse = setMute("false", zone, this.host);
180                         response = gson.fromJson(httpResponse, Response.class);
181                         if (response != null) {
182                             localValueToCheck = response.getResponseCode();
183                             if (!"0".equals(localValueToCheck)) {
184                                 updateState(channelUID, OnOffType.ON);
185                             }
186                         }
187                     }
188                     break;
189                 case CHANNEL_VOLUME:
190                     volumePercent = Integer.parseInt(command.toString().replace(".0", ""));
191                     volumeAbsValue = (maxVolumeState * volumePercent) / 100;
192                     setVolume(volumeAbsValue, zone, this.host);
193                     localSyncVolume = Boolean.parseBoolean(getThing().getConfiguration().get("syncVolume").toString());
194                     if (localSyncVolume == Boolean.TRUE) {
195                         tmpString = getDistributionInfo(this.host);
196                         distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
197                         if (distributioninfo != null) {
198                             localRole = distributioninfo.getRole();
199                             if ("server".equals(localRole)) {
200                                 for (JsonElement ip : distributioninfo.getClientList()) {
201                                     JsonObject clientObject = ip.getAsJsonObject();
202                                     setVolumeLinkedDevice(volumePercent, zone,
203                                             clientObject.get("ip_address").getAsString());
204                                 }
205                             }
206                         }
207                     } // END config.syncVolume
208                     break;
209                 case CHANNEL_VOLUMEABS:
210                     volumeAbsValue = Integer.parseInt(command.toString().replace(".0", ""));
211                     volumePercent = (volumeAbsValue / maxVolumeState) * 100;
212                     setVolume(volumeAbsValue, zone, this.host);
213                     localSyncVolume = Boolean.parseBoolean(getThing().getConfiguration().get("syncVolume").toString());
214                     if (localSyncVolume == Boolean.TRUE) {
215                         tmpString = getDistributionInfo(this.host);
216                         distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
217                         if (distributioninfo != null) {
218                             localRole = distributioninfo.getRole();
219                             if ("server".equals(localRole)) {
220                                 for (JsonElement ip : distributioninfo.getClientList()) {
221                                     JsonObject clientObject = ip.getAsJsonObject();
222                                     setVolumeLinkedDevice(volumePercent, zone,
223                                             clientObject.get("ip_address").getAsString());
224                                 }
225                             }
226                         }
227                     }
228                     break;
229                 case CHANNEL_VOLUMEDB:
230                     setVolumeDb(((QuantityType<?>) command).floatValue(), zone, this.host);
231                     localSyncVolume = Boolean.parseBoolean(getThing().getConfiguration().get("syncVolume").toString());
232                     if (localSyncVolume == Boolean.TRUE) {
233                         tmpString = getDistributionInfo(this.host);
234                         distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
235                         if (distributioninfo != null) {
236                             localRole = distributioninfo.getRole();
237                             if ("server".equals(localRole)) {
238                                 for (JsonElement ip : distributioninfo.getClientList()) {
239                                     JsonObject clientObject = ip.getAsJsonObject();
240                                     setVolumeDbLinkedDevice(((DecimalType) command).floatValue(), zone,
241                                             clientObject.get("ip_address").getAsString());
242                                 }
243                             }
244                         }
245                     }
246                     break;
247                 case CHANNEL_INPUT:
248                     // if it is a client, disconnect it first.
249                     tmpString = getDistributionInfo(this.host);
250                     distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
251                     if (distributioninfo != null) {
252                         localRole = distributioninfo.getRole();
253                         if ("client".equals(localRole)) {
254                             json = "{\"group_id\":\"\"}";
255                             httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
256                         }
257                     }
258                     setInput(command.toString(), zone, this.host);
259                     break;
260                 case CHANNEL_SOUNDPROGRAM:
261                     setSoundProgram(command.toString(), zone, this.host);
262                     break;
263                 case CHANNEL_SELECTPRESET:
264                     setPreset(command.toString(), zone, this.host);
265                     break;
266                 case CHANNEL_PLAYER:
267                     if (command.equals(PlayPauseType.PLAY)) {
268                         setPlayback("play", this.host);
269                     } else if (command.equals(PlayPauseType.PAUSE)) {
270                         setPlayback("pause", this.host);
271                     } else if (command.equals(NextPreviousType.NEXT)) {
272                         setPlayback("next", this.host);
273                     } else if (command.equals(NextPreviousType.PREVIOUS)) {
274                         setPlayback("previous", this.host);
275                     } else if (command.equals(RewindFastforwardType.REWIND)) {
276                         setPlayback("fast_reverse_start", this.host);
277                     } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
278                         setPlayback("fast_forward_end", this.host);
279                     }
280                     break;
281                 case CHANNEL_SLEEP:
282                     setSleep(command.toString(), zone, this.host);
283                     break;
284                 case CHANNEL_MCLINKSTATUS:
285                     action = "";
286                     json = "";
287                     tmpString = getDistributionInfo(this.host);
288                     distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
289                     if (distributioninfo != null) {
290                         responseCode = distributioninfo.getResponseCode();
291                         localRole = distributioninfo.getRole();
292                         if (command.toString().equals("")) {
293                             action = "unlink";
294                             groupId = distributioninfo.getGroupId();
295                         } else if (command.toString().contains("***")) {
296                             action = "link";
297                             String[] parts = command.toString().split("\\*\\*\\*");
298                             if (parts.length > 1) {
299                                 mclinkSetupServer = parts[0];
300                                 mclinkSetupZone = parts[1];
301                                 tmpString = getDistributionInfo(mclinkSetupServer);
302                                 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
303                                 if (distributioninfo != null) {
304                                     responseCode = distributioninfo.getResponseCode();
305                                     localRoleSelectedThing = distributioninfo.getRole();
306                                     groupId = distributioninfo.getGroupId();
307                                     if (localRoleSelectedThing != null) {
308                                         if ("server".equals(localRoleSelectedThing)) {
309                                             groupId = distributioninfo.getGroupId();
310                                         } else if ("client".equals(localRoleSelectedThing)) {
311                                             groupId = "";
312                                         } else if ("none".equals(localRoleSelectedThing)) {
313                                             groupId = generateGroupId();
314                                         }
315                                     }
316                                 }
317                             }
318                         }
319
320                         if ("unlink".equals(action)) {
321                             json = "{\"group_id\":\"\"}";
322                             if (localRole != null) {
323                                 if ("server".equals(localRole)) {
324                                     httpResponse = setClientServerInfo(this.host, json, "setServerInfo");
325                                     // Set GroupId = "" for linked clients
326                                     if (distributioninfo != null) {
327                                         for (JsonElement ip : distributioninfo.getClientList()) {
328                                             JsonObject clientObject = ip.getAsJsonObject();
329                                             setClientServerInfo(clientObject.get("ip_address").getAsString(), json,
330                                                     "setClientInfo");
331                                         }
332                                     }
333                                 } else if ("client".equals(localRole)) {
334                                     mclinkSetupServer = connectedServer();
335                                     // Step 1. empty group on client
336                                     httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
337                                     // empty zone to respect defaults
338                                     if (!"".equals(mclinkSetupServer)) {
339                                         // Step 2. remove client from server
340                                         json = "{\"group_id\":\"" + groupId
341                                                 + "\", \"type\":\"remove\", \"client_list\":[\"" + this.host + "\"]}";
342                                         httpResponse = setClientServerInfo(mclinkSetupServer, json, "setServerInfo");
343                                         // Step 3. reflect changes to master
344                                         httpResponse = startDistribution(mclinkSetupServer);
345                                         localDefaultAfterMCLink = getThing().getConfiguration()
346                                                 .get("defaultAfterMCLink").toString();
347                                         httpResponse = setInput(localDefaultAfterMCLink.toString(), zone, this.host);
348                                     } else if ("".equals(mclinkSetupServer)) {
349                                         // fallback in case client is removed from group by ending group on server side
350                                         localDefaultAfterMCLink = getThing().getConfiguration()
351                                                 .get("defaultAfterMCLink").toString();
352                                         httpResponse = setInput(localDefaultAfterMCLink.toString(), zone, this.host);
353                                     }
354                                 }
355                             }
356                         } else if ("link".equals(action)) {
357                             if (localRole != null) {
358                                 if ("none".equals(localRole)) {
359                                     json = "{\"group_id\":\"" + groupId + "\", \"zone\":\"" + mclinkSetupZone
360                                             + "\", \"type\":\"add\", \"client_list\":[\"" + this.host + "\"]}";
361                                     logger.trace("setServerInfo json: {}", json);
362                                     httpResponse = setClientServerInfo(mclinkSetupServer, json, "setServerInfo");
363                                     // All zones of Model are required for MC Link
364                                     tmpString = "";
365                                     for (int i = 1; i <= zoneNum; i++) {
366                                         switch (i) {
367                                             case 1:
368                                                 tmpString = "\"main\"";
369                                                 break;
370                                             case 2:
371                                                 tmpString = tmpString + ", \"zone2\"";
372                                                 break;
373                                             case 3:
374                                                 tmpString = tmpString + ", \"zone3\"";
375                                                 break;
376                                             case 4:
377                                                 tmpString = tmpString + ", \"zone4\"";
378                                                 break;
379                                         }
380                                     }
381                                     json = "{\"group_id\":\"" + groupId + "\", \"zone\":[" + tmpString + "]}";
382                                     logger.trace("setClientInfo json: {}", json);
383                                     httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
384                                     httpResponse = startDistribution(mclinkSetupServer);
385                                 }
386                             }
387                         }
388                     }
389                     updateMCLinkStatus();
390                     break;
391                 case CHANNEL_RECALLSCENE:
392                     recallScene(command.toString(), zone, this.host);
393                     break;
394                 case CHANNEL_REPEAT:
395                     setRepeat(command.toString(), this.host);
396                     break;
397                 case CHANNEL_SHUFFLE:
398                     setShuffle(command.toString(), this.host);
399                     break;
400             } // END Switch Channel
401         }
402     }
403
404     @Override
405     public void initialize() {
406         String localHost = "";
407         thingLabel = thing.getLabel();
408         updateStatus(ThingStatus.UNKNOWN);
409         localHost = getThing().getConfiguration().get("host").toString();
410         this.host = localHost;
411         if (!"".equals(this.host)) {
412             zoneNum = getNumberOfZones(this.host);
413             logger.trace("Zones found: {} - {}", zoneNum, thingLabel);
414
415             if (zoneNum > 0) {
416                 refreshOnStartup();
417                 generalHousekeepingTask = scheduler.scheduleWithFixedDelay(this::generalHousekeeping, 5, 300,
418                         TimeUnit.SECONDS);
419                 updateStatus(ThingStatus.ONLINE);
420             } else {
421                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No host found");
422             }
423         }
424     }
425
426     private void generalHousekeeping() {
427         thingLabel = thing.getLabel();
428         logger.trace("YXC - Start Keep Alive UDP events (5 minutes - {}) ", thingLabel);
429         keepUdpEventsAlive(this.host);
430         fillOptionsForMCLink();
431         updateMCLinkStatus();
432     }
433
434     private void refreshOnStartup() {
435         for (int i = 1; i <= zoneNum; i++) {
436             switch (i) {
437                 case 1:
438                     createChannels("main");
439                     updateStatusZone("main");
440                     break;
441                 case 2:
442                     createChannels("zone2");
443                     updateStatusZone("zone2");
444                     break;
445                 case 3:
446                     createChannels("zone3");
447                     updateStatusZone("zone3");
448                     break;
449                 case 4:
450                     createChannels("zone4");
451                     updateStatusZone("zone4");
452                     break;
453             }
454         }
455         updatePresets(0);
456         updateNetUSBPlayer();
457         fillOptionsForMCLink();
458         updateMCLinkStatus();
459     }
460
461     @Override
462     public void dispose() {
463         ScheduledFuture<?> localGeneralHousekeepingTask = generalHousekeepingTask;
464         if (localGeneralHousekeepingTask != null) {
465             localGeneralHousekeepingTask.cancel(true);
466         }
467     }
468
469     // Various functions
470
471     private void createChannels(String zone) {
472         createChannel(zone, CHANNEL_POWER, CHANNEL_TYPE_UID_POWER, "Switch");
473         createChannel(zone, CHANNEL_MUTE, CHANNEL_TYPE_UID_MUTE, "Switch");
474         createChannel(zone, CHANNEL_VOLUME, CHANNEL_TYPE_UID_VOLUME, "Dimmer");
475         createChannel(zone, CHANNEL_VOLUMEABS, CHANNEL_TYPE_UID_VOLUMEABS, "Number");
476         createChannel(zone, CHANNEL_VOLUMEDB, CHANNEL_TYPE_UID_VOLUMEDB, "Number:Dimensionless");
477         createChannel(zone, CHANNEL_INPUT, CHANNEL_TYPE_UID_INPUT, "String");
478         createChannel(zone, CHANNEL_SOUNDPROGRAM, CHANNEL_TYPE_UID_SOUNDPROGRAM, "String");
479         createChannel(zone, CHANNEL_SLEEP, CHANNEL_TYPE_UID_SLEEP, "Number");
480         createChannel(zone, CHANNEL_SELECTPRESET, CHANNEL_TYPE_UID_SELECTPRESET, "String");
481         createChannel(zone, CHANNEL_RECALLSCENE, CHANNEL_TYPE_UID_RECALLSCENE, "Number");
482         createChannel(zone, CHANNEL_MCLINKSTATUS, CHANNEL_TYPE_UID_MCLINKSTATUS, "String");
483     }
484
485     private void createChannel(String zone, String channel, ChannelTypeUID channelTypeUID, String itemType) {
486         ChannelUID channelToCheck = new ChannelUID(thing.getUID(), zone, channel);
487         if (thing.getChannel(channelToCheck) == null) {
488             ThingBuilder thingBuilder = editThing();
489             Channel testchannel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), zone, channel), itemType)
490                     .withType(channelTypeUID).build();
491             thingBuilder.withChannel(testchannel);
492             updateThing(thingBuilder.build());
493         }
494     }
495
496     private void powerOffCleanup() {
497         ChannelUID channel;
498         channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ARTIST);
499         updateState(channel, StringType.valueOf("-"));
500         channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TRACK);
501         updateState(channel, StringType.valueOf("-"));
502         channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUM);
503         updateState(channel, StringType.valueOf("-"));
504     }
505
506     public void processUDPEvent(String json, String trackingID) {
507         logger.trace("UDP package: {} (Tracking: {})", json, trackingID);
508         @Nullable
509         UdpMessage targetObject = gson.fromJson(json, UdpMessage.class);
510         if (targetObject != null) {
511             if (Objects.nonNull(targetObject.getMain())) {
512                 updateStateFromUDPEvent("main", targetObject);
513             }
514             if (Objects.nonNull(targetObject.getZone2())) {
515                 updateStateFromUDPEvent("zone2", targetObject);
516             }
517             if (Objects.nonNull(targetObject.getZone3())) {
518                 updateStateFromUDPEvent("zone3", targetObject);
519             }
520             if (Objects.nonNull(targetObject.getZone4())) {
521                 updateStateFromUDPEvent("zone4", targetObject);
522             }
523             if (Objects.nonNull(targetObject.getNetUSB())) {
524                 updateStateFromUDPEvent("netusb", targetObject);
525             }
526             if (Objects.nonNull(targetObject.getDist())) {
527                 updateStateFromUDPEvent("dist", targetObject);
528             }
529         }
530     }
531
532     private void updateStateFromUDPEvent(String zoneToUpdate, UdpMessage targetObject) {
533         ChannelUID channel;
534         String playInfoUpdated = "";
535         String statusUpdated = "";
536         String powerState = "";
537         String muteState = "";
538         String inputState = "";
539         int volumeState = 0;
540         float volumeDbState = -90f; // -90.0 dB
541         int presetNumber = 0;
542         int playTime = 0;
543         String distInfoUpdated = "";
544         logger.trace("Handling UDP for {}", zoneToUpdate);
545         switch (zoneToUpdate) {
546             case "main":
547                 powerState = targetObject.getMain().getPower();
548                 muteState = targetObject.getMain().getMute();
549                 inputState = targetObject.getMain().getInput();
550                 volumeState = targetObject.getMain().getVolume();
551                 volumeDbState = targetObject.getMain().getVolumeDb();
552                 statusUpdated = targetObject.getMain().getstatusUpdated();
553                 break;
554             case "zone2":
555                 powerState = targetObject.getZone2().getPower();
556                 muteState = targetObject.getZone2().getMute();
557                 inputState = targetObject.getZone2().getInput();
558                 volumeState = targetObject.getZone2().getVolume();
559                 volumeDbState = targetObject.getZone2().getVolumeDb();
560                 statusUpdated = targetObject.getZone2().getstatusUpdated();
561                 break;
562             case "zone3":
563                 powerState = targetObject.getZone3().getPower();
564                 muteState = targetObject.getZone3().getMute();
565                 inputState = targetObject.getZone3().getInput();
566                 volumeState = targetObject.getZone3().getVolume();
567                 volumeDbState = targetObject.getZone3().getVolumeDb();
568                 statusUpdated = targetObject.getZone3().getstatusUpdated();
569                 break;
570             case "zone4":
571                 powerState = targetObject.getZone4().getPower();
572                 muteState = targetObject.getZone4().getMute();
573                 inputState = targetObject.getZone4().getInput();
574                 volumeState = targetObject.getZone4().getVolume();
575                 volumeDbState = targetObject.getZone4().getVolumeDb();
576                 statusUpdated = targetObject.getZone4().getstatusUpdated();
577                 break;
578             case "netusb":
579                 if (Objects.isNull(targetObject.getNetUSB().getPresetControl())) {
580                     presetNumber = 0;
581                 } else {
582                     presetNumber = targetObject.getNetUSB().getPresetControl().getNum();
583                 }
584                 playInfoUpdated = targetObject.getNetUSB().getPlayInfoUpdated();
585                 playTime = targetObject.getNetUSB().getPlayTime();
586                 // totalTime is not in UDP event
587                 break;
588             case "dist":
589                 distInfoUpdated = targetObject.getDist().getDistInfoUpdated();
590                 break;
591         }
592
593         if (!powerState.isEmpty()) {
594             channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_POWER);
595             if ("on".equals(powerState)) {
596                 updateState(channel, OnOffType.ON);
597             } else if ("standby".equals(powerState)) {
598                 updateState(channel, OnOffType.OFF);
599                 powerOffCleanup();
600             }
601         }
602
603         if (!muteState.isEmpty()) {
604             channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_MUTE);
605             if ("true".equals(muteState)) {
606                 updateState(channel, OnOffType.ON);
607             } else if ("false".equals(muteState)) {
608                 updateState(channel, OnOffType.OFF);
609             }
610         }
611
612         if (!inputState.isEmpty()) {
613             channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_INPUT);
614             updateState(channel, StringType.valueOf(inputState));
615         }
616
617         if (volumeState != 0) {
618             channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_VOLUME);
619             updateState(channel, new PercentType((volumeState * 100) / maxVolumeState));
620             channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_VOLUMEABS);
621             updateState(channel, new DecimalType(volumeState));
622         }
623
624         if (volumeDbState != -90f) {
625             channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_VOLUMEDB);
626             updateState(channel, new QuantityType<>(volumeDbState, Units.DECIBEL));
627         }
628
629         if (presetNumber != 0) {
630             logger.trace("Preset detected: {}", presetNumber);
631             updatePresets(presetNumber);
632         }
633
634         if ("true".equals(playInfoUpdated)) {
635             updateNetUSBPlayer();
636         }
637
638         if (!statusUpdated.isEmpty()) {
639             updateStatusZone(zoneToUpdate);
640         }
641         if (playTime != 0) {
642             channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYTIME);
643             updateState(channel, StringType.valueOf(String.valueOf(playTime)));
644         }
645         if ("true".equals(distInfoUpdated)) {
646             updateMCLinkStatus();
647         }
648     }
649
650     private void updateStatusZone(String zoneToUpdate) {
651         String localZone = "";
652         tmpString = getStatus(this.host, zoneToUpdate);
653         @Nullable
654         Status targetObject = gson.fromJson(tmpString, Status.class);
655         if (targetObject != null) {
656             String responseCode = targetObject.getResponseCode();
657             String powerState = targetObject.getPower();
658             String muteState = targetObject.getMute();
659             volumeState = targetObject.getVolume();
660             volumeDbState = targetObject.getVolumeDb();
661             maxVolumeState = targetObject.getMaxVolume();
662             inputState = targetObject.getInput();
663             soundProgramState = targetObject.getSoundProgram();
664             sleepState = targetObject.getSleep();
665
666             logger.trace("{} - Response: {}", zoneToUpdate, responseCode);
667             logger.trace("{} - Power: {}", zoneToUpdate, powerState);
668             logger.trace("{} - Mute: {}", zoneToUpdate, muteState);
669             logger.trace("{} - Volume: {}", zoneToUpdate, volumeState);
670             logger.trace("{} - Volume in dB: {}", zoneToUpdate, volumeDbState);
671             logger.trace("{} - Max Volume: {}", zoneToUpdate, maxVolumeState);
672             logger.trace("{} - Input: {}", zoneToUpdate, inputState);
673             logger.trace("{} - Soundprogram: {}", zoneToUpdate, soundProgramState);
674             logger.trace("{} - Sleep: {}", zoneToUpdate, sleepState);
675
676             switch (responseCode) {
677                 case "0":
678                     for (Channel channel : getThing().getChannels()) {
679                         ChannelUID channelUID = channel.getUID();
680                         channelWithoutGroup = channelUID.getIdWithoutGroup();
681                         localZone = channelUID.getGroupId();
682                         if (localZone != null) {
683                             if (isLinked(channelUID)) {
684                                 switch (channelWithoutGroup) {
685                                     case CHANNEL_POWER:
686                                         if ("on".equals(powerState)) {
687                                             if (localZone.equals(zoneToUpdate)) {
688                                                 updateState(channelUID, OnOffType.ON);
689                                             }
690                                         } else if ("standby".equals(powerState)) {
691                                             if (localZone.equals(zoneToUpdate)) {
692                                                 updateState(channelUID, OnOffType.OFF);
693                                             }
694                                         }
695                                         break;
696                                     case CHANNEL_MUTE:
697                                         if ("true".equals(muteState)) {
698                                             if (localZone.equals(zoneToUpdate)) {
699                                                 updateState(channelUID, OnOffType.ON);
700                                             }
701                                         } else if ("false".equals(muteState)) {
702                                             if (localZone.equals(zoneToUpdate)) {
703                                                 updateState(channelUID, OnOffType.OFF);
704                                             }
705                                         }
706                                         break;
707                                     case CHANNEL_VOLUME:
708                                         if (localZone.equals(zoneToUpdate)) {
709                                             updateState(channelUID,
710                                                     new PercentType((volumeState * 100) / maxVolumeState));
711                                         }
712                                         break;
713                                     case CHANNEL_VOLUMEABS:
714                                         if (localZone.equals(zoneToUpdate)) {
715                                             updateState(channelUID, new DecimalType(volumeState));
716                                         }
717                                         break;
718                                     case CHANNEL_VOLUMEDB:
719                                         if (localZone.equals(zoneToUpdate)) {
720                                             updateState(channelUID, new QuantityType<>(volumeDbState, Units.DECIBEL));
721                                         }
722                                         break;
723                                     case CHANNEL_INPUT:
724                                         if (localZone.equals(zoneToUpdate)) {
725                                             updateState(channelUID, StringType.valueOf(inputState));
726                                         }
727                                         break;
728                                     case CHANNEL_SOUNDPROGRAM:
729                                         if (localZone.equals(zoneToUpdate)) {
730                                             updateState(channelUID, StringType.valueOf(soundProgramState));
731                                         }
732                                         break;
733                                     case CHANNEL_SLEEP:
734                                         if (localZone.equals(zoneToUpdate)) {
735                                             updateState(channelUID, new DecimalType(sleepState));
736                                         }
737                                         break;
738                                 } // END switch (channelWithoutGroup)
739                             } // END IsLinked
740                         }
741                     }
742                     break;
743                 case "999":
744                     logger.trace("Nothing to do! - {} ({})", thingLabel, zoneToUpdate);
745                     break;
746             }
747         }
748     }
749
750     private void updatePresets(int value) {
751         String inputText = "";
752         int presetCounter = 0;
753         int currentPreset = 0;
754         tmpString = getPresetInfo(this.host);
755
756         PresetInfo presetinfo = gson.fromJson(tmpString, PresetInfo.class);
757         if (presetinfo != null) {
758             String responseCode = presetinfo.getResponseCode();
759             if ("0".equals(responseCode)) {
760                 List<StateOption> optionsPresets = new ArrayList<>();
761                 inputText = getLastInput();
762                 if (inputText != null) {
763                     for (JsonElement pr : presetinfo.getPresetInfo()) {
764                         presetCounter = presetCounter + 1;
765                         JsonObject presetObject = pr.getAsJsonObject();
766                         String text = presetObject.get("text").getAsString();
767                         if (!"".equals(text)) {
768                             optionsPresets.add(new StateOption(String.valueOf(presetCounter),
769                                     "#" + String.valueOf(presetCounter) + " " + text));
770                             if (inputText.equals(text)) {
771                                 currentPreset = presetCounter;
772                             }
773                         }
774                     }
775                 }
776                 if (value != 0) {
777                     currentPreset = value;
778                 }
779                 for (Channel channel : getThing().getChannels()) {
780                     ChannelUID channelUID = channel.getUID();
781                     channelWithoutGroup = channelUID.getIdWithoutGroup();
782                     if (isLinked(channelUID)) {
783                         switch (channelWithoutGroup) {
784                             case CHANNEL_SELECTPRESET:
785                                 stateDescriptionProvider.setStateOptions(channelUID, optionsPresets);
786                                 updateState(channelUID, StringType.valueOf(String.valueOf(currentPreset)));
787                                 break;
788                         }
789                     }
790                 }
791             }
792         }
793     }
794
795     private void updateNetUSBPlayer() {
796         tmpString = getPlayInfo(this.host);
797
798         @Nullable
799         PlayInfo targetObject = gson.fromJson(tmpString, PlayInfo.class);
800         if (targetObject != null) {
801             String responseCode = targetObject.getResponseCode();
802             String playbackState = targetObject.getPlayback();
803             artistState = targetObject.getArtist();
804             trackState = targetObject.getTrack();
805             albumState = targetObject.getAlbum();
806             String albumArtUrlState = targetObject.getAlbumArtUrl();
807             repeatState = targetObject.getRepeat();
808             shuffleState = targetObject.getShuffle();
809             playTimeState = targetObject.getPlayTime();
810             totalTimeState = targetObject.getTotalTime();
811
812             if ("0".equals(responseCode)) {
813                 ChannelUID testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYER);
814                 switch (playbackState) {
815                     case "play":
816                         updateState(testchannel, PlayPauseType.PLAY);
817                         break;
818                     case "stop":
819                         updateState(testchannel, PlayPauseType.PAUSE);
820                         break;
821                     case "pause":
822                         updateState(testchannel, PlayPauseType.PAUSE);
823                         break;
824                     case "fast_reverse":
825                         updateState(testchannel, RewindFastforwardType.REWIND);
826                         break;
827                     case "fast_forward":
828                         updateState(testchannel, RewindFastforwardType.FASTFORWARD);
829                         break;
830                 }
831                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ARTIST);
832                 updateState(testchannel, StringType.valueOf(artistState));
833                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TRACK);
834                 updateState(testchannel, StringType.valueOf(trackState));
835                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUM);
836                 updateState(testchannel, StringType.valueOf(albumState));
837                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUMART);
838                 if (!"".equals(albumArtUrlState)) {
839                     albumArtUrlState = HTTP + this.host + albumArtUrlState;
840                 }
841                 updateState(testchannel, StringType.valueOf(albumArtUrlState));
842                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_REPEAT);
843                 updateState(testchannel, StringType.valueOf(repeatState));
844                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_SHUFFLE);
845                 updateState(testchannel, StringType.valueOf(shuffleState));
846                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYTIME);
847                 updateState(testchannel, StringType.valueOf(String.valueOf(playTimeState)));
848                 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TOTALTIME);
849                 updateState(testchannel, StringType.valueOf(String.valueOf(totalTimeState)));
850             }
851         }
852     }
853
854     private @Nullable String getLastInput() {
855         String text = "";
856         tmpString = getRecentInfo(this.host);
857         RecentInfo recentinfo = gson.fromJson(tmpString, RecentInfo.class);
858         if (recentinfo != null) {
859             String responseCode = recentinfo.getResponseCode();
860             if ("0".equals(responseCode)) {
861                 for (JsonElement ri : recentinfo.getRecentInfo()) {
862                     JsonObject recentObject = ri.getAsJsonObject();
863                     text = recentObject.get("text").getAsString();
864                     break;
865                 }
866             }
867         }
868         return text;
869     }
870
871     private String connectedServer() {
872         DistributionInfo distributioninfo = new DistributionInfo();
873         Bridge bridge = getBridge();
874         String remotehost = "";
875         String result = "";
876         String localHost = "";
877         if (bridge != null) {
878             for (Thing thing : bridge.getThings()) {
879                 remotehost = thing.getConfiguration().get("host").toString();
880                 tmpString = getDistributionInfo(remotehost);
881                 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
882                 if (distributioninfo != null) {
883                     String localRole = distributioninfo.getRole();
884                     if ("server".equals(localRole)) {
885                         for (JsonElement ip : distributioninfo.getClientList()) {
886                             JsonObject clientObject = ip.getAsJsonObject();
887                             localHost = getThing().getConfiguration().get("host").toString();
888                             if (localHost.equals(clientObject.get("ip_address").getAsString())) {
889                                 result = remotehost;
890                                 break;
891                             }
892                         }
893                     }
894                 }
895             }
896         }
897         return result;
898     }
899
900     private void fillOptionsForMCLink() {
901         Bridge bridge = getBridge();
902         String host = "";
903         String label = "";
904         int zonesPerHost = 1;
905         int clients = 0;
906         tmpString = getDistributionInfo(this.host);
907         DistributionInfo targetObject = gson.fromJson(tmpString, DistributionInfo.class);
908         if (targetObject != null) {
909             clients = targetObject.getClientList().size();
910         }
911
912         List<StateOption> options = new ArrayList<>();
913         // first add 3 options for MC Link
914         options.add(new StateOption("", "Standalone"));
915         options.add(new StateOption("server", "Server: " + clients + " clients"));
916         options.add(new StateOption("client", "Client"));
917
918         if (bridge != null) {
919             for (Thing thing : bridge.getThings()) {
920                 label = thing.getLabel();
921                 host = thing.getConfiguration().get("host").toString();
922                 logger.trace("Thing found on Bridge: {} - {}", label, host);
923                 zonesPerHost = getNumberOfZones(host);
924                 for (int i = 1; i <= zonesPerHost; i++) {
925                     switch (i) {
926                         case 1:
927                             options.add(new StateOption(host + "***main", label + " - main (" + host + ")"));
928                             break;
929                         case 2:
930                             options.add(new StateOption(host + "***zone2", label + " - zone2 (" + host + ")"));
931                             break;
932                         case 3:
933                             options.add(new StateOption(host + "***zone3", label + " - zone3 (" + host + ")"));
934                             break;
935                         case 4:
936                             options.add(new StateOption(host + "***zone4", label + " - zone4 (" + host + ")"));
937                             break;
938                     }
939                 }
940
941             }
942         }
943         // for each zone of the device, set all the possible combinations
944         ChannelUID testchannel;
945         for (int i = 1; i <= zoneNum; i++) {
946             switch (i) {
947                 case 1:
948                     testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
949                     if (isLinked(testchannel)) {
950                         stateDescriptionProvider.setStateOptions(testchannel, options);
951                     }
952                     break;
953                 case 2:
954                     testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
955                     if (isLinked(testchannel)) {
956                         stateDescriptionProvider.setStateOptions(testchannel, options);
957                     }
958                     break;
959                 case 3:
960                     testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
961                     if (isLinked(testchannel)) {
962                         stateDescriptionProvider.setStateOptions(testchannel, options);
963                     }
964                     break;
965                 case 4:
966                     testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
967                     if (isLinked(testchannel)) {
968                         stateDescriptionProvider.setStateOptions(testchannel, options);
969                     }
970                     break;
971             }
972         }
973     }
974
975     private String generateGroupId() {
976         return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
977     }
978
979     private int getNumberOfZones(@Nullable String host) {
980         int numberOfZones = 0;
981         tmpString = getFeatures(host);
982         @Nullable
983         Features targetObject = gson.fromJson(tmpString, Features.class);
984         if (targetObject != null) {
985             responseCode = targetObject.getResponseCode();
986             if ("0".equals(responseCode)) {
987                 numberOfZones = targetObject.getSystem().getZoneNum();
988             }
989         }
990         return numberOfZones;
991     }
992
993     public @Nullable String getDeviceId() {
994         tmpString = getDeviceInfo(this.host);
995         String localValueToCheck = "";
996         @Nullable
997         DeviceInfo targetObject = gson.fromJson(tmpString, DeviceInfo.class);
998         if (targetObject != null) {
999             localValueToCheck = targetObject.getDeviceId();
1000         }
1001         return localValueToCheck;
1002     }
1003
1004     private void setVolumeLinkedDevice(int value, @Nullable String zone, String host) {
1005         logger.trace("setVolumeLinkedDevice: {}", host);
1006         int zoneNumLinkedDevice = getNumberOfZones(host);
1007         int maxVolumeLinkedDevice = 0;
1008         @Nullable
1009         Status targetObject = new Status();
1010         int newVolume = 0;
1011         for (int i = 1; i <= zoneNumLinkedDevice; i++) {
1012             switch (i) {
1013                 case 1:
1014                     tmpString = getStatus(host, "main");
1015                     targetObject = gson.fromJson(tmpString, Status.class);
1016                     if (targetObject != null) {
1017                         responseCode = targetObject.getResponseCode();
1018                         maxVolumeLinkedDevice = targetObject.getMaxVolume();
1019                         newVolume = maxVolumeLinkedDevice * value / 100;
1020                         setVolume(newVolume, "main", host);
1021                     }
1022                     break;
1023                 case 2:
1024                     tmpString = getStatus(host, "zone2");
1025                     targetObject = gson.fromJson(tmpString, Status.class);
1026                     if (targetObject != null) {
1027                         responseCode = targetObject.getResponseCode();
1028                         maxVolumeLinkedDevice = targetObject.getMaxVolume();
1029                         newVolume = maxVolumeLinkedDevice * value / 100;
1030                         setVolume(newVolume, "zone2", host);
1031                     }
1032                     break;
1033                 case 3:
1034                     tmpString = getStatus(host, "zone3");
1035                     targetObject = gson.fromJson(tmpString, Status.class);
1036                     if (targetObject != null) {
1037                         responseCode = targetObject.getResponseCode();
1038                         maxVolumeLinkedDevice = targetObject.getMaxVolume();
1039                         newVolume = maxVolumeLinkedDevice * value / 100;
1040                         setVolume(newVolume, "zone3", host);
1041                     }
1042                     break;
1043                 case 4:
1044                     tmpString = getStatus(host, "zone4");
1045                     targetObject = gson.fromJson(tmpString, Status.class);
1046                     if (targetObject != null) {
1047                         responseCode = targetObject.getResponseCode();
1048                         maxVolumeLinkedDevice = targetObject.getMaxVolume();
1049                         newVolume = maxVolumeLinkedDevice * value / 100;
1050                         setVolume(newVolume, "zone4", host);
1051                     }
1052                     break;
1053             }
1054         }
1055     }
1056
1057     private void setVolumeDbLinkedDevice(float value, @Nullable String zone, String host) {
1058         logger.trace("setVolumeDbLinkedDevice: {}", host);
1059         int zoneNumLinkedDevice = getNumberOfZones(host);
1060         for (int i = 1; i <= zoneNumLinkedDevice; i++) {
1061             switch (i) {
1062                 case 1:
1063                     setVolumeDb(value, "main", host);
1064                     break;
1065                 case 2:
1066                     setVolumeDb(value, "zone2", host);
1067                     break;
1068                 case 3:
1069                     setVolumeDb(value, "zone3", host);
1070                     break;
1071                 case 4:
1072                     setVolumeDb(value, "zone4", host);
1073                     break;
1074             }
1075         }
1076     }
1077
1078     public void updateMCLinkStatus() {
1079         tmpString = getDistributionInfo(this.host);
1080         @Nullable
1081         DistributionInfo targetObject = gson.fromJson(tmpString, DistributionInfo.class);
1082         if (targetObject != null) {
1083             String localRole = targetObject.getRole();
1084             groupId = targetObject.getGroupId();
1085             switch (localRole) {
1086                 case "none":
1087                     setMCLinkToStandalone();
1088                     break;
1089                 case "server":
1090                     setMCLinkToServer();
1091                     break;
1092                 case "client":
1093                     setMCLinkToClient();
1094                     break;
1095             }
1096         }
1097     }
1098
1099     private void setMCLinkToStandalone() {
1100         ChannelUID testchannel;
1101         for (int i = 1; i <= zoneNum; i++) {
1102             switch (i) {
1103                 case 1:
1104                     testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
1105                     updateState(testchannel, StringType.valueOf(""));
1106                     break;
1107                 case 2:
1108                     testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
1109                     updateState(testchannel, StringType.valueOf(""));
1110                     break;
1111                 case 3:
1112                     testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
1113                     updateState(testchannel, StringType.valueOf(""));
1114                     break;
1115                 case 4:
1116                     testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
1117                     updateState(testchannel, StringType.valueOf(""));
1118                     break;
1119             }
1120         }
1121     }
1122
1123     private void setMCLinkToClient() {
1124         ChannelUID testchannel;
1125         for (int i = 1; i <= zoneNum; i++) {
1126             switch (i) {
1127                 case 1:
1128                     testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
1129                     updateState(testchannel, StringType.valueOf("client"));
1130                     break;
1131                 case 2:
1132                     testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
1133                     updateState(testchannel, StringType.valueOf("client"));
1134                     break;
1135                 case 3:
1136                     testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
1137                     updateState(testchannel, StringType.valueOf("client"));
1138                     break;
1139                 case 4:
1140                     testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
1141                     updateState(testchannel, StringType.valueOf("client"));
1142                     break;
1143             }
1144         }
1145     }
1146
1147     private void setMCLinkToServer() {
1148         ChannelUID testchannel;
1149         for (int i = 1; i <= zoneNum; i++) {
1150             switch (i) {
1151                 case 1:
1152                     testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
1153                     updateState(testchannel, StringType.valueOf("server"));
1154                     break;
1155                 case 2:
1156                     testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
1157                     updateState(testchannel, StringType.valueOf("server"));
1158                     break;
1159                 case 3:
1160                     testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
1161                     updateState(testchannel, StringType.valueOf("server"));
1162                     break;
1163                 case 4:
1164                     testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
1165                     updateState(testchannel, StringType.valueOf("server"));
1166                     break;
1167             }
1168         }
1169     }
1170
1171     private String makeRequest(@Nullable String topicAVR, String url) {
1172         String response = "";
1173         try {
1174             response = HttpUtil.executeUrl("GET", HTTP + url, LONG_CONNECTION_TIMEOUT_MILLISEC);
1175             logger.trace("{} - {}", topicAVR, response);
1176             return response;
1177         } catch (IOException e) {
1178             logger.trace("IO Exception - {} - {}", topicAVR, e.getMessage());
1179             return "{\"response_code\":\"999\"}";
1180         }
1181     }
1182     // End Various functions
1183
1184     // API calls to AVR
1185
1186     // Start Zone Related
1187
1188     private @Nullable String getStatus(@Nullable String host, String zone) {
1189         return makeRequest("Status", host + YAMAHA_EXTENDED_CONTROL + zone + "/getStatus");
1190     }
1191
1192     private @Nullable String setPower(String value, @Nullable String zone, @Nullable String host) {
1193         return makeRequest("Power", host + YAMAHA_EXTENDED_CONTROL + zone + "/setPower?power=" + value);
1194     }
1195
1196     private @Nullable String setMute(String value, @Nullable String zone, @Nullable String host) {
1197         return makeRequest("Mute", host + YAMAHA_EXTENDED_CONTROL + zone + "/setMute?enable=" + value);
1198     }
1199
1200     private @Nullable String setVolume(int value, @Nullable String zone, @Nullable String host) {
1201         return makeRequest("Volume", host + YAMAHA_EXTENDED_CONTROL + zone + "/setVolume?volume=" + value);
1202     }
1203
1204     /**
1205      * Sets the volume in decibels (dB).
1206      *
1207      * @param value volume in dB (decibels)
1208      * @param zone name of zone
1209      * @param host hostname or ip address
1210      * @return HTTP request
1211      */
1212     private @Nullable String setVolumeDb(float value, @Nullable String zone, @Nullable String host) {
1213         float volumeDbMin = Float.parseFloat(getThing().getConfiguration().get("volumeDbMin").toString());
1214         float volumeDbMax = Float.parseFloat(getThing().getConfiguration().get("volumeDbMax").toString());
1215         if (value < volumeDbMin) {
1216             value = volumeDbMin;
1217         }
1218         if (value > volumeDbMax) {
1219             value = volumeDbMax;
1220         }
1221
1222         // Yamaha accepts only integer values with .0 or .5 at the end only (-20.5dB, -20.0dB) - at least on RX-S601D.
1223         // The order matters here. We want to cast to integer first and then scale by 10.
1224         // Effectively we're only allowing dB values with .0 at the end.
1225         logger.trace("setVolumeDb: {} dB", value);
1226         return makeRequest("Volume", host + YAMAHA_EXTENDED_CONTROL + zone + "/setActualVolume?mode=db&value=" + value);
1227     }
1228
1229     private @Nullable String setInput(String value, @Nullable String zone, @Nullable String host) {
1230         return makeRequest("setInput", host + YAMAHA_EXTENDED_CONTROL + zone + "/setInput?input=" + value);
1231     }
1232
1233     private @Nullable String setSoundProgram(String value, @Nullable String zone, @Nullable String host) {
1234         return makeRequest("setSoundProgram",
1235                 host + YAMAHA_EXTENDED_CONTROL + zone + "/setSoundProgram?program=" + value);
1236     }
1237
1238     private @Nullable String setPreset(String value, @Nullable String zone, @Nullable String host) {
1239         return makeRequest("setPreset",
1240                 host + YAMAHA_EXTENDED_CONTROL + "netusb/recallPreset?zone=" + zone + "&num=" + value);
1241     }
1242
1243     private @Nullable String setSleep(String value, @Nullable String zone, @Nullable String host) {
1244         return makeRequest("setSleep", host + YAMAHA_EXTENDED_CONTROL + zone + "/setSleep?sleep=" + value);
1245     }
1246
1247     private @Nullable String recallScene(String value, @Nullable String zone, @Nullable String host) {
1248         return makeRequest("recallScene", host + YAMAHA_EXTENDED_CONTROL + zone + "/recallScene?num=" + value);
1249     }
1250     // End Zone Related
1251
1252     // Start Net Radio/USB Related
1253
1254     private @Nullable String getPresetInfo(@Nullable String host) {
1255         return makeRequest("PresetInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getPresetInfo");
1256     }
1257
1258     private @Nullable String getRecentInfo(@Nullable String host) {
1259         return makeRequest("RecentInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getRecentInfo");
1260     }
1261
1262     private @Nullable String getPlayInfo(@Nullable String host) {
1263         return makeRequest("PlayInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getPlayInfo");
1264     }
1265
1266     private @Nullable String setPlayback(String value, @Nullable String host) {
1267         return makeRequest("Playback", host + YAMAHA_EXTENDED_CONTROL + "netusb/setPlayback?playback=" + value);
1268     }
1269
1270     private @Nullable String setRepeat(String value, @Nullable String host) {
1271         return makeRequest("Repeat", host + YAMAHA_EXTENDED_CONTROL + "netusb/setRepeat?mode=" + value);
1272     }
1273
1274     private @Nullable String setShuffle(String value, @Nullable String host) {
1275         return makeRequest("Shuffle", host + YAMAHA_EXTENDED_CONTROL + "netusb/setShuffle?mode=" + value);
1276     }
1277
1278     // End Net Radio/USB Related
1279
1280     // Start Music Cast API calls
1281     private @Nullable String getDistributionInfo(@Nullable String host) {
1282         return makeRequest("DistributionInfo", host + YAMAHA_EXTENDED_CONTROL + "dist/getDistributionInfo");
1283     }
1284
1285     private @Nullable String setClientServerInfo(@Nullable String host, String json, String type) {
1286         InputStream is = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
1287         try {
1288             url = "http://" + host + YAMAHA_EXTENDED_CONTROL + "dist/" + type;
1289             httpResponse = HttpUtil.executeUrl("POST", url, is, "", LONG_CONNECTION_TIMEOUT_MILLISEC);
1290             logger.trace("MC Link/Unlink Client {}", httpResponse);
1291             return httpResponse;
1292         } catch (IOException e) {
1293             logger.trace("IO Exception - {} - {}", type, e.getMessage());
1294             return "{\"response_code\":\"999\"}";
1295         }
1296     }
1297
1298     private @Nullable String startDistribution(@Nullable String host) {
1299         Random ran = new Random();
1300         int nxt = ran.nextInt(200000);
1301         return makeRequest("StartDistribution", host + YAMAHA_EXTENDED_CONTROL + "dist/startDistribution?num=" + nxt);
1302     }
1303
1304     // End Music Cast API calls
1305
1306     // Start General/System API calls
1307
1308     private @Nullable String getFeatures(@Nullable String host) {
1309         return makeRequest("Features", host + YAMAHA_EXTENDED_CONTROL + "system/getFeatures");
1310     }
1311
1312     private @Nullable String getDeviceInfo(@Nullable String host) {
1313         return makeRequest("DeviceInfo", host + YAMAHA_EXTENDED_CONTROL + "system/getDeviceInfo");
1314     }
1315
1316     private void keepUdpEventsAlive(@Nullable String host) {
1317         Properties appProps = new Properties();
1318         appProps.setProperty("X-AppName", "MusicCast/1");
1319         appProps.setProperty("X-AppPort", "41100");
1320         try {
1321             httpResponse = HttpUtil.executeUrl("GET", HTTP + host + YAMAHA_EXTENDED_CONTROL + "netusb/getPlayInfo",
1322                     appProps, null, "", LONG_CONNECTION_TIMEOUT_MILLISEC);
1323             // logger.trace("{}", httpResponse);
1324             logger.trace("{} - {}", "UDP task", httpResponse);
1325         } catch (IOException e) {
1326             logger.trace("UDP refresh failed - {}", e.getMessage());
1327         }
1328     }
1329     // End General/System API calls
1330 }