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.vizio.internal.handler;
15 import static org.openhab.binding.vizio.internal.VizioBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Locale;
21 import java.util.Optional;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.vizio.internal.VizioConfiguration;
29 import org.openhab.binding.vizio.internal.VizioException;
30 import org.openhab.binding.vizio.internal.VizioStateDescriptionOptionProvider;
31 import org.openhab.binding.vizio.internal.communication.VizioCommunicator;
32 import org.openhab.binding.vizio.internal.communication.VizioTlsTrustManagerProvider;
33 import org.openhab.binding.vizio.internal.dto.app.CurrentApp;
34 import org.openhab.binding.vizio.internal.dto.applist.VizioApp;
35 import org.openhab.binding.vizio.internal.dto.applist.VizioApps;
36 import org.openhab.binding.vizio.internal.dto.audio.Audio;
37 import org.openhab.binding.vizio.internal.dto.audio.ItemAudio;
38 import org.openhab.binding.vizio.internal.dto.input.CurrentInput;
39 import org.openhab.binding.vizio.internal.dto.inputlist.InputList;
40 import org.openhab.binding.vizio.internal.dto.power.PowerMode;
41 import org.openhab.binding.vizio.internal.enums.KeyCommand;
42 import org.openhab.core.config.core.Configuration;
43 import org.openhab.core.io.net.http.TlsTrustManagerProvider;
44 import org.openhab.core.library.types.NextPreviousType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.PercentType;
47 import org.openhab.core.library.types.PlayPauseType;
48 import org.openhab.core.library.types.RewindFastforwardType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.thing.Channel;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseThingHandler;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.StateOption;
59 import org.openhab.core.types.UnDefType;
60 import org.osgi.framework.FrameworkUtil;
61 import org.osgi.framework.ServiceRegistration;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
65 import com.google.gson.Gson;
66 import com.google.gson.JsonSyntaxException;
69 * The {@link VizioHandler} is responsible for handling commands, which are
70 * sent to one of the channels.
72 * @author Michael Lobstein - Initial contribution
75 public class VizioHandler extends BaseThingHandler {
76 private final Logger logger = LoggerFactory.getLogger(VizioHandler.class);
77 private final HttpClient httpClient;
78 private final VizioStateDescriptionOptionProvider stateDescriptionProvider;
79 private final String dbAppsJson;
81 private @Nullable ServiceRegistration<?> serviceRegistration;
82 private @Nullable ScheduledFuture<?> refreshJob;
83 private @Nullable ScheduledFuture<?> metadataRefreshJob;
85 private VizioCommunicator communicator;
86 private List<VizioApp> userConfigApps = new ArrayList<VizioApp>();
87 private Object sequenceLock = new Object();
89 private int pairingDeviceId = -1;
90 private int pairingToken = -1;
91 private Long currentInputHash = 0L;
92 private Long currentVolumeHash = 0L;
93 private String currentApp = EMPTY;
94 private String currentInput = EMPTY;
95 private boolean currentMute = false;
96 private int currentVolume = -1;
97 private boolean powerOn = false;
98 private boolean debounce = true;
100 public VizioHandler(Thing thing, HttpClient httpClient,
101 VizioStateDescriptionOptionProvider stateDescriptionProvider, String vizioAppsJson) {
103 this.httpClient = httpClient;
104 this.stateDescriptionProvider = stateDescriptionProvider;
105 this.dbAppsJson = vizioAppsJson;
106 this.communicator = new VizioCommunicator(httpClient, EMPTY, -1, EMPTY);
110 public void initialize() {
111 logger.debug("Initializing Vizio handler");
112 final Gson gson = new Gson();
113 VizioConfiguration config = getConfigAs(VizioConfiguration.class);
116 String host = config.hostName;
117 final @Nullable String authToken = config.authToken;
119 String appListJson = config.appListJson;
121 if (host == null || host.isEmpty()) {
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
123 "@text/offline.configuration-error-hostname");
125 } else if (host.contains(":")) {
127 host = "[" + host + "]";
130 this.communicator = new VizioCommunicator(httpClient, host, config.port, authToken != null ? authToken : EMPTY);
132 // register trustmanager service to allow httpClient to accept self signed cert from the Vizio TV
133 VizioTlsTrustManagerProvider tlsTrustManagerProvider = new VizioTlsTrustManagerProvider(
134 host + ":" + config.port);
135 serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext()
136 .registerService(TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null);
138 if (authToken == null) {
139 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
140 "@text/offline.configuration-error-authtoken");
144 // if app list is not supplied in thing configuration, populate it from the json db
145 if (appListJson == null) {
146 appListJson = dbAppsJson;
148 // Update thing configuration (persistent) - store app list from db into thing so the user can update it
149 Configuration configuration = this.getConfig();
150 configuration.put(PROPERTY_APP_LIST_JSON, appListJson);
151 this.updateConfiguration(configuration);
155 VizioApps appsFromJson = gson.fromJson(appListJson, VizioApps.class);
156 if (appsFromJson != null && !appsFromJson.getApps().isEmpty()) {
157 userConfigApps = appsFromJson.getApps();
159 List<StateOption> appListOptions = new ArrayList<>();
160 userConfigApps.forEach(app -> {
161 appListOptions.add(new StateOption(app.getName(), app.getName()));
164 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
167 } catch (JsonSyntaxException e) {
168 logger.debug("Invalid App List Configuration in thing configuration. Exception: {}", e.getMessage(), e);
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
170 "@text/offline.configuration-error-applist");
174 updateStatus(ThingStatus.UNKNOWN);
176 startVizioStateRefresh();
177 startPeriodicRefresh();
181 * Start the job that queries the Vizio TV every 10 seconds to get its current status
183 private void startVizioStateRefresh() {
184 ScheduledFuture<?> refreshJob = this.refreshJob;
185 if (refreshJob == null || refreshJob.isCancelled()) {
186 this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshVizioState, 5, 10, TimeUnit.SECONDS);
191 * Get current status from the Vizio TV and update the channels
193 private void refreshVizioState() {
194 synchronized (sequenceLock) {
196 PowerMode polledPowerMode = communicator.getPowerMode();
198 if (debounce && !polledPowerMode.getItems().isEmpty()) {
199 int powerMode = polledPowerMode.getItems().get(0).getValue();
200 if (powerMode == 1) {
202 updateState(POWER, OnOffType.ON);
203 } else if (powerMode == 0) {
205 updateState(POWER, OnOffType.OFF);
207 logger.debug("Unknown power mode {}, for response object: {}", powerMode, polledPowerMode);
210 updateStatus(ThingStatus.ONLINE);
211 } catch (VizioException e) {
212 logger.debug("Unable to retrieve Vizio TV power mode info. Exception: {}", e.getMessage(), e);
213 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
214 "@text/offline.communication-error-get-power");
217 if (powerOn && (isLinked(VOLUME) || isLinked(MUTE))) {
219 Audio audioSettings = communicator.getCurrentAudioSettings();
221 Optional<ItemAudio> volumeItem = audioSettings.getItems().stream()
222 .filter(i -> VOLUME.equals(i.getCname())).findFirst();
223 if (debounce && volumeItem.isPresent()) {
224 currentVolumeHash = volumeItem.get().getHashval();
227 int polledVolume = Integer.parseInt(volumeItem.get().getValue());
228 if (polledVolume != currentVolume) {
229 currentVolume = polledVolume;
230 updateState(VOLUME, new PercentType(BigDecimal.valueOf(currentVolume)));
232 } catch (NumberFormatException e) {
233 logger.debug("Unable to parse volume value {} as int", volumeItem.get().getValue());
237 Optional<ItemAudio> muteItem = audioSettings.getItems().stream()
238 .filter(i -> MUTE.equals(i.getCname())).findFirst();
239 if (debounce && muteItem.isPresent()) {
240 String polledMute = muteItem.get().getValue().toUpperCase(Locale.ENGLISH);
242 if (ON.equals(polledMute) || OFF.equals(polledMute)) {
243 if (ON.equals(polledMute) && !currentMute) {
244 updateState(MUTE, OnOffType.ON);
246 } else if (OFF.equals(polledMute) && currentMute) {
247 updateState(MUTE, OnOffType.OFF);
251 logger.debug("Unknown mute mode {}, for response object: {}", polledMute, audioSettings);
254 } catch (VizioException e) {
255 logger.debug("Unable to retrieve Vizio TV current audio settings. Exception: {}", e.getMessage(),
257 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
258 "@text/offline.communication-error-get-audio");
262 if (powerOn && isLinked(SOURCE)) {
264 CurrentInput polledInputState = communicator.getCurrentInput();
266 if (debounce && !polledInputState.getItems().isEmpty()
267 && !currentInput.equals(polledInputState.getItems().get(0).getValue())) {
268 currentInput = polledInputState.getItems().get(0).getValue();
269 currentInputHash = polledInputState.getItems().get(0).getHashval();
270 updateState(SOURCE, new StringType(currentInput));
272 } catch (VizioException e) {
273 logger.debug("Unable to retrieve Vizio TV current input. Exception: {}", e.getMessage(), e);
274 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
275 "@text/offline.communication-error-get-input");
279 if (powerOn && isLinked(ACTIVE_APP)) {
282 CurrentApp polledApp = communicator.getCurrentApp();
283 Optional<VizioApp> currentAppData = userConfigApps.stream()
284 .filter(a -> a.getConfig().getAppId().equals(polledApp.getItem().getValue().getAppId())
285 && a.getConfig().getNameSpace()
286 .equals(polledApp.getItem().getValue().getNameSpace()))
289 if (currentAppData.isPresent()) {
290 if (!currentApp.equals(currentAppData.get().getName())) {
291 currentApp = currentAppData.get().getName();
292 updateState(ACTIVE_APP, new StringType(currentApp));
297 int appId = Integer.parseInt(polledApp.getItem().getValue().getAppId());
298 updateState(ACTIVE_APP, new StringType(String.format(UNKNOWN_APP_STR, appId,
299 polledApp.getItem().getValue().getNameSpace())));
300 } catch (NumberFormatException nfe) {
301 // Non-numeric appId received, eg: hdmi1
302 updateState(ACTIVE_APP, UnDefType.UNDEF);
305 logger.debug("Unknown app_id: {}, name_space: {}",
306 polledApp.getItem().getValue().getAppId(),
307 polledApp.getItem().getValue().getNameSpace());
310 } catch (VizioException e) {
311 logger.debug("Unable to retrieve Vizio TV current running app. Exception: {}", e.getMessage(), e);
312 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
313 "@text/offline.communication-error-get-app");
321 * Start the job to periodically retrieve various metadata from the Vizio TV every 10 minutes
323 private void startPeriodicRefresh() {
324 ScheduledFuture<?> metadataRefreshJob = this.metadataRefreshJob;
325 if (metadataRefreshJob == null || metadataRefreshJob.isCancelled()) {
326 this.metadataRefreshJob = scheduler.scheduleWithFixedDelay(this::refreshVizioMetadata, 1, 600,
332 * Update source list (hashes) and other metadata from the Vizio TV
334 private void refreshVizioMetadata() {
335 synchronized (sequenceLock) {
337 InputList inputList = communicator.getSourceInputList();
339 List<StateOption> sourceListOptions = new ArrayList<>();
340 inputList.getItems().forEach(source -> {
341 sourceListOptions.add(new StateOption(source.getName(), source.getValue().getName()));
344 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), SOURCE),
346 } catch (VizioException e) {
347 logger.debug("Unable to retrieve the Vizio TV input list. Exception: {}", e.getMessage(), e);
353 public void dispose() {
354 ScheduledFuture<?> refreshJob = this.refreshJob;
355 if (refreshJob != null) {
356 refreshJob.cancel(true);
357 this.refreshJob = null;
360 ScheduledFuture<?> metadataRefreshJob = this.metadataRefreshJob;
361 if (metadataRefreshJob != null) {
362 metadataRefreshJob.cancel(true);
363 this.metadataRefreshJob = null;
366 ServiceRegistration<?> localServiceRegistration = serviceRegistration;
367 if (localServiceRegistration != null) {
368 // remove trustmanager service
369 localServiceRegistration.unregister();
370 serviceRegistration = null;
375 public void handleCommand(ChannelUID channelUID, Command command) {
376 if (command instanceof RefreshType) {
377 logger.debug("Unsupported refresh command: {}", command);
379 switch (channelUID.getId()) {
382 synchronized (sequenceLock) {
384 if (command == OnOffType.ON) {
385 communicator.sendKeyPress(KeyCommand.POWERON.getJson());
388 communicator.sendKeyPress(KeyCommand.POWEROFF.getJson());
391 } catch (VizioException e) {
392 logger.debug("Unable to send power {} command to the Vizio TV, Exception: {}", command,
394 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
395 "@text/offline.communication-error-set-power");
401 synchronized (sequenceLock) {
403 int volume = Integer.parseInt(command.toString());
405 // volume changed again before polling has run, get current volume hash from the TV first
406 if (currentVolumeHash.equals(0L)) {
407 Audio audioSettings = communicator.getCurrentAudioSettings();
409 Optional<ItemAudio> volumeItem = audioSettings.getItems().stream()
410 .filter(i -> VOLUME.equals(i.getCname())).findFirst();
411 if (volumeItem.isPresent()) {
412 currentVolumeHash = volumeItem.get().getHashval();
414 logger.debug("Unable to get current volume hash on the Vizio TV");
418 .changeVolume(String.format(MODIFY_INT_SETTING_JSON, volume, currentVolumeHash));
419 currentVolumeHash = 0L;
420 } catch (VizioException e) {
421 logger.debug("Unable to set volume on the Vizio TV, command volume: {}, Exception: {}",
422 command, e.getMessage());
423 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
424 "@text/offline.communication-error-set-volume");
425 } catch (NumberFormatException e) {
426 logger.debug("Unable to parse command volume value {} as int", command);
432 synchronized (sequenceLock) {
434 if (command == OnOffType.ON && !currentMute) {
435 communicator.sendKeyPress(KeyCommand.MUTETOGGLE.getJson());
437 } else if (command == OnOffType.OFF && currentMute) {
438 communicator.sendKeyPress(KeyCommand.MUTETOGGLE.getJson());
441 } catch (VizioException e) {
442 logger.debug("Unable to send mute {} command to the Vizio TV, Exception: {}", command,
444 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
445 "@text/offline.communication-error-set-mute");
451 synchronized (sequenceLock) {
453 // if input changed again before polling has run, get current input hash from the TV
455 if (currentInputHash.equals(0L)) {
456 CurrentInput polledInput = communicator.getCurrentInput();
457 if (!polledInput.getItems().isEmpty()) {
458 currentInputHash = polledInput.getItems().get(0).getHashval();
462 .changeInput(String.format(MODIFY_STRING_SETTING_JSON, command, currentInputHash));
463 currentInputHash = 0L;
464 } catch (VizioException e) {
465 logger.debug("Unable to set current source on the Vizio TV, source: {}, Exception: {}",
466 command, e.getMessage());
467 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
468 "@text/offline.communication-error-set-source");
474 synchronized (sequenceLock) {
476 Optional<VizioApp> selectedApp = userConfigApps.stream()
477 .filter(a -> command.toString().equals(a.getName())).findFirst();
479 if (selectedApp.isPresent()) {
480 communicator.launchApp(selectedApp.get().getConfig());
482 logger.debug("Unknown app name: '{}', check that it exists in App List configuration",
485 } catch (VizioException e) {
486 logger.debug("Unable to launch app name: '{}' on the Vizio TV, Exception: {}", command,
488 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
489 "@text/offline.communication-error-launch-app");
495 synchronized (sequenceLock) {
497 handleControlCommand(command);
498 } catch (VizioException e) {
499 logger.debug("Unable to send control command: '{}' to the Vizio TV, Exception: {}", command,
501 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
502 "@text/offline.communication-error-send-cmd");
507 synchronized (sequenceLock) {
509 KeyCommand keyCommand = KeyCommand.valueOf(command.toString().toUpperCase(Locale.ENGLISH));
510 communicator.sendKeyPress(keyCommand.getJson());
511 } catch (IllegalArgumentException | VizioException e) {
512 logger.debug("Unable to send keypress to the Vizio TV, key: {}, Exception: {}", command,
514 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
515 "@text/offline.communication-error-send-key");
520 logger.warn("Unknown channel: '{}'", channelUID.getId());
526 private void handleControlCommand(Command command) throws VizioException {
527 if (command instanceof PlayPauseType) {
528 if (command == PlayPauseType.PLAY) {
529 communicator.sendKeyPress(KeyCommand.PLAY.getJson());
530 } else if (command == PlayPauseType.PAUSE) {
531 communicator.sendKeyPress(KeyCommand.PAUSE.getJson());
533 } else if (command instanceof NextPreviousType) {
534 if (command == NextPreviousType.NEXT) {
535 communicator.sendKeyPress(KeyCommand.RIGHT.getJson());
536 } else if (command == NextPreviousType.PREVIOUS) {
537 communicator.sendKeyPress(KeyCommand.LEFT.getJson());
539 } else if (command instanceof RewindFastforwardType) {
540 if (command == RewindFastforwardType.FASTFORWARD) {
541 communicator.sendKeyPress(KeyCommand.SEEKFWD.getJson());
542 } else if (command == RewindFastforwardType.REWIND) {
543 communicator.sendKeyPress(KeyCommand.SEEKBACK.getJson());
546 logger.warn("Unknown control command: {}", command);
551 public boolean isLinked(String channelName) {
552 Channel channel = this.thing.getChannel(channelName);
553 if (channel != null) {
554 return isLinked(channel.getUID());
560 // The remaining methods are used by the console when obtaining the auth token from the TV.
561 public void saveAuthToken(String authToken) {
562 // Store the auth token in the configuration and restart the thing
563 Configuration configuration = this.getConfig();
564 configuration.put(PROPERTY_AUTH_TOKEN, authToken);
565 this.updateConfiguration(configuration);
566 this.thingUpdated(this.getThing());
569 public int getPairingDeviceId() {
570 return pairingDeviceId;
573 public void setPairingDeviceId(int pairingDeviceId) {
574 this.pairingDeviceId = pairingDeviceId;
577 public int getPairingToken() {
581 public void setPairingToken(int pairingToken) {
582 this.pairingToken = pairingToken;