[androidtv] AndroidTV Binding initial contribution (#14282)

Signed-off-by: Ben Rosenblum <rosenblumb@gmail.com>
This commit is contained in:
morph166955
2023-06-26 01:49:42 -05:00
committed by GitHub
parent 5a1daa252e
commit 46039efd0a
30 changed files with 5749 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.androidtv-${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-androidtv" description="AndroidTV 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.androidtv/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link AndroidTVBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class AndroidTVBindingConstants {
private static final String BINDING_ID = "androidtv";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_GOOGLETV = new ThingTypeUID(BINDING_ID, "googletv");
public static final ThingTypeUID THING_TYPE_SHIELDTV = new ThingTypeUID(BINDING_ID, "shieldtv");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GOOGLETV, THING_TYPE_SHIELDTV);
// List of all Channel ids
public static final String CHANNEL_DEBUG = "debug";
public static final String CHANNEL_KEYBOARD = "keyboard";
public static final String CHANNEL_KEYPRESS = "keypress";
public static final String CHANNEL_KEYCODE = "keycode";
public static final String CHANNEL_PINCODE = "pincode";
public static final String CHANNEL_APP = "app";
public static final String CHANNEL_APPNAME = "appname";
public static final String CHANNEL_APPURL = "appurl";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_MUTE = "mute";
public static final String CHANNEL_PLAYER = "player";
// List of all config properties
public static final String PROPERTY_IP_ADDRESS = "ipAddress";
// List of all static String literals
public static final String PIN_REQUEST = "REQUEST";
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of command options.
*
* @author Ben Rosenblum - Initial contribution
*
* Originally written for ADB by Christoph Weitkamp - Initial contribution
*/
@Component(service = { DynamicCommandDescriptionProvider.class, AndroidTVDynamicCommandDescriptionProvider.class })
@NonNullByDefault
public class AndroidTVDynamicCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider {
@Activate
public AndroidTVDynamicCommandDescriptionProvider(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,261 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConfiguration;
import org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConnectionManager;
import org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConfiguration;
import org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConnectionManager;
import org.openhab.core.library.types.StringType;
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.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandOption;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AndroidTVHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* Significant portions reused from Lutron binding with permission from Bob A.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class AndroidTVHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(AndroidTVHandler.class);
private @Nullable ShieldTVConnectionManager shieldtvConnectionManager;
private @Nullable GoogleTVConnectionManager googletvConnectionManager;
private @Nullable ScheduledFuture<?> monitorThingStatusJob;
private final Object monitorThingStatusJobLock = new Object();
private static final int THING_STATUS_FREQUENCY = 250;
private final AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider;
private final ThingTypeUID thingTypeUID;
private final String thingID;
public AndroidTVHandler(Thing thing, AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider,
ThingTypeUID thingTypeUID) {
super(thing);
this.commandDescriptionProvider = commandDescriptionProvider;
this.thingTypeUID = thingTypeUID;
this.thingID = this.getThing().getUID().getId();
}
public void setThingProperty(String property, String value) {
thing.setProperty(property, value);
}
public String getThingID() {
return this.thingID;
}
public void updateChannelState(String channel, State state) {
updateState(channel, state);
}
public ScheduledExecutorService getScheduler() {
return scheduler;
}
public void updateCDP(String channelName, Map<String, String> cdpMap) {
logger.trace("{} - Updating CDP for {}", this.thingID, channelName);
List<CommandOption> commandOptions = new ArrayList<CommandOption>();
cdpMap.forEach((key, value) -> commandOptions.add(new CommandOption(key, value)));
logger.trace("{} - CDP List: {}", this.thingID, commandOptions);
commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), channelName), commandOptions);
}
private void monitorThingStatus() {
synchronized (monitorThingStatusJobLock) {
checkThingStatus();
monitorThingStatusJob = scheduler.schedule(this::monitorThingStatus, THING_STATUS_FREQUENCY,
TimeUnit.MILLISECONDS);
}
}
public void checkThingStatus() {
String statusMessage = "";
boolean failed = false;
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
if (googletvConnectionManager != null) {
if (!googletvConnectionManager.getLoggedIn()) {
statusMessage = "GoogleTV: " + googletvConnectionManager.getStatusMessage();
failed = true;
} else {
statusMessage = "GoogleTV: ONLINE";
}
}
if (THING_TYPE_SHIELDTV.equals(thingTypeUID)) {
if (shieldtvConnectionManager != null) {
if (!shieldtvConnectionManager.getLoggedIn()) {
statusMessage = statusMessage + " | ShieldTV: " + shieldtvConnectionManager.getStatusMessage();
failed = true;
} else {
statusMessage = statusMessage + " | ShieldTV: ONLINE";
}
}
}
if (failed) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, statusMessage);
} else {
updateStatus(ThingStatus.ONLINE);
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.protocols-starting");
GoogleTVConfiguration googletvConfig = getConfigAs(GoogleTVConfiguration.class);
String ipAddress = googletvConfig.ipAddress;
if (ipAddress.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.googletv-address-not-specified");
return;
}
googletvConnectionManager = new GoogleTVConnectionManager(this, googletvConfig);
if (THING_TYPE_SHIELDTV.equals(thingTypeUID)) {
ShieldTVConfiguration shieldtvConfig = getConfigAs(ShieldTVConfiguration.class);
ipAddress = shieldtvConfig.ipAddress;
if (ipAddress.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.shieldtv-address-not-specified");
return;
}
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
}
monitorThingStatusJob = scheduler.schedule(this::monitorThingStatus, THING_STATUS_FREQUENCY,
TimeUnit.MILLISECONDS);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("{} - Command received at handler: {} {}", this.thingID, channelUID.getId(), command);
if (command.toString().equals("REFRESH")) {
// REFRESH causes issues on some channels. Block for now until implemented.
return;
}
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
if (CHANNEL_DEBUG.equals(channelUID.getId())) {
if (command instanceof StringType) {
if (command.toString().equals("GOOGLETV_HALT") && (googletvConnectionManager != null)) {
googletvConnectionManager.dispose();
googletvConnectionManager = null;
} else if (command.toString().equals("GOOGLETV_START")) {
GoogleTVConfiguration googletvConfig = getConfigAs(GoogleTVConfiguration.class);
googletvConnectionManager = new GoogleTVConnectionManager(this, googletvConfig);
} else if (command.toString().equals("GOOGLETV_SHIM") && (googletvConnectionManager == null)) {
GoogleTVConfiguration googletvConfig = getConfigAs(GoogleTVConfiguration.class);
googletvConfig.shim = true;
googletvConnectionManager = new GoogleTVConnectionManager(this, googletvConfig);
} else if (command.toString().equals("SHIELDTV_HALT") && (shieldtvConnectionManager != null)) {
shieldtvConnectionManager.dispose();
shieldtvConnectionManager = null;
} else if (command.toString().equals("SHIELDTV_START")) {
ShieldTVConfiguration shieldtvConfig = getConfigAs(ShieldTVConfiguration.class);
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
} else if (command.toString().equals("SHIELDTV_SHIM") && (shieldtvConnectionManager == null)) {
ShieldTVConfiguration shieldtvConfig = getConfigAs(ShieldTVConfiguration.class);
shieldtvConfig.shim = true;
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
} else if (command.toString().startsWith("GOOGLETV") && (googletvConnectionManager != null)) {
googletvConnectionManager.handleCommand(channelUID, command);
} else if (command.toString().startsWith("SHIELDTV") && (shieldtvConnectionManager != null)) {
shieldtvConnectionManager.handleCommand(channelUID, command);
}
}
return;
}
if (THING_TYPE_SHIELDTV.equals(thingTypeUID) && (shieldtvConnectionManager != null)) {
if (CHANNEL_PINCODE.equals(channelUID.getId())) {
if (command instanceof StringType) {
if (!shieldtvConnectionManager.getLoggedIn()) {
shieldtvConnectionManager.handleCommand(channelUID, command);
return;
}
}
} else if (CHANNEL_APP.equals(channelUID.getId())) {
if (command instanceof StringType) {
shieldtvConnectionManager.handleCommand(channelUID, command);
return;
}
}
}
if (googletvConnectionManager != null) {
googletvConnectionManager.handleCommand(channelUID, command);
return;
}
logger.warn("{} - Commands All Failed. Please report this as a bug. {} {}", thingID, channelUID.getId(),
command);
}
@Override
public void dispose() {
synchronized (monitorThingStatusJobLock) {
ScheduledFuture<?> monitorThingStatusJob = this.monitorThingStatusJob;
if (monitorThingStatusJob != null) {
monitorThingStatusJob.cancel(true);
}
}
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
if (shieldtvConnectionManager != null) {
shieldtvConnectionManager.dispose();
}
if (googletvConnectionManager != null) {
googletvConnectionManager.dispose();
}
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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 AndroidTVHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.androidtv", service = ThingHandlerFactory.class)
public class AndroidTVHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GOOGLETV,
THING_TYPE_SHIELDTV);
private final AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider;
@Activate
public AndroidTVHandlerFactory(
final @Reference AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider) {
this.commandDescriptionProvider = commandDescriptionProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
return new AndroidTVHandler(thing, commandDescriptionProvider, thingTypeUID);
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.discovery;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.net.InetAddress;
import java.util.Map;
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.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link MDNSDiscoveryParticipant} that will discover GOOGLETV(s).
*
* @author Ben Rosenblum - initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, immediate = true, configurationPid = "discovery.googletv")
public class GoogleTVDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(GoogleTVDiscoveryParticipant.class);
private static final String GOOGLETV_MDNS_SERVICE_TYPE = "_androidtvremote2._tcp.local.";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES;
}
@Override
public String getServiceType() {
return GOOGLETV_MDNS_SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(@Nullable ServiceInfo service) {
if ((service == null) || !service.hasData()) {
return null;
}
InetAddress[] ipAddresses = service.getInet4Addresses();
if (ipAddresses.length > 0) {
String ipAddress = ipAddresses[0].getHostAddress();
String macAddress = service.getPropertyString("bt");
if (logger.isDebugEnabled()) {
String nice = service.getNiceTextString();
String qualifiedName = service.getQualifiedName();
logger.debug("GoogleTV mDNS discovery notified of GoogleTV mDNS service: {}", nice);
logger.trace("GoogleTV mDNS service qualifiedName: {}", qualifiedName);
logger.trace("GoogleTV mDNS service ipAddresses: {} ({})", ipAddresses, ipAddresses.length);
logger.trace("GoogleTV mDNS service selected ipAddress: {}", ipAddress);
logger.trace("GoogleTV mDNS service property macAddress: {}", macAddress);
}
final ThingUID uid = getThingUID(service);
if (uid != null) {
final String id = uid.getId();
final String label = service.getName() + " (" + id + ")";
final Map<String, Object> properties = Map.of(PROPERTY_IP_ADDRESS, ipAddress);
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label).build();
} else {
return null;
}
} else {
return null;
}
}
@Override
public @Nullable ThingUID getThingUID(@Nullable ServiceInfo service) {
if ((service == null) || !service.hasData() || (service.getPropertyString("bt") == null)) {
return null;
}
return new ThingUID(THING_TYPE_GOOGLETV, service.getPropertyString("bt").replace(":", ""));
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.discovery;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.net.InetAddress;
import java.util.Map;
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.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link MDNSDiscoveryParticipant} that will discover SHIELDTV(s).
*
* @author Ben Rosenblum - initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, immediate = true, configurationPid = "discovery.shieldtv")
public class ShieldTVDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(ShieldTVDiscoveryParticipant.class);
private static final String SHIELDTV_MDNS_SERVICE_TYPE = "_nv_shield_remote._tcp.local.";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES;
}
@Override
public String getServiceType() {
return SHIELDTV_MDNS_SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(@Nullable ServiceInfo service) {
if (service == null || !service.hasData()) {
return null;
}
InetAddress[] ipAddresses = service.getInet4Addresses();
if (ipAddresses.length > 0) {
String ipAddress = ipAddresses[0].getHostAddress();
String serverId = service.getPropertyString("SERVER");
String serverCapability = service.getPropertyString("SERVER_CAPABILITY");
if (logger.isDebugEnabled()) {
String nice = service.getNiceTextString();
String qualifiedName = service.getQualifiedName();
logger.debug("ShieldTV mDNS discovery notified of ShieldTV mDNS service: {}", nice);
logger.trace("ShieldTV mDNS service qualifiedName: {}", qualifiedName);
logger.trace("ShieldTV mDNS service ipAddresses: {} ({})", ipAddresses, ipAddresses.length);
logger.trace("ShieldTV mDNS service selected ipAddress: {}", ipAddress);
logger.trace("ShieldTV mDNS service property SERVER: {}", serverId);
logger.trace("ShieldTV mDNS service property SERVER_CAPABILITY: {}", serverCapability);
}
final ThingUID uid = getThingUID(service);
if (uid != null) {
final String id = uid.getId();
final String label = service.getName() + " (" + id + ")";
final Map<String, Object> properties = Map.of(PROPERTY_IP_ADDRESS, ipAddress);
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label).build();
} else {
return null;
}
} else {
return null;
}
}
@Override
public @Nullable ThingUID getThingUID(@Nullable ServiceInfo service) {
if (service == null || !service.hasData() || (service.getPropertyString("SERVER") == null)) {
return null;
}
return new ThingUID(THING_TYPE_SHIELDTV, service.getPropertyString("SERVER").substring(8));
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.googletv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* GoogleTVCommand represents a GoogleTV protocol command
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVCommand {
private String command;
public GoogleTVCommand(String command) {
this.command = command;
}
@Override
public String toString() {
return command;
}
public boolean isEmpty() {
return command.isEmpty();
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.googletv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GoogleTVConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVConfiguration {
public String ipAddress = "";
public int port = 6466;
public int reconnect;
public int heartbeat;
public String keystoreFileName = "";
public String keystorePassword = "";
public int delay = 0;
public boolean shim;
public boolean shimNewKeys;
public String mode = "";
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.googletv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GoogleTVConstants} class defines common constants, which are
* used across the googletv protocol.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVConstants {
// List of all static String literals
public static final String DELIMITER_00 = "00";
public static final String DELIMITER_01 = "01";
public static final String DELIMITER_02 = "02";
public static final String DELIMITER_08 = "08";
public static final String DELIMITER_0A = "0a";
public static final String DELIMITER_10 = "10";
public static final String DELIMITER_12 = "12";
public static final String DELIMITER_1A = "1a";
public static final String DELIMITER_42 = "42";
public static final String DELIMITER_92 = "92";
public static final String DELIMITER_A2 = "a2";
public static final String DELIMITER_C2 = "c2";
public static final String MESSAGE_POWEROFF = "c202020800";
public static final String MESSAGE_POWERON = "c202020801";
public static final String MESSAGE_PINSUCCESS = "080210c801ca02";
public static final String HARD_DROP = "ffffffff";
}

View File

@@ -0,0 +1,336 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.googletv;
import static org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class responsible for parsing incoming GoogleTV messages. Calls back to an object implementing the
* GoogleTVMessageParserCallbacks interface.
*
* Adapted from Lutron Leap binding
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVMessageParser {
private final Logger logger = LoggerFactory.getLogger(GoogleTVMessageParser.class);
private final GoogleTVConnectionManager callback;
public GoogleTVMessageParser(GoogleTVConnectionManager callback) {
this.callback = callback;
}
public void handleMessage(String msg) {
if (msg.trim().isEmpty()) {
return; // Ignore empty lines
}
String thingId = callback.getThingID();
char[] charArray = msg.toCharArray();
String lenString = "" + charArray[0] + charArray[1];
int len = Integer.parseInt(lenString, 16);
msg = msg.substring(2);
charArray = msg.toCharArray();
logger.trace("{} - Received GoogleTV message - Length: {} Message: {}", thingId, len, msg);
callback.validMessageReceived();
try {
if (msg.startsWith(DELIMITER_1A)) {
logger.warn("{} - GoogleTV Error Message: {}", thingId, msg);
} else if (msg.startsWith(DELIMITER_0A)) {
// First message on connection from GTV
//
// 0a 5b08 ff 041256 0a 11 534849454c4420416e64726f6964205456 12 06 4e5649444941 18 01 22 02 3131 2a
// ---------------------LEN-SHIELD Android TV--------------------LEN-NVIDIA---------LEN---LEN-Android
// 24 636f6d2e676f6f676c652e616e64726f69642e74762e72656d6f74652e73657276696365 32
// LEN-com.google.android.tv.remote.service
// 0d 352e322e343733323534313333
// LEN-5.2.473254133
//
// 0a 5308 ff 04124e 0a 0c 42524156494120344b204742 12 04 536f6e79 18 01 22 01 39 2a
// ---------------------LEN-BRAVIA 4K GB---------------LEN-Sony-------LEN---LEN-Android Version
// 24 636f6d2e676f6f676c652e616e64726f69642e74762e72656d6f74652e73657276696365 32
// 0d 352e322e343733323534313333
//
// 0a 5408 ff 04124f 0a 0a 4368726f6d6563617374 12 06 476f6f676c65 18 01 22 02 3132 2a
// ---------------------LEN-Chromecast-------------LEN-Google---------LEN---LEN-Android Version
// 24 636f6d2e676f6f676c652e616e64726f69642e74762e72656d6f74652e73657276696365 32
// 0d 352e322e343733323534313333
//
// 0a 5708 ff 041252 0a 0d 4368726f6d6563617374204844 12 06 476f6f676c65 18 01 22 02 3132 2a
// ---------------------LEN-Chromecast HD----------------LEN-Google---------LEN---LEN-Android Version
// 24 636f6d2e676f6f676c652e616e64726f69642e74762e72656d6f74652e73657276696365 32
// 0d352e322e343733323534313333
if (callback.getLoggedIn()) {
logger.warn("{} - Unexpected Login Message: {}", thingId, msg);
} else {
callback.sendCommand(
new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(4))));
}
String st = "";
int length = 0;
StringBuilder preambleSb = new StringBuilder();
StringBuilder manufacturerSb = new StringBuilder();
StringBuilder modelSb = new StringBuilder();
StringBuilder androidVersionSb = new StringBuilder();
StringBuilder remoteServerSb = new StringBuilder();
StringBuilder remoteServerVersionSb = new StringBuilder();
int i = 0;
int current = 0;
for (; i < 14; i++) {
preambleSb.append(charArray[i]);
}
i += 2; // 0a delimiter
st = "" + charArray[i] + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i++) {
modelSb.append(charArray[i]);
}
i += 2; // 12 delimiter
st = "" + charArray[i] + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i++) {
manufacturerSb.append(charArray[i]);
}
i += 6; // 18 01 22
st = "" + charArray[i] + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i++) {
androidVersionSb.append(charArray[i]);
}
i += 2; // 2a delimiter
st = "" + charArray[i] + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i++) {
remoteServerSb.append(charArray[i]);
}
i += 2; // 32 delimiter
st = "" + charArray[i] + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i++) {
remoteServerVersionSb.append(charArray[i]);
}
String preamble = preambleSb.toString();
String model = GoogleTVRequest.encodeMessage(modelSb.toString());
String manufacturer = GoogleTVRequest.encodeMessage(manufacturerSb.toString());
String androidVersion = GoogleTVRequest.encodeMessage(androidVersionSb.toString());
String remoteServer = GoogleTVRequest.encodeMessage(remoteServerSb.toString());
String remoteServerVersion = GoogleTVRequest.encodeMessage(remoteServerVersionSb.toString());
logger.debug("{} - {} \"{}\" \"{}\" {} {} {}", thingId, preamble, model, manufacturer, androidVersion,
remoteServer, remoteServerVersion);
callback.setModel(model);
callback.setManufacturer(manufacturer);
callback.setAndroidVersion(androidVersion);
callback.setRemoteServer(remoteServer);
callback.setRemoteServerVersion(remoteServerVersion);
} else if (msg.startsWith(DELIMITER_12)) {
// Second message on connection from GTV
// Login successful
callback.sendCommand(
new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(5))));
logger.info("{} - Login Successful", thingId);
callback.setLoggedIn(true);
} else if (msg.startsWith(DELIMITER_92)) {
// Third message on connection from GTV
// Also sent on power state change (to ON only unless keypress triggers)i
// 9203 21 08 02 10 02 1a 11 534849454c4420416e64726f6964205456 20 02 2800 30 0f 38 0e 40 00
// --------DD----DD----DD-LEN-SHIELD Android TV
// 9203 1e 08 9610 10 09 1a 0d 4368726f6d6563617374204844 20 02 2800 30 19 38 0a 40 00
// --------DD------DD----DD-LEN-Chromecast HD
// 9203 1a 08 f304 10 09 1a 11 534849454c4420416e64726f6964205456 20 01
// 9203 1a 08 8205 10 09 1a 11 534849454c4420416e64726f6964205456 20 01
// --------DD------DD----DD-LEN-SHIELD Android TV
//
// VOLUME:
// ---------------DD----DD----DD-LEN-BRAVIA 4K GB------------DD---------DD-MAX---VOL---MUTE
// 00 --- 9203 1c 08 03 10 06 1a 0c 42524156494120344b204742 20 02 2800 30 64 38 00 40 00
// 01 --- 9203 1c 08 03 10 06 1a 0c 42524156494120344b204742 20 02 2800 30 64 38 01 40 00
// 100 -- 9203 1c 08 03 10 06 1a 0c 42524156494120344b204742 20 02 2800 30 64 38 64 40 00
// MUTE - 9203 1c 08 03 10 06 1a 0c 42524156494120344b204742 20 02 2800 30 64 38 00 40 01
String st = "";
int length = 0;
StringBuilder preambleSb = new StringBuilder();
StringBuilder modelSb = new StringBuilder();
String volMax = "";
String volCurr = "";
String volMute = "";
String audioMode = "";
int i = 0;
int current = 0;
for (; i < 12; i++) {
preambleSb.append(charArray[i]);
}
st = "" + charArray[i] + charArray[i + 1];
do {
if (!DELIMITER_1A.equals(st)) {
preambleSb.append(st);
i += 2;
st = "" + charArray[i] + charArray[i + 1];
}
} while (!DELIMITER_1A.equals(st));
i += 2; // 1a delimiter
st = "" + charArray[i] + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i++) {
modelSb.append(charArray[i]);
}
i += 2; // 20 delimiter
st = "" + charArray[i] + charArray[i + 1];
audioMode = st; // 01 remote audio - 02 local audio
if (DELIMITER_02.equals(st)) {
i += 2; // 02 longer message
i += 4; // Unknown 2800 message
i += 2; // 30 delimiter
volMax = "" + charArray[i] + charArray[i + 1];
i += 4; // volMax + 38 delimiter
volCurr = "" + charArray[i] + charArray[i + 1];
i += 4; // volCurr + 40 delimiter
volMute = "" + charArray[i] + charArray[i + 1];
callback.setVolMax(volMax);
callback.setVolCurr(volCurr);
callback.setVolMute(volMute);
}
String preamble = preambleSb.toString();
String model = GoogleTVRequest.encodeMessage(modelSb.toString());
logger.debug("{} - Device Update: {} \"{}\" {} {} {} {}", thingId, preamble, model, audioMode, volMax,
volCurr, volMute);
callback.setAudioMode(audioMode);
} else if (msg.startsWith(DELIMITER_08)) {
// PIN Process Messages. Only used on 6467.
if (msg.startsWith(MESSAGE_PINSUCCESS)) {
// PIN Process Successful
logger.debug("{} - PIN Process Successful!", thingId);
callback.finishPinProcess();
} else {
// 080210c801a201081204080310061801
// 080210c801fa0100
logger.debug("{} - PIN Intermediary Message: {}", thingId, msg);
}
} else if (msg.startsWith(DELIMITER_C2)) {
// Power State
// c202020800 - OFF
// c202020801 - ON
if (MESSAGE_POWEROFF.equals(msg)) {
callback.setPower(false);
} else if (MESSAGE_POWERON.equals(msg)) {
callback.setPower(true);
} else {
logger.info("{} - Unknown power state received. {}", thingId, msg);
}
} else if (msg.startsWith(DELIMITER_42)) {
// Keepalive request
callback.sendKeepAlive(msg);
} else if (msg.startsWith(DELIMITER_A2)) {
// Current app name. Sent on keypress and power change.
// a201 21 0a 1f 62 1d 636f6d2e676f6f676c652e616e64726f69642e796f75747562652e7476
// -----------------LEN-com.google.android.youtube.tv
// a201 21 0a 1f 62 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
// -----------------LEN-com.google.android.tvlauncher
// a201 14 0a 12 62 10 636f6d2e736f6e792e6474762e747678
// -----------------LEN-com.sony.dtv.tvx
// a201 15 0a 13 62 11 636f6d2e6e6574666c69782e6e696e6a61
// -----------------LEN-com.netflix.ninja
StringBuilder preambleSb = new StringBuilder();
StringBuilder appNameSb = new StringBuilder();
int i = 0;
int current = 0;
for (; i < 10; i++) {
preambleSb.append(charArray[i]);
}
i += 2; // 62 delimiter
String st = "" + charArray[i] + charArray[i + 1];
int length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i++) {
appNameSb.append(charArray[i]);
}
String preamble = preambleSb.toString();
String appName = GoogleTVRequest.encodeMessage(appNameSb.toString());
logger.debug("{} - Current App: {} {}", thingId, preamble, appName);
callback.setCurrentApp(appName);
} else {
logger.info("{} - Unknown payload received. {} {}", thingId, len, msg);
}
} catch (Exception e) {
logger.debug("{} - Message Parser Exception on {}", thingId, msg);
logger.debug("Message Parser Caught Exception", e);
}
}
}

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.googletv;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import static org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains static methods for constructing LEAP messages
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVRequest {
public static String encodeMessage(String message) {
StringBuilder reply = new StringBuilder();
char[] charArray = message.toCharArray();
for (int i = 0; i < charArray.length; i = i + 2) {
String st = "" + charArray[i] + "" + charArray[i + 1];
char ch = (char) Integer.parseInt(st, 16);
reply.append(ch);
}
return reply.toString();
}
public static String decodeMessage(String message) {
StringBuilder sb = new StringBuilder();
char ch[] = message.toCharArray();
for (int i = 0; i < ch.length; i++) {
String hexString = Integer.toHexString(ch[i]);
if (hexString.length() % 2 > 0) {
sb.append('0');
}
sb.append(hexString);
}
return sb.toString();
}
public static String pinRequest(String pin) {
// OLD
if (PIN_REQUEST.equals(pin)) {
return loginRequest(3);
} else {
// 080210c801c202 22 0a 20 0e066c3d1c3a6686edb6b2648ff25fcb3f0bf9cc81deeee9fad1a26073645e17
// 080210c801c202 22 0a 20 530bb7c7ba06069997285aff6e0106adfb19ab23c18a7422f5f643b35a6467b3
// -------------------------SHA HASH OF PIN
int length = pin.length() / 2;
String len1 = GoogleTVRequest.fixMessage(Integer.toHexString(length + 2));
String len2 = GoogleTVRequest.fixMessage(Integer.toHexString(length));
return "080210c801c202" + len1 + "0a" + len2 + pin;
}
}
public static String loginRequest(int messageId) {
String message = "";
if (messageId == 1) {
// Send app and device name
// 080210c801522d 0a 19 636f6d2e676f6f676c652e616e64726f69642e766964656f73 12 10
// 73616d73756e6720534d2d4739393855
// ------------------LEN com.google.android.videos----------------------------LEN samsung SM-G998U
message = "080210c801522d0a19636f6d2e676f6f676c652e616e64726f69642e766964656f73121073616d73756e6720534d2d4739393855";
} else if (messageId == 2) {
// Unknown but required
// 080210c801a201 0e 0a 04 08031006 0a 04 08031004 1802
// ---------------LEN---LEN------------LEN
message = "080210c801a2010e0a04080310060a04080310041802";
} else if (messageId == 3) {
// Trigger PIN OSD
// ---------------LEN---LEN
// 080210c801a201 08 12 04 08031006 1801
// 080210c801f201 08 0a 04 08031006 1001
message = "080210c801f201080a04080310061001";
} else if (messageId == 4) {
// 0a41087e123d0a 08 534d2d4739393855 12 07 73616d73756e67 18 01 22 02 3133 2a
// ---------------LEN--SM-G998U----------LEN--samsung---------
// 19 636f6d2e676f6f676c652e616e64726f69642e766964656f73 32
// LEN-com.google.android.videos----------------------------
// 07 342e33382e3138
// LEN-4.38.18
// message =
// "0a41087e123d0a08534d2d4739393855120773616d73756e671801220231332a19636f6d2e676f6f676c652e616e64726f69642e766964656f733207342e33382e3138";
// 0a5708fe0412520a 08 534d2d4739393855 12 07 73616d73756e67 18 01 22 02 3133 2a
// -----------------LEN--SM-G998U----------LEN--samsung---------
// 19 636f6d2e676f6f676c652e616e64726f69642e766964656f73 32
// LEN-com.google.android.videos---------------------------
// 1c 342e33392e3538342e3532393538383538332e372d72656c65617365
// LEN-4.39.584.529588583.7-release
message = "0a5708fe0412520a08534d2d4739393855120773616d73756e671801220231332a19636f6d2e676f6f676c652e616e64726f69642e766964656f73321c342e33392e3538342e3532393538383538332e372d72656c65617365";
} else if (messageId == 5) {
// Unknown. Sent after "1200" received
message = "1202087e";
}
return message;
}
public static String keepAlive(String request) {
// 42 07 08 01 10 e4f1 8d01
// 4a 02 08 01
// 42 08 08 7f 10 b4 908a a819
// 4a 02 08 7f
// 42 09 08 8001 10 ed b78a a819
// 4a 03 08 8001
char[] charArray = request.toCharArray();
StringBuilder sb = new StringBuilder();
sb.append(request);
sb.setLength(sb.toString().length() - 6);
String st = "";
do {
int sbLen = sb.toString().length();
st = "" + charArray[sbLen - 2] + charArray[sbLen - 1];
if (!DELIMITER_10.equals(st)) {
sb.setLength(sbLen - 2);
}
} while (!DELIMITER_10.equals(st));
sb.setLength(sb.toString().length() - 2);
StringBuilder sbReply = new StringBuilder();
for (int i = 4; i < sb.toString().length(); i++) {
sbReply.append(charArray[i]);
}
return "4a" + fixMessage(Integer.toHexString(sbReply.toString().length() / 2)) + sbReply.toString();
}
public static String fixMessage(String tempMsg) {
if (tempMsg.length() % 2 > 0) {
tempMsg = "0" + tempMsg;
}
return tempMsg;
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.googletv;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.interfaces.RSAPublicKey;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* GoogleTVCommand represents a GoogleTV protocol command
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(GoogleTVUtils.class);
private static String processMag(final byte[] magnitude) {
final int length = magnitude.length;
if (length != 0) {
final BigInteger bigInteger = new BigInteger(1, magnitude);
final StringBuilder sb = new StringBuilder();
sb.append("%0");
sb.append(length + length);
sb.append("x");
return String.format(sb.toString(), bigInteger);
}
return "";
}
private static final byte[] processDigestArray(final byte[] array) {
int n = 0;
int length;
while (true) {
length = array.length;
if (n >= length || array[n] != 0) {
break;
}
++n;
}
final int n2 = length - n;
final byte[] array2 = new byte[n2];
System.arraycopy(array, n, array2, 0, n2);
return array2;
}
public static final byte[] processDigest(byte[] digest, Certificate clientCert, Certificate serverCert) {
final PublicKey clientPublicKey = clientCert.getPublicKey();
final PublicKey serverPublicKey = serverCert.getPublicKey();
processMag(digest);
if (clientPublicKey instanceof RSAPublicKey && serverPublicKey instanceof RSAPublicKey) {
final RSAPublicKey clientRSAPublicKey = (RSAPublicKey) clientPublicKey;
final RSAPublicKey serverRSAPublicKey = (RSAPublicKey) serverPublicKey;
try {
final MessageDigest instance = MessageDigest.getInstance("SHA-256");
final byte[] byteArray1 = clientRSAPublicKey.getModulus().abs().toByteArray();
final byte[] byteArray2 = clientRSAPublicKey.getPublicExponent().abs().toByteArray();
final byte[] byteArray3 = serverRSAPublicKey.getModulus().abs().toByteArray();
final byte[] byteArray4 = serverRSAPublicKey.getPublicExponent().abs().toByteArray();
final byte[] r1 = processDigestArray(byteArray1);
final byte[] r2 = processDigestArray(byteArray2);
final byte[] r3 = processDigestArray(byteArray3);
final byte[] r4 = processDigestArray(byteArray4);
processMag(r1);
processMag(r2);
processMag(r3);
processMag(r4);
processMag(digest);
instance.update(r1);
instance.update(r2);
instance.update(r3);
instance.update(r4);
instance.update(digest);
digest = instance.digest();
processMag(digest);
} catch (NoSuchAlgorithmException e) {
LOGGER.warn("NoSuchAlgorithmException Exception", e);
}
}
return digest;
}
public static String byteArrayToString(byte[] array) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.length; i++) {
sb.append((char) (array[i] & 0xFF));
}
return sb.toString();
}
public static String validatePIN(String pin, Certificate clientCert, Certificate serverCert) {
char[] charArray = pin.toCharArray();
String s1 = "" + charArray[0] + charArray[1];
String s2 = "" + charArray[2] + charArray[3];
String s3 = "" + charArray[4] + charArray[5];
int si1 = Integer.parseInt(s1, 16);
int si2 = Integer.parseInt(s2, 16);
int si3 = Integer.parseInt(s3, 16);
byte[] sb123 = new byte[] { (byte) si1, (byte) si2, (byte) si3 };
byte[] sb23 = new byte[] { (byte) si2, (byte) si3 };
byte[] digest = processDigest(sb23, clientCert, serverCert);
String digestString = GoogleTVRequest.decodeMessage(byteArrayToString(digest));
byte[] validPinB = new byte[] { digest[0], (byte) si2, (byte) si3 };
String validPin = GoogleTVRequest.decodeMessage(byteArrayToString(validPinB));
LOGGER.trace("validatePIN {} {} {} {} {} {}", sb123, digest[0], sb23, validPinB, validPin, digestString);
return digestString;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.shieldtv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* ShieldTVCommand represents a ShieldTV protocol command
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVCommand {
private String command;
public ShieldTVCommand(String command) {
this.command = command;
}
@Override
public String toString() {
return command;
}
public boolean isEmpty() {
return command.isEmpty();
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.shieldtv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShieldTVConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVConfiguration {
public String ipAddress = "";
public int port = 8987;
public int reconnect;
public int heartbeat;
public String keystoreFileName = "";
public String keystorePassword = "";
public int delay = 0;
public boolean shim;
public boolean shimNewKeys;
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.shieldtv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShieldTVConstants} class defines common constants, which are
* used across the shieldtv protocol.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVConstants {
// List of all static String literals
public static final String DELIMITER_0 = "0";
public static final String DELIMITER_00 = "00";
public static final String DELIMITER_08 = "08";
public static final String DELIMITER_0A = "0a";
public static final String DELIMITER_12 = "12";
public static final String DELIMITER_18 = "18";
public static final String DELIMITER_22 = "22";
public static final String DELIMITER_2A = "2a";
public static final String DELIMITER_E9 = "e9";
public static final String DELIMITER_EC = "ec";
public static final String DELIMITER_F0 = "f0";
public static final String DELIMITER_F1 = "f1";
public static final String DELIMITER_F3 = "f3";
public static final String HARD_DROP = "ffffffff";
public static final String APP_START_SUCCESS = "08f1071202080318f107";
public static final String APP_START_FAILED = "08f107120608031202080118f107";
public static final String KEEPALIVE_REPLY = "080028fae0a6c0d130";
public static final String TIMEOUT = "080a121108b510120c0804120854696d65206f7574180a";
public static final String MESSAGE_LOWPRIV = "080a12";
public static final String MESSAGE_HOSTNAME = "080b12";
public static final String MESSAGE_APPDB = "08f10712";
public static final String MESSAGE_GOOD_COMMAND = "08f30712";
public static final String MESSAGE_PINSTART = "0308cf08";
public static final String MESSAGE_CERT_COMING = "20";
public static final String MESSAGE_SUCCESS = "08f007";
public static final String MESSAGE_APP_SUCCESS = "08ec07";
public static final String MESSAGE_APP_GET_SUCCESS = "0803";
public static final String MESSAGE_APP_CURRENT = "0807";
public static final String MESSAGE_SHORTNAME = "08e807";
public static final String MESSAGE_CERT = "08b510";
public static final String MESSAGE_CERT_PAYLOAD = "0753756363657373";
}

View File

@@ -0,0 +1,445 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.shieldtv;
import static org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConstants.*;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.bind.DatatypeConverter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class responsible for parsing incoming ShieldTV messages. Calls back to an object implementing the
* ShieldTVMessageParserCallbacks interface.
*
* Adapted from Lutron Leap binding
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVMessageParser {
private final Logger logger = LoggerFactory.getLogger(ShieldTVMessageParser.class);
private final ShieldTVConnectionManager callback;
public ShieldTVMessageParser(ShieldTVConnectionManager callback) {
this.callback = callback;
}
public void handleMessage(String msg) {
if (msg.trim().isEmpty()) {
return; // Ignore empty lines
}
String thingId = callback.getThingID();
String hostName = callback.getHostName();
logger.trace("{} - Received ShieldTV message from: {} - Message: {}", thingId, hostName, msg);
callback.validMessageReceived();
char[] charArray = msg.toCharArray();
try {
// All lengths are little endian when larger than 0xff
if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_SHORTNAME, 8)) {
// Pre-login Hostname of Shield Replied
// 080a 12 1408e807 12 0f08e807 12 LEN Hostname 18d7fd04 180a
// 080a 12 1d08e807 12 180801 12 LEN Hostname 18d7fd04 180a
// 080a 12 2208e807 12 1d08e807 12 LEN Hostname 18d7fd04 180a
// Each chunk ends in 12
// 4th chunk represent length of the name.
// 5th chunk is the name
int chunk = 0;
int i = 0;
String st = "";
StringBuilder hostname = new StringBuilder();
while (chunk < 3) {
st = "" + charArray[i] + "" + charArray[i + 1];
if (DELIMITER_12.equals(st)) {
chunk++;
}
i += 2;
}
st = "" + charArray[i] + "" + charArray[i + 1];
i += 2;
int length = Integer.parseInt(st, 16) * 2;
int current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
hostname.append(st);
}
logger.trace("{} - Shield Hostname: {} {}", thingId, hostname, length);
String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
logger.debug("{} - Shield Hostname Encoded: {}", thingId, encHostname);
callback.setHostName(encHostname);
} else if (msg.startsWith(MESSAGE_HOSTNAME)) {
// Longer hostname reply
// 080b 12 5b08b510 12 TOTALLEN? 0a LEN Hostname 12 LEN IPADDR Padding? 22 LEN DeviceID 2a LEN arm64-v8a
// 2a LEN armeabi-v7a 2a LEN armeabi 180b
// It's possible for there to be more or less of the arm lists
logger.trace("{} - Longer Hostname Reply", thingId);
int i = 20;
int length;
int current;
// Hostname
String st = "" + charArray[i] + "" + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
StringBuilder hostname = new StringBuilder();
current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
hostname.append(st);
}
i += 2; // 12
// ipAddress
st = "" + charArray[i] + "" + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
StringBuilder ipAddress = new StringBuilder();
current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
ipAddress.append(st);
}
st = "" + charArray[i] + "" + charArray[i + 1];
while (!DELIMITER_22.equals(st)) {
i += 2;
st = "" + charArray[i] + "" + charArray[i + 1];
}
i += 2; // 22
// deviceId
st = "" + charArray[i] + "" + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
StringBuilder deviceId = new StringBuilder();
current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
deviceId.append(st);
}
// architectures
st = "" + charArray[i] + "" + charArray[i + 1];
StringBuilder arch = new StringBuilder();
while (DELIMITER_2A.equals(st)) {
i += 2;
st = "" + charArray[i] + "" + charArray[i + 1];
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
arch.append(st);
}
st = "" + charArray[i] + "" + charArray[i + 1];
if (DELIMITER_2A.equals(st)) {
arch.append("2c");
}
}
String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
String encIpAddress = ShieldTVRequest.encodeMessage(ipAddress.toString());
String encDeviceId = ShieldTVRequest.encodeMessage(deviceId.toString());
String encArch = ShieldTVRequest.encodeMessage(arch.toString());
logger.debug("{} - Hostname: {} - ipAddress: {} - deviceId: {} - arch: {}", thingId, encHostname,
encIpAddress, encDeviceId, encArch);
callback.setHostName(encHostname);
callback.setDeviceID(encDeviceId);
callback.setArch(encArch);
} else if (APP_START_SUCCESS.equals(msg)) {
// App successfully started
logger.debug("{} - App started successfully", thingId);
} else if (APP_START_FAILED.equals(msg)) {
// App failed to start
logger.debug("{} - App failed to start", thingId);
} else if (msg.startsWith(MESSAGE_APPDB) && msg.startsWith(DELIMITER_0A, 18)) {
// Individual update?
// 08f10712 5808061254 0a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 300118f107
logger.info("{} - Individual App Update - Please Report This: {}", thingId, msg);
} else if (msg.startsWith(MESSAGE_APPDB) && (msg.length() > 30)) {
// Massive dump of currently installed apps
// 08f10712 d81f080112 d31f0a540a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 30010a650a LEN
Map<String, String> appNameDB = new HashMap<>();
Map<String, String> appURLDB = new HashMap<>();
int appCount = 0;
int i = 18;
String st = "";
int length;
int current;
StringBuilder appSBPrepend = new StringBuilder();
StringBuilder appSBDN = new StringBuilder();
// Load default apps that don't get sent in payload
appNameDB.put("com.google.android.tvlauncher", "Android TV Home");
appURLDB.put("com.google.android.tvlauncher", "");
appNameDB.put("com.google.android.katniss", "Google app for Android TV");
appURLDB.put("com.google.android.katniss", "");
appNameDB.put("com.google.android.katnisspx", "Google app for Android TV (Pictures)");
appURLDB.put("com.google.android.katnisspx", "");
appNameDB.put("com.google.android.backdrop", "Backdrop Daydream");
appURLDB.put("com.google.android.backdrop", "");
// Packet will end with 300118f107 after last entry
while (i < msg.length() - 10) {
StringBuilder appSBName = new StringBuilder();
StringBuilder appSBURL = new StringBuilder();
// There are instances such as plex where multiple apps are sent as part of the same payload
// This is identified when 12 is the beginning of the set
st = "" + charArray[i] + "" + charArray[i + 1];
if (!DELIMITER_12.equals(st)) {
appSBPrepend = new StringBuilder();
appSBDN = new StringBuilder();
appCount++;
// App Prepend
// Usually 10 in length but can be longer or shorter so look for 0a twice
do {
st = "" + charArray[i] + "" + charArray[i + 1];
appSBPrepend.append(st);
i += 2;
} while (!DELIMITER_0A.equals(st));
do {
st = "" + charArray[i] + "" + charArray[i + 1];
appSBPrepend.append(st);
i += 2;
} while (!DELIMITER_0A.equals(st));
st = "" + charArray[i] + "" + charArray[i + 1];
// Look for a third 0a, but only if 12 is not down the line
// If 12 is exactly 20 away from 0a that means that the DN was actually 10 long
String st2 = "" + charArray[i + 22] + "" + charArray[i + 23];
if (DELIMITER_0A.equals(st.toString()) && !DELIMITER_12.equals(st2)) {
appSBPrepend.append(st);
i += 2;
st = "" + charArray[i] + "" + charArray[i + 1];
}
// app DN
length = Integer.parseInt(st, 16) * 2;
i += 2;
current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
appSBDN.append(st);
}
} else {
logger.trace("Second Entry");
}
// App Name
i += 2; // 12 delimiter
st = "" + charArray[i] + "" + charArray[i + 1];
i += 2;
length = Integer.parseInt(st, 16) * 2;
current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
appSBName.append(st);
}
// There are times where there is padding here for no reason beyond the specified length.
// Proceed forward until we get to the 22 delimiter
st = "" + charArray[i] + "" + charArray[i + 1];
while (!DELIMITER_22.equals(st)) {
i += 2;
st = "" + charArray[i] + "" + charArray[i + 1];
}
// App URL
i += 2; // 22 delimiter
st = "" + charArray[i] + "" + charArray[i + 1];
i += 2;
length = Integer.parseInt(st, 16) * 2;
current = i;
for (; i < current + length; i = i + 2) {
st = "" + charArray[i] + "" + charArray[i + 1];
appSBURL.append(st);
}
st = "" + charArray[i] + "" + charArray[i + 1];
if (!DELIMITER_12.equals(st)) {
i += 4; // terminates 2801
}
String appPrepend = appSBPrepend.toString();
String appDN = ShieldTVRequest.encodeMessage(appSBDN.toString());
String appName = ShieldTVRequest.encodeMessage(appSBName.toString());
String appURL = ShieldTVRequest.encodeMessage(appSBURL.toString());
logger.debug("{} - AppPrepend: {} AppDN: {} AppName: {} AppURL: {}", thingId, appPrepend, appDN,
appName, appURL);
appNameDB.put(appDN, appName);
appURLDB.put(appDN, appURL);
}
if (appCount > 0) {
Map<String, String> sortedAppNameDB = new LinkedHashMap<>();
List<String> valueList = new ArrayList<>();
for (Map.Entry<String, String> entry : appNameDB.entrySet()) {
valueList.add(entry.getValue());
}
Collections.sort(valueList);
for (String str : valueList) {
for (Entry<String, String> entry : appNameDB.entrySet()) {
if (entry.getValue().equals(str)) {
sortedAppNameDB.put(entry.getKey(), str);
}
}
}
logger.trace("{} - MP appNameDB: {} sortedAppNameDB: {} appURLDB: {}", thingId,
appNameDB.toString(), sortedAppNameDB.toString(), appURLDB.toString());
callback.setAppDB(sortedAppNameDB, appURLDB);
} else {
logger.warn("{} - MP empty msg: {} appDB appNameDB: {} appURLDB: {}", thingId, msg,
appNameDB.toString(), appURLDB.toString());
}
} else if (msg.startsWith(MESSAGE_GOOD_COMMAND)) {
// This has something to do with successful command response, maybe.
} else if (KEEPALIVE_REPLY.equals(msg)) {
// Keepalive Reply
} else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
// 080a 12 0308cf08 180a
logger.debug("PIN Process Started");
} else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
// This seems to be 20**** when observed. It is unclear what this does.
// This seems to send immediately before the certificate reply and as a reply to the pin being sent
} else if (msg.startsWith(MESSAGE_SUCCESS)) {
// Successful command received
// 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
// 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
//
// 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
// 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
// 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
callback.setLoggedIn(true);
} else if (TIMEOUT.equals(msg)) {
// Timeout
// 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
// 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
logger.debug("{} - Timeout {}", thingId, msg);
} else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
// Get current app command successful. Usually paired with 0807 reply below.
} else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
// Current App
// 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
// 18ec07
// 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
StringBuilder appName = new StringBuilder();
String lengthStr = "" + charArray[34] + charArray[35];
int length = Integer.parseInt(lengthStr, 16) * 2;
for (int i = 36; i < 36 + length; i++) {
appName.append(charArray[i]);
}
logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
} else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
// Certificate Reply
// |--6-----------12-----------10---------------16---------6--- = 50 characters long
// |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
// |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
// |--------Little Endian Total Payload Length
// |-----------------------Little Endian Remaining Payload Length
// |-----------------------------------Length of SUCCESS
// |--------------------------------------ASCII: SUCCESS
// |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
// |------------------------------------------------------------Priv Key RSA 2048
// |--------------------------------------------------------------------Cert X.509
if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
StringBuilder preamble = new StringBuilder();
StringBuilder privKey = new StringBuilder();
StringBuilder pubKey = new StringBuilder();
int i = 0;
int current;
for (; i < 44; i++) {
preamble.append(charArray[i]);
}
logger.trace("{} - Cert Preamble: {}", thingId, preamble.toString());
i += 2; // 1a
String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
+ charArray[i + 1];
int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
i += 4; // length
current = i;
logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
for (; i < current + privLen; i++) {
privKey.append(charArray[i]);
}
logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
for (; i < msg.length() - 4; i++) {
pubKey.append(charArray[i]);
}
logger.trace("{} - Cert pubKey: {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
msg.length() - privLen - 4);
byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
callback.setKeys(privKeyB64, pubKeyB64);
} else {
logger.info("{} - Pin Process Failed.", thingId);
}
} else {
logger.info("{} - Unknown payload received. {}", thingId, msg);
}
} catch (Exception e) {
logger.info("{} - Message Parser Caught Exception", thingId, e);
}
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.protocol.shieldtv;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains static methods for constructing LEAP messages
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVRequest {
public static String encodeMessage(String message) {
StringBuilder reply = new StringBuilder();
char[] charArray = message.toCharArray();
for (int i = 0; i < charArray.length; i = i + 2) {
String st = "" + charArray[i] + "" + charArray[i + 1];
char ch = (char) Integer.parseInt(st, 16);
reply.append(ch);
}
return reply.toString();
}
public static String decodeMessage(String message) {
StringBuilder sb = new StringBuilder();
char ch[] = message.toCharArray();
for (int i = 0; i < ch.length; i++) {
String hexString = Integer.toHexString(ch[i]);
if (hexString.length() % 2 > 0) {
sb.append('0');
}
sb.append(hexString);
}
return sb.toString();
}
public static String pinRequest(String pin) {
if (PIN_REQUEST.equals(pin)) {
String message = "080a120308cd08";
return message;
} else {
String prefix = "080a121f08d108121a0a06";
String encodedPin = decodeMessage(pin);
String suffix = "121036646564646461326639366635646261";
return prefix + encodedPin + suffix;
}
}
public static String loginRequest() {
String message = "0801121a0801121073616d73756e6720534d2d4739393855180128fbff04";
return message;
}
public static String keepAlive() {
String message = "080028fae0a6c0d130";
return message;
}
private static String fixMessage(String tempMsg) {
if (tempMsg.length() % 2 > 0) {
tempMsg = "0" + tempMsg;
}
return tempMsg;
}
public static String startApp(String message) {
int length = message.length();
String len1 = fixMessage(Integer.toHexString(length + 6));
String len2 = fixMessage(Integer.toHexString(length + 2));
String len3 = fixMessage(Integer.toHexString(length));
String reply = "08f10712" + len1 + "080212" + len2 + "0a" + len3 + decodeMessage(message);
return reply;
}
// 080b120308cd08 - Longer Hostname Reply
// 08f30712020805 - Unknown
// 08f10712020800 - Get all apps
// 08ec0712020806 - Get current app
public static String keyboardEntry(String entry) {
// 08ec07120d08081205616263646532020a0a
// 08ec0712 0d 0808 12 05 6162636465 3202 0a0a
int length = entry.length();
String len1 = fixMessage(Integer.toHexString(length + 8));
String len2 = fixMessage(Integer.toHexString(length));
String len3 = fixMessage(Integer.toHexString(length * 2));
String reply = "08ec0712" + len1 + "080812" + len2 + decodeMessage(entry) + "3202" + len3 + len3;
return reply;
}
}

View File

@@ -0,0 +1,291 @@
/**
* Copyright (c) 2010-2023 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.androidtv.internal.utils;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AndroidTVPKI} class controls all aspects of the PKI/keyStore
*
* Some methods adapted from Bosch binding
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class AndroidTVPKI {
private final Logger logger = LoggerFactory.getLogger(AndroidTVPKI.class);
private final int keySize = 128;
private final int dataLength = 128;
private String privKey = "";
private String cert = "";
private String keystoreFileName = "";
private String keystoreAlgorithm = "RSA";
private int keyLength = 2048;
private String alias = "openhab";
private String distName = "CN=openHAB, O=openHAB, L=None, ST=None, C=None";
private String cipher = "AES/GCM/NoPadding";
private String keyAlgorithm = "";
private @Nullable Cipher encryptionCipher;
public AndroidTVPKI() {
try {
encryptionCipher = Cipher.getInstance(cipher);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
logger.debug("Could not get cipher instance", e);
}
}
public byte[] generateEncryptionKey() {
Key key;
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(keySize);
key = keyGenerator.generateKey();
byte[] newKey = key.getEncoded();
this.keyAlgorithm = key.getAlgorithm();
return newKey;
} catch (NoSuchAlgorithmException e) {
logger.debug("Could not generate encryption keys", e);
}
return new byte[0];
}
private Key convertByteToKey(byte[] keyString) {
Key key = new SecretKeySpec(keyString, keyAlgorithm);
return key;
}
public String encrypt(String data, Key key) throws Exception {
return encrypt(data, key, this.cipher);
}
public String encrypt(String data, Key key, String cipher) throws Exception {
byte[] dataInBytes = data.getBytes();
Cipher encryptionCipher = this.encryptionCipher;
if (encryptionCipher != null) {
encryptionCipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = encryptionCipher.doFinal(dataInBytes);
return Base64.getEncoder().encodeToString(encryptedBytes);
} else {
return "";
}
}
public String decrypt(String encryptedData, Key key) throws Exception {
return decrypt(encryptedData, key, this.cipher);
}
public String decrypt(String encryptedData, Key key, String cipher) throws Exception {
byte[] dataInBytes = Base64.getDecoder().decode(encryptedData);
Cipher decryptionCipher = Cipher.getInstance(cipher);
Cipher encryptionCipher = this.encryptionCipher;
if (encryptionCipher != null) {
GCMParameterSpec spec = new GCMParameterSpec(dataLength, encryptionCipher.getIV());
decryptionCipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] decryptedBytes = decryptionCipher.doFinal(dataInBytes);
return new String(decryptedBytes);
} else {
return "";
}
}
public void setPrivKey(String privKey, byte[] keyString) throws Exception {
Key key = convertByteToKey(keyString);
this.privKey = encrypt(privKey, key);
}
public String getPrivKey(byte[] keyString) throws Exception {
Key key = convertByteToKey(keyString);
return decrypt(this.privKey, key);
}
public void setCert(String cert) {
this.cert = cert;
}
public void setCert(Certificate cert) throws CertificateEncodingException {
this.cert = new String(Base64.getEncoder().encode(cert.getEncoded()));
}
public Certificate getCert() throws CertificateException {
Certificate cert = CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(this.cert.getBytes())));
return cert;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getAlias() {
return this.alias;
}
public void setAlgorithm(String keystoreAlgorithm) {
this.keystoreAlgorithm = keystoreAlgorithm;
}
public String getAlgorithm() {
return this.keystoreAlgorithm;
}
public void setKeyLength(int keyLength) {
this.keyLength = keyLength;
}
public int getKeyLength() {
return this.keyLength;
}
public void setDistName(String distName) {
this.distName = distName;
}
public String getDistName() {
return this.distName;
}
public void setKeystoreFileName(String keystoreFileName) {
this.keystoreFileName = keystoreFileName;
}
public String getKeystoreFileName() {
return this.keystoreFileName;
}
public void setKeys(String privKey, byte[] keyString, String cert) throws GeneralSecurityException, Exception {
setPrivKey(privKey, keyString);
setCert(cert);
}
public void setKeyStore(String keystoreFileName) {
this.keystoreFileName = keystoreFileName;
}
public void loadFromKeyStore(String keystoreFileName, String keystorePassword, byte[] keyString)
throws GeneralSecurityException, IOException, Exception {
this.keystoreFileName = keystoreFileName;
loadFromKeyStore(keystorePassword, keyString);
}
public void loadFromKeyStore(String keystorePassword, byte[] keyString)
throws GeneralSecurityException, IOException, Exception {
Key key = convertByteToKey(keyString);
KeyStore keystore = KeyStore.getInstance("JKS");
FileInputStream keystoreInputStream = new FileInputStream(this.keystoreFileName);
keystore.load(keystoreInputStream, keystorePassword.toCharArray());
byte[] byteKey = keystore.getKey(this.alias, keystorePassword.toCharArray()).getEncoded();
this.privKey = encrypt(new String(Base64.getEncoder().encode(byteKey)), key);
setCert(keystore.getCertificate(this.alias));
}
public KeyStore getKeyStore(String keystorePassword, byte[] keyString)
throws GeneralSecurityException, IOException, Exception {
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null, null);
byte[] pkcs8EncodedBytes = Base64.getDecoder().decode(getPrivKey(keyString));
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8EncodedBytes);
KeyFactory kf = KeyFactory.getInstance(this.keystoreAlgorithm);
keystore.setKeyEntry(this.alias, kf.generatePrivate(keySpec), keystorePassword.toCharArray(),
new java.security.cert.Certificate[] { getCert() });
return keystore;
}
public void saveKeyStore(String keystorePassword, byte[] keyString)
throws GeneralSecurityException, IOException, Exception {
saveKeyStore(this.keystoreFileName, keystorePassword, keyString);
}
public void saveKeyStore(String keystoreFileName, String keystorePassword, byte[] keyString)
throws GeneralSecurityException, IOException, Exception {
FileOutputStream keystoreStream = new FileOutputStream(keystoreFileName);
KeyStore keystore = getKeyStore(keystorePassword, keyString);
keystore.store(keystoreStream, keystorePassword.toCharArray());
}
private X509Certificate generateSelfSignedCertificate(KeyPair keyPair, String distName)
throws GeneralSecurityException, OperatorCreationException {
final Instant now = Instant.now();
final Date notBefore = Date.from(now);
final Date notAfter = Date.from(now.plus(Duration.ofDays(365 * 10)));
X500Name name = new X500Name(distName);
X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(name,
BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, name, keyPair.getPublic());
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider())
.getCertificate(certificateBuilder.build(contentSigner));
}
public void generateNewKeyPair(byte[] keyString)
throws GeneralSecurityException, OperatorCreationException, IOException, Exception {
Key key = convertByteToKey(keyString);
KeyPairGenerator kpg = KeyPairGenerator.getInstance(this.keystoreAlgorithm);
kpg.initialize(this.keyLength);
KeyPair kp = kpg.generateKeyPair();
Security.addProvider(new BouncyCastleProvider());
Signature signer = Signature.getInstance("SHA256withRSA", "BC");
signer.initSign(kp.getPrivate());
signer.update("openhab".getBytes(StandardCharsets.UTF_8));
signer.sign();
X509Certificate signedcert = generateSelfSignedCertificate(kp, this.distName);
this.privKey = encrypt(new String(Base64.getEncoder().encode(kp.getPrivate().getEncoded())), key);
setCert(signedcert);
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="androidtv" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>AndroidTV Binding</name>
<description>This is the add-on for AndroidTV.</description>
<connection>local</connection>
</addon:addon>

View File

@@ -0,0 +1,69 @@
# add-on
addon.androidtv.name = AndroidTV Binding
addon.androidtv.description = This is the add-on for AndroidTV.
# thing types
thing-type.androidtv.googletv.label = GoogleTV
thing-type.androidtv.googletv.description = GoogleTV
thing-type.androidtv.shieldtv.label = ShieldTV
thing-type.androidtv.shieldtv.description = Nvidia ShieldTV
# thing types config
thing-type.config.androidtv.googletv.delay.label = Delay
thing-type.config.androidtv.googletv.delay.description = Delay between messages
thing-type.config.androidtv.googletv.heartbeat.label = Heartbeat Frequency
thing-type.config.androidtv.googletv.heartbeat.description = Frequency of heartbeats
thing-type.config.androidtv.googletv.ipAddress.label = Hostname
thing-type.config.androidtv.googletv.ipAddress.description = Hostname or IP address of the device
thing-type.config.androidtv.googletv.keystoreFileName.label = Keystore File Name
thing-type.config.androidtv.googletv.keystoreFileName.description = Java keystore containing key and certs
thing-type.config.androidtv.googletv.keystorePassword.label = Keystore Password
thing-type.config.androidtv.googletv.keystorePassword.description = Password for the keystore file
thing-type.config.androidtv.googletv.port.label = Port
thing-type.config.androidtv.googletv.port.description = Port to connect to
thing-type.config.androidtv.googletv.reconnect.label = Reconnect Delay
thing-type.config.androidtv.googletv.reconnect.description = Delay between reconnection attempts
thing-type.config.androidtv.shieldtv.delay.label = Delay
thing-type.config.androidtv.shieldtv.delay.description = Delay between messages
thing-type.config.androidtv.shieldtv.heartbeat.label = Hearbeat Frequency
thing-type.config.androidtv.shieldtv.heartbeat.description = Frequency of heartbeats
thing-type.config.androidtv.shieldtv.ipAddress.label = Hostname
thing-type.config.androidtv.shieldtv.ipAddress.description = Hostname or IP address of the device
thing-type.config.androidtv.shieldtv.keystoreFileName.label = Keystore File Name
thing-type.config.androidtv.shieldtv.keystoreFileName.description = Java keystore containing key and certs
thing-type.config.androidtv.shieldtv.keystorePassword.label = Keystore Password
thing-type.config.androidtv.shieldtv.keystorePassword.description = Password for the keystore file
thing-type.config.androidtv.shieldtv.port.label = Port
thing-type.config.androidtv.shieldtv.port.description = Port to connect to
thing-type.config.androidtv.shieldtv.reconnect.label = Reconnect Delay
thing-type.config.androidtv.shieldtv.reconnect.description = Delay between reconnection attempts
# channel types
channel-type.androidtv.app.label = App
channel-type.androidtv.app.description = App Control
channel-type.androidtv.appname.label = App Name
channel-type.androidtv.appname.description = App Name
channel-type.androidtv.appurl.label = App URL
channel-type.androidtv.appurl.description = App URL
channel-type.androidtv.debug.label = DEBUG Command
channel-type.androidtv.debug.description = Binding control (for debugging)
channel-type.androidtv.keyboard.label = Keyboard
channel-type.androidtv.keyboard.description = Keyboard Entry
channel-type.androidtv.keycode.label = Keycode
channel-type.androidtv.keycode.description = Send keycode
channel-type.androidtv.keypress.label = Key Press
channel-type.androidtv.keypress.description = Send key press
channel-type.androidtv.pincode.label = Pin Code
channel-type.androidtv.pincode.description = Send Pin Code
channel-type.androidtv.player.label = Player
channel-type.androidtv.player.description = Player Control
# custom thing status
offline.protocols-starting = Protocols Starting
offline.googletv-address-not-specified = googletv address not specified
offline.shieldtv-address-not-specified = shieldtv address not specified

View File

@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="androidtv"
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">
<thing-type id="shieldtv">
<label>ShieldTV</label>
<description>Nvidia ShieldTV</description>
<channels>
<channel id="debug" typeId="debug"/>
<channel id="keypress" typeId="keypress"/>
<channel id="keyboard" typeId="keyboard"/>
<channel id="keycode" typeId="keycode"/>
<channel id="pincode" typeId="pincode"/>
<channel id="app" typeId="app"/>
<channel id="appname" typeId="appname"/>
<channel id="appurl" typeId="appurl"/>
<channel id="player" typeId="player"/>
<channel id="power" typeId="system.power"/>
<channel id="volume" typeId="system.volume"/>
<channel id="mute" typeId="system.mute"/>
</channels>
<properties>
<property name="deviceName">unknown</property>
<property name="deviceID">unknown</property>
<property name="architectures">unknown</property>
<property name="manufacturer">unknown</property>
<property name="model">unknown</property>
<property name="androidVersion">unknown</property>
<property name="remoteServer">unknown</property>
<property name="remoteServerVersion">unknown</property>
</properties>
<representation-property>ipAddress</representation-property>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>Hostname</label>
<description>Hostname or IP address of the device</description>
</parameter>
<parameter name="port" type="integer">
<label>Port</label>
<description>Port to connect to</description>
</parameter>
<parameter name="keystoreFileName" type="text">
<label>Keystore File Name</label>
<description>Java keystore containing key and certs</description>
</parameter>
<parameter name="keystorePassword" type="text">
<context>password</context>
<label>Keystore Password</label>
<description>Password for the keystore file</description>
</parameter>
<parameter name="reconnect" type="integer">
<label>Reconnect Delay</label>
<description>Delay between reconnection attempts</description>
</parameter>
<parameter name="heartbeat" type="integer">
<label>Hearbeat Frequency</label>
<description>Frequency of heartbeats</description>
</parameter>
<parameter name="delay" type="integer">
<label>Delay</label>
<description>Delay between messages</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="googletv">
<label>GoogleTV</label>
<description>GoogleTV</description>
<channels>
<channel id="debug" typeId="debug"/>
<channel id="keypress" typeId="keypress"/>
<channel id="keyboard" typeId="keyboard"/>
<channel id="keycode" typeId="keycode"/>
<channel id="pincode" typeId="pincode"/>
<channel id="app" typeId="app"/>
<channel id="player" typeId="player"/>
<channel id="power" typeId="system.power"/>
<channel id="volume" typeId="system.volume"/>
<channel id="mute" typeId="system.mute"/>
</channels>
<properties>
<property name="manufacturer">unknown</property>
<property name="model">unknown</property>
<property name="androidVersion">unknown</property>
<property name="remoteServer">unknown</property>
<property name="remoteServerVersion">unknown</property>
</properties>
<representation-property>ipAddress</representation-property>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>Hostname</label>
<description>Hostname or IP address of the device</description>
</parameter>
<parameter name="port" type="integer">
<label>Port</label>
<description>Port to connect to</description>
</parameter>
<parameter name="keystoreFileName" type="text">
<label>Keystore File Name</label>
<description>Java keystore containing key and certs</description>
</parameter>
<parameter name="keystorePassword" type="text">
<context>password</context>
<label>Keystore Password</label>
<description>Password for the keystore file</description>
</parameter>
<parameter name="reconnect" type="integer">
<label>Reconnect Delay</label>
<description>Delay between reconnection attempts</description>
</parameter>
<parameter name="heartbeat" type="integer">
<label>Heartbeat Frequency</label>
<description>Frequency of heartbeats</description>
</parameter>
<parameter name="delay" type="integer">
<label>Delay</label>
<description>Delay between messages</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="debug" advanced="true">
<item-type>String</item-type>
<label>DEBUG Command</label>
<description>Binding control (for debugging)</description>
</channel-type>
<channel-type id="app">
<item-type>String</item-type>
<label>App</label>
<description>App Control</description>
</channel-type>
<channel-type id="appname">
<item-type>String</item-type>
<label>App Name</label>
<description>App Name</description>
</channel-type>
<channel-type id="appurl">
<item-type>String</item-type>
<label>App URL</label>
<description>App URL</description>
</channel-type>
<channel-type id="keypress">
<item-type>String</item-type>
<label>Key Press</label>
<description>Send key press</description>
</channel-type>
<channel-type id="keycode">
<item-type>String</item-type>
<label>Keycode</label>
<description>Send keycode</description>
</channel-type>
<channel-type id="keyboard">
<item-type>String</item-type>
<label>Keyboard</label>
<description>Keyboard Entry</description>
</channel-type>
<channel-type id="pincode">
<item-type>String</item-type>
<label>Pin Code</label>
<description>Send Pin Code</description>
</channel-type>
<channel-type id="player">
<item-type>Player</item-type>
<label>Player</label>
<description>Player Control</description>
</channel-type>
</thing:thing-descriptions>