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