2 * Copyright (c) 2010-2023 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.sonyaudio.internal.protocol;
15 import java.io.IOException;
16 import java.lang.reflect.Type;
18 import java.net.URISyntaxException;
19 import java.util.Arrays;
20 import java.util.HashMap;
21 import java.util.Iterator;
22 import java.util.List;
24 import java.util.Optional;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
29 import org.eclipse.jetty.websocket.client.WebSocketClient;
30 import org.openhab.binding.sonyaudio.internal.SonyAudioEventListener;
31 import org.openhab.binding.sonyaudio.internal.protocol.SwitchNotifications.Notification;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.google.gson.Gson;
36 import com.google.gson.JsonElement;
37 import com.google.gson.JsonObject;
38 import com.google.gson.reflect.TypeToken;
41 * The {@link SonyAudioConnection} is responsible for communicating with SONY audio products
44 * @author David Ã…berg - Initial contribution
46 public class SonyAudioConnection implements SonyAudioClientSocketEventListener {
47 private final Logger logger = LoggerFactory.getLogger(SonyAudioConnection.class);
49 private final String host;
50 private final int port;
51 private final String path;
52 private final URI base_uri;
54 private final WebSocketClient webSocketClient;
56 private SonyAudioClientSocket avContentSocket;
57 private SonyAudioClientSocket audioSocket;
58 private SonyAudioClientSocket systemSocket;
60 private final SonyAudioEventListener listener;
62 private int min_volume = 0;
63 private int max_volume = 50;
65 private final Gson gson;
67 public SonyAudioConnection(String host, int port, String path, SonyAudioEventListener listener,
68 ScheduledExecutorService scheduler, WebSocketClient webSocketClient) throws URISyntaxException {
72 this.listener = listener;
73 this.gson = new Gson();
74 this.webSocketClient = webSocketClient;
76 base_uri = new URI(String.format("ws://%s:%d/%s", host, port, path)).normalize();
78 URI wsAvContentUri = base_uri.resolve(base_uri.getPath() + "/avContent").normalize();
79 avContentSocket = new SonyAudioClientSocket(this, wsAvContentUri, scheduler);
80 URI wsAudioUri = base_uri.resolve(base_uri.getPath() + "/audio").normalize();
81 audioSocket = new SonyAudioClientSocket(this, wsAudioUri, scheduler);
83 URI wsSystemUri = base_uri.resolve(base_uri.getPath() + "/system").normalize();
84 systemSocket = new SonyAudioClientSocket(this, wsSystemUri, scheduler);
88 public void handleEvent(JsonObject json) {
93 param = json.getAsJsonArray("params").get(0).getAsJsonObject();
94 } catch (NullPointerException e) {
95 logger.debug("Invalid json in handleEvent");
97 } catch (IndexOutOfBoundsException e) {
98 logger.debug("Invalid json in handleEvent");
103 logger.debug("Unable to get params form json in handleEvent");
107 if (param.has("output")) {
108 String outputStr = param.get("output").getAsString();
109 Pattern pattern = Pattern.compile(".*zone=(\\d+)");
110 Matcher m = pattern.matcher(outputStr);
113 zone = Integer.parseInt(m.group(1));
114 } catch (NumberFormatException e) {
115 logger.error("This should never happen, pattern should only match integers");
121 if ("notifyPlayingContentInfo".equalsIgnoreCase(json.get("method").getAsString())) {
122 SonyAudioInput input = new SonyAudioInput();
123 input.input = param.get("uri").getAsString();
124 if (param.has("broadcastFreq")) {
125 int freq = param.get("broadcastFreq").getAsInt();
126 input.radio_freq = Optional.of(freq);
127 checkRadioPreset(input.input);
129 listener.updateInput(zone, input);
130 listener.updateSeekStation("");
133 if ("notifyVolumeInformation".equalsIgnoreCase(json.get("method").getAsString())) {
134 SonyAudioVolume volume = new SonyAudioVolume();
136 int rawVolume = param.get("volume").getAsInt();
137 volume.volume = Math.round(100 * (rawVolume - min_volume) / (max_volume - min_volume));
139 volume.mute = "on".equalsIgnoreCase(param.get("mute").getAsString());
140 listener.updateVolume(zone, volume);
143 if ("notifyPowerStatus".equalsIgnoreCase(json.get("method").getAsString())) {
144 String power = param.get("status").getAsString();
145 listener.updatePowerStatus(zone, "active".equalsIgnoreCase(power));
148 listener.updateConnectionState(true);
151 private void checkRadioPreset(String input) {
152 Pattern pattern = Pattern.compile(".*contentId=(\\d+)");
153 Matcher m = pattern.matcher(input);
155 listener.updateCurrentRadioStation(Integer.parseInt(m.group(1)));
160 public synchronized void onConnectionClosed() {
161 listener.updateConnectionState(false);
164 private class Notifications {
165 public List<Notification> enabled;
166 public List<Notification> disabled;
169 private Notifications getSwitches(SonyAudioClientSocket socket, Notifications notifications) throws IOException {
170 SwitchNotifications switchNotifications = new SwitchNotifications(notifications.enabled,
171 notifications.disabled);
172 JsonElement switches = socket.callMethod(switchNotifications);
174 Type notificationListType = new TypeToken<List<Notification>>() {
176 notifications.enabled = gson.fromJson(switches.getAsJsonArray().get(0).getAsJsonObject().get("enabled"),
177 notificationListType);
178 notifications.disabled = gson.fromJson(switches.getAsJsonArray().get(0).getAsJsonObject().get("disabled"),
179 notificationListType);
181 return notifications;
185 public synchronized void onConnectionOpened(URI resource) {
187 Notifications notifications = new Notifications();
188 notifications.enabled = Arrays.asList(new Notification[] {});
189 notifications.disabled = Arrays.asList(new Notification[] {});
191 if (avContentSocket.getURI().equals(resource)) {
192 notifications = getSwitches(avContentSocket, notifications);
194 for (Iterator<Notification> iter = notifications.disabled.listIterator(); iter.hasNext();) {
195 Notification a = iter.next();
196 if ("notifyPlayingContentInfo".equalsIgnoreCase(a.name)) {
197 notifications.enabled.add(a);
202 SwitchNotifications switchNotifications = new SwitchNotifications(notifications.enabled,
203 notifications.disabled);
204 avContentSocket.callMethod(switchNotifications);
207 if (audioSocket.getURI().equals(resource)) {
208 notifications = getSwitches(audioSocket, notifications);
210 for (Iterator<Notification> iter = notifications.disabled.listIterator(); iter.hasNext();) {
211 Notification a = iter.next();
212 if ("notifyVolumeInformation".equalsIgnoreCase(a.name)) {
213 notifications.enabled.add(a);
218 SwitchNotifications switchNotifications = new SwitchNotifications(notifications.enabled,
219 notifications.disabled);
220 audioSocket.callMethod(switchNotifications);
223 if (systemSocket.getURI().equals(resource)) {
224 notifications = getSwitches(systemSocket, notifications);
226 for (Iterator<Notification> iter = notifications.disabled.listIterator(); iter.hasNext();) {
227 Notification a = iter.next();
228 if ("notifyPowerStatus".equalsIgnoreCase(a.name)) {
229 notifications.enabled.add(a);
234 SwitchNotifications switchNotifications = new SwitchNotifications(notifications.enabled,
235 notifications.disabled);
236 systemSocket.callMethod(switchNotifications);
238 listener.updateConnectionState(true);
239 } catch (IOException e) {
240 logger.debug("Failed to setup connection");
241 listener.updateConnectionState(false);
245 public synchronized void close() {
246 logger.debug("SonyAudio closing connections");
247 if (avContentSocket != null) {
248 avContentSocket.close();
250 avContentSocket = null;
252 if (audioSocket != null) {
257 if (systemSocket != null) {
258 systemSocket.close();
263 private boolean checkConnection(SonyAudioClientSocket socket) {
264 if (!socket.isConnected()) {
265 logger.debug("checkConnection: try to connect to {}", socket.getURI().toString());
266 socket.open(webSocketClient);
267 return socket.isConnected();
272 public boolean checkConnection() {
273 return checkConnection(avContentSocket) && checkConnection(audioSocket) && checkConnection(systemSocket);
276 public String getConnectionName() {
277 if (base_uri != null) {
278 return base_uri.toString();
280 return String.format("ws://%s:%d/%s", host, port, path);
283 public Boolean getPower(int zone) throws IOException {
285 if (avContentSocket == null) {
286 throw new IOException("AvContent Socket not connected");
288 GetCurrentExternalTerminalsStatus getCurrentExternalTerminalsStatus = new GetCurrentExternalTerminalsStatus();
289 JsonElement element = avContentSocket.callMethod(getCurrentExternalTerminalsStatus);
291 if (element != null && element.isJsonArray()) {
292 Iterator<JsonElement> terminals = element.getAsJsonArray().get(0).getAsJsonArray().iterator();
293 while (terminals.hasNext()) {
294 JsonObject terminal = terminals.next().getAsJsonObject();
295 String uri = terminal.get("uri").getAsString();
296 if (uri.equalsIgnoreCase("extOutput:zone?zone=" + Integer.toString(zone))) {
297 return "active".equalsIgnoreCase(terminal.get("active").getAsString()) ? true : false;
301 throw new IOException(
302 "Unexpected responses: Unable to parse GetCurrentExternalTerminalsStatus response message");
304 if (systemSocket == null) {
305 throw new IOException("System Socket not connected");
308 GetPowerStatus getPowerStatus = new GetPowerStatus();
309 JsonElement element = systemSocket.callMethod(getPowerStatus);
311 if (element != null && element.isJsonArray()) {
312 String powerStatus = element.getAsJsonArray().get(0).getAsJsonObject().get("status").getAsString();
313 return "active".equalsIgnoreCase(powerStatus) ? true : false;
315 throw new IOException("Unexpected responses: Unable to parse GetPowerStatus response message");
319 public void setPower(boolean power) throws IOException {
323 public void setPower(boolean power, int zone) throws IOException {
325 if (avContentSocket == null) {
326 throw new IOException("AvContent Socket not connected");
328 SetActiveTerminal setActiveTerminal = new SetActiveTerminal(power, zone);
329 avContentSocket.callMethod(setActiveTerminal);
331 if (systemSocket == null) {
332 throw new IOException("System Socket not connected");
334 SetPowerStatus setPowerStatus = new SetPowerStatus(power);
335 systemSocket.callMethod(setPowerStatus);
339 public class SonyAudioInput {
340 public String input = "";
341 public Optional<Integer> radio_freq = Optional.empty();
344 public SonyAudioInput getInput() throws IOException {
345 GetPlayingContentInfo getPlayingContentInfo = new GetPlayingContentInfo();
346 return getInput(getPlayingContentInfo);
349 public SonyAudioInput getInput(int zone) throws IOException {
350 GetPlayingContentInfo getPlayingContentInfo = new GetPlayingContentInfo(zone);
351 return getInput(getPlayingContentInfo);
354 private SonyAudioInput getInput(GetPlayingContentInfo getPlayingContentInfo) throws IOException {
355 if (avContentSocket == null) {
356 throw new IOException("AvContent Socket not connected");
358 JsonElement element = avContentSocket.callMethod(getPlayingContentInfo);
360 if (element != null && element.isJsonArray()) {
361 SonyAudioInput ret = new SonyAudioInput();
363 JsonObject result = element.getAsJsonArray().get(0).getAsJsonArray().get(0).getAsJsonObject();
364 String uri = result.get("uri").getAsString();
365 checkRadioPreset(uri);
368 if (result.has("broadcastFreq")) {
369 int freq = result.get("broadcastFreq").getAsInt();
370 ret.radio_freq = Optional.of(freq);
374 throw new IOException("Unexpected responses: Unable to parse GetPlayingContentInfo response message");
377 public void setInput(String input) throws IOException {
378 if (avContentSocket == null) {
379 throw new IOException("AvContent Socket not connected");
381 SetPlayContent setPlayContent = new SetPlayContent(input);
382 avContentSocket.callMethod(setPlayContent);
385 public void setInput(String input, int zone) throws IOException {
386 if (avContentSocket == null) {
387 throw new IOException("AvContent Socket not connected");
389 SetPlayContent setPlayContent = new SetPlayContent(input, zone);
390 avContentSocket.callMethod(setPlayContent);
393 public void radioSeekFwd() throws IOException {
394 if (avContentSocket == null) {
395 throw new IOException("AvContent Socket not connected");
397 SeekBroadcastStation seekBroadcastStation = new SeekBroadcastStation(true);
398 avContentSocket.callMethod(seekBroadcastStation);
401 public void radioSeekBwd() throws IOException {
402 if (avContentSocket == null) {
403 throw new IOException("AvContent Socket not connected");
405 SeekBroadcastStation seekBroadcastStation = new SeekBroadcastStation(false);
406 avContentSocket.callMethod(seekBroadcastStation);
409 public class SonyAudioVolume {
410 public Integer volume = 0;
411 public Boolean mute = false;
414 public SonyAudioVolume getVolume(int zone) throws IOException {
415 GetVolumeInformation getVolumeInformation = new GetVolumeInformation(zone);
417 if (audioSocket == null || !audioSocket.isConnected()) {
418 throw new IOException("Audio Socket not connected");
420 JsonElement element = audioSocket.callMethod(getVolumeInformation);
422 if (element != null && element.isJsonArray()) {
423 JsonObject result = element.getAsJsonArray().get(0).getAsJsonArray().get(0).getAsJsonObject();
425 SonyAudioVolume ret = new SonyAudioVolume();
427 int volume = result.get("volume").getAsInt();
428 min_volume = result.get("minVolume").getAsInt();
429 max_volume = result.get("maxVolume").getAsInt();
430 int vol = Math.round(100 * (volume - min_volume) / (max_volume - min_volume));
436 String mute = result.get("mute").getAsString();
437 ret.mute = "on".equalsIgnoreCase(mute) ? true : false;
441 throw new IOException("Unexpected responses: Unable to parse GetVolumeInformation response message");
444 public void setVolume(int volume) throws IOException {
445 if (audioSocket == null) {
446 throw new IOException("Audio Socket not connected");
448 SetAudioVolume setAudioVolume = new SetAudioVolume(volume, min_volume, max_volume);
449 audioSocket.callMethod(setAudioVolume);
452 public void setVolume(String volumeChange) throws IOException {
453 if (audioSocket == null) {
454 throw new IOException("Audio Socket not connected");
456 SetAudioVolume setAudioVolume = new SetAudioVolume(volumeChange);
457 audioSocket.callMethod(setAudioVolume);
460 public void setVolume(int volume, int zone) throws IOException {
461 if (audioSocket == null) {
462 throw new IOException("Audio Socket not connected");
464 SetAudioVolume setAudioVolume = new SetAudioVolume(zone, volume, min_volume, max_volume);
465 audioSocket.callMethod(setAudioVolume);
468 public void setVolume(String volumeChange, int zone) throws IOException {
469 if (audioSocket == null) {
470 throw new IOException("Audio Socket not connected");
472 SetAudioVolume setAudioVolume = new SetAudioVolume(zone, volumeChange);
473 audioSocket.callMethod(setAudioVolume);
476 public void setMute(boolean mute) throws IOException {
477 if (audioSocket == null) {
478 throw new IOException("Audio Socket not connected");
480 SetAudioMute setAudioMute = new SetAudioMute(mute);
481 audioSocket.callMethod(setAudioMute);
484 public void setMute(boolean mute, int zone) throws IOException {
485 if (audioSocket == null) {
486 throw new IOException("Audio Socket not connected");
488 SetAudioMute setAudioMute = new SetAudioMute(mute, zone);
489 audioSocket.callMethod(setAudioMute);
492 public Map<String, String> getSoundSettings() throws IOException {
493 if (audioSocket == null) {
494 throw new IOException("Audio Socket not connected");
496 Map<String, String> m = new HashMap<>();
498 GetSoundSettings getSoundSettings = new GetSoundSettings();
499 JsonElement element = audioSocket.callMethod(getSoundSettings);
501 if (element == null || !element.isJsonArray()) {
502 throw new IOException("Unexpected responses: Unable to parse GetSoundSettings response message");
504 Iterator<JsonElement> iterator = element.getAsJsonArray().get(0).getAsJsonArray().iterator();
505 while (iterator.hasNext()) {
506 JsonObject item = iterator.next().getAsJsonObject();
508 m.put(item.get("target").getAsString(), item.get("currentValue").getAsString());
513 public void setSoundSettings(String target, String value) throws IOException {
514 if (audioSocket == null) {
515 throw new IOException("Audio Socket not connected");
517 SetSoundSettings setSoundSettings = new SetSoundSettings(target, value);
518 audioSocket.callMethod(setSoundSettings);