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.handler;
15 import static org.openhab.binding.sonyaudio.internal.SonyAudioBindingConstants.*;
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;
23 import java.util.concurrent.CompletionException;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.function.Supplier;
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;
51 * The {@link SonyAudioHandler} is responsible for handling commands, which are
52 * sent to one of the channels.
54 * @author David Ã…berg - Initial contribution
56 abstract class SonyAudioHandler extends BaseThingHandler implements SonyAudioEventListener {
58 private final Logger logger = LoggerFactory.getLogger(SonyAudioHandler.class);
60 private WebSocketClient webSocketClient;
62 protected SonyAudioConnection connection;
63 private ScheduledFuture<?> connectionCheckerFuture;
64 private ScheduledFuture<?> refreshJob;
66 private int currentRadioStation = 0;
67 private final Map<Integer, String> input_zone = new HashMap<>();
69 private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
71 protected ExpiringCache<Boolean>[] powerCache;
72 protected ExpiringCache<SonyAudioConnection.SonyAudioInput>[] inputCache;
73 protected ExpiringCache<SonyAudioConnection.SonyAudioVolume>[] volumeCache;
74 protected ExpiringCache<Map<String, String>> soundSettingsCache;
76 protected Supplier<Boolean>[] powerSupplier;
77 protected Supplier<SonyAudioConnection.SonyAudioInput>[] inputSupplier;
78 protected Supplier<SonyAudioConnection.SonyAudioVolume>[] volumeSupplier;
79 protected Supplier<Map<String, String>> soundSettingsSupplier;
81 @SuppressWarnings("unchecked")
82 public SonyAudioHandler(Thing thing, WebSocketClient webSocketClient) {
85 this.webSocketClient = webSocketClient;
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];
94 for (int i = 0; i < 5; i++) {
97 inputSupplier[i] = () -> {
99 return connection.getInput(index);
100 } catch (IOException ex) {
101 throw new CompletionException(ex);
105 powerSupplier[i] = () -> {
107 return connection.getPower(index);
108 } catch (IOException ex) {
109 throw new CompletionException(ex);
113 volumeSupplier[i] = () -> {
115 return connection.getVolume(index);
116 } catch (IOException ex) {
117 throw new CompletionException(ex);
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]);
126 soundSettingsSupplier = () -> {
128 return connection.getSoundSettings();
129 } catch (IOException ex) {
130 throw new CompletionException(ex);
134 soundSettingsCache = new ExpiringCache<>(CACHE_EXPIRY, soundSettingsSupplier);
138 public void handleCommand(ChannelUID channelUID, Command command) {
139 if (connection == null || !connection.checkConnection()) {
140 logger.debug("Thing not yet initialized!");
144 String id = channelUID.getId();
146 logger.debug("Handle command {} {}", channelUID, command);
148 if (getThing().getStatusInfo().getStatus() != ThingStatus.ONLINE) {
151 case CHANNEL_MASTER_POWER:
152 logger.debug("Device powered off sending {} {}", channelUID, command);
155 logger.debug("Device powered off ignore command {} {}", channelUID, command);
163 case CHANNEL_MASTER_POWER:
164 handlePowerCommand(command, channelUID);
166 case CHANNEL_ZONE1_POWER:
167 handlePowerCommand(command, channelUID, 1);
169 case CHANNEL_ZONE2_POWER:
170 handlePowerCommand(command, channelUID, 2);
172 case CHANNEL_ZONE3_POWER:
173 handlePowerCommand(command, channelUID, 3);
175 case CHANNEL_ZONE4_POWER:
176 handlePowerCommand(command, channelUID, 4);
179 handleInputCommand(command, channelUID);
181 case CHANNEL_ZONE1_INPUT:
182 handleInputCommand(command, channelUID, 1);
184 case CHANNEL_ZONE2_INPUT:
185 handleInputCommand(command, channelUID, 2);
187 case CHANNEL_ZONE3_INPUT:
188 handleInputCommand(command, channelUID, 3);
190 case CHANNEL_ZONE4_INPUT:
191 handleInputCommand(command, channelUID, 4);
194 handleVolumeCommand(command, channelUID);
196 case CHANNEL_ZONE1_VOLUME:
197 handleVolumeCommand(command, channelUID, 1);
199 case CHANNEL_ZONE2_VOLUME:
200 handleVolumeCommand(command, channelUID, 2);
202 case CHANNEL_ZONE3_VOLUME:
203 handleVolumeCommand(command, channelUID, 3);
205 case CHANNEL_ZONE4_VOLUME:
206 handleVolumeCommand(command, channelUID, 4);
209 handleMuteCommand(command, channelUID);
211 case CHANNEL_ZONE1_MUTE:
212 handleMuteCommand(command, channelUID, 1);
214 case CHANNEL_ZONE2_MUTE:
215 handleMuteCommand(command, channelUID, 2);
217 case CHANNEL_ZONE3_MUTE:
218 handleMuteCommand(command, channelUID, 3);
220 case CHANNEL_ZONE4_MUTE:
221 handleMuteCommand(command, channelUID, 4);
223 case CHANNEL_MASTER_SOUND_FIELD:
224 case CHANNEL_SOUND_FIELD:
225 handleSoundSettings(command, channelUID);
227 case CHANNEL_RADIO_FREQ:
228 handleRadioCommand(command, channelUID);
230 case CHANNEL_RADIO_STATION:
231 handleRadioStationCommand(command, channelUID);
233 case CHANNEL_RADIO_SEEK_STATION:
234 handleRadioSeekStationCommand(command, channelUID);
236 case CHANNEL_NIGHTMODE:
237 handleNightMode(command, channelUID);
240 logger.error("Command {}, {} not supported by {}!", id, command, channelUID);
242 } catch (IOException e) {
243 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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")));
255 if (command instanceof StringType stringCommand) {
256 logger.debug("handleSoundSettings set {}", command);
257 connection.setSoundSettings("soundField", stringCommand.toString());
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")));
269 if (command instanceof OnOffType onOffCommand) {
270 logger.debug("handleNightMode set {}", command);
271 connection.setSoundSettings("nightMode", onOffCommand == OnOffType.ON ? "on" : "off");
275 public void handlePowerCommand(Command command, ChannelUID channelUID) throws IOException {
276 handlePowerCommand(command, channelUID, 0);
279 public void handlePowerCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
280 if (command instanceof RefreshType) {
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());
289 if (command instanceof OnOffType onOffCommand) {
290 logger.debug("handlePowerCommand set {} {}", zone, command);
291 connection.setPower(onOffCommand == OnOffType.ON, zone);
295 public void handleInputCommand(Command command, ChannelUID channelUID) throws IOException {
296 handleInputCommand(command, channelUID, 0);
299 public void handleInputCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
300 if (command instanceof RefreshType) {
301 logger.debug("handleInputCommand RefreshType {}", zone);
303 SonyAudioConnection.SonyAudioInput result = inputCache[zone].getValue();
304 if (result != null) {
306 input_zone.put(zone, result.input);
308 updateState(channelUID, inputSource(result.input));
310 if (result.radio_freq.isPresent()) {
311 updateState(SonyAudioBindingConstants.CHANNEL_RADIO_FREQ,
312 new DecimalType(result.radio_freq.get() / 1000000.0));
315 } catch (CompletionException ex) {
316 throw new IOException(ex.getCause());
319 if (command instanceof StringType) {
320 logger.debug("handleInputCommand set {} {}", zone, command);
321 connection.setInput(setInputCommand(command), zone);
325 public void handleVolumeCommand(Command command, ChannelUID channelUID) throws IOException {
326 handleVolumeCommand(command, channelUID, 0);
329 public void handleVolumeCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
330 if (command instanceof RefreshType) {
332 logger.debug("handleVolumeCommand RefreshType {}", zone);
333 SonyAudioConnection.SonyAudioVolume result = volumeCache[zone].getValue();
334 if (result != null) {
335 updateState(channelUID, new PercentType(result.volume));
337 } catch (CompletionException ex) {
338 throw new IOException(ex.getCause());
341 if (command instanceof PercentType percentCommand) {
342 logger.debug("handleVolumeCommand PercentType set {} {}", zone, command);
343 connection.setVolume(percentCommand.intValue(), zone);
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);
350 if (command instanceof OnOffType onOffCommand) {
351 logger.debug("handleVolumeCommand OnOffType set {} {}", zone, command);
352 connection.setMute(onOffCommand == OnOffType.ON, zone);
356 public void handleMuteCommand(Command command, ChannelUID channelUID) throws IOException {
357 handleMuteCommand(command, channelUID, 0);
360 public void handleMuteCommand(Command command, ChannelUID channelUID, int zone) throws IOException {
361 if (command instanceof RefreshType) {
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);
368 } catch (CompletionException ex) {
369 throw new IOException(ex.getCause());
372 if (command instanceof OnOffType onOffCommand) {
373 logger.debug("handleMuteCommand set {} {}", zone, command);
374 connection.setMute(onOffCommand == OnOffType.ON, zone);
378 public void handleRadioCommand(Command command, ChannelUID channelUID) throws IOException {
381 public void handleRadioStationCommand(Command command, ChannelUID channelUID) throws IOException {
382 if (command instanceof RefreshType) {
383 updateState(channelUID, new DecimalType(currentRadioStation));
385 if (command instanceof DecimalType decimalCommand) {
386 currentRadioStation = decimalCommand.intValue();
387 String radioCommand = "radio:fm?contentId=" + currentRadioStation;
389 for (int i = 1; i <= 4; i++) {
390 String input = input_zone.get(i);
391 if (input != null && input.startsWith("radio:fm")) {
392 connection.setInput(radioCommand, i);
398 public void handleRadioSeekStationCommand(Command command, ChannelUID channelUID) throws IOException {
399 if (command instanceof RefreshType) {
400 updateState(channelUID, new StringType(""));
402 if (command instanceof StringType stringCommand) {
403 switch (stringCommand.toString()) {
405 connection.radioSeekFwd();
408 connection.radioSeekBwd();
414 public abstract String setInputCommand(Command command);
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 port_o = config.get(SonyAudioBindingConstants.SCALAR_PORT_PARAMETER);
423 if (port_o instanceof BigDecimal decimalValue) {
424 port = decimalValue.intValue();
425 } else if (port_o instanceof Integer) {
429 Object refresh_o = config.get(SonyAudioBindingConstants.REFRESHINTERVAL);
431 if (refresh_o instanceof BigDecimal decimalValue) {
432 refresh = decimalValue.intValue();
433 } else if (refresh_o instanceof Integer) {
434 refresh = (int) refresh_o;
438 connection = new SonyAudioConnection(ipAddress, port, path, this, scheduler, webSocketClient);
440 Runnable connectionChecker = () -> {
442 if (!connection.checkConnection()) {
443 if (getThing().getStatus() != ThingStatus.OFFLINE) {
444 logger.debug("Lost connection");
445 updateStatus(ThingStatus.OFFLINE);
448 } catch (Exception ex) {
449 logger.warn("Exception in check connection to @{}. Cause: {}", connection.getConnectionName(),
450 ex.getMessage(), ex);
454 connectionCheckerFuture = scheduler.scheduleWithFixedDelay(connectionChecker, 1, 10, TimeUnit.SECONDS);
456 // Start the status updater
457 startAutomaticRefresh(refresh);
458 } catch (URISyntaxException e) {
459 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
464 public void dispose() {
465 logger.debug("Disposing SonyAudioHandler");
467 if (connectionCheckerFuture != null) {
468 connectionCheckerFuture.cancel(true);
469 connectionCheckerFuture = null;
471 if (refreshJob != null) {
472 refreshJob.cancel(true);
475 if (connection != null) {
482 public void updateConnectionState(boolean connected) {
483 logger.debug("Changing connection status to {}", connected);
485 updateStatus(ThingStatus.ONLINE);
487 updateStatus(ThingStatus.OFFLINE);
492 public void updateInput(int zone, SonyAudioConnection.SonyAudioInput input) {
493 inputCache[zone].putValue(input);
496 updateState(SonyAudioBindingConstants.CHANNEL_INPUT, inputSource(input.input));
499 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_INPUT, inputSource(input.input));
502 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_INPUT, inputSource(input.input));
505 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_INPUT, inputSource(input.input));
508 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_INPUT, inputSource(input.input));
512 if (input.radio_freq.isPresent()) {
513 updateState(SonyAudioBindingConstants.CHANNEL_RADIO_FREQ,
514 new DecimalType(input.radio_freq.get() / 1000000.0));
518 public abstract StringType inputSource(String input);
521 public void updateCurrentRadioStation(int radioStation) {
522 currentRadioStation = radioStation;
523 updateState(SonyAudioBindingConstants.CHANNEL_RADIO_STATION, new DecimalType(currentRadioStation));
527 public void updateSeekStation(String seek) {
528 updateState(SonyAudioBindingConstants.CHANNEL_RADIO_SEEK_STATION, new StringType(seek));
532 public void updateVolume(int zone, SonyAudioConnection.SonyAudioVolume volume) {
533 volumeCache[zone].putValue(volume);
536 updateState(SonyAudioBindingConstants.CHANNEL_VOLUME, new PercentType(volume.volume));
537 updateState(SonyAudioBindingConstants.CHANNEL_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
540 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_VOLUME, new PercentType(volume.volume));
541 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
544 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_VOLUME, new PercentType(volume.volume));
545 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
548 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_VOLUME, new PercentType(volume.volume));
549 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
552 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_VOLUME, new PercentType(volume.volume));
553 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_MUTE, volume.mute ? OnOffType.ON : OnOffType.OFF);
559 public void updatePowerStatus(int zone, boolean power) {
560 powerCache[zone].invalidateValue();
563 updateState(SonyAudioBindingConstants.CHANNEL_POWER, power ? OnOffType.ON : OnOffType.OFF);
564 updateState(SonyAudioBindingConstants.CHANNEL_MASTER_POWER, power ? OnOffType.ON : OnOffType.OFF);
567 updateState(SonyAudioBindingConstants.CHANNEL_ZONE1_POWER, power ? OnOffType.ON : OnOffType.OFF);
570 updateState(SonyAudioBindingConstants.CHANNEL_ZONE2_POWER, power ? OnOffType.ON : OnOffType.OFF);
573 updateState(SonyAudioBindingConstants.CHANNEL_ZONE3_POWER, power ? OnOffType.ON : OnOffType.OFF);
576 updateState(SonyAudioBindingConstants.CHANNEL_ZONE4_POWER, power ? OnOffType.ON : OnOffType.OFF);
581 private void startAutomaticRefresh(int refresh) {
586 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
587 List<Channel> channels = getThing().getChannels();
588 for (Channel channel : channels) {
589 if (!isLinked(channel.getUID())) {
592 handleCommand(channel.getUID(), RefreshType.REFRESH);
594 }, 5, refresh, TimeUnit.SECONDS);