]> git.basschouten.com Git - openhab-addons.git/blob
791281bf4786fd5b93c9f1862b56dad72d4da1fc
[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.sonyaudio.internal.protocol;
14
15 import java.io.IOException;
16 import java.lang.reflect.Type;
17 import java.net.URI;
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;
23 import java.util.Map;
24 import java.util.Optional;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
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;
34
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;
39
40 /**
41  * The {@link SonyAudioConnection} is responsible for communicating with SONY audio products
42  * handlers.
43  *
44  * @author David Ã…berg - Initial contribution
45  */
46 public class SonyAudioConnection implements SonyAudioClientSocketEventListener {
47     private final Logger logger = LoggerFactory.getLogger(SonyAudioConnection.class);
48
49     private final String host;
50     private final int port;
51     private final String path;
52     private final URI baseUri;
53
54     private final WebSocketClient webSocketClient;
55
56     private SonyAudioClientSocket avContentSocket;
57     private SonyAudioClientSocket audioSocket;
58     private SonyAudioClientSocket systemSocket;
59
60     private final SonyAudioEventListener listener;
61
62     private int minVolume = 0;
63     private int maxVolume = 50;
64
65     private final Gson gson;
66
67     public SonyAudioConnection(String host, int port, String path, SonyAudioEventListener listener,
68             ScheduledExecutorService scheduler, WebSocketClient webSocketClient) throws URISyntaxException {
69         this.host = host;
70         this.port = port;
71         this.path = path;
72         this.listener = listener;
73         this.gson = new Gson();
74         this.webSocketClient = webSocketClient;
75
76         baseUri = new URI(String.format("ws://%s:%d/%s", host, port, path)).normalize();
77
78         URI wsAvContentUri = baseUri.resolve(baseUri.getPath() + "/avContent").normalize();
79         avContentSocket = new SonyAudioClientSocket(this, wsAvContentUri, scheduler);
80         URI wsAudioUri = baseUri.resolve(baseUri.getPath() + "/audio").normalize();
81         audioSocket = new SonyAudioClientSocket(this, wsAudioUri, scheduler);
82
83         URI wsSystemUri = baseUri.resolve(baseUri.getPath() + "/system").normalize();
84         systemSocket = new SonyAudioClientSocket(this, wsSystemUri, scheduler);
85     }
86
87     @Override
88     public void handleEvent(JsonObject json) {
89         int zone = 0;
90         JsonObject param;
91
92         try {
93             param = json.getAsJsonArray("params").get(0).getAsJsonObject();
94         } catch (NullPointerException e) {
95             logger.debug("Invalid json in handleEvent");
96             return;
97         } catch (IndexOutOfBoundsException e) {
98             logger.debug("Invalid json in handleEvent");
99             return;
100         }
101
102         if (param == null) {
103             logger.debug("Unable to get params form json in handleEvent");
104             return;
105         }
106
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);
111             if (m.matches()) {
112                 try {
113                     zone = Integer.parseInt(m.group(1));
114                 } catch (NumberFormatException e) {
115                     logger.error("This should never happen, pattern should only match integers");
116                     return;
117                 }
118             }
119         }
120
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.radioFrequency = Optional.of(freq);
127                 checkRadioPreset(input.input);
128             }
129             listener.updateInput(zone, input);
130             listener.updateSeekStation("");
131         }
132
133         if ("notifyVolumeInformation".equalsIgnoreCase(json.get("method").getAsString())) {
134             SonyAudioVolume volume = new SonyAudioVolume();
135
136             int rawVolume = param.get("volume").getAsInt();
137             volume.volume = Math.round(100 * (rawVolume - minVolume) / (maxVolume - minVolume));
138
139             volume.mute = "on".equalsIgnoreCase(param.get("mute").getAsString());
140             listener.updateVolume(zone, volume);
141         }
142
143         if ("notifyPowerStatus".equalsIgnoreCase(json.get("method").getAsString())) {
144             String power = param.get("status").getAsString();
145             listener.updatePowerStatus(zone, "active".equalsIgnoreCase(power));
146         }
147
148         listener.updateConnectionState(true);
149     }
150
151     private void checkRadioPreset(String input) {
152         Pattern pattern = Pattern.compile(".*contentId=(\\d+)");
153         Matcher m = pattern.matcher(input);
154         if (m.matches()) {
155             listener.updateCurrentRadioStation(Integer.parseInt(m.group(1)));
156         }
157     }
158
159     @Override
160     public synchronized void onConnectionClosed() {
161         listener.updateConnectionState(false);
162     }
163
164     private class Notifications {
165         public List<Notification> enabled;
166         public List<Notification> disabled;
167     }
168
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);
173
174         Type notificationListType = new TypeToken<List<Notification>>() {
175         }.getType();
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);
180
181         return notifications;
182     }
183
184     @Override
185     public synchronized void onConnectionOpened(URI resource) {
186         try {
187             Notifications notifications = new Notifications();
188             notifications.enabled = Arrays.asList(new Notification[] {});
189             notifications.disabled = Arrays.asList(new Notification[] {});
190
191             if (avContentSocket.getURI().equals(resource)) {
192                 notifications = getSwitches(avContentSocket, notifications);
193
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);
198                         iter.remove();
199                     }
200                 }
201
202                 SwitchNotifications switchNotifications = new SwitchNotifications(notifications.enabled,
203                         notifications.disabled);
204                 avContentSocket.callMethod(switchNotifications);
205             }
206
207             if (audioSocket.getURI().equals(resource)) {
208                 notifications = getSwitches(audioSocket, notifications);
209
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);
214                         iter.remove();
215                     }
216                 }
217
218                 SwitchNotifications switchNotifications = new SwitchNotifications(notifications.enabled,
219                         notifications.disabled);
220                 audioSocket.callMethod(switchNotifications);
221             }
222
223             if (systemSocket.getURI().equals(resource)) {
224                 notifications = getSwitches(systemSocket, notifications);
225
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);
230                         iter.remove();
231                     }
232                 }
233
234                 SwitchNotifications switchNotifications = new SwitchNotifications(notifications.enabled,
235                         notifications.disabled);
236                 systemSocket.callMethod(switchNotifications);
237             }
238             listener.updateConnectionState(true);
239         } catch (IOException e) {
240             logger.debug("Failed to setup connection");
241             listener.updateConnectionState(false);
242         }
243     }
244
245     public synchronized void close() {
246         logger.debug("SonyAudio closing connections");
247         if (avContentSocket != null) {
248             avContentSocket.close();
249         }
250         avContentSocket = null;
251
252         if (audioSocket != null) {
253             audioSocket.close();
254         }
255         audioSocket = null;
256
257         if (systemSocket != null) {
258             systemSocket.close();
259         }
260         systemSocket = null;
261     }
262
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();
268         }
269         return true;
270     }
271
272     public boolean checkConnection() {
273         return checkConnection(avContentSocket) && checkConnection(audioSocket) && checkConnection(systemSocket);
274     }
275
276     public String getConnectionName() {
277         if (baseUri != null) {
278             return baseUri.toString();
279         }
280         return String.format("ws://%s:%d/%s", host, port, path);
281     }
282
283     public Boolean getPower(int zone) throws IOException {
284         if (zone > 0) {
285             if (avContentSocket == null) {
286                 throw new IOException("AvContent Socket not connected");
287             }
288             GetCurrentExternalTerminalsStatus getCurrentExternalTerminalsStatus = new GetCurrentExternalTerminalsStatus();
289             JsonElement element = avContentSocket.callMethod(getCurrentExternalTerminalsStatus);
290
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 zoneUri = "extOutput:zone?zone=" + Integer.toString(zone);
296                     String uri = terminal.get("uri").getAsString();
297                     if (uri.equalsIgnoreCase(zoneUri)) {
298                         return "active".equalsIgnoreCase(terminal.get("active").getAsString()) ? true : false;
299                     }
300                 }
301             }
302             throw new IOException(
303                     "Unexpected responses: Unable to parse GetCurrentExternalTerminalsStatus response message");
304         } else {
305             if (systemSocket == null) {
306                 throw new IOException("System Socket not connected");
307             }
308
309             GetPowerStatus getPowerStatus = new GetPowerStatus();
310             JsonElement element = systemSocket.callMethod(getPowerStatus);
311
312             if (element != null && element.isJsonArray()) {
313                 String powerStatus = element.getAsJsonArray().get(0).getAsJsonObject().get("status").getAsString();
314                 return "active".equalsIgnoreCase(powerStatus) ? true : false;
315             }
316             throw new IOException("Unexpected responses: Unable to parse GetPowerStatus response message");
317         }
318     }
319
320     public void setPower(boolean power) throws IOException {
321         setPower(power, 0);
322     }
323
324     public void setPower(boolean power, int zone) throws IOException {
325         if (zone > 0) {
326             if (avContentSocket == null) {
327                 throw new IOException("AvContent Socket not connected");
328             }
329             SetActiveTerminal setActiveTerminal = new SetActiveTerminal(power, zone);
330             avContentSocket.callMethod(setActiveTerminal);
331         } else {
332             if (systemSocket == null) {
333                 throw new IOException("System Socket not connected");
334             }
335             SetPowerStatus setPowerStatus = new SetPowerStatus(power);
336             systemSocket.callMethod(setPowerStatus);
337         }
338     }
339
340     public class SonyAudioInput {
341         public String input = "";
342         public Optional<Integer> radioFrequency = Optional.empty();
343     }
344
345     public SonyAudioInput getInput() throws IOException {
346         GetPlayingContentInfo getPlayingContentInfo = new GetPlayingContentInfo();
347         return getInput(getPlayingContentInfo);
348     }
349
350     public SonyAudioInput getInput(int zone) throws IOException {
351         GetPlayingContentInfo getPlayingContentInfo = new GetPlayingContentInfo(zone);
352         return getInput(getPlayingContentInfo);
353     }
354
355     private SonyAudioInput getInput(GetPlayingContentInfo getPlayingContentInfo) throws IOException {
356         if (avContentSocket == null) {
357             throw new IOException("AvContent Socket not connected");
358         }
359         JsonElement element = avContentSocket.callMethod(getPlayingContentInfo);
360
361         if (element != null && element.isJsonArray()) {
362             SonyAudioInput ret = new SonyAudioInput();
363
364             JsonObject result = element.getAsJsonArray().get(0).getAsJsonArray().get(0).getAsJsonObject();
365             String uri = result.get("uri").getAsString();
366             checkRadioPreset(uri);
367             ret.input = uri;
368
369             if (result.has("broadcastFreq")) {
370                 int freq = result.get("broadcastFreq").getAsInt();
371                 ret.radioFrequency = Optional.of(freq);
372             }
373             return ret;
374         }
375         throw new IOException("Unexpected responses: Unable to parse GetPlayingContentInfo response message");
376     }
377
378     public void setInput(String input) throws IOException {
379         if (avContentSocket == null) {
380             throw new IOException("AvContent Socket not connected");
381         }
382         SetPlayContent setPlayContent = new SetPlayContent(input);
383         avContentSocket.callMethod(setPlayContent);
384     }
385
386     public void setInput(String input, int zone) throws IOException {
387         if (avContentSocket == null) {
388             throw new IOException("AvContent Socket not connected");
389         }
390         SetPlayContent setPlayContent = new SetPlayContent(input, zone);
391         avContentSocket.callMethod(setPlayContent);
392     }
393
394     public void radioSeekFwd() throws IOException {
395         if (avContentSocket == null) {
396             throw new IOException("AvContent Socket not connected");
397         }
398         SeekBroadcastStation seekBroadcastStation = new SeekBroadcastStation(true);
399         avContentSocket.callMethod(seekBroadcastStation);
400     }
401
402     public void radioSeekBwd() throws IOException {
403         if (avContentSocket == null) {
404             throw new IOException("AvContent Socket not connected");
405         }
406         SeekBroadcastStation seekBroadcastStation = new SeekBroadcastStation(false);
407         avContentSocket.callMethod(seekBroadcastStation);
408     }
409
410     public class SonyAudioVolume {
411         public Integer volume = 0;
412         public Boolean mute = false;
413     }
414
415     public SonyAudioVolume getVolume(int zone) throws IOException {
416         GetVolumeInformation getVolumeInformation = new GetVolumeInformation(zone);
417
418         if (audioSocket == null || !audioSocket.isConnected()) {
419             throw new IOException("Audio Socket not connected");
420         }
421         JsonElement element = audioSocket.callMethod(getVolumeInformation);
422
423         if (element != null && element.isJsonArray()) {
424             JsonObject result = element.getAsJsonArray().get(0).getAsJsonArray().get(0).getAsJsonObject();
425
426             SonyAudioVolume ret = new SonyAudioVolume();
427
428             int volume = result.get("volume").getAsInt();
429             minVolume = result.get("minVolume").getAsInt();
430             maxVolume = result.get("maxVolume").getAsInt();
431             int vol = Math.round(100 * (volume - minVolume) / (maxVolume - minVolume));
432             if (vol < 0) {
433                 vol = 0;
434             }
435             ret.volume = vol;
436
437             String mute = result.get("mute").getAsString();
438             ret.mute = "on".equalsIgnoreCase(mute) ? true : false;
439
440             return ret;
441         }
442         throw new IOException("Unexpected responses: Unable to parse GetVolumeInformation response message");
443     }
444
445     public void setVolume(int volume) throws IOException {
446         if (audioSocket == null) {
447             throw new IOException("Audio Socket not connected");
448         }
449         SetAudioVolume setAudioVolume = new SetAudioVolume(volume, minVolume, maxVolume);
450         audioSocket.callMethod(setAudioVolume);
451     }
452
453     public void setVolume(String volumeChange) throws IOException {
454         if (audioSocket == null) {
455             throw new IOException("Audio Socket not connected");
456         }
457         SetAudioVolume setAudioVolume = new SetAudioVolume(volumeChange);
458         audioSocket.callMethod(setAudioVolume);
459     }
460
461     public void setVolume(int volume, int zone) throws IOException {
462         if (audioSocket == null) {
463             throw new IOException("Audio Socket not connected");
464         }
465         SetAudioVolume setAudioVolume = new SetAudioVolume(zone, volume, minVolume, maxVolume);
466         audioSocket.callMethod(setAudioVolume);
467     }
468
469     public void setVolume(String volumeChange, int zone) throws IOException {
470         if (audioSocket == null) {
471             throw new IOException("Audio Socket not connected");
472         }
473         SetAudioVolume setAudioVolume = new SetAudioVolume(zone, volumeChange);
474         audioSocket.callMethod(setAudioVolume);
475     }
476
477     public void setMute(boolean mute) throws IOException {
478         if (audioSocket == null) {
479             throw new IOException("Audio Socket not connected");
480         }
481         SetAudioMute setAudioMute = new SetAudioMute(mute);
482         audioSocket.callMethod(setAudioMute);
483     }
484
485     public void setMute(boolean mute, int zone) throws IOException {
486         if (audioSocket == null) {
487             throw new IOException("Audio Socket not connected");
488         }
489         SetAudioMute setAudioMute = new SetAudioMute(mute, zone);
490         audioSocket.callMethod(setAudioMute);
491     }
492
493     public Map<String, String> getSoundSettings() throws IOException {
494         if (audioSocket == null) {
495             throw new IOException("Audio Socket not connected");
496         }
497         Map<String, String> m = new HashMap<>();
498
499         GetSoundSettings getSoundSettings = new GetSoundSettings();
500         JsonElement element = audioSocket.callMethod(getSoundSettings);
501
502         if (element == null || !element.isJsonArray()) {
503             throw new IOException("Unexpected responses: Unable to parse GetSoundSettings response message");
504         }
505         Iterator<JsonElement> iterator = element.getAsJsonArray().get(0).getAsJsonArray().iterator();
506         while (iterator.hasNext()) {
507             JsonObject item = iterator.next().getAsJsonObject();
508
509             m.put(item.get("target").getAsString(), item.get("currentValue").getAsString());
510         }
511         return m;
512     }
513
514     public void setSoundSettings(String target, String value) throws IOException {
515         if (audioSocket == null) {
516             throw new IOException("Audio Socket not connected");
517         }
518         SetSoundSettings setSoundSettings = new SetSoundSettings(target, value);
519         audioSocket.callMethod(setSoundSettings);
520     }
521 }