2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.yamahamusiccast.internal;
15 import static org.openhab.binding.yamahamusiccast.internal.YamahaMusiccastBindingConstants.*;
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;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.yamahamusiccast.internal.dto.DeviceInfo;
33 import org.openhab.binding.yamahamusiccast.internal.dto.DistributionInfo;
34 import org.openhab.binding.yamahamusiccast.internal.dto.Features;
35 import org.openhab.binding.yamahamusiccast.internal.dto.PlayInfo;
36 import org.openhab.binding.yamahamusiccast.internal.dto.PresetInfo;
37 import org.openhab.binding.yamahamusiccast.internal.dto.RecentInfo;
38 import org.openhab.binding.yamahamusiccast.internal.dto.Response;
39 import org.openhab.binding.yamahamusiccast.internal.dto.Status;
40 import org.openhab.binding.yamahamusiccast.internal.dto.UdpMessage;
41 import org.openhab.core.io.net.http.HttpUtil;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.NextPreviousType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.PercentType;
46 import org.openhab.core.library.types.PlayPauseType;
47 import org.openhab.core.library.types.RewindFastforwardType;
48 import org.openhab.core.library.types.StringType;
49 import org.openhab.core.thing.Bridge;
50 import org.openhab.core.thing.Channel;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseThingHandler;
56 import org.openhab.core.thing.binding.builder.ChannelBuilder;
57 import org.openhab.core.thing.binding.builder.ThingBuilder;
58 import org.openhab.core.thing.type.ChannelTypeUID;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.RefreshType;
61 import org.openhab.core.types.StateOption;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
65 import com.google.gson.Gson;
66 import com.google.gson.JsonElement;
67 import com.google.gson.JsonObject;
70 * The {@link YamahaMusiccastHandler} is responsible for handling commands, which are
71 * sent to one of the channels.
73 * @author Lennert Coopman - Initial contribution
76 public class YamahaMusiccastHandler extends BaseThingHandler {
77 private Gson gson = new Gson();
78 private Logger logger = LoggerFactory.getLogger(YamahaMusiccastHandler.class);
79 private @Nullable ScheduledFuture<?> generalHousekeepingTask;
80 private @Nullable String httpResponse;
81 private @Nullable String tmpString = "";
82 private int volumePercent = 0;
83 private int volumeAbsValue = 0;
84 private @Nullable String responseCode = "";
85 private int volumeState = 0;
86 private int maxVolumeState = 0;
87 private @Nullable String inputState = "";
88 private @Nullable String soundProgramState = "";
89 private int sleepState = 0;
90 private @Nullable String artistState = "";
91 private @Nullable String trackState = "";
92 private @Nullable String albumState = "";
93 private @Nullable String repeatState = "";
94 private @Nullable String shuffleState = "";
95 private int playTimeState = 0;
96 private int totalTimeState = 0;
97 private @Nullable String zone = "main";
98 private String channelWithoutGroup = "";
99 private @Nullable String thingLabel = "";
100 private @Nullable String mclinkSetupServer = "";
101 private @Nullable String mclinkSetupZone = "";
102 private String url = "";
103 private String json = "";
104 private String action = "";
105 private int zoneNum = 0;
106 private @Nullable String groupId = "";
107 private @Nullable String host;
108 public @Nullable String deviceId = "";
110 private YamahaMusiccastStateDescriptionProvider stateDescriptionProvider;
112 public YamahaMusiccastHandler(Thing thing, YamahaMusiccastStateDescriptionProvider stateDescriptionProvider) {
114 this.stateDescriptionProvider = stateDescriptionProvider;
118 public void handleCommand(ChannelUID channelUID, Command command) {
119 String localValueToCheck = "";
120 String localRole = "";
121 boolean localSyncVolume;
122 String localDefaultAfterMCLink = "";
123 String localRoleSelectedThing = "";
124 if (command != RefreshType.REFRESH) {
125 logger.trace("Handling command {} for channel {}", command, channelUID);
126 channelWithoutGroup = channelUID.getIdWithoutGroup();
127 zone = channelUID.getGroupId();
128 DistributionInfo distributioninfo = new DistributionInfo();
129 Response response = new Response();
130 switch (channelWithoutGroup) {
132 if (command == OnOffType.ON) {
133 httpResponse = setPower("on", zone, this.host);
134 response = gson.fromJson(httpResponse, Response.class);
135 if (response != null) {
136 localValueToCheck = response.getResponseCode();
137 if (!"0".equals(localValueToCheck)) {
138 updateState(channelUID, OnOffType.OFF);
141 // check on scheduler task for UDP events
142 ScheduledFuture<?> localGeneralHousekeepingTask = generalHousekeepingTask;
143 if (localGeneralHousekeepingTask == null) {
144 logger.trace("YXC - No scheduler task found!");
145 generalHousekeepingTask = scheduler.scheduleWithFixedDelay(this::generalHousekeeping, 5,
146 300, TimeUnit.SECONDS);
149 logger.trace("Scheduler task found!");
152 } else if (command == OnOffType.OFF) {
153 httpResponse = setPower("standby", zone, this.host);
154 response = gson.fromJson(httpResponse, Response.class);
156 if (response != null) {
157 localValueToCheck = response.getResponseCode();
158 if (!"0".equals(localValueToCheck)) {
159 updateState(channelUID, OnOffType.ON);
165 if (command == OnOffType.ON) {
166 httpResponse = setMute("true", zone, this.host);
167 response = gson.fromJson(httpResponse, Response.class);
168 if (response != null) {
169 localValueToCheck = response.getResponseCode();
170 if (!"0".equals(localValueToCheck)) {
171 updateState(channelUID, OnOffType.OFF);
174 } else if (command == OnOffType.OFF) {
175 httpResponse = setMute("false", zone, this.host);
176 response = gson.fromJson(httpResponse, Response.class);
177 if (response != null) {
178 localValueToCheck = response.getResponseCode();
179 if (!"0".equals(localValueToCheck)) {
180 updateState(channelUID, OnOffType.ON);
186 volumePercent = Integer.parseInt(command.toString().replace(".0", ""));
187 volumeAbsValue = (maxVolumeState * volumePercent) / 100;
188 setVolume(volumeAbsValue, zone, this.host);
189 localSyncVolume = Boolean.parseBoolean(getThing().getConfiguration().get("syncVolume").toString());
190 if (localSyncVolume == Boolean.TRUE) {
191 tmpString = getDistributionInfo(this.host);
192 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
193 if (distributioninfo != null) {
194 localRole = distributioninfo.getRole();
195 if ("server".equals(localRole)) {
196 for (JsonElement ip : distributioninfo.getClientList()) {
197 JsonObject clientObject = ip.getAsJsonObject();
198 setVolumeLinkedDevice(volumePercent, zone,
199 clientObject.get("ip_address").getAsString());
203 } // END config.syncVolume
205 case CHANNEL_VOLUMEABS:
206 volumeAbsValue = Integer.parseInt(command.toString().replace(".0", ""));
207 volumePercent = (volumeAbsValue / maxVolumeState) * 100;
208 setVolume(volumeAbsValue, zone, this.host);
209 localSyncVolume = Boolean.parseBoolean(getThing().getConfiguration().get("syncVolume").toString());
210 if (localSyncVolume == Boolean.TRUE) {
211 tmpString = getDistributionInfo(this.host);
212 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
213 if (distributioninfo != null) {
214 localRole = distributioninfo.getRole();
215 if ("server".equals(localRole)) {
216 for (JsonElement ip : distributioninfo.getClientList()) {
217 JsonObject clientObject = ip.getAsJsonObject();
218 setVolumeLinkedDevice(volumePercent, zone,
219 clientObject.get("ip_address").getAsString());
226 // if it is a client, disconnect it first.
227 tmpString = getDistributionInfo(this.host);
228 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
229 if (distributioninfo != null) {
230 localRole = distributioninfo.getRole();
231 if ("client".equals(localRole)) {
232 json = "{\"group_id\":\"\"}";
233 httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
236 setInput(command.toString(), zone, this.host);
238 case CHANNEL_SOUNDPROGRAM:
239 setSoundProgram(command.toString(), zone, this.host);
241 case CHANNEL_SELECTPRESET:
242 setPreset(command.toString(), zone, this.host);
245 if (command.equals(PlayPauseType.PLAY)) {
246 setPlayback("play", this.host);
247 } else if (command.equals(PlayPauseType.PAUSE)) {
248 setPlayback("pause", this.host);
249 } else if (command.equals(NextPreviousType.NEXT)) {
250 setPlayback("next", this.host);
251 } else if (command.equals(NextPreviousType.PREVIOUS)) {
252 setPlayback("previous", this.host);
253 } else if (command.equals(RewindFastforwardType.REWIND)) {
254 setPlayback("fast_reverse_start", this.host);
255 } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
256 setPlayback("fast_forward_end", this.host);
260 setSleep(command.toString(), zone, this.host);
262 case CHANNEL_MCLINKSTATUS:
265 tmpString = getDistributionInfo(this.host);
266 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
267 if (distributioninfo != null) {
268 responseCode = distributioninfo.getResponseCode();
269 localRole = distributioninfo.getRole();
270 if (command.toString().equals("")) {
272 groupId = distributioninfo.getGroupId();
273 } else if (command.toString().contains("***")) {
275 String[] parts = command.toString().split("\\*\\*\\*");
276 if (parts.length > 1) {
277 mclinkSetupServer = parts[0];
278 mclinkSetupZone = parts[1];
279 tmpString = getDistributionInfo(mclinkSetupServer);
280 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
281 if (distributioninfo != null) {
282 responseCode = distributioninfo.getResponseCode();
283 localRoleSelectedThing = distributioninfo.getRole();
284 groupId = distributioninfo.getGroupId();
285 if (localRoleSelectedThing != null) {
286 if ("server".equals(localRoleSelectedThing)) {
287 groupId = distributioninfo.getGroupId();
288 } else if ("client".equals(localRoleSelectedThing)) {
290 } else if ("none".equals(localRoleSelectedThing)) {
291 groupId = generateGroupId();
298 if ("unlink".equals(action)) {
299 json = "{\"group_id\":\"\"}";
300 if (localRole != null) {
301 if ("server".equals(localRole)) {
302 httpResponse = setClientServerInfo(this.host, json, "setServerInfo");
303 // Set GroupId = "" for linked clients
304 if (distributioninfo != null) {
305 for (JsonElement ip : distributioninfo.getClientList()) {
306 JsonObject clientObject = ip.getAsJsonObject();
307 setClientServerInfo(clientObject.get("ip_address").getAsString(), json,
311 } else if ("client".equals(localRole)) {
312 mclinkSetupServer = connectedServer();
313 // Step 1. empty group on client
314 httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
315 // empty zone to respect defaults
316 if (!"".equals(mclinkSetupServer)) {
317 // Step 2. remove client from server
318 json = "{\"group_id\":\"" + groupId
319 + "\", \"type\":\"remove\", \"client_list\":[\"" + this.host + "\"]}";
320 httpResponse = setClientServerInfo(mclinkSetupServer, json, "setServerInfo");
321 // Step 3. reflect changes to master
322 httpResponse = startDistribution(mclinkSetupServer);
323 localDefaultAfterMCLink = getThing().getConfiguration()
324 .get("defaultAfterMCLink").toString();
325 httpResponse = setInput(localDefaultAfterMCLink.toString(), zone, this.host);
326 } else if ("".equals(mclinkSetupServer)) {
327 // fallback in case client is removed from group by ending group on server side
328 localDefaultAfterMCLink = getThing().getConfiguration()
329 .get("defaultAfterMCLink").toString();
330 httpResponse = setInput(localDefaultAfterMCLink.toString(), zone, this.host);
334 } else if ("link".equals(action)) {
335 if (localRole != null) {
336 if ("none".equals(localRole)) {
337 json = "{\"group_id\":\"" + groupId + "\", \"zone\":\"" + mclinkSetupZone
338 + "\", \"type\":\"add\", \"client_list\":[\"" + this.host + "\"]}";
339 logger.trace("setServerInfo json: {}", json);
340 httpResponse = setClientServerInfo(mclinkSetupServer, json, "setServerInfo");
341 // All zones of Model are required for MC Link
343 for (int i = 1; i <= zoneNum; i++) {
346 tmpString = "\"main\"";
349 tmpString = tmpString + ", \"zone2\"";
352 tmpString = tmpString + ", \"zone3\"";
355 tmpString = tmpString + ", \"zone4\"";
359 json = "{\"group_id\":\"" + groupId + "\", \"zone\":[" + tmpString + "]}";
360 logger.trace("setClientInfo json: {}", json);
361 httpResponse = setClientServerInfo(this.host, json, "setClientInfo");
362 httpResponse = startDistribution(mclinkSetupServer);
367 updateMCLinkStatus();
369 case CHANNEL_RECALLSCENE:
370 recallScene(command.toString(), zone, this.host);
373 setRepeat(command.toString(), this.host);
375 case CHANNEL_SHUFFLE:
376 setShuffle(command.toString(), this.host);
378 } // END Switch Channel
383 public void initialize() {
384 String localHost = "";
385 thingLabel = thing.getLabel();
386 updateStatus(ThingStatus.UNKNOWN);
387 localHost = getThing().getConfiguration().get("host").toString();
388 this.host = localHost;
389 if (!"".equals(this.host)) {
390 zoneNum = getNumberOfZones(this.host);
391 logger.trace("Zones found: {} - {}", zoneNum, thingLabel);
395 generalHousekeepingTask = scheduler.scheduleWithFixedDelay(this::generalHousekeeping, 5, 300,
397 updateStatus(ThingStatus.ONLINE);
399 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No host found");
404 private void generalHousekeeping() {
405 thingLabel = thing.getLabel();
406 logger.trace("YXC - Start Keep Alive UDP events (5 minutes - {}) ", thingLabel);
407 keepUdpEventsAlive(this.host);
408 fillOptionsForMCLink();
409 updateMCLinkStatus();
412 private void refreshOnStartup() {
413 for (int i = 1; i <= zoneNum; i++) {
416 createChannels("main");
417 updateStatusZone("main");
420 createChannels("zone2");
421 updateStatusZone("zone2");
424 createChannels("zone3");
425 updateStatusZone("zone3");
428 createChannels("zone4");
429 updateStatusZone("zone4");
434 updateNetUSBPlayer();
435 fillOptionsForMCLink();
436 updateMCLinkStatus();
440 public void dispose() {
441 ScheduledFuture<?> localGeneralHousekeepingTask = generalHousekeepingTask;
442 if (localGeneralHousekeepingTask != null) {
443 localGeneralHousekeepingTask.cancel(true);
449 private void createChannels(String zone) {
450 createChannel(zone, CHANNEL_POWER, CHANNEL_TYPE_UID_POWER, "Switch");
451 createChannel(zone, CHANNEL_MUTE, CHANNEL_TYPE_UID_MUTE, "Switch");
452 createChannel(zone, CHANNEL_VOLUME, CHANNEL_TYPE_UID_VOLUME, "Dimmer");
453 createChannel(zone, CHANNEL_VOLUMEABS, CHANNEL_TYPE_UID_VOLUMEABS, "Number");
454 createChannel(zone, CHANNEL_INPUT, CHANNEL_TYPE_UID_INPUT, "String");
455 createChannel(zone, CHANNEL_SOUNDPROGRAM, CHANNEL_TYPE_UID_SOUNDPROGRAM, "String");
456 createChannel(zone, CHANNEL_SLEEP, CHANNEL_TYPE_UID_SLEEP, "Number");
457 createChannel(zone, CHANNEL_SELECTPRESET, CHANNEL_TYPE_UID_SELECTPRESET, "String");
458 createChannel(zone, CHANNEL_RECALLSCENE, CHANNEL_TYPE_UID_RECALLSCENE, "Number");
459 createChannel(zone, CHANNEL_MCLINKSTATUS, CHANNEL_TYPE_UID_MCLINKSTATUS, "String");
462 private void createChannel(String zone, String channel, ChannelTypeUID channelTypeUID, String itemType) {
463 ChannelUID channelToCheck = new ChannelUID(thing.getUID(), zone, channel);
464 if (thing.getChannel(channelToCheck) == null) {
465 ThingBuilder thingBuilder = editThing();
466 Channel testchannel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), zone, channel), itemType)
467 .withType(channelTypeUID).build();
468 thingBuilder.withChannel(testchannel);
469 updateThing(thingBuilder.build());
473 private void powerOffCleanup() {
475 channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ARTIST);
476 updateState(channel, StringType.valueOf("-"));
477 channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TRACK);
478 updateState(channel, StringType.valueOf("-"));
479 channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUM);
480 updateState(channel, StringType.valueOf("-"));
483 public void processUDPEvent(String json, String trackingID) {
484 logger.trace("UDP package: {} (Tracking: {})", json, trackingID);
486 UdpMessage targetObject = gson.fromJson(json, UdpMessage.class);
487 if (targetObject != null) {
488 if (Objects.nonNull(targetObject.getMain())) {
489 updateStateFromUDPEvent("main", targetObject);
491 if (Objects.nonNull(targetObject.getZone2())) {
492 updateStateFromUDPEvent("zone2", targetObject);
494 if (Objects.nonNull(targetObject.getZone3())) {
495 updateStateFromUDPEvent("zone3", targetObject);
497 if (Objects.nonNull(targetObject.getZone4())) {
498 updateStateFromUDPEvent("zone4", targetObject);
500 if (Objects.nonNull(targetObject.getNetUSB())) {
501 updateStateFromUDPEvent("netusb", targetObject);
503 if (Objects.nonNull(targetObject.getDist())) {
504 updateStateFromUDPEvent("dist", targetObject);
509 private void updateStateFromUDPEvent(String zoneToUpdate, UdpMessage targetObject) {
511 String playInfoUpdated = "";
512 String statusUpdated = "";
513 String powerState = "";
514 String muteState = "";
515 String inputState = "";
517 int presetNumber = 0;
519 String distInfoUpdated = "";
520 logger.trace("Handling UDP for {}", zoneToUpdate);
521 switch (zoneToUpdate) {
523 powerState = targetObject.getMain().getPower();
524 muteState = targetObject.getMain().getMute();
525 inputState = targetObject.getMain().getInput();
526 volumeState = targetObject.getMain().getVolume();
527 statusUpdated = targetObject.getMain().getstatusUpdated();
530 powerState = targetObject.getZone2().getPower();
531 muteState = targetObject.getZone2().getMute();
532 inputState = targetObject.getZone2().getInput();
533 volumeState = targetObject.getZone2().getVolume();
534 statusUpdated = targetObject.getZone2().getstatusUpdated();
537 powerState = targetObject.getZone3().getPower();
538 muteState = targetObject.getZone3().getMute();
539 inputState = targetObject.getZone3().getInput();
540 volumeState = targetObject.getZone3().getVolume();
541 statusUpdated = targetObject.getZone3().getstatusUpdated();
544 powerState = targetObject.getZone4().getPower();
545 muteState = targetObject.getZone4().getMute();
546 inputState = targetObject.getZone4().getInput();
547 volumeState = targetObject.getZone4().getVolume();
548 statusUpdated = targetObject.getZone4().getstatusUpdated();
551 if (Objects.isNull(targetObject.getNetUSB().getPresetControl())) {
554 presetNumber = targetObject.getNetUSB().getPresetControl().getNum();
556 playInfoUpdated = targetObject.getNetUSB().getPlayInfoUpdated();
557 playTime = targetObject.getNetUSB().getPlayTime();
558 // totalTime is not in UDP event
561 distInfoUpdated = targetObject.getDist().getDistInfoUpdated();
565 if (!powerState.isEmpty()) {
566 channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_POWER);
567 if ("on".equals(powerState)) {
568 updateState(channel, OnOffType.ON);
569 } else if ("standby".equals(powerState)) {
570 updateState(channel, OnOffType.OFF);
575 if (!muteState.isEmpty()) {
576 channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_MUTE);
577 if ("true".equals(muteState)) {
578 updateState(channel, OnOffType.ON);
579 } else if ("false".equals(muteState)) {
580 updateState(channel, OnOffType.OFF);
584 if (!inputState.isEmpty()) {
585 channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_INPUT);
586 updateState(channel, StringType.valueOf(inputState));
589 if (volumeState != 0) {
590 channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_VOLUME);
591 updateState(channel, new PercentType((volumeState * 100) / maxVolumeState));
592 channel = new ChannelUID(getThing().getUID(), zoneToUpdate, CHANNEL_VOLUMEABS);
593 updateState(channel, new DecimalType(volumeState));
596 if (presetNumber != 0) {
597 logger.trace("Preset detected: {}", presetNumber);
598 updatePresets(presetNumber);
601 if ("true".equals(playInfoUpdated)) {
602 updateNetUSBPlayer();
605 if (!statusUpdated.isEmpty()) {
606 updateStatusZone(zoneToUpdate);
609 channel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYTIME);
610 updateState(channel, StringType.valueOf(String.valueOf(playTime)));
612 if ("true".equals(distInfoUpdated)) {
613 updateMCLinkStatus();
617 private void updateStatusZone(String zoneToUpdate) {
618 String localZone = "";
619 tmpString = getStatus(this.host, zoneToUpdate);
621 Status targetObject = gson.fromJson(tmpString, Status.class);
622 if (targetObject != null) {
623 String responseCode = targetObject.getResponseCode();
624 String powerState = targetObject.getPower();
625 String muteState = targetObject.getMute();
626 volumeState = targetObject.getVolume();
627 maxVolumeState = targetObject.getMaxVolume();
628 inputState = targetObject.getInput();
629 soundProgramState = targetObject.getSoundProgram();
630 sleepState = targetObject.getSleep();
632 logger.trace("{} - Response: {}", zoneToUpdate, responseCode);
633 logger.trace("{} - Power: {}", zoneToUpdate, powerState);
634 logger.trace("{} - Mute: {}", zoneToUpdate, muteState);
635 logger.trace("{} - Volume: {}", zoneToUpdate, volumeState);
636 logger.trace("{} - Max Volume: {}", zoneToUpdate, maxVolumeState);
637 logger.trace("{} - Input: {}", zoneToUpdate, inputState);
638 logger.trace("{} - Soundprogram: {}", zoneToUpdate, soundProgramState);
639 logger.trace("{} - Sleep: {}", zoneToUpdate, sleepState);
641 switch (responseCode) {
643 for (Channel channel : getThing().getChannels()) {
644 ChannelUID channelUID = channel.getUID();
645 channelWithoutGroup = channelUID.getIdWithoutGroup();
646 localZone = channelUID.getGroupId();
647 if (localZone != null) {
648 if (isLinked(channelUID)) {
649 switch (channelWithoutGroup) {
651 if ("on".equals(powerState)) {
652 if (localZone.equals(zoneToUpdate)) {
653 updateState(channelUID, OnOffType.ON);
655 } else if ("standby".equals(powerState)) {
656 if (localZone.equals(zoneToUpdate)) {
657 updateState(channelUID, OnOffType.OFF);
662 if ("true".equals(muteState)) {
663 if (localZone.equals(zoneToUpdate)) {
664 updateState(channelUID, OnOffType.ON);
666 } else if ("false".equals(muteState)) {
667 if (localZone.equals(zoneToUpdate)) {
668 updateState(channelUID, OnOffType.OFF);
673 if (localZone.equals(zoneToUpdate)) {
674 updateState(channelUID,
675 new PercentType((volumeState * 100) / maxVolumeState));
678 case CHANNEL_VOLUMEABS:
679 if (localZone.equals(zoneToUpdate)) {
680 updateState(channelUID, new DecimalType(volumeState));
684 if (localZone.equals(zoneToUpdate)) {
685 updateState(channelUID, StringType.valueOf(inputState));
688 case CHANNEL_SOUNDPROGRAM:
689 if (localZone.equals(zoneToUpdate)) {
690 updateState(channelUID, StringType.valueOf(soundProgramState));
694 if (localZone.equals(zoneToUpdate)) {
695 updateState(channelUID, new DecimalType(sleepState));
698 } // END switch (channelWithoutGroup)
704 logger.trace("Nothing to do! - {} ({})", thingLabel, zoneToUpdate);
710 private void updatePresets(int value) {
711 String inputText = "";
712 int presetCounter = 0;
713 int currentPreset = 0;
714 tmpString = getPresetInfo(this.host);
716 PresetInfo presetinfo = gson.fromJson(tmpString, PresetInfo.class);
717 if (presetinfo != null) {
718 String responseCode = presetinfo.getResponseCode();
719 if ("0".equals(responseCode)) {
720 List<StateOption> optionsPresets = new ArrayList<>();
721 inputText = getLastInput();
722 if (inputText != null) {
723 for (JsonElement pr : presetinfo.getPresetInfo()) {
724 presetCounter = presetCounter + 1;
725 JsonObject presetObject = pr.getAsJsonObject();
726 String text = presetObject.get("text").getAsString();
727 if (!"".equals(text)) {
728 optionsPresets.add(new StateOption(String.valueOf(presetCounter),
729 "#" + String.valueOf(presetCounter) + " " + text));
730 if (inputText.equals(text)) {
731 currentPreset = presetCounter;
737 currentPreset = value;
739 for (Channel channel : getThing().getChannels()) {
740 ChannelUID channelUID = channel.getUID();
741 channelWithoutGroup = channelUID.getIdWithoutGroup();
742 if (isLinked(channelUID)) {
743 switch (channelWithoutGroup) {
744 case CHANNEL_SELECTPRESET:
745 stateDescriptionProvider.setStateOptions(channelUID, optionsPresets);
746 updateState(channelUID, StringType.valueOf(String.valueOf(currentPreset)));
755 private void updateNetUSBPlayer() {
756 tmpString = getPlayInfo(this.host);
759 PlayInfo targetObject = gson.fromJson(tmpString, PlayInfo.class);
760 if (targetObject != null) {
761 String responseCode = targetObject.getResponseCode();
762 String playbackState = targetObject.getPlayback();
763 artistState = targetObject.getArtist();
764 trackState = targetObject.getTrack();
765 albumState = targetObject.getAlbum();
766 String albumArtUrlState = targetObject.getAlbumArtUrl();
767 repeatState = targetObject.getRepeat();
768 shuffleState = targetObject.getShuffle();
769 playTimeState = targetObject.getPlayTime();
770 totalTimeState = targetObject.getTotalTime();
772 if ("0".equals(responseCode)) {
773 ChannelUID testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYER);
774 switch (playbackState) {
776 updateState(testchannel, PlayPauseType.PLAY);
779 updateState(testchannel, PlayPauseType.PAUSE);
782 updateState(testchannel, PlayPauseType.PAUSE);
785 updateState(testchannel, RewindFastforwardType.REWIND);
788 updateState(testchannel, RewindFastforwardType.FASTFORWARD);
791 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ARTIST);
792 updateState(testchannel, StringType.valueOf(artistState));
793 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TRACK);
794 updateState(testchannel, StringType.valueOf(trackState));
795 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUM);
796 updateState(testchannel, StringType.valueOf(albumState));
797 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_ALBUMART);
798 if (!"".equals(albumArtUrlState)) {
799 albumArtUrlState = HTTP + this.host + albumArtUrlState;
801 updateState(testchannel, StringType.valueOf(albumArtUrlState));
802 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_REPEAT);
803 updateState(testchannel, StringType.valueOf(repeatState));
804 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_SHUFFLE);
805 updateState(testchannel, StringType.valueOf(shuffleState));
806 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_PLAYTIME);
807 updateState(testchannel, StringType.valueOf(String.valueOf(playTimeState)));
808 testchannel = new ChannelUID(getThing().getUID(), "playerControls", CHANNEL_TOTALTIME);
809 updateState(testchannel, StringType.valueOf(String.valueOf(totalTimeState)));
814 private @Nullable String getLastInput() {
816 tmpString = getRecentInfo(this.host);
817 RecentInfo recentinfo = gson.fromJson(tmpString, RecentInfo.class);
818 if (recentinfo != null) {
819 String responseCode = recentinfo.getResponseCode();
820 if ("0".equals(responseCode)) {
821 for (JsonElement ri : recentinfo.getRecentInfo()) {
822 JsonObject recentObject = ri.getAsJsonObject();
823 text = recentObject.get("text").getAsString();
831 private String connectedServer() {
832 DistributionInfo distributioninfo = new DistributionInfo();
833 Bridge bridge = getBridge();
834 String remotehost = "";
836 String localHost = "";
837 if (bridge != null) {
838 for (Thing thing : bridge.getThings()) {
839 remotehost = thing.getConfiguration().get("host").toString();
840 tmpString = getDistributionInfo(remotehost);
841 distributioninfo = gson.fromJson(tmpString, DistributionInfo.class);
842 if (distributioninfo != null) {
843 String localRole = distributioninfo.getRole();
844 if ("server".equals(localRole)) {
845 for (JsonElement ip : distributioninfo.getClientList()) {
846 JsonObject clientObject = ip.getAsJsonObject();
847 localHost = getThing().getConfiguration().get("host").toString();
848 if (localHost.equals(clientObject.get("ip_address").getAsString())) {
860 private void fillOptionsForMCLink() {
861 Bridge bridge = getBridge();
864 int zonesPerHost = 1;
866 tmpString = getDistributionInfo(this.host);
867 DistributionInfo targetObject = gson.fromJson(tmpString, DistributionInfo.class);
868 if (targetObject != null) {
869 clients = targetObject.getClientList().size();
872 List<StateOption> options = new ArrayList<>();
873 // first add 3 options for MC Link
874 options.add(new StateOption("", "Standalone"));
875 options.add(new StateOption("server", "Server: " + clients + " clients"));
876 options.add(new StateOption("client", "Client"));
878 if (bridge != null) {
879 for (Thing thing : bridge.getThings()) {
880 label = thing.getLabel();
881 host = thing.getConfiguration().get("host").toString();
882 logger.trace("Thing found on Bridge: {} - {}", label, host);
883 zonesPerHost = getNumberOfZones(host);
884 for (int i = 1; i <= zonesPerHost; i++) {
887 options.add(new StateOption(host + "***main", label + " - main (" + host + ")"));
890 options.add(new StateOption(host + "***zone2", label + " - zone2 (" + host + ")"));
893 options.add(new StateOption(host + "***zone3", label + " - zone3 (" + host + ")"));
896 options.add(new StateOption(host + "***zone4", label + " - zone4 (" + host + ")"));
903 // for each zone of the device, set all the possible combinations
904 ChannelUID testchannel;
905 for (int i = 1; i <= zoneNum; i++) {
908 testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
909 if (isLinked(testchannel)) {
910 stateDescriptionProvider.setStateOptions(testchannel, options);
914 testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
915 if (isLinked(testchannel)) {
916 stateDescriptionProvider.setStateOptions(testchannel, options);
920 testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
921 if (isLinked(testchannel)) {
922 stateDescriptionProvider.setStateOptions(testchannel, options);
926 testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
927 if (isLinked(testchannel)) {
928 stateDescriptionProvider.setStateOptions(testchannel, options);
935 private String generateGroupId() {
936 return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
939 private int getNumberOfZones(@Nullable String host) {
940 int numberOfZones = 0;
941 tmpString = getFeatures(host);
943 Features targetObject = gson.fromJson(tmpString, Features.class);
944 if (targetObject != null) {
945 responseCode = targetObject.getResponseCode();
946 if ("0".equals(responseCode)) {
947 numberOfZones = targetObject.getSystem().getZoneNum();
950 return numberOfZones;
953 public @Nullable String getDeviceId() {
954 tmpString = getDeviceInfo(this.host);
955 String localValueToCheck = "";
957 DeviceInfo targetObject = gson.fromJson(tmpString, DeviceInfo.class);
958 if (targetObject != null) {
959 localValueToCheck = targetObject.getDeviceId();
961 return localValueToCheck;
964 private void setVolumeLinkedDevice(int value, @Nullable String zone, String host) {
965 logger.trace("setVolumeLinkedDevice: {}", host);
966 int zoneNumLinkedDevice = getNumberOfZones(host);
967 int maxVolumeLinkedDevice = 0;
969 Status targetObject = new Status();
971 for (int i = 1; i <= zoneNumLinkedDevice; i++) {
974 tmpString = getStatus(host, "main");
975 targetObject = gson.fromJson(tmpString, Status.class);
976 if (targetObject != null) {
977 responseCode = targetObject.getResponseCode();
978 maxVolumeLinkedDevice = targetObject.getMaxVolume();
979 newVolume = maxVolumeLinkedDevice * value / 100;
980 setVolume(newVolume, "main", host);
984 tmpString = getStatus(host, "zone2");
985 targetObject = gson.fromJson(tmpString, Status.class);
986 if (targetObject != null) {
987 responseCode = targetObject.getResponseCode();
988 maxVolumeLinkedDevice = targetObject.getMaxVolume();
989 newVolume = maxVolumeLinkedDevice * value / 100;
990 setVolume(newVolume, "zone2", host);
994 tmpString = getStatus(host, "zone3");
995 targetObject = gson.fromJson(tmpString, Status.class);
996 if (targetObject != null) {
997 responseCode = targetObject.getResponseCode();
998 maxVolumeLinkedDevice = targetObject.getMaxVolume();
999 newVolume = maxVolumeLinkedDevice * value / 100;
1000 setVolume(newVolume, "zone3", host);
1004 tmpString = getStatus(host, "zone4");
1005 targetObject = gson.fromJson(tmpString, Status.class);
1006 if (targetObject != null) {
1007 responseCode = targetObject.getResponseCode();
1008 maxVolumeLinkedDevice = targetObject.getMaxVolume();
1009 newVolume = maxVolumeLinkedDevice * value / 100;
1010 setVolume(newVolume, "zone4", host);
1017 public void updateMCLinkStatus() {
1018 tmpString = getDistributionInfo(this.host);
1020 DistributionInfo targetObject = gson.fromJson(tmpString, DistributionInfo.class);
1021 if (targetObject != null) {
1022 String localRole = targetObject.getRole();
1023 groupId = targetObject.getGroupId();
1024 switch (localRole) {
1026 setMCLinkToStandalone();
1029 setMCLinkToServer();
1032 setMCLinkToClient();
1038 private void setMCLinkToStandalone() {
1039 ChannelUID testchannel;
1040 for (int i = 1; i <= zoneNum; i++) {
1043 testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
1044 updateState(testchannel, StringType.valueOf(""));
1047 testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
1048 updateState(testchannel, StringType.valueOf(""));
1051 testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
1052 updateState(testchannel, StringType.valueOf(""));
1055 testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
1056 updateState(testchannel, StringType.valueOf(""));
1062 private void setMCLinkToClient() {
1063 ChannelUID testchannel;
1064 for (int i = 1; i <= zoneNum; i++) {
1067 testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
1068 updateState(testchannel, StringType.valueOf("client"));
1071 testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
1072 updateState(testchannel, StringType.valueOf("client"));
1075 testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
1076 updateState(testchannel, StringType.valueOf("client"));
1079 testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
1080 updateState(testchannel, StringType.valueOf("client"));
1086 private void setMCLinkToServer() {
1087 ChannelUID testchannel;
1088 for (int i = 1; i <= zoneNum; i++) {
1091 testchannel = new ChannelUID(getThing().getUID(), "main", CHANNEL_MCLINKSTATUS);
1092 updateState(testchannel, StringType.valueOf("server"));
1095 testchannel = new ChannelUID(getThing().getUID(), "zone2", CHANNEL_MCLINKSTATUS);
1096 updateState(testchannel, StringType.valueOf("server"));
1099 testchannel = new ChannelUID(getThing().getUID(), "zone3", CHANNEL_MCLINKSTATUS);
1100 updateState(testchannel, StringType.valueOf("server"));
1103 testchannel = new ChannelUID(getThing().getUID(), "zone4", CHANNEL_MCLINKSTATUS);
1104 updateState(testchannel, StringType.valueOf("server"));
1110 private String makeRequest(@Nullable String topicAVR, String url) {
1111 String response = "";
1113 response = HttpUtil.executeUrl("GET", HTTP + url, LONG_CONNECTION_TIMEOUT_MILLISEC);
1114 logger.trace("{} - {}", topicAVR, response);
1116 } catch (IOException e) {
1117 logger.trace("IO Exception - {} - {}", topicAVR, e.getMessage());
1118 return "{\"response_code\":\"999\"}";
1121 // End Various functions
1125 // Start Zone Related
1127 private @Nullable String getStatus(@Nullable String host, String zone) {
1128 return makeRequest("Status", host + YAMAHA_EXTENDED_CONTROL + zone + "/getStatus");
1131 private @Nullable String setPower(String value, @Nullable String zone, @Nullable String host) {
1132 return makeRequest("Power", host + YAMAHA_EXTENDED_CONTROL + zone + "/setPower?power=" + value);
1135 private @Nullable String setMute(String value, @Nullable String zone, @Nullable String host) {
1136 return makeRequest("Mute", host + YAMAHA_EXTENDED_CONTROL + zone + "/setMute?enable=" + value);
1139 private @Nullable String setVolume(int value, @Nullable String zone, @Nullable String host) {
1140 return makeRequest("Volume", host + YAMAHA_EXTENDED_CONTROL + zone + "/setVolume?volume=" + value);
1143 private @Nullable String setInput(String value, @Nullable String zone, @Nullable String host) {
1144 return makeRequest("setInput", host + YAMAHA_EXTENDED_CONTROL + zone + "/setInput?input=" + value);
1147 private @Nullable String setSoundProgram(String value, @Nullable String zone, @Nullable String host) {
1148 return makeRequest("setSoundProgram",
1149 host + YAMAHA_EXTENDED_CONTROL + zone + "/setSoundProgram?program=" + value);
1152 private @Nullable String setPreset(String value, @Nullable String zone, @Nullable String host) {
1153 return makeRequest("setPreset",
1154 host + YAMAHA_EXTENDED_CONTROL + "netusb/recallPreset?zone=" + zone + "&num=" + value);
1157 private @Nullable String setSleep(String value, @Nullable String zone, @Nullable String host) {
1158 return makeRequest("setSleep", host + YAMAHA_EXTENDED_CONTROL + zone + "/setSleep?sleep=" + value);
1161 private @Nullable String recallScene(String value, @Nullable String zone, @Nullable String host) {
1162 return makeRequest("recallScene", host + YAMAHA_EXTENDED_CONTROL + zone + "/recallScene?num=" + value);
1166 // Start Net Radio/USB Related
1168 private @Nullable String getPresetInfo(@Nullable String host) {
1169 return makeRequest("PresetInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getPresetInfo");
1172 private @Nullable String getRecentInfo(@Nullable String host) {
1173 return makeRequest("RecentInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getRecentInfo");
1176 private @Nullable String getPlayInfo(@Nullable String host) {
1177 return makeRequest("PlayInfo", host + YAMAHA_EXTENDED_CONTROL + "netusb/getPlayInfo");
1180 private @Nullable String setPlayback(String value, @Nullable String host) {
1181 return makeRequest("Playback", host + YAMAHA_EXTENDED_CONTROL + "netusb/setPlayback?playback=" + value);
1184 private @Nullable String setRepeat(String value, @Nullable String host) {
1185 return makeRequest("Repeat", host + YAMAHA_EXTENDED_CONTROL + "netusb/setRepeat?mode=" + value);
1188 private @Nullable String setShuffle(String value, @Nullable String host) {
1189 return makeRequest("Shuffle", host + YAMAHA_EXTENDED_CONTROL + "netusb/setShuffle?mode=" + value);
1192 // End Net Radio/USB Related
1194 // Start Music Cast API calls
1195 private @Nullable String getDistributionInfo(@Nullable String host) {
1196 return makeRequest("DistributionInfo", host + YAMAHA_EXTENDED_CONTROL + "dist/getDistributionInfo");
1199 private @Nullable String setClientServerInfo(@Nullable String host, String json, String type) {
1200 InputStream is = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
1202 url = "http://" + host + YAMAHA_EXTENDED_CONTROL + "dist/" + type;
1203 httpResponse = HttpUtil.executeUrl("POST", url, is, "", LONG_CONNECTION_TIMEOUT_MILLISEC);
1204 logger.trace("MC Link/Unlink Client {}", httpResponse);
1205 return httpResponse;
1206 } catch (IOException e) {
1207 logger.trace("IO Exception - {} - {}", type, e.getMessage());
1208 return "{\"response_code\":\"999\"}";
1212 private @Nullable String startDistribution(@Nullable String host) {
1213 Random ran = new Random();
1214 int nxt = ran.nextInt(200000);
1215 return makeRequest("StartDistribution", host + YAMAHA_EXTENDED_CONTROL + "dist/startDistribution?num=" + nxt);
1218 // End Music Cast API calls
1220 // Start General/System API calls
1222 private @Nullable String getFeatures(@Nullable String host) {
1223 return makeRequest("Features", host + YAMAHA_EXTENDED_CONTROL + "system/getFeatures");
1226 private @Nullable String getDeviceInfo(@Nullable String host) {
1227 return makeRequest("DeviceInfo", host + YAMAHA_EXTENDED_CONTROL + "system/getDeviceInfo");
1230 private void keepUdpEventsAlive(@Nullable String host) {
1231 Properties appProps = new Properties();
1232 appProps.setProperty("X-AppName", "MusicCast/1");
1233 appProps.setProperty("X-AppPort", "41100");
1235 httpResponse = HttpUtil.executeUrl("GET", HTTP + host + YAMAHA_EXTENDED_CONTROL + "netusb/getPlayInfo",
1236 appProps, null, "", LONG_CONNECTION_TIMEOUT_MILLISEC);
1237 // logger.trace("{}", httpResponse);
1238 logger.trace("{} - {}", "UDP task", httpResponse);
1239 } catch (IOException e) {
1240 logger.trace("UDP refresh failed - {}", e.getMessage());
1243 // End General/System API calls