[vizio] Vizio TV binding - Initial contribution (#13309)

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein
2022-12-06 08:37:54 -06:00
committed by GitHub
parent a9d4244fd8
commit fc529777e4
45 changed files with 3611 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.vizio-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-vizio" description="Vizio Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.vizio/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link VizioBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class VizioBindingConstants {
public static final String BINDING_ID = "vizio";
public static final String PROPERTY_UUID = "uuid";
public static final String PROPERTY_HOST_NAME = "hostName";
public static final String PROPERTY_PORT = "port";
public static final String PROPERTY_AUTH_TOKEN = "authToken";
public static final String PROPERTY_APP_LIST_JSON = "appListJson";
public static final String EMPTY = "";
public static final String MODIFY_STRING_SETTING_JSON = "{\"REQUEST\": \"MODIFY\",\"VALUE\": \"%s\",\"HASHVAL\": %d}";
public static final String MODIFY_INT_SETTING_JSON = "{\"REQUEST\": \"MODIFY\",\"VALUE\": %d,\"HASHVAL\": %d}";
public static final String UNKNOWN_APP_STR = "Unknown app_id: %d, name_space: %d";
public static final String ON = "ON";
public static final String OFF = "OFF";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_VIZIO_TV = new ThingTypeUID(BINDING_ID, "vizio_tv");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_VIZIO_TV);
// List of all Channel id's
public static final String POWER = "power";
public static final String VOLUME = "volume";
public static final String MUTE = "mute";
public static final String SOURCE = "source";
public static final String ACTIVE_APP = "activeApp";
public static final String CONTROL = "control";
public static final String BUTTON = "button";
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link VizioConfiguration} is the class used to match the
* thing configuration.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class VizioConfiguration {
public @Nullable String hostName;
public Integer port = 7345;
public @Nullable String authToken;
public @Nullable String appListJson;
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link VizioException} extends Exception
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class VizioException extends Exception {
private static final long serialVersionUID = 1L;
public VizioException(String errorMessage) {
super(errorMessage);
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal;
import static org.openhab.binding.vizio.internal.VizioBindingConstants.SUPPORTED_THING_TYPES_UIDS;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.vizio.internal.appdb.VizioAppDbService;
import org.openhab.binding.vizio.internal.handler.VizioHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link VizioHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.vizio")
public class VizioHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
private final VizioStateDescriptionOptionProvider stateDescriptionProvider;
private final String vizioAppsJson;
@Activate
public VizioHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference VizioStateDescriptionOptionProvider provider,
final @Reference VizioAppDbService vizioAppDbService) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.stateDescriptionProvider = provider;
this.vizioAppsJson = vizioAppDbService.getVizioAppsJson();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
VizioHandler handler = new VizioHandler(thing, httpClient, stateDescriptionProvider, vizioAppsJson);
return handler;
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link VizioStateDescriptionOptionProvider} is a Dynamic provider of state options while leaving other state
* description fields as original.
*
* @author Michael Lobstein - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, VizioStateDescriptionOptionProvider.class })
@NonNullByDefault
public class VizioStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
@Activate
public VizioStateDescriptionOptionProvider(final @Reference EventPublisher eventPublisher,
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.appdb;
import static org.openhab.binding.vizio.internal.VizioBindingConstants.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VizioAppDbService} class makes available a JSON list of known apps on Vizio TVs.
*
* @author Michael Lobstein - Initial Contribution
*/
@Component(service = VizioAppDbService.class)
@NonNullByDefault
public class VizioAppDbService {
private final Logger logger = LoggerFactory.getLogger(VizioAppDbService.class);
private String vizioAppsJson;
@Activate
public VizioAppDbService() {
try {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("/db/apps.json");
if (is != null) {
vizioAppsJson = new String(is.readAllBytes(), StandardCharsets.UTF_8);
} else {
vizioAppsJson = EMPTY;
}
} catch (IOException e) {
logger.warn("Unable to load Vizio app list : {}", e.getMessage());
vizioAppsJson = EMPTY;
}
}
public String getVizioAppsJson() {
return vizioAppsJson;
}
}

View File

@@ -0,0 +1,293 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.communication;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.vizio.internal.VizioException;
import org.openhab.binding.vizio.internal.dto.PutResponse;
import org.openhab.binding.vizio.internal.dto.app.CurrentApp;
import org.openhab.binding.vizio.internal.dto.applist.VizioAppConfig;
import org.openhab.binding.vizio.internal.dto.audio.Audio;
import org.openhab.binding.vizio.internal.dto.input.CurrentInput;
import org.openhab.binding.vizio.internal.dto.inputlist.InputList;
import org.openhab.binding.vizio.internal.dto.pairing.PairingComplete;
import org.openhab.binding.vizio.internal.dto.pairing.PairingStart;
import org.openhab.binding.vizio.internal.dto.power.PowerMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VizioCommunicator} class contains methods for accessing the HTTP interface of Vizio TVs
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class VizioCommunicator {
private final Logger logger = LoggerFactory.getLogger(VizioCommunicator.class);
private static final String AUTH_HEADER = "AUTH";
private static final String JSON_CONTENT_TYPE = "application/json";
private static final String JSON_VALUE = "{\"VALUE\": %s}";
private final HttpClient httpClient;
private final Gson gson = new GsonBuilder().serializeNulls().create();
private final String authToken;
private final String urlPowerMode;
private final String urlCurrentAudio;
private final String urlCurrentInput;
private final String urlInputList;
private final String urlChangeVolume;
private final String urlCurrentApp;
private final String urlLaunchApp;
private final String urlKeyPress;
private final String urlStartPairing;
private final String urlSubmitPairingCode;
public VizioCommunicator(HttpClient httpClient, String host, int port, String authToken) {
this.httpClient = httpClient;
this.authToken = authToken;
final String baseUrl = "https://" + host + ":" + port;
urlPowerMode = baseUrl + "/state/device/power_mode";
urlCurrentAudio = baseUrl + "/menu_native/dynamic/tv_settings/audio";
urlChangeVolume = baseUrl + "/menu_native/dynamic/tv_settings/audio/volume";
urlCurrentInput = baseUrl + "/menu_native/dynamic/tv_settings/devices/current_input";
urlInputList = baseUrl + "/menu_native/dynamic/tv_settings/devices/name_input";
urlCurrentApp = baseUrl + "/app/current";
urlLaunchApp = baseUrl + "/app/launch";
urlKeyPress = baseUrl + "/key_command/";
urlStartPairing = baseUrl + "/pairing/start";
urlSubmitPairingCode = baseUrl + "/pairing/pair";
}
/**
* Get the current power state of the Vizio TV
*
* @return A PowerMode response object
* @throws VizioException
*
*/
public PowerMode getPowerMode() throws VizioException {
return fromJson(getCommand(urlPowerMode), PowerMode.class);
}
/**
* Get the current audio settings of the Vizio TV
*
* @return An Audio response object
* @throws VizioException
*
*/
public Audio getCurrentAudioSettings() throws VizioException {
return fromJson(getCommand(urlCurrentAudio), Audio.class);
}
/**
* Change the volume of the Vizio TV
*
* @param the command JSON for the desired volue
* @return A PutResponse response object
* @throws VizioException
*
*/
public PutResponse changeVolume(String commandJSON) throws VizioException {
return fromJson(putCommand(urlChangeVolume, commandJSON), PutResponse.class);
}
/**
* Get the currently selected input of the Vizio TV
*
* @return A CurrentInput response object
* @throws VizioException
*
*/
public CurrentInput getCurrentInput() throws VizioException {
return fromJson(getCommand(urlCurrentInput), CurrentInput.class);
}
/**
* Change the currently selected input of the Vizio TV
*
* @param the command JSON for the selected input
* @return A PutResponse response object
* @throws VizioException
*
*/
public PutResponse changeInput(String commandJSON) throws VizioException {
return fromJson(putCommand(urlCurrentInput, commandJSON), PutResponse.class);
}
/**
* Get the list of available source inputs from the Vizio TV
*
* @return An InputList response object
* @throws VizioException
*
*/
public InputList getSourceInputList() throws VizioException {
return fromJson(getCommand(urlInputList), InputList.class);
}
/**
* Get the id of the app currently running on the Vizio TV
*
* @return A CurrentApp response object
* @throws VizioException
*
*/
public CurrentApp getCurrentApp() throws VizioException {
return fromJson(getCommand(urlCurrentApp), CurrentApp.class);
}
/**
* Launch a given streaming app on the Vizio TV
*
* @param the VizioAppConfig data for the app to launch
* @return A PutResponse response object
* @throws VizioException
*
*/
public PutResponse launchApp(VizioAppConfig appConfig) throws VizioException {
return fromJson(putCommand(urlLaunchApp, String.format(JSON_VALUE, gson.toJson(appConfig))), PutResponse.class);
}
/**
* Send a key press command to the Vizio TV
*
* @param the command JSON for the key press
* @return A PutResponse response object
* @throws VizioException
*
*/
public PutResponse sendKeyPress(String commandJSON) throws VizioException {
return fromJson(putCommand(urlKeyPress, commandJSON), PutResponse.class);
}
/**
* Start the pairing process to obtain an auth token from the TV
*
* @param the deviceName that is displayed in the TV settings after the device is registered
* @param the deviceId a unique number that identifies this pairing request
* @return A PairingStart response object
* @throws VizioException
*
*/
public PairingStart starPairing(String deviceName, int deviceId) throws VizioException {
return fromJson(
putCommand(urlStartPairing,
String.format("{ \"DEVICE_NAME\": \"%s\", \"DEVICE_ID\": \"%d\" }", deviceName, deviceId)),
PairingStart.class);
}
/**
* Finish the pairing process by submitting the code that was displayed on the TV to obtain the auth token
*
* @param the same deviceId that was used by startPairing()
* @param the pairingCode that was displayed on the TV
* @param the pairingToken returned by startPairing()
* @return A PairingComplete response object
* @throws VizioException
*
*/
public PairingComplete submitPairingCode(int deviceId, String pairingCode, int pairingToken) throws VizioException {
return fromJson(putCommand(urlSubmitPairingCode, String.format(
"{\"DEVICE_ID\": \"%d\",\"CHALLENGE_TYPE\": 1,\"RESPONSE_VALUE\": \"%s\",\"PAIRING_REQ_TOKEN\": %d}",
deviceId, pairingCode, pairingToken)), PairingComplete.class);
}
/**
* Sends a GET request to the Vizio TV
*
* @param url The url used to retrieve status information from the Vizio TV
* @return The response content of the http request
* @throws VizioException
*
*/
private String getCommand(String url) throws VizioException {
try {
final Request request = httpClient.newRequest(url).method(HttpMethod.GET);
request.header(AUTH_HEADER, authToken);
request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
final ContentResponse response = request.send();
logger.trace("GET url: {}, response: {}", url, response.getContentAsString());
return response.getContentAsString();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new VizioException("Error executing vizio GET command, URL: " + url + " " + e.getMessage());
}
}
/**
* Sends a PUT request to the Vizio TV
*
* @param url The url used to send a command to the Vizio TV
* @param commandJSON The JSON data needed to execute the command
* @return The response content of the http request
* @throws VizioException
*
*/
private String putCommand(String url, String commandJSON) throws VizioException {
try {
final Request request = httpClient.newRequest(url).method(HttpMethod.PUT);
if (!url.contains("pairing")) {
request.header(AUTH_HEADER, authToken);
}
request.content(new StringContentProvider(commandJSON), JSON_CONTENT_TYPE);
final ContentResponse response = request.send();
logger.trace("PUT url: {}, response: {}", url, response.getContentAsString());
return response.getContentAsString();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new VizioException("Error executing vizio PUT command, URL: " + url + e.getMessage());
}
}
/**
* Wrapper for the Gson fromJson() method that encapsulates exception handling
*
* @param json The JSON string to be deserialized
* @param classOfT The type of class to be returned
* @return the deserialized object
* @throws VizioException
*
*/
private <T> T fromJson(String json, Class<T> classOfT) throws VizioException {
Object obj = null;
try {
obj = gson.fromJson(json, classOfT);
} catch (JsonSyntaxException e) {
throw new VizioException("Error Parsing JSON string: " + json + ", Exception: " + e.getMessage());
}
if (obj != null) {
return classOfT.cast(obj);
} else {
throw new VizioException("Error creating " + classOfT.getSimpleName() + " object for JSON string: " + json);
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.communication;
import java.net.MalformedURLException;
import java.security.cert.CertificateException;
import javax.net.ssl.X509ExtendedTrustManager;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.net.http.PEMTrustManager;
import org.openhab.core.io.net.http.TlsTrustManagerProvider;
import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides a {@link PEMTrustManager} to allow secure connections to a Vizio TV that uses self signed
* certificates.
*
* @author Christoph Weitkamp - Initial Contribution
* @author Michael Lobstein - Adapted for Vizio binding
*/
@NonNullByDefault
public class VizioTlsTrustManagerProvider implements TlsTrustManagerProvider {
private final String hostname;
private final Logger logger = LoggerFactory.getLogger(VizioTlsTrustManagerProvider.class);
public VizioTlsTrustManagerProvider(String hostname) {
this.hostname = hostname;
}
@Override
public String getHostName() {
return hostname;
}
@Override
public X509ExtendedTrustManager getTrustManager() {
try {
logger.trace("Use self-signed certificate downloaded from Vizio TV.");
return PEMTrustManager.getInstanceFromServer("https://" + getHostName());
} catch (CertificateException | MalformedURLException e) {
logger.debug("An unexpected exception occurred - returning a TrustAllTrustManager: {}", e.getMessage(), e);
}
return TrustAllTrustManager.getInstance();
}
}

View File

@@ -0,0 +1,183 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.console;
import static org.openhab.binding.vizio.internal.VizioBindingConstants.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Random;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.vizio.internal.VizioException;
import org.openhab.binding.vizio.internal.communication.VizioCommunicator;
import org.openhab.binding.vizio.internal.dto.pairing.PairingComplete;
import org.openhab.binding.vizio.internal.handler.VizioHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link VizioCommandExtension} is responsible for handling console commands
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class VizioCommandExtension extends AbstractConsoleCommandExtension {
private static final String START_PAIRING = "start_pairing";
private static final String SUBMIT_CODE = "submit_code";
private final ThingRegistry thingRegistry;
private final HttpClient httpClient;
@Activate
public VizioCommandExtension(final @Reference ThingRegistry thingRegistry,
final @Reference HttpClientFactory httpClientFactory) {
super("vizio", "Interact with the Vizio binding to get an authentication token from the TV.");
this.thingRegistry = thingRegistry;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public void execute(String[] args, Console console) {
if (args.length == 3) {
Thing thing = null;
try {
ThingUID thingUID = new ThingUID(args[0]);
thing = thingRegistry.get(thingUID);
} catch (IllegalArgumentException e) {
thing = null;
}
ThingHandler thingHandler = null;
VizioHandler handler = null;
if (thing != null) {
thingHandler = thing.getHandler();
if (thingHandler instanceof VizioHandler) {
handler = (VizioHandler) thingHandler;
}
}
if (thing == null) {
console.println("Bad thing id '" + args[0] + "'");
printUsage(console);
} else if (thingHandler == null) {
console.println("No handler initialized for the thing id '" + args[0] + "'");
printUsage(console);
} else if (handler == null) {
console.println("'" + args[0] + "' is not a Vizio thing id");
printUsage(console);
} else {
String host = (String) thing.getConfiguration().get(PROPERTY_HOST_NAME);
BigDecimal port = (BigDecimal) thing.getConfiguration().get(PROPERTY_PORT);
if (host == null || host.isEmpty() || port.signum() < 1) {
console.println(
"Error! Host Name and Port must be specified in thing configuration before paring.");
return;
} else if (host.contains(":")) {
// format for ipv6
host = "[" + host + "]";
}
VizioCommunicator communicator = new VizioCommunicator(httpClient, host, port.intValue(), EMPTY);
switch (args[1]) {
case START_PAIRING:
try {
Random rng = new Random();
int pairingDeviceId = rng.nextInt(100000);
int pairingToken = communicator.starPairing(args[2], pairingDeviceId).getItem()
.getPairingReqToken();
if (pairingToken != -1) {
handler.setPairingDeviceId(pairingDeviceId);
handler.setPairingToken(pairingToken);
console.println("Pairing has been started!");
console.println(
"Please note the 4 digit code displayed on the TV and substitute it into the following console command:");
console.println(
"openhab:vizio " + handler.getThing().getUID() + " " + SUBMIT_CODE + " <NNNN>");
} else {
console.println("Unable to obtain pairing token!");
}
} catch (VizioException e) {
console.println("Error! Unable to start pairing process.");
console.println("Exception was: " + e.getMessage());
}
break;
case SUBMIT_CODE:
try {
int pairingDeviceId = handler.getPairingDeviceId();
int pairingToken = handler.getPairingToken();
if (pairingDeviceId < 0 || pairingToken < 0) {
console.println("Error! '" + START_PAIRING + "' command must be completed first.");
console.println(
"Please issue the following command and substitute the desired device name.");
console.println("openhab:vizio " + handler.getThing().getUID() + " " + START_PAIRING
+ " <deviceName>");
break;
}
Integer.valueOf(args[2]);
PairingComplete authTokenResp = communicator.submitPairingCode(pairingDeviceId, args[2],
pairingToken);
if (authTokenResp.getItem().getAuthToken() != EMPTY) {
console.println("Pairing complete!");
console.println("The auth token: " + authTokenResp.getItem().getAuthToken()
+ " was received and will be added to the thing configuration.");
console.println(
"If the thing is provisioned via a file, the token must be manually added to the thing configuration.");
handler.setPairingDeviceId(-1);
handler.setPairingToken(-1);
handler.saveAuthToken(authTokenResp.getItem().getAuthToken());
} else {
console.println("Unable to obtain auth token!");
}
} catch (NumberFormatException nfe) {
console.println(
"Error! Pairing code must be numeric. Check console command and try again.");
} catch (VizioException e) {
console.println("Error! Unable to complete pairing process.");
console.println("Exception was: " + e.getMessage());
}
break;
default:
printUsage(console);
break;
}
}
} else {
printUsage(console);
}
}
@Override
public List<String> getUsages() {
return List.of(new String[] {
buildCommandUsage("<thingUID> " + START_PAIRING + " <deviceName>", "start pairing process"),
buildCommandUsage("<thingUID> " + SUBMIT_CODE + " <pairingCode>", "submit pairing code") });
}
}

View File

@@ -0,0 +1,132 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.discovery;
import static org.openhab.binding.vizio.internal.VizioBindingConstants.*;
import java.net.InetAddress;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VizioDiscoveryParticipant} is responsible processing the
* results of searches for mDNS services of type _viziocast._tcp.local.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "discovery.vizio")
public class VizioDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(VizioDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return "_viziocast._tcp.local.";
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
DiscoveryResult result = null;
ThingUID thingUid = getThingUID(service);
if (thingUid != null) {
InetAddress ip = getIpAddress(service);
if (ip == null) {
return null;
}
String inetAddress = ip.toString().substring(1); // trim leading slash
String label = service.getName();
int port = service.getPort();
result = DiscoveryResultBuilder.create(thingUid).withLabel(label).withRepresentationProperty(PROPERTY_UUID)
.withProperty(PROPERTY_UUID, thingUid.getId())
.withProperty(Thing.PROPERTY_MODEL_ID, service.getPropertyString("mdl"))
.withProperty(PROPERTY_HOST_NAME, inetAddress).withProperty(PROPERTY_PORT, port).build();
logger.debug("Created {} for Vizio TV at {}, name: '{}'", result, inetAddress, label);
}
return result;
}
/**
* @see org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant#getThingUID(javax.jmdns.ServiceInfo)
*/
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
if (service.getType() != null && service.getType().equals(getServiceType())) {
String uidName = getUIDName(service);
return uidName != null ? new ThingUID(THING_TYPE_VIZIO_TV, uidName) : null;
}
return null;
}
/**
* Gets the UID name from the mdns record txt info (mac address), fall back with IP address
*
* @param service the mdns service
* @return the UID name
*/
private @Nullable String getUIDName(ServiceInfo service) {
String uid = service.getPropertyString("eth");
if (uid == null || uid.endsWith("000") || uid.length() < 12) {
uid = service.getPropertyString("wifi");
}
if (uid == null || uid.endsWith("000") || uid.length() < 12) {
InetAddress ip = getIpAddress(service);
if (ip == null) {
return null;
} else {
uid = ip.toString();
}
}
return uid.replaceAll("[^A-Za-z0-9_]", "_");
}
/**
* {@link InetAddress} gets the IP address of the device in v4 or v6 format.
*
* @param ServiceInfo service
* @return InetAddress the IP address
*
*/
private @Nullable InetAddress getIpAddress(ServiceInfo service) {
InetAddress address = null;
for (InetAddress addr : service.getInet4Addresses()) {
return addr;
}
// Fall back for Inet6addresses
for (InetAddress addr : service.getInet6Addresses()) {
return addr;
}
return address;
}
}

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Item} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class Item {
@SerializedName("HASHVAL")
private Long hashval;
@SerializedName("CNAME")
private String cname;
@SerializedName("NAME")
private String name = "";
@SerializedName("TYPE")
private String type;
@SerializedName("ENABLED")
private String enabled;
@SerializedName("READONLY")
private String readonly;
@SerializedName("VALUE")
private Value value = new Value();
public Long getHashval() {
return hashval;
}
public void setHashval(Long hashval) {
this.hashval = hashval;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getEnabled() {
return enabled;
}
public void setEnabled(String enabled) {
this.enabled = enabled;
}
public String getReadonly() {
return readonly;
}
public void setReadonly(String readonly) {
this.readonly = readonly;
}
public Value getValue() {
return value;
}
public void setValue(Value value) {
this.value = value;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Parameters} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class Parameters {
@SerializedName("FLAT")
private String flat;
@SerializedName("HELPTEXT")
private String helptext;
@SerializedName("HASHONLY")
private String hashonly;
public String getFlat() {
return flat;
}
public void setFlat(String flat) {
this.flat = flat;
}
public String getHelptext() {
return helptext;
}
public void setHelptext(String helptext) {
this.helptext = helptext;
}
public String getHashonly() {
return hashonly;
}
public void setHashonly(String hashonly) {
this.hashonly = hashonly;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PutResponse} class maps the JSON data response from several Vizio TV endpoints
*
* @author Michael Lobstein - Initial contribution
*/
public class PutResponse {
@SerializedName("STATUS")
private Status status;
@SerializedName("URI")
private String uri;
@SerializedName("TIME")
private String time;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Status} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class Status {
@SerializedName("RESULT")
private String result;
@SerializedName("DETAIL")
private String detail;
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Value} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class Value {
@SerializedName("NAME")
private String name = "";
@SerializedName("METADATA")
private String metadata;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMetadata() {
return metadata;
}
public void setMetadata(String metadata) {
this.metadata = metadata;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.app;
import org.openhab.binding.vizio.internal.dto.Status;
import com.google.gson.annotations.SerializedName;
/**
* The {@link CurrentApp} class maps the JSON data response from the Vizio TV endpoint:
* '/app/current'
*
* @author Michael Lobstein - Initial contribution
*/
public class CurrentApp {
@SerializedName("STATUS")
private Status status;
@SerializedName("ITEM")
private ItemApp item = new ItemApp();
@SerializedName("URI")
private String uri;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public ItemApp getItem() {
return item;
}
public void setItem(ItemApp item) {
this.item = item;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.app;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ItemApp} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class ItemApp {
@SerializedName("TYPE")
private String type;
@SerializedName("VALUE")
private ItemAppValue value = new ItemAppValue();
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ItemAppValue getValue() {
return value;
}
public void setValue(ItemAppValue value) {
this.value = value;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.app;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ItemAppValue} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class ItemAppValue {
@SerializedName("MESSAGE")
private String message;
@SerializedName("NAME_SPACE")
private Integer nameSpace = -1;
@SerializedName("APP_ID")
private String appId = "";
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getNameSpace() {
return nameSpace;
}
public void setNameSpace(Integer nameSpace) {
this.nameSpace = nameSpace;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.applist;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VizioApp} class contains the name and config data for an app that runs on a Vizio TV
*
* @author Michael Lobstein - Initial contribution
*/
public class VizioApp {
@SerializedName("name")
private String name = "";
@SerializedName("config")
private VizioAppConfig config = new VizioAppConfig();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public VizioAppConfig getConfig() {
return config;
}
public void setConfig(VizioAppConfig config) {
this.config = config;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.applist;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VizioAppConfig} class maps the JSON data needed to launch an app on a Vizio TV
*
* @author Michael Lobstein - Initial contribution
*/
public class VizioAppConfig {
@SerializedName("NAME_SPACE")
private Integer nameSpace;
@SerializedName("APP_ID")
private String appId;
@SerializedName("MESSAGE")
private String message;
public Integer getNameSpace() {
return nameSpace;
}
public void setNameSpace(Integer nameSpace) {
this.nameSpace = nameSpace;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.applist;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* The {@link VizioApps} class contains a list of VizioApp objects
*
* @author Michael Lobstein - Initial contribution
*/
public class VizioApps {
@SerializedName("Apps")
private List<VizioApp> apps = new ArrayList<VizioApp>();
public List<VizioApp> getApps() {
return apps;
}
public void setApps(List<VizioApp> apps) {
this.apps = apps;
}
}

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.audio;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.vizio.internal.dto.Parameters;
import org.openhab.binding.vizio.internal.dto.Status;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Audio} class maps the JSON data response from the Vizio TV endpoint:
* '/menu_native/dynamic/tv_settings/audio'
*
* @author Michael Lobstein - Initial contribution
*/
public class Audio {
@SerializedName("STATUS")
private Status status;
@SerializedName("HASHLIST")
private List<Long> hashlist = new ArrayList<Long>();
@SerializedName("GROUP")
private String group;
@SerializedName("NAME")
private String name;
@SerializedName("PARAMETERS")
private Parameters parameters;
@SerializedName("ITEMS")
private List<ItemAudio> items = new ArrayList<ItemAudio>();
@SerializedName("URI")
private String uri;
@SerializedName("CNAME")
private String cname;
@SerializedName("TYPE")
private String type;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public List<Long> getHashlist() {
return hashlist;
}
public void setHashlist(List<Long> hashlist) {
this.hashlist = hashlist;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parameters getParameters() {
return parameters;
}
public void setParameters(Parameters parameters) {
this.parameters = parameters;
}
public List<ItemAudio> getItems() {
return items;
}
public void setItems(List<ItemAudio> items) {
this.items = items;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.audio;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ItemAudio} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class ItemAudio {
@SerializedName("HASHVAL")
private Long hashval = 0L;
@SerializedName("CNAME")
private String cname;
@SerializedName("NAME")
private String name;
@SerializedName("TYPE")
private String type;
@SerializedName("ENABLED")
private String enabled;
@SerializedName("READONLY")
private String readonly;
@SerializedName("VALUE")
private String value = "";
public Long getHashval() {
return hashval;
}
public void setHashval(Long hashval) {
this.hashval = hashval;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getEnabled() {
return enabled;
}
public void setEnabled(String enabled) {
this.enabled = enabled;
}
public String getReadonly() {
return readonly;
}
public void setReadonly(String readonly) {
this.readonly = readonly;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.input;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.vizio.internal.dto.Parameters;
import org.openhab.binding.vizio.internal.dto.Status;
import com.google.gson.annotations.SerializedName;
/**
* The {@link CurrentInput} class maps the JSON data response from the Vizio TV endpoint:
* '/menu_native/dynamic/tv_settings/devices/current_input'
*
* @author Michael Lobstein - Initial contribution
*/
public class CurrentInput {
@SerializedName("STATUS")
private Status status;
@SerializedName("ITEMS")
private List<ItemInput> items = new ArrayList<ItemInput>();
@SerializedName("HASHLIST")
private List<Long> hashlist = new ArrayList<Long>();
@SerializedName("URI")
private String uri;
@SerializedName("PARAMETERS")
private Parameters parameters;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public List<ItemInput> getItems() {
return items;
}
public void setItems(List<ItemInput> items) {
this.items = items;
}
public List<Long> getHashlist() {
return hashlist;
}
public void setHashlist(List<Long> hashlist) {
this.hashlist = hashlist;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public Parameters getParameters() {
return parameters;
}
public void setParameters(Parameters parameters) {
this.parameters = parameters;
}
}

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.input;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ItemInput} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class ItemInput {
@SerializedName("HASHVAL")
private Long hashval = 0L;
@SerializedName("NAME")
private String name;
@SerializedName("ENABLED")
private String enabled;
@SerializedName("VALUE")
private String value = "";
@SerializedName("CNAME")
private String cname;
@SerializedName("HIDDEN")
private String hidden;
@SerializedName("TYPE")
private String type;
public Long getHashval() {
return hashval;
}
public void setHashval(Long hashval) {
this.hashval = hashval;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEnabled() {
return enabled;
}
public void setEnabled(String enabled) {
this.enabled = enabled;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public String getHidden() {
return hidden;
}
public void setHidden(String hidden) {
this.hidden = hidden;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.inputlist;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.vizio.internal.dto.Item;
import org.openhab.binding.vizio.internal.dto.Parameters;
import org.openhab.binding.vizio.internal.dto.Status;
import com.google.gson.annotations.SerializedName;
/**
* The {@link InputList} class maps the JSON data response from the Vizio TV endpoint:
* '/menu_native/dynamic/tv_settings/devices/name_input'
*
* @author Michael Lobstein - Initial contribution
*/
public class InputList {
@SerializedName("STATUS")
private Status status;
@SerializedName("HASHLIST")
private List<Long> hashlist = new ArrayList<Long>();
@SerializedName("GROUP")
private String group;
@SerializedName("NAME")
private String name;
@SerializedName("PARAMETERS")
private Parameters parameters;
@SerializedName("ITEMS")
private List<Item> items = new ArrayList<Item>();
@SerializedName("URI")
private String uri;
@SerializedName("CNAME")
private String cname;
@SerializedName("TYPE")
private String type;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public List<Long> getHashlist() {
return hashlist;
}
public void setHashlist(List<Long> hashlist) {
this.hashlist = hashlist;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parameters getParameters() {
return parameters;
}
public void setParameters(Parameters parameters) {
this.parameters = parameters;
}
public List<Item> getItems() {
return items;
}
public void setItems(List<Item> items) {
this.items = items;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.pairing;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ItemAuthToken} class contains data from the Vizio TV in response to completing the pairing process
*
* @author Michael Lobstein - Initial contribution
*/
public class ItemAuthToken {
@SerializedName("AUTH_TOKEN")
private String authToken = "";
public String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.pairing;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ItemPairing} class contains data from the Vizio TV in response to starting the pairing process
*
* @author Michael Lobstein - Initial contribution
*/
public class ItemPairing {
@SerializedName("PAIRING_REQ_TOKEN")
private Integer pairingReqToken = -1;
@SerializedName("CHALLENGE_TYPE")
private Integer challengeType = -1;
public Integer getPairingReqToken() {
return pairingReqToken;
}
public void setPairingReqToken(Integer pairingReqToken) {
this.pairingReqToken = pairingReqToken;
}
public Integer getChallengeType() {
return challengeType;
}
public void setChallengeType(Integer challengeType) {
this.challengeType = challengeType;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.pairing;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PairingComplete} class maps the JSON data response from the Vizio TV endpoint:
* '/pairing/pair'
*
* @author Michael Lobstein - Initial contribution
*/
public class PairingComplete {
@SerializedName("ITEM")
private ItemAuthToken item = new ItemAuthToken();
public ItemAuthToken getItem() {
return item;
}
public void setItem(ItemAuthToken item) {
this.item = item;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.pairing;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PairingStart} class maps the JSON data response from the Vizio TV endpoint:
* '/pairing/start'
*
* @author Michael Lobstein - Initial contribution
*/
public class PairingStart {
@SerializedName("ITEM")
private ItemPairing item = new ItemPairing();
public ItemPairing getItem() {
return item;
}
public void setItem(ItemPairing item) {
this.item = item;
}
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.power;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ItemPower} class contains data from the Vizio TV JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class ItemPower {
@SerializedName("CNAME")
private String cname;
@SerializedName("TYPE")
private String type;
@SerializedName("NAME")
private String name;
@SerializedName("VALUE")
private int value;
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.dto.power;
import java.util.ArrayList;
import java.util.List;
import org.openhab.binding.vizio.internal.dto.Status;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PowerMode} class maps the JSON data response from the Vizio TV endpoint:
* '/state/device/power_mode'
*
* @author Michael Lobstein - Initial contribution
*/
public class PowerMode {
@SerializedName("STATUS")
private Status status;
@SerializedName("ITEMS")
private List<ItemPower> items = new ArrayList<ItemPower>();
@SerializedName("URI")
private String uri;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public List<ItemPower> getItems() {
return items;
}
public void setItems(List<ItemPower> items) {
this.items = items;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.enums;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link KeyCommand} class provides enum values for remote control button press commands.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public enum KeyCommand {
SEEKFWD(2, 0),
SEEKBACK(2, 1),
PAUSE(2, 2),
PLAY(2, 3),
DOWN(3, 0),
LEFT(3, 1),
OK(3, 2),
LEFT2(3, 4),
RIGHT(3, 7),
UP(3, 8),
BACK(4, 0),
SMARTCAST(4, 3),
CCTOGGLE(4, 4),
INFO(4, 6),
MENU(4, 8),
HOME(4, 15),
VOLUMEDOWN(5, 0),
VOLUMEUP(5, 1),
MUTEOFF(5, 2),
MUTEON(5, 3),
MUTETOGGLE(5, 4),
PICTUREMODE(6, 0),
WIDEMODE(6, 1),
WIDETOGGLE(6, 2),
INPUTTOGGLE(7, 1),
CHANNELDOWN(8, 0),
CHANNELUP(8, 1),
PREVIOUSCH(8, 2),
EXIT(9, 0),
POWEROFF(11, 0),
POWERON(11, 1),
POWERTOGGLE(11, 2);
private static final String KEY_COMMAND_STR = "{\"KEYLIST\": [{\"CODESET\": %d,\"CODE\": %d,\"ACTION\":\"KEYPRESS\"}]}";
private final int codeSet;
private final int code;
KeyCommand(int codeSet, int code) {
this.codeSet = codeSet;
this.code = code;
}
public String getJson() {
return String.format(KEY_COMMAND_STR, codeSet, code);
}
}

View File

@@ -0,0 +1,584 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.vizio.internal.handler;
import static org.openhab.binding.vizio.internal.VizioBindingConstants.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.vizio.internal.VizioConfiguration;
import org.openhab.binding.vizio.internal.VizioException;
import org.openhab.binding.vizio.internal.VizioStateDescriptionOptionProvider;
import org.openhab.binding.vizio.internal.communication.VizioCommunicator;
import org.openhab.binding.vizio.internal.communication.VizioTlsTrustManagerProvider;
import org.openhab.binding.vizio.internal.dto.app.CurrentApp;
import org.openhab.binding.vizio.internal.dto.applist.VizioApp;
import org.openhab.binding.vizio.internal.dto.applist.VizioApps;
import org.openhab.binding.vizio.internal.dto.audio.Audio;
import org.openhab.binding.vizio.internal.dto.audio.ItemAudio;
import org.openhab.binding.vizio.internal.dto.input.CurrentInput;
import org.openhab.binding.vizio.internal.dto.inputlist.InputList;
import org.openhab.binding.vizio.internal.dto.power.PowerMode;
import org.openhab.binding.vizio.internal.enums.KeyCommand;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.TlsTrustManagerProvider;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VizioHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class VizioHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(VizioHandler.class);
private final HttpClient httpClient;
private final VizioStateDescriptionOptionProvider stateDescriptionProvider;
private final String dbAppsJson;
private @Nullable ServiceRegistration<?> serviceRegistration;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> metadataRefreshJob;
private VizioCommunicator communicator;
private List<VizioApp> userConfigApps = new ArrayList<VizioApp>();
private Object sequenceLock = new Object();
private int pairingDeviceId = -1;
private int pairingToken = -1;
private Long currentInputHash = 0L;
private Long currentVolumeHash = 0L;
private String currentApp = EMPTY;
private String currentInput = EMPTY;
private boolean currentMute = false;
private int currentVolume = -1;
private boolean powerOn = false;
private boolean debounce = true;
public VizioHandler(Thing thing, HttpClient httpClient,
VizioStateDescriptionOptionProvider stateDescriptionProvider, String vizioAppsJson) {
super(thing);
this.httpClient = httpClient;
this.stateDescriptionProvider = stateDescriptionProvider;
this.dbAppsJson = vizioAppsJson;
this.communicator = new VizioCommunicator(httpClient, EMPTY, -1, EMPTY);
}
@Override
public void initialize() {
logger.debug("Initializing Vizio handler");
final Gson gson = new Gson();
VizioConfiguration config = getConfigAs(VizioConfiguration.class);
@Nullable
String host = config.hostName;
final @Nullable String authToken = config.authToken;
@Nullable
String appListJson = config.appListJson;
if (host == null || host.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error-hostname");
return;
} else if (host.contains(":")) {
// format for ipv6
host = "[" + host + "]";
}
this.communicator = new VizioCommunicator(httpClient, host, config.port, authToken != null ? authToken : EMPTY);
// register trustmanager service to allow httpClient to accept self signed cert from the Vizio TV
VizioTlsTrustManagerProvider tlsTrustManagerProvider = new VizioTlsTrustManagerProvider(
host + ":" + config.port);
serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext()
.registerService(TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null);
if (authToken == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/offline.configuration-error-authtoken");
return;
}
// if app list is not supplied in thing configuration, populate it from the json db
if (appListJson == null) {
appListJson = dbAppsJson;
// Update thing configuration (persistent) - store app list from db into thing so the user can update it
Configuration configuration = this.getConfig();
configuration.put(PROPERTY_APP_LIST_JSON, appListJson);
this.updateConfiguration(configuration);
}
try {
VizioApps appsFromJson = gson.fromJson(appListJson, VizioApps.class);
if (appsFromJson != null && !appsFromJson.getApps().isEmpty()) {
userConfigApps = appsFromJson.getApps();
List<StateOption> appListOptions = new ArrayList<>();
userConfigApps.forEach(app -> {
appListOptions.add(new StateOption(app.getName(), app.getName()));
});
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
appListOptions);
}
} catch (JsonSyntaxException e) {
logger.debug("Invalid App List Configuration in thing configuration. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error-applist");
return;
}
updateStatus(ThingStatus.UNKNOWN);
startVizioStateRefresh();
startPeriodicRefresh();
}
/**
* Start the job that queries the Vizio TV every 10 seconds to get its current status
*/
private void startVizioStateRefresh() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob == null || refreshJob.isCancelled()) {
this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshVizioState, 5, 10, TimeUnit.SECONDS);
}
}
/**
* Get current status from the Vizio TV and update the channels
*/
private void refreshVizioState() {
synchronized (sequenceLock) {
try {
PowerMode polledPowerMode = communicator.getPowerMode();
if (debounce && !polledPowerMode.getItems().isEmpty()) {
int powerMode = polledPowerMode.getItems().get(0).getValue();
if (powerMode == 1) {
powerOn = true;
updateState(POWER, OnOffType.ON);
} else if (powerMode == 0) {
powerOn = false;
updateState(POWER, OnOffType.OFF);
} else {
logger.debug("Unknown power mode {}, for response object: {}", powerMode, polledPowerMode);
}
}
updateStatus(ThingStatus.ONLINE);
} catch (VizioException e) {
logger.debug("Unable to retrieve Vizio TV power mode info. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-get-power");
}
if (powerOn && (isLinked(VOLUME) || isLinked(MUTE))) {
try {
Audio audioSettings = communicator.getCurrentAudioSettings();
Optional<ItemAudio> volumeItem = audioSettings.getItems().stream()
.filter(i -> VOLUME.equals(i.getCname())).findFirst();
if (debounce && volumeItem.isPresent()) {
currentVolumeHash = volumeItem.get().getHashval();
try {
int polledVolume = Integer.parseInt(volumeItem.get().getValue());
if (polledVolume != currentVolume) {
currentVolume = polledVolume;
updateState(VOLUME, new PercentType(BigDecimal.valueOf(currentVolume)));
}
} catch (NumberFormatException e) {
logger.debug("Unable to parse volume value {} as int", volumeItem.get().getValue());
}
}
Optional<ItemAudio> muteItem = audioSettings.getItems().stream()
.filter(i -> MUTE.equals(i.getCname())).findFirst();
if (debounce && muteItem.isPresent()) {
String polledMute = muteItem.get().getValue().toUpperCase(Locale.ENGLISH);
if (ON.equals(polledMute) || OFF.equals(polledMute)) {
if (ON.equals(polledMute) && !currentMute) {
updateState(MUTE, OnOffType.ON);
currentMute = true;
} else if (OFF.equals(polledMute) && currentMute) {
updateState(MUTE, OnOffType.OFF);
currentMute = false;
}
} else {
logger.debug("Unknown mute mode {}, for response object: {}", polledMute, audioSettings);
}
}
} catch (VizioException e) {
logger.debug("Unable to retrieve Vizio TV current audio settings. Exception: {}", e.getMessage(),
e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-get-audio");
}
}
if (powerOn && isLinked(SOURCE)) {
try {
CurrentInput polledInputState = communicator.getCurrentInput();
if (debounce && !polledInputState.getItems().isEmpty()
&& !currentInput.equals(polledInputState.getItems().get(0).getValue())) {
currentInput = polledInputState.getItems().get(0).getValue();
currentInputHash = polledInputState.getItems().get(0).getHashval();
updateState(SOURCE, new StringType(currentInput));
}
} catch (VizioException e) {
logger.debug("Unable to retrieve Vizio TV current input. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-get-input");
}
}
if (powerOn && isLinked(ACTIVE_APP)) {
try {
if (debounce) {
CurrentApp polledApp = communicator.getCurrentApp();
Optional<VizioApp> currentAppData = userConfigApps.stream()
.filter(a -> a.getConfig().getAppId().equals(polledApp.getItem().getValue().getAppId())
&& a.getConfig().getNameSpace()
.equals(polledApp.getItem().getValue().getNameSpace()))
.findFirst();
if (currentAppData.isPresent()) {
if (!currentApp.equals(currentAppData.get().getName())) {
currentApp = currentAppData.get().getName();
updateState(ACTIVE_APP, new StringType(currentApp));
}
} else {
currentApp = EMPTY;
try {
int appId = Integer.parseInt(polledApp.getItem().getValue().getAppId());
updateState(ACTIVE_APP, new StringType(String.format(UNKNOWN_APP_STR, appId,
polledApp.getItem().getValue().getNameSpace())));
} catch (NumberFormatException nfe) {
// Non-numeric appId received, eg: hdmi1
updateState(ACTIVE_APP, UnDefType.UNDEF);
}
logger.debug("Unknown app_id: {}, name_space: {}",
polledApp.getItem().getValue().getAppId(),
polledApp.getItem().getValue().getNameSpace());
}
}
} catch (VizioException e) {
logger.debug("Unable to retrieve Vizio TV current running app. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-get-app");
}
}
}
debounce = true;
}
/**
* Start the job to periodically retrieve various metadata from the Vizio TV every 10 minutes
*/
private void startPeriodicRefresh() {
ScheduledFuture<?> metadataRefreshJob = this.metadataRefreshJob;
if (metadataRefreshJob == null || metadataRefreshJob.isCancelled()) {
this.metadataRefreshJob = scheduler.scheduleWithFixedDelay(this::refreshVizioMetadata, 1, 600,
TimeUnit.SECONDS);
}
}
/**
* Update source list (hashes) and other metadata from the Vizio TV
*/
private void refreshVizioMetadata() {
synchronized (sequenceLock) {
try {
InputList inputList = communicator.getSourceInputList();
List<StateOption> sourceListOptions = new ArrayList<>();
inputList.getItems().forEach(source -> {
sourceListOptions.add(new StateOption(source.getName(), source.getValue().getName()));
});
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), SOURCE),
sourceListOptions);
} catch (VizioException e) {
logger.debug("Unable to retrieve the Vizio TV input list. Exception: {}", e.getMessage(), e);
}
}
}
@Override
public void dispose() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null) {
refreshJob.cancel(true);
this.refreshJob = null;
}
ScheduledFuture<?> metadataRefreshJob = this.metadataRefreshJob;
if (metadataRefreshJob != null) {
metadataRefreshJob.cancel(true);
this.metadataRefreshJob = null;
}
ServiceRegistration<?> localServiceRegistration = serviceRegistration;
if (localServiceRegistration != null) {
// remove trustmanager service
localServiceRegistration.unregister();
serviceRegistration = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
logger.debug("Unsupported refresh command: {}", command);
} else {
switch (channelUID.getId()) {
case POWER:
debounce = false;
synchronized (sequenceLock) {
try {
if (command == OnOffType.ON) {
communicator.sendKeyPress(KeyCommand.POWERON.getJson());
powerOn = true;
} else {
communicator.sendKeyPress(KeyCommand.POWEROFF.getJson());
powerOn = false;
}
} catch (VizioException e) {
logger.debug("Unable to send power {} command to the Vizio TV, Exception: {}", command,
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-set-power");
}
}
break;
case VOLUME:
debounce = false;
synchronized (sequenceLock) {
try {
int volume = Integer.parseInt(command.toString());
// volume changed again before polling has run, get current volume hash from the TV first
if (currentVolumeHash.equals(0L)) {
Audio audioSettings = communicator.getCurrentAudioSettings();
Optional<ItemAudio> volumeItem = audioSettings.getItems().stream()
.filter(i -> VOLUME.equals(i.getCname())).findFirst();
if (volumeItem.isPresent()) {
currentVolumeHash = volumeItem.get().getHashval();
} else {
logger.debug("Unable to get current volume hash on the Vizio TV");
}
}
communicator
.changeVolume(String.format(MODIFY_INT_SETTING_JSON, volume, currentVolumeHash));
currentVolumeHash = 0L;
} catch (VizioException e) {
logger.debug("Unable to set volume on the Vizio TV, command volume: {}, Exception: {}",
command, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-set-volume");
} catch (NumberFormatException e) {
logger.debug("Unable to parse command volume value {} as int", command);
}
}
break;
case MUTE:
debounce = false;
synchronized (sequenceLock) {
try {
if (command == OnOffType.ON && !currentMute) {
communicator.sendKeyPress(KeyCommand.MUTETOGGLE.getJson());
currentMute = true;
} else if (command == OnOffType.OFF && currentMute) {
communicator.sendKeyPress(KeyCommand.MUTETOGGLE.getJson());
currentMute = false;
}
} catch (VizioException e) {
logger.debug("Unable to send mute {} command to the Vizio TV, Exception: {}", command,
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-set-mute");
}
}
break;
case SOURCE:
debounce = false;
synchronized (sequenceLock) {
try {
// if input changed again before polling has run, get current input hash from the TV
// first
if (currentInputHash.equals(0L)) {
CurrentInput polledInput = communicator.getCurrentInput();
if (!polledInput.getItems().isEmpty()) {
currentInputHash = polledInput.getItems().get(0).getHashval();
}
}
communicator
.changeInput(String.format(MODIFY_STRING_SETTING_JSON, command, currentInputHash));
currentInputHash = 0L;
} catch (VizioException e) {
logger.debug("Unable to set current source on the Vizio TV, source: {}, Exception: {}",
command, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-set-source");
}
}
break;
case ACTIVE_APP:
debounce = false;
synchronized (sequenceLock) {
try {
Optional<VizioApp> selectedApp = userConfigApps.stream()
.filter(a -> command.toString().equals(a.getName())).findFirst();
if (selectedApp.isPresent()) {
communicator.launchApp(selectedApp.get().getConfig());
} else {
logger.debug("Unknown app name: '{}', check that it exists in App List configuration",
command);
}
} catch (VizioException e) {
logger.debug("Unable to launch app name: '{}' on the Vizio TV, Exception: {}", command,
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-launch-app");
}
}
break;
case CONTROL:
debounce = false;
synchronized (sequenceLock) {
try {
handleControlCommand(command);
} catch (VizioException e) {
logger.debug("Unable to send control command: '{}' to the Vizio TV, Exception: {}", command,
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-send-cmd");
}
}
break;
case BUTTON:
synchronized (sequenceLock) {
try {
KeyCommand keyCommand = KeyCommand.valueOf(command.toString().toUpperCase(Locale.ENGLISH));
communicator.sendKeyPress(keyCommand.getJson());
} catch (IllegalArgumentException | VizioException e) {
logger.debug("Unable to send keypress to the Vizio TV, key: {}, Exception: {}", command,
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error-send-key");
}
}
break;
default:
logger.warn("Unknown channel: '{}'", channelUID.getId());
break;
}
}
}
private void handleControlCommand(Command command) throws VizioException {
if (command instanceof PlayPauseType) {
if (command == PlayPauseType.PLAY) {
communicator.sendKeyPress(KeyCommand.PLAY.getJson());
} else if (command == PlayPauseType.PAUSE) {
communicator.sendKeyPress(KeyCommand.PAUSE.getJson());
}
} else if (command instanceof NextPreviousType) {
if (command == NextPreviousType.NEXT) {
communicator.sendKeyPress(KeyCommand.RIGHT.getJson());
} else if (command == NextPreviousType.PREVIOUS) {
communicator.sendKeyPress(KeyCommand.LEFT.getJson());
}
} else if (command instanceof RewindFastforwardType) {
if (command == RewindFastforwardType.FASTFORWARD) {
communicator.sendKeyPress(KeyCommand.SEEKFWD.getJson());
} else if (command == RewindFastforwardType.REWIND) {
communicator.sendKeyPress(KeyCommand.SEEKBACK.getJson());
}
} else {
logger.warn("Unknown control command: {}", command);
}
}
@Override
public boolean isLinked(String channelName) {
Channel channel = this.thing.getChannel(channelName);
if (channel != null) {
return isLinked(channel.getUID());
} else {
return false;
}
}
// The remaining methods are used by the console when obtaining the auth token from the TV.
public void saveAuthToken(String authToken) {
// Store the auth token in the configuration and restart the thing
Configuration configuration = this.getConfig();
configuration.put(PROPERTY_AUTH_TOKEN, authToken);
this.updateConfiguration(configuration);
this.thingUpdated(this.getThing());
}
public int getPairingDeviceId() {
return pairingDeviceId;
}
public void setPairingDeviceId(int pairingDeviceId) {
this.pairingDeviceId = pairingDeviceId;
}
public int getPairingToken() {
return pairingToken;
}
public void setPairingToken(int pairingToken) {
this.pairingToken = pairingToken;
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="vizio" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Vizio Binding</name>
<description>Controls Vizio TVs w/SmartCast API (2016+ Models)</description>
</binding:binding>

View File

@@ -0,0 +1,81 @@
# binding
binding.vizio.name = Vizio Binding
binding.vizio.description = Controls Vizio TVs w/SmartCast API (2016+ Models)
# thing types
thing-type.vizio.vizio_tv.label = Vizio TV
thing-type.vizio.vizio_tv.description = A Vizio SmartCast TV
# thing types config
thing-type.config.vizio.vizio_tv.appListJson.label = App List Configuration
thing-type.config.vizio.vizio_tv.appListJson.description = The JSON configuration string for the list of apps available in the activeApp channel drop down
thing-type.config.vizio.vizio_tv.authToken.label = Auth Token
thing-type.config.vizio.vizio_tv.authToken.description = Auth Token that is obtained via the pairing process; See documentation for details
thing-type.config.vizio.vizio_tv.hostName.label = Host Name/IP Address
thing-type.config.vizio.vizio_tv.hostName.description = Host Name or IP Address of the Vizio TV
thing-type.config.vizio.vizio_tv.port.label = Port
thing-type.config.vizio.vizio_tv.port.description = Port for the Vizio TV
thing-type.config.vizio.vizio_tv.port.option.7345 = 7345 (Newer Models)
thing-type.config.vizio.vizio_tv.port.option.9000 = 9000 (Older Models)
# channel types
channel-type.vizio.activeApp.label = Active App
channel-type.vizio.activeApp.description = The currently running App on the TV
channel-type.vizio.buttonTv.label = Remote Button
channel-type.vizio.buttonTv.description = A Remote Button press to send to the TV
channel-type.vizio.buttonTv.state.option.PowerOn = Power On
channel-type.vizio.buttonTv.state.option.PowerOff = Power Off
channel-type.vizio.buttonTv.state.option.PowerToggle = Power Toggle
channel-type.vizio.buttonTv.state.option.VolumeUp = Volume Up
channel-type.vizio.buttonTv.state.option.VolumeDown = Volume Down
channel-type.vizio.buttonTv.state.option.MuteOn = Mute On
channel-type.vizio.buttonTv.state.option.MuteOff = Mute Off
channel-type.vizio.buttonTv.state.option.MuteToggle = Mute Toggle
channel-type.vizio.buttonTv.state.option.ChannelUp = Channel Up
channel-type.vizio.buttonTv.state.option.ChannelDown = Channel Down
channel-type.vizio.buttonTv.state.option.PreviousCh = Previous Channel
channel-type.vizio.buttonTv.state.option.InputToggle = Input Toggle
channel-type.vizio.buttonTv.state.option.SeekFwd = Seek Fwd
channel-type.vizio.buttonTv.state.option.SeekBack = Seek Back
channel-type.vizio.buttonTv.state.option.Play = Play
channel-type.vizio.buttonTv.state.option.Pause = Pause
channel-type.vizio.buttonTv.state.option.Up = Up
channel-type.vizio.buttonTv.state.option.Down = Down
channel-type.vizio.buttonTv.state.option.Left = Left
channel-type.vizio.buttonTv.state.option.Right = Right
channel-type.vizio.buttonTv.state.option.Ok = Ok
channel-type.vizio.buttonTv.state.option.Back = Back
channel-type.vizio.buttonTv.state.option.Info = Info
channel-type.vizio.buttonTv.state.option.Menu = Menu
channel-type.vizio.buttonTv.state.option.Home = Home
channel-type.vizio.buttonTv.state.option.Exit = Exit
channel-type.vizio.buttonTv.state.option.Smartcast = Smartcast
channel-type.vizio.buttonTv.state.option.ccToggle = CC Toggle
channel-type.vizio.buttonTv.state.option.PictureMode = Picture Mode
channel-type.vizio.buttonTv.state.option.WideMode = Wide Mode
channel-type.vizio.buttonTv.state.option.WideToggle = Wide Toggle
channel-type.vizio.control.label = Control
channel-type.vizio.control.description = Transport Controls e.g. Play/Pause/Next/Previous/FForward/Rewind
channel-type.vizio.source.label = Source Input
channel-type.vizio.source.description = Select the Source Input for the TV
# message strings
offline.configuration-error-hostname = Host Name must be specified
offline.configuration-error-authtoken = Auth Token must be specified, see documentation for details
offline.configuration-error-applist = Invalid App List Configuration in thing configuration
offline.communication-error-get-power = Unable to retrieve power mode info from TV
offline.communication-error-get-audio = Unable to retrieve current audio settings from TV
offline.communication-error-get-input = Unable to retrieve current input from TV
offline.communication-error-get-app = Unable to retrieve current running app from TV
offline.communication-error-set-power = Unable to send power command to the TV
offline.communication-error-set-volume = Unable to set volume on the TV
offline.communication-error-set-mute = Unable to send mute command to the TV
offline.communication-error-set-source = Unable to set current source on the TV
offline.communication-error-launch-app = Unable to launch app on the TV
offline.communication-error-send-cmd = Unable to send control command to the TV
offline.communication-error-send-key = Unable to send keypress to the TV

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="vizio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Vizio TV Thing -->
<thing-type id="vizio_tv">
<label>Vizio TV</label>
<description>
A Vizio SmartCast TV
</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="volume" typeId="system.volume"/>
<channel id="mute" typeId="system.mute"/>
<channel id="source" typeId="source"/>
<channel id="activeApp" typeId="activeApp"/>
<channel id="control" typeId="control"/>
<channel id="button" typeId="buttonTv"/>
</channels>
<properties>
<property name="modelId">unknown</property>
</properties>
<representation-property>uuid</representation-property>
<config-description>
<parameter name="hostName" type="text" required="true">
<context>network-address</context>
<label>Host Name/IP Address</label>
<description>Host Name or IP Address of the Vizio TV</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" required="true">
<label>Port</label>
<description>Port for the Vizio TV</description>
<default>7345</default>
<limitToOptions>true</limitToOptions>
<options>
<option value="7345">7345 (Newer Models)</option>
<option value="9000">9000 (Older Models)</option>
</options>
</parameter>
<parameter name="authToken" type="text" required="false">
<label>Auth Token</label>
<description>Auth Token that is obtained via the pairing process; See documentation for details</description>
</parameter>
<parameter name="appListJson" type="text" required="false">
<context>script</context>
<label>App List Configuration</label>
<description>The JSON configuration string for the list of apps available in the activeApp channel drop down</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="source">
<item-type>String</item-type>
<label>Source Input</label>
<description>Select the Source Input for the TV</description>
</channel-type>
<channel-type id="activeApp">
<item-type>String</item-type>
<label>Active App</label>
<description>The currently running App on the TV</description>
</channel-type>
<channel-type id="control">
<item-type>Player</item-type>
<label>Control</label>
<description>Transport Controls e.g. Play/Pause/Next/Previous/FForward/Rewind</description>
<category>Player</category>
</channel-type>
<channel-type id="buttonTv">
<item-type>String</item-type>
<label>Remote Button</label>
<description>A Remote Button press to send to the TV</description>
<state>
<options>
<option value="PowerOn">Power On</option>
<option value="PowerOff">Power Off</option>
<option value="PowerToggle">Power Toggle</option>
<option value="VolumeUp">Volume Up</option>
<option value="VolumeDown">Volume Down</option>
<option value="MuteOn">Mute On</option>
<option value="MuteOff">Mute Off</option>
<option value="MuteToggle">Mute Toggle</option>
<option value="ChannelUp">Channel Up</option>
<option value="ChannelDown">Channel Down</option>
<option value="PreviousCh">Previous Channel</option>
<option value="InputToggle">Input Toggle</option>
<option value="SeekFwd">Seek Fwd</option>
<option value="SeekBack">Seek Back</option>
<option value="Play">Play</option>
<option value="Pause">Pause</option>
<option value="Up">Up</option>
<option value="Down">Down</option>
<option value="Left">Left</option>
<option value="Right">Right</option>
<option value="Ok">Ok</option>
<option value="Back">Back</option>
<option value="Info">Info</option>
<option value="Menu">Menu</option>
<option value="Home">Home</option>
<option value="Exit">Exit</option>
<option value="Smartcast">Smartcast</option>
<option value="ccToggle">CC Toggle</option>
<option value="PictureMode">Picture Mode</option>
<option value="WideMode">Wide Mode</option>
<option value="WideToggle">Wide Toggle</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,228 @@
{
"Apps": [
{
"name": "SmartCast Home",
"config": {
"APP_ID": "1",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Apple TV+",
"config": {
"APP_ID": "4",
"NAME_SPACE": 3,
"MESSAGE": null
}
},
{
"name": "CBS News",
"config": {
"APP_ID": "42",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Crackle",
"config": {
"APP_ID": "5",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "discovery+",
"config": {
"APP_ID": "130",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Disney+",
"config": {
"APP_ID": "75",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "FilmRise",
"config": {
"APP_ID": "24",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Haystack News",
"config": {
"APP_ID": "60",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "HBO Max",
"config": {
"APP_ID": "128",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Hulu",
"config": {
"APP_ID": "3",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "iHeartRadio",
"config": {
"APP_ID": "6",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Movies Anywhere",
"config": {
"APP_ID": "38",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "NBC",
"config": {
"APP_ID": "10",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Netflix",
"config": {
"APP_ID": "1",
"NAME_SPACE": 3,
"MESSAGE": null
}
},
{
"name": "Newsy",
"config": {
"APP_ID": "15",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Paramount+",
"config": {
"APP_ID": "37",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Peacock",
"config": {
"APP_ID": "88",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Plex",
"config": {
"APP_ID": "9",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Pluto TV",
"config": {
"APP_ID": "E6F74C01",
"NAME_SPACE": 0,
"MESSAGE": "{\"CAST_NAMESPACE\": \"urn:x-cast:tv.pluto\",\"CAST_MESSAGE\": {\"command\": \"initializePlayback\",\"channel\": \"\",\"episode\": \"\",\"time\": 0}}"
}
},
{
"name": "Prime Video",
"config": {
"APP_ID": "3",
"NAME_SPACE": 3,
"MESSAGE": null
}
},
{
"name": "Redbox",
"config": {
"APP_ID": "41",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Starz",
"config": {
"APP_ID": "151",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "Vudu",
"config": {
"APP_ID": "31",
"NAME_SPACE": 4,
"MESSAGE": "https://my.vudu.com/castReceiver/index.html?launch-source=app-icon"
}
},
{
"name": "XUMO",
"config": {
"APP_ID": "62",
"NAME_SPACE": 4,
"MESSAGE": "{\"CAST_NAMESPACE\": \"urn:x-cast:com.google.cast.media\",\"CAST_MESSAGE\": {\"type\": \"LOAD\",\"media\": {},\"autoplay\": true,\"currentTime\": 0,\"customData\": {}}}"
}
},
{
"name": "YouTube",
"config": {
"APP_ID": "1",
"NAME_SPACE": 5,
"MESSAGE": null
}
},
{
"name": "YouTubeTV",
"config": {
"APP_ID": "3",
"NAME_SPACE": 5,
"MESSAGE": null
}
},
{
"name": "WatchFree Plus",
"config": {
"APP_ID": "3014",
"NAME_SPACE": 4,
"MESSAGE": null
}
},
{
"name": "WatchFree+ AVOD",
"config": {
"APP_ID": "145",
"NAME_SPACE": 4,
"MESSAGE": null
}
}
]
}