]> git.basschouten.com Git - openhab-addons.git/blob
fee53a1bb182bdbc300dcacdeb34489419598976
[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 base_uri;
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 min_volume = 0;
63     private int max_volume = 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         base_uri = new URI(String.format("ws://%s:%d/%s", host, port, path)).normalize();
77
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);
82
83         URI wsSystemUri = base_uri.resolve(base_uri.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 (json.get("method").getAsString().equalsIgnoreCase("notifyPlayingContentInfo")) {
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);
128             }
129             listener.updateInput(zone, input);
130             listener.updateSeekStation("");
131         }
132
133         if (json.get("method").getAsString().equalsIgnoreCase("notifyVolumeInformation")) {
134             SonyAudioVolume volume = new SonyAudioVolume();
135
136             int rawVolume = param.get("volume").getAsInt();
137             volume.volume = Math.round(100 * (rawVolume - min_volume) / (max_volume - min_volume));
138
139             volume.mute = param.get("mute").getAsString().equalsIgnoreCase("on");
140             listener.updateVolume(zone, volume);
141         }
142
143         if (json.get("method").getAsString().equalsIgnoreCase("notifyPowerStatus")) {
144             String power = param.get("status").getAsString();
145             listener.updatePowerStatus(zone, power.equalsIgnoreCase("active"));
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 (a.name.equalsIgnoreCase("notifyPlayingContentInfo")) {
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 (a.name.equalsIgnoreCase("notifyVolumeInformation")) {
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 (a.name.equalsIgnoreCase("notifyPowerStatus")) {
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 (base_uri != null) {
278             return base_uri.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 uri = terminal.get("uri").getAsString();
296                     if (uri.equalsIgnoreCase("extOutput:zone?zone=" + Integer.toString(zone))) {
297                         return terminal.get("active").getAsString().equalsIgnoreCase("active") ? true : false;
298                     }
299                 }
300             }
301             throw new IOException(
302                     "Unexpected responses: Unable to parse GetCurrentExternalTerminalsStatus response message");
303         } else {
304             if (systemSocket == null) {
305                 throw new IOException("System Socket not connected");
306             }
307
308             GetPowerStatus getPowerStatus = new GetPowerStatus();
309             JsonElement element = systemSocket.callMethod(getPowerStatus);
310
311             if (element != null && element.isJsonArray()) {
312                 String powerStatus = element.getAsJsonArray().get(0).getAsJsonObject().get("status").getAsString();
313                 return powerStatus.equalsIgnoreCase("active") ? true : false;
314             }
315             throw new IOException("Unexpected responses: Unable to parse GetPowerStatus response message");
316         }
317     }
318
319     public void setPower(boolean power) throws IOException {
320         setPower(power, 0);
321     }
322
323     public void setPower(boolean power, int zone) throws IOException {
324         if (zone > 0) {
325             if (avContentSocket == null) {
326                 throw new IOException("AvContent Socket not connected");
327             }
328             SetActiveTerminal setActiveTerminal = new SetActiveTerminal(power, zone);
329             avContentSocket.callMethod(setActiveTerminal);
330         } else {
331             if (systemSocket == null) {
332                 throw new IOException("System Socket not connected");
333             }
334             SetPowerStatus setPowerStatus = new SetPowerStatus(power);
335             systemSocket.callMethod(setPowerStatus);
336         }
337     }
338
339     public class SonyAudioInput {
340         public String input = "";
341         public Optional<Integer> radio_freq = Optional.empty();
342     }
343
344     public SonyAudioInput getInput() throws IOException {
345         GetPlayingContentInfo getPlayingContentInfo = new GetPlayingContentInfo();
346         return getInput(getPlayingContentInfo);
347     }
348
349     public SonyAudioInput getInput(int zone) throws IOException {
350         GetPlayingContentInfo getPlayingContentInfo = new GetPlayingContentInfo(zone);
351         return getInput(getPlayingContentInfo);
352     }
353
354     private SonyAudioInput getInput(GetPlayingContentInfo getPlayingContentInfo) throws IOException {
355         if (avContentSocket == null) {
356             throw new IOException("AvContent Socket not connected");
357         }
358         JsonElement element = avContentSocket.callMethod(getPlayingContentInfo);
359
360         if (element != null && element.isJsonArray()) {
361             SonyAudioInput ret = new SonyAudioInput();
362
363             JsonObject result = element.getAsJsonArray().get(0).getAsJsonArray().get(0).getAsJsonObject();
364             String uri = result.get("uri").getAsString();
365             checkRadioPreset(uri);
366             ret.input = uri;
367
368             if (result.has("broadcastFreq")) {
369                 int freq = result.get("broadcastFreq").getAsInt();
370                 ret.radio_freq = Optional.of(freq);
371             }
372             return ret;
373         }
374         throw new IOException("Unexpected responses: Unable to parse GetPlayingContentInfo response message");
375     }
376
377     public void setInput(String input) throws IOException {
378         if (avContentSocket == null) {
379             throw new IOException("AvContent Socket not connected");
380         }
381         SetPlayContent setPlayContent = new SetPlayContent(input);
382         avContentSocket.callMethod(setPlayContent);
383     }
384
385     public void setInput(String input, int zone) throws IOException {
386         if (avContentSocket == null) {
387             throw new IOException("AvContent Socket not connected");
388         }
389         SetPlayContent setPlayContent = new SetPlayContent(input, zone);
390         avContentSocket.callMethod(setPlayContent);
391     }
392
393     public void radioSeekFwd() throws IOException {
394         if (avContentSocket == null) {
395             throw new IOException("AvContent Socket not connected");
396         }
397         SeekBroadcastStation seekBroadcastStation = new SeekBroadcastStation(true);
398         avContentSocket.callMethod(seekBroadcastStation);
399     }
400
401     public void radioSeekBwd() throws IOException {
402         if (avContentSocket == null) {
403             throw new IOException("AvContent Socket not connected");
404         }
405         SeekBroadcastStation seekBroadcastStation = new SeekBroadcastStation(false);
406         avContentSocket.callMethod(seekBroadcastStation);
407     }
408
409     public class SonyAudioVolume {
410         public Integer volume = 0;
411         public Boolean mute = false;
412     }
413
414     public SonyAudioVolume getVolume(int zone) throws IOException {
415         GetVolumeInformation getVolumeInformation = new GetVolumeInformation(zone);
416
417         if (audioSocket == null || !audioSocket.isConnected()) {
418             throw new IOException("Audio Socket not connected");
419         }
420         JsonElement element = audioSocket.callMethod(getVolumeInformation);
421
422         if (element != null && element.isJsonArray()) {
423             JsonObject result = element.getAsJsonArray().get(0).getAsJsonArray().get(0).getAsJsonObject();
424
425             SonyAudioVolume ret = new SonyAudioVolume();
426
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));
431             if (vol < 0) {
432                 vol = 0;
433             }
434             ret.volume = vol;
435
436             String mute = result.get("mute").getAsString();
437             ret.mute = mute.equalsIgnoreCase("on") ? true : false;
438
439             return ret;
440         }
441         throw new IOException("Unexpected responses: Unable to parse GetVolumeInformation response message");
442     }
443
444     public void setVolume(int volume) throws IOException {
445         if (audioSocket == null) {
446             throw new IOException("Audio Socket not connected");
447         }
448         SetAudioVolume setAudioVolume = new SetAudioVolume(volume, min_volume, max_volume);
449         audioSocket.callMethod(setAudioVolume);
450     }
451
452     public void setVolume(String volumeChange) throws IOException {
453         if (audioSocket == null) {
454             throw new IOException("Audio Socket not connected");
455         }
456         SetAudioVolume setAudioVolume = new SetAudioVolume(volumeChange);
457         audioSocket.callMethod(setAudioVolume);
458     }
459
460     public void setVolume(int volume, int zone) throws IOException {
461         if (audioSocket == null) {
462             throw new IOException("Audio Socket not connected");
463         }
464         SetAudioVolume setAudioVolume = new SetAudioVolume(zone, volume, min_volume, max_volume);
465         audioSocket.callMethod(setAudioVolume);
466     }
467
468     public void setVolume(String volumeChange, int zone) throws IOException {
469         if (audioSocket == null) {
470             throw new IOException("Audio Socket not connected");
471         }
472         SetAudioVolume setAudioVolume = new SetAudioVolume(zone, volumeChange);
473         audioSocket.callMethod(setAudioVolume);
474     }
475
476     public void setMute(boolean mute) throws IOException {
477         if (audioSocket == null) {
478             throw new IOException("Audio Socket not connected");
479         }
480         SetAudioMute setAudioMute = new SetAudioMute(mute);
481         audioSocket.callMethod(setAudioMute);
482     }
483
484     public void setMute(boolean mute, int zone) throws IOException {
485         if (audioSocket == null) {
486             throw new IOException("Audio Socket not connected");
487         }
488         SetAudioMute setAudioMute = new SetAudioMute(mute, zone);
489         audioSocket.callMethod(setAudioMute);
490     }
491
492     public Map<String, String> getSoundSettings() throws IOException {
493         if (audioSocket == null) {
494             throw new IOException("Audio Socket not connected");
495         }
496         Map<String, String> m = new HashMap<>();
497
498         GetSoundSettings getSoundSettings = new GetSoundSettings();
499         JsonElement element = audioSocket.callMethod(getSoundSettings);
500
501         if (element == null || !element.isJsonArray()) {
502             throw new IOException("Unexpected responses: Unable to parse GetSoundSettings response message");
503         }
504         Iterator<JsonElement> iterator = element.getAsJsonArray().get(0).getAsJsonArray().iterator();
505         while (iterator.hasNext()) {
506             JsonObject item = iterator.next().getAsJsonObject();
507
508             m.put(item.get("target").getAsString(), item.get("currentValue").getAsString());
509         }
510         return m;
511     }
512
513     public void setSoundSettings(String target, String value) throws IOException {
514         if (audioSocket == null) {
515             throw new IOException("Audio Socket not connected");
516         }
517         SetSoundSettings setSoundSettings = new SetSoundSettings(target, value);
518         audioSocket.callMethod(setSoundSettings);
519     }
520 }