]> git.basschouten.com Git - openhab-addons.git/blob
be6a5ca099d706cfb9ab56606c8cd72ed0b30850
[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.handler;
14
15 import static org.openhab.binding.sonyaudio.internal.SonyAudioBindingConstants.*;
16
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.net.URISyntaxException;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.CompletionException;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.function.Supplier;
27
28 import org.eclipse.jetty.websocket.client.WebSocketClient;
29 import org.openhab.binding.sonyaudio.internal.SonyAudioBindingConstants;
30 import org.openhab.binding.sonyaudio.internal.SonyAudioEventListener;
31 import org.openhab.binding.sonyaudio.internal.protocol.SonyAudioConnection;
32 import org.openhab.core.cache.ExpiringCache;
33 import org.openhab.core.config.core.Configuration;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.IncreaseDecreaseType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.Channel;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 /**
51  * The {@link SonyAudioHandler} is responsible for handling commands, which are
52  * sent to one of the channels.
53  *
54  * @author David Ã…berg - Initial contribution
55  */
56 abstract class SonyAudioHandler extends BaseThingHandler implements SonyAudioEventListener {
57
58     private final Logger logger = LoggerFactory.getLogger(SonyAudioHandler.class);
59
60     private WebSocketClient webSocketClient;
61
62     protected SonyAudioConnection connection;
63     private ScheduledFuture<?> connectionCheckerFuture;
64     private ScheduledFuture<?> refreshJob;
65
66     private int currentRadioStation = 0;
67     private final Map<Integer, String> inputZone = new HashMap<>();
68
69     private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
70
71     protected ExpiringCache<Boolean>[] powerCache;
72     protected ExpiringCache<SonyAudioConnection.SonyAudioInput>[] inputCache;
73     protected ExpiringCache<SonyAudioConnection.SonyAudioVolume>[] volumeCache;
74     protected ExpiringCache<Map<String, String>> soundSettingsCache;
75
76     protected Supplier<Boolean>[] powerSupplier;
77     protected Supplier<SonyAudioConnection.SonyAudioInput>[] inputSupplier;
78     protected Supplier<SonyAudioConnection.SonyAudioVolume>[] volumeSupplier;
79     protected Supplier<Map<String, String>> soundSettingsSupplier;
80
81     @SuppressWarnings("unchecked")
82     public SonyAudioHandler(Thing thing, WebSocketClient webSocketClient) {
83         super(thing);
84
85         this.webSocketClient = webSocketClient;
86
87         powerCache = new ExpiringCache[5];
88         powerSupplier = new Supplier[5];
89         inputCache = new ExpiringCache[5];
90         inputSupplier = new Supplier[5];
91         volumeCache = new ExpiringCache[5];
92         volumeSupplier = new Supplier[5];
93
94         for (int i = 0; i < 5; i++) {
95             final int index = i;
96
97             inputSupplier[i] = () -> {
98                 try {
99                     return connection.getInput(index);
100                 } catch (IOException ex) {
101                     throw new CompletionException(ex);
102                 }
103             };
104
105             powerSupplier[i] = () -> {
106                 try {
107                     return connection.getPower(index);
108                 } catch (IOException ex) {
109                     throw new CompletionException(ex);
110                 }
111             };
112
113             volumeSupplier[i] = () -> {
114                 try {
115                     return connection.getVolume(index);
116                 } catch (IOException ex) {
117                     throw new CompletionException(ex);
118                 }
119             };
120
121             powerCache[i] = new ExpiringCache<>(CACHE_EXPIRY, powerSupplier[i]);
122             inputCache[i] = new ExpiringCache<>(CACHE_EXPIRY, inputSupplier[i]);
123             volumeCache[i] = new ExpiringCache<>(CACHE_EXPIRY, volumeSupplier[i]);
124         }
125
126         soundSettingsSupplier = () -> {
127             try {
128                 return connection.getSoundSettings();
129             } catch (IOException ex) {
130                 throw new CompletionException(ex);
131             }
132         };
133
134         soundSettingsCache = new ExpiringCache<>(CACHE_EXPIRY, soundSettingsSupplier);
135     }
136
137     @Override
138     public void handleCommand(ChannelUID channelUID, Command command) {
139         if (connection == null || !connection.checkConnection()) {
140             logger.debug("Thing not yet initialized!");
141             return;
142         }
143
144         String id = channelUID.getId();
145
146         logger.debug("Handle command {} {}", channelUID, command);
147
148         if (getThing().getStatusInfo().getStatus() != ThingStatus.ONLINE) {
149             switch (id) {
150                 case CHANNEL_POWER:
151                 case CHANNEL_MASTER_POWER:
152                     logger.debug("Device powered off sending {} {}", channelUID, command);
153                     break;
154                 default:
155                     logger.debug("Device powered off ignore command {} {}", channelUID, command);
156                     return;
157             }
158         }
159
160         try {
161             switch (id) {
162                 case CHANNEL_POWER:
163                 case CHANNEL_MASTER_POWER:
164                     handlePowerCommand(command, channelUID);
165                     break;
166                 case CHANNEL_ZONE1_POWER:
167                     handlePowerCommand(command, channelUID, 1);
168                     break;
169                 case CHANNEL_ZONE2_POWER:
170                     handlePowerCommand(command, channelUID, 2);
171                     break;
172                 case CHANNEL_ZONE3_POWER:
173                     handlePowerCommand(command, channelUID, 3);
174                     break;
175                 case CHANNEL_ZONE4_POWER:
176                     handlePowerCommand(command, channelUID, 4);
177                     break;
178                 case CHANNEL_INPUT:
179                     handleInputCommand(command, channelUID);
180                     break;
181                 case CHANNEL_ZONE1_INPUT:
182                     handleInputCommand(command, channelUID, 1);
183                     break;
184                 case CHANNEL_ZONE2_INPUT:
185                     handleInputCommand(command, channelUID, 2);
186                     break;
187                 case CHANNEL_ZONE3_INPUT:
188                     handleInputCommand(command, channelUID, 3);
189                     break;
190                 case CHANNEL_ZONE4_INPUT:
191                     handleInputCommand(command, channelUID, 4);
192                     break;
193                 case CHANNEL_VOLUME:
194                     handleVolumeCommand(command, channelUID);
195                     break;
196                 case CHANNEL_ZONE1_VOLUME:
197                     handleVolumeCommand(command, channelUID, 1);
198                     break;
199                 case CHANNEL_ZONE2_VOLUME:
200                     handleVolumeCommand(command, channelUID, 2);
201                     break;
202                 case CHANNEL_ZONE3_VOLUME:
203                     handleVolumeCommand(command, channelUID, 3);
204                     break;
205                 case CHANNEL_ZONE4_VOLUME:
206                     handleVolumeCommand(command, channelUID, 4);
207                     break;
208                 case CHANNEL_MUTE:
209                     handleMuteCommand(command, channelUID);
210                     break;
211                 case CHANNEL_ZONE1_MUTE:
212                     handleMuteCommand(command, channelUID, 1);
213                     break;
214                 case CHANNEL_ZONE2_MUTE:
215                     handleMuteCommand(command, channelUID, 2);
216                     break;
217                 case CHANNEL_ZONE3_MUTE:
218                     handleMuteCommand(command, channelUID, 3);
219                     break;
220                 case CHANNEL_ZONE4_MUTE:
221                     handleMuteCommand(command, channelUID, 4);
222                     break;
223                 case CHANNEL_MASTER_SOUND_FIELD:
224                 case CHANNEL_SOUND_FIELD:
225                     handleSoundSettings(command, channelUID);
226                     break;
227                 case CHANNEL_RADIO_FREQ:
228                     handleRadioCommand(command, channelUID);
229                     break;
230                 case CHANNEL_RADIO_STATION:
231                     handleRadioStationCommand(command, channelUID);
232                     break;
233                 case CHANNEL_RADIO_SEEK_STATION:
234                     handleRadioSeekStationCommand(command, channelUID);
235                     break;
236                 case CHANNEL_NIGHTMODE:
237                     handleNightMode(command, channelUID);
238                     break;
239                 default:
240                     logger.error("Command {}, {} not supported by {}!", id, command, channelUID);
241             }
242         } catch (IOException e) {
243             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
244         }
245     }
246
247     public void handleSoundSettings(Command command, ChannelUID channelUID) throws IOException {
248         if (command instanceof RefreshType) {
249             logger.debug("handleSoundSettings RefreshType");
250             Map<String, String> result = soundSettingsCache.getValue();
251             if (result != null) {
252                 updateState(channelUID, new StringType(result.get("soundField")));
253             }
254         }
255         if (command instanceof StringType stringCommand) {
256             logger.debug("handleSoundSettings set {}", command);
257             connection.setSoundSettings("soundField", stringCommand.toString());
258         }
259     }
260
261     public void handleNightMode(Command command, ChannelUID channelUID) throws IOException {
262         if (command instanceof RefreshType) {
263             logger.debug("handleNightMode RefreshType");
264             Map<String, String> result = soundSettingsCache.getValue();
265             if (result != null) {
266                 updateState(channelUID, new StringType(result.get("nightMode")));
267             }
268         }
269         if (command instanceof OnOffType onOffCommand) {
270             logger.debug("handleNightMode set {}", command);
271             connection.setSoundSettings("nightMode", onOffCommand == OnOffType.ON ? "on" : "off");
272         }
273     }
274
275     public void handlePowerCommand(Command command, ChannelUID channelUID) throws IOException {
276         handlePowerCommand(command, channelUID, 0);
277     }
278
279     public void handlePowerCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
280         if (command instanceof RefreshType) {
281             try {
282                 logger.debug("handlePowerCommand RefreshType {}", zone);
283                 Boolean result = powerCache[zone].getValue();
284                 updateState(channelUID, result ? OnOffType.ON : OnOffType.OFF);
285             } catch (CompletionException ex) {
286                 throw new IOException(ex.getCause());
287             }
288         }
289         if (command instanceof OnOffType onOffCommand) {
290             logger.debug("handlePowerCommand set {} {}", zone, command);
291             connection.setPower(onOffCommand == OnOffType.ON, zone);
292         }
293     }
294
295     public void handleInputCommand(Command command, ChannelUID channelUID) throws IOException {
296         handleInputCommand(command, channelUID, 0);
297     }
298
299     public void handleInputCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
300         if (command instanceof RefreshType) {
301             logger.debug("handleInputCommand RefreshType {}", zone);
302             try {
303                 SonyAudioConnection.SonyAudioInput result = inputCache[zone].getValue();
304                 if (result != null) {
305                     if (zone > 0) {
306                         inputZone.put(zone, result.input);
307                     }
308                     updateState(channelUID, inputSource(result.input));
309
310                     if (result.radioFrequency.isPresent()) {
311                         updateState(SonyAudioBindingConstants.CHANNEL_RADIO_FREQ,
312                                 new DecimalType(result.radioFrequency.get() / 1000000.0));
313                     }
314                 }
315             } catch (CompletionException ex) {
316                 throw new IOException(ex.getCause());
317             }
318         }
319         if (command instanceof StringType) {
320             logger.debug("handleInputCommand set {} {}", zone, command);
321             connection.setInput(setInputCommand(command), zone);
322         }
323     }
324
325     public void handleVolumeCommand(Command command, ChannelUID channelUID) throws IOException {
326         handleVolumeCommand(command, channelUID, 0);
327     }
328
329     public void handleVolumeCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
330         if (command instanceof RefreshType) {
331             try {
332                 logger.debug("handleVolumeCommand RefreshType {}", zone);
333                 SonyAudioConnection.SonyAudioVolume result = volumeCache[zone].getValue();
334                 if (result != null) {
335                     updateState(channelUID, new PercentType(result.volume));
336                 }
337             } catch (CompletionException ex) {
338                 throw new IOException(ex.getCause());
339             }
340         }
341         if (command instanceof PercentType percentCommand) {
342             logger.debug("handleVolumeCommand PercentType set {} {}", zone, command);
343             connection.setVolume(percentCommand.intValue(), zone);
344         }
345         if (command instanceof IncreaseDecreaseType) {
346             logger.debug("handleVolumeCommand IncreaseDecreaseType set {} {}", zone, command);
347             String change = command == IncreaseDecreaseType.INCREASE ? "+1" : "-1";
348             connection.setVolume(change, zone);
349         }
350         if (command instanceof OnOffType onOffCommand) {
351             logger.debug("handleVolumeCommand OnOffType set {} {}", zone, command);
352             connection.setMute(onOffCommand == OnOffType.ON, zone);
353         }
354     }
355
356     public void handleMuteCommand(Command command, ChannelUID channelUID) throws IOException {
357         handleMuteCommand(command, channelUID, 0);
358     }
359
360     public void handleMuteCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
361         if (command instanceof RefreshType) {
362             try {
363                 logger.debug("handleMuteCommand RefreshType {}", zone);
364                 SonyAudioConnection.SonyAudioVolume result = volumeCache[zone].getValue();
365                 if (result != null) {
366                     updateState(channelUID, result.mute ? OnOffType.ON : OnOffType.OFF);
367                 }
368             } catch (CompletionException ex) {
369                 throw new IOException(ex.getCause());
370             }
371         }
372         if (command instanceof OnOffType onOffCommand) {
373             logger.debug("handleMuteCommand set {} {}", zone, command);
374             connection.setMute(onOffCommand == OnOffType.ON, zone);
375         }
376     }
377
378     public void handleRadioCommand(Command command, ChannelUID channelUID) throws IOException {
379     }
380
381     public void handleRadioStationCommand(Command command, ChannelUID channelUID) throws IOException {
382         if (command instanceof RefreshType) {
383             updateState(channelUID, new DecimalType(currentRadioStation));
384         }
385         if (command instanceof DecimalType decimalCommand) {
386             currentRadioStation = decimalCommand.intValue();
387             String radioCommand = "radio:fm?contentId=" + currentRadioStation;
388
389             for (int i = 1; i <= 4; i++) {
390                 String input = inputZone.get(i);
391                 if (input != null && input.startsWith("radio:fm")) {
392                     connection.setInput(radioCommand, i);
393                 }
394             }
395         }
396     }
397
398     public void handleRadioSeekStationCommand(Command command, ChannelUID channelUID) throws IOException {
399         if (command instanceof RefreshType) {
400             updateState(channelUID, new StringType(""));
401         }
402         if (command instanceof StringType stringCommand) {
403             switch (stringCommand.toString()) {
404                 case "fwdSeeking":
405                     connection.radioSeekFwd();
406                     break;
407                 case "bwdSeeking":
408                     connection.radioSeekBwd();
409                     break;
410             }
411         }
412     }
413
414     public abstract String setInputCommand(Command command);
415
416     @Override
417     public void initialize() {
418         Configuration config = getThing().getConfiguration();
419         String ipAddress = (String) config.get(SonyAudioBindingConstants.HOST_PARAMETER);
420         String path = (String) config.get(SonyAudioBindingConstants.SCALAR_PATH_PARAMETER);
421         Object portO = config.get(SonyAudioBindingConstants.SCALAR_PORT_PARAMETER);
422         int port = 10000;
423         if (portO instanceof BigDecimal decimalValue) {
424             port = decimalValue.intValue();
425         } else if (portO instanceof Integer) {
426             port = (int) portO;
427         }
428
429         Object refreshO = config.get(SonyAudioBindingConstants.REFRESHINTERVAL);
430         int refresh = 0;
431         if (refreshO instanceof BigDecimal decimalValue) {
432             refresh = decimalValue.intValue();
433         } else if (refreshO instanceof Integer) {
434             refresh = (int) refreshO;
435         }
436
437         try {
438             connection = new SonyAudioConnection(ipAddress, port, path, this, scheduler, webSocketClient);
439
440             Runnable connectionChecker = () -> {
441                 try {
442                     if (!connection.checkConnection()) {
443                         if (getThing().getStatus() != ThingStatus.OFFLINE) {
444                             logger.debug("Lost connection");
445                             updateStatus(ThingStatus.OFFLINE);
446                         }
447                     }
448                 } catch (Exception ex) {
449                     logger.warn("Exception in check connection to @{}. Cause: {}", connection.getConnectionName(),
450                             ex.getMessage(), ex);
451                 }
452             };
453
454             connectionCheckerFuture = scheduler.scheduleWithFixedDelay(connectionChecker, 1, 10, TimeUnit.SECONDS);
455
456             // Start the status updater
457             startAutomaticRefresh(refresh);
458         } catch (URISyntaxException e) {
459             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
460         }
461     }
462
463     @Override
464     public void dispose() {
465         logger.debug("Disposing SonyAudioHandler");
466         super.dispose();
467         if (connectionCheckerFuture != null) {
468             connectionCheckerFuture.cancel(true);
469             connectionCheckerFuture = null;
470         }
471         if (refreshJob != null) {
472             refreshJob.cancel(true);
473             refreshJob = null;
474         }
475         if (connection != null) {
476             connection.close();
477             connection = null;
478         }
479     }
480
481     @Override
482     public void updateConnectionState(boolean connected) {
483         logger.debug("Changing connection status to {}", connected);
484         if (connected) {
485             updateStatus(ThingStatus.ONLINE);
486         } else {
487             updateStatus(ThingStatus.OFFLINE);
488         }
489     }
490
491     @Override
492     public void updateInput(int zone, SonyAudioConnection.SonyAudioInput input) {
493         inputCache[zone].putValue(input);
494         switch (zone) {
495             case 0:
496                 updateState(SonyAudioBindingConstants.CHANNEL_INPUT, inputSource(input.input));
497                 break;
498             case 1:
499                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_INPUT, inputSource(input.input));
500                 break;
501             case 2:
502                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_INPUT, inputSource(input.input));
503                 break;
504             case 3:
505                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_INPUT, inputSource(input.input));
506                 break;
507             case 4:
508                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_INPUT, inputSource(input.input));
509                 break;
510         }
511
512         if (input.radioFrequency.isPresent()) {
513             updateState(SonyAudioBindingConstants.CHANNEL_RADIO_FREQ,
514                     new DecimalType(input.radioFrequency.get() / 1000000.0));
515         }
516     }
517
518     public abstract StringType inputSource(String input);
519
520     @Override
521     public void updateCurrentRadioStation(int radioStation) {
522         currentRadioStation = radioStation;
523         updateState(SonyAudioBindingConstants.CHANNEL_RADIO_STATION, new DecimalType(currentRadioStation));
524     }
525
526     @Override
527     public void updateSeekStation(String seek) {
528         updateState(SonyAudioBindingConstants.CHANNEL_RADIO_SEEK_STATION, new StringType(seek));
529     }
530
531     @Override
532     public void updateVolume(int zone, SonyAudioConnection.SonyAudioVolume volume) {
533         volumeCache[zone].putValue(volume);
534         switch (zone) {
535             case 0:
536                 updateState(SonyAudioBindingConstants.CHANNEL_VOLUME, new PercentType(volume.volume));
537                 updateState(SonyAudioBindingConstants.CHANNEL_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
538                 break;
539             case 1:
540                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_VOLUME, new PercentType(volume.volume));
541                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
542                 break;
543             case 2:
544                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_VOLUME, new PercentType(volume.volume));
545                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
546                 break;
547             case 3:
548                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_VOLUME, new PercentType(volume.volume));
549                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
550                 break;
551             case 4:
552                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_VOLUME, new PercentType(volume.volume));
553                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
554                 break;
555         }
556     }
557
558     @Override
559     public void updatePowerStatus(int zone, boolean power) {
560         powerCache[zone].invalidateValue();
561         switch (zone) {
562             case 0:
563                 updateState(SonyAudioBindingConstants.CHANNEL_POWER, power ? OnOffType.ON : OnOffType.OFF);
564                 updateState(SonyAudioBindingConstants.CHANNEL_MASTER_POWER, power ? OnOffType.ON : OnOffType.OFF);
565                 break;
566             case 1:
567                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_POWER, power ? OnOffType.ON : OnOffType.OFF);
568                 break;
569             case 2:
570                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_POWER, power ? OnOffType.ON : OnOffType.OFF);
571                 break;
572             case 3:
573                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_POWER, power ? OnOffType.ON : OnOffType.OFF);
574                 break;
575             case 4:
576                 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_POWER, power ? OnOffType.ON : OnOffType.OFF);
577                 break;
578         }
579     }
580
581     private void startAutomaticRefresh(int refresh) {
582         if (refresh <= 0) {
583             return;
584         }
585
586         refreshJob = scheduler.scheduleWithFixedDelay(() -> {
587             List<Channel> channels = getThing().getChannels();
588             for (Channel channel : channels) {
589                 if (!isLinked(channel.getUID())) {
590                     continue;
591                 }
592                 handleCommand(channel.getUID(), RefreshType.REFRESH);
593             }
594         }, 5, refresh, TimeUnit.SECONDS);
595     }
596 }