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