added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.magentatv-${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-magentatv" description="MagentaTV Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-upnp</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.magentatv/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "magentatv";
|
||||
public static final String VENDOR = "Deutsche Telekom";
|
||||
public static final String OEM_VENDOR = "HUAWEI";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_RECEIVER = new ThingTypeUID(BINDING_ID, "receiver");
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_RECEIVER);
|
||||
|
||||
/**
|
||||
* Property names for config/status properties
|
||||
*/
|
||||
public static final String PROPERTY_UDN = "udn";
|
||||
public static final String PROPERTY_FRIENDLYNAME = "friendlyName";
|
||||
public static final String PROPERTY_MODEL_NUMBER = "modelRev";
|
||||
public static final String PROPERTY_HOST = "host";
|
||||
public static final String PROPERTY_IP = "ipAddress";
|
||||
public static final String PROPERTY_PORT = "port";
|
||||
public static final String PROPERTY_DESC_URL = "descriptionUrl";
|
||||
public static final String PROPERTY_PAIRINGCODE = "pairingCode";
|
||||
public static final String PROPERTY_VERIFICATIONCODE = "verificationCode";
|
||||
public static final String PROPERTY_ACCT_NAME = "accountName";
|
||||
public static final String PROPERTY_ACCT_PWD = "accountPassword";
|
||||
public static final String PROPERTY_USERID = "userId";
|
||||
public static final String PROPERTY_LOCAL_IP = "localIP";
|
||||
public static final String PROPERTY_LOCAL_MAC = "localMAC";
|
||||
public static final String PROPERTY_TERMINALID = "terminalID";
|
||||
public static final String PROPERTY_WAKEONLAN = "wakeOnLAN";
|
||||
|
||||
/**
|
||||
* Channel names
|
||||
*/
|
||||
public static final String CHGROUP_CONTROL = "control";
|
||||
public static final String CHANNEL_POWER = CHGROUP_CONTROL + "#" + "power";
|
||||
public static final String CHANNEL_PLAYER = CHGROUP_CONTROL + "#" + "player";
|
||||
public static final String CHANNEL_MUTE = CHGROUP_CONTROL + "#" + "mute";
|
||||
public static final String CHANNEL_CHANNEL = CHGROUP_CONTROL + "#" + "channel";
|
||||
public static final String CHANNEL_KEY = CHGROUP_CONTROL + "#" + "key";
|
||||
|
||||
public static final String CHGROUP_PROGRAM = "program";
|
||||
public static final String CHANNEL_PROG_TITLE = CHGROUP_PROGRAM + "#" + "title";
|
||||
public static final String CHANNEL_PROG_TEXT = CHGROUP_PROGRAM + "#" + "text";
|
||||
public static final String CHANNEL_PROG_START = CHGROUP_PROGRAM + "#" + "start";
|
||||
public static final String CHANNEL_PROG_DURATION = CHGROUP_PROGRAM + "#" + "duration";
|
||||
public static final String CHANNEL_PROG_POS = CHGROUP_PROGRAM + "#" + "position";
|
||||
|
||||
public static final String CHGROUP_STATUS = "status";
|
||||
public static final String CHANNEL_CHANNEL_CODE = CHGROUP_STATUS + "#" + "channelCode";
|
||||
public static final String CHANNEL_RUN_STATUS = CHGROUP_STATUS + "#" + "runStatus";
|
||||
public static final String CHANNEL_PLAY_MODE = CHGROUP_STATUS + "#" + "playMode";
|
||||
|
||||
/**
|
||||
* Definitions for the control interface
|
||||
*/
|
||||
public static final String CONTENT_TYPE_XML = "text/xml; charset=UTF-8";
|
||||
|
||||
public static final String PAIRING_NOTIFY_URI = "/magentatv/notify";
|
||||
public static final String NOTIFY_PAIRING_CODE = "X-pairingCheck:";
|
||||
|
||||
public static final String MODEL_MR400 = "DMS_TPB"; // Old DSL receiver
|
||||
public static final String MODEL_MR401B = "MR401B"; // New DSL receiver
|
||||
public static final String MODEL_MR601 = "MR601"; // SAT receiver
|
||||
public static final String MODEL_MR201 = "MR201"; // sub receiver
|
||||
|
||||
public static final String MR400_DEF_REMOTE_PORT = "49152";
|
||||
public static final String MR400_DEF_DESCRIPTION_URL = "/description.xml";
|
||||
public static final String MR401B_DEF_REMOTE_PORT = "8081";
|
||||
public static final String MR401B_DEF_DESCRIPTION_URL = "/xml/dial.xml";
|
||||
public static final String DEF_FRIENDLY_NAME = "PAD:openHAB";
|
||||
|
||||
public static final int DEF_REFRESH_INTERVAL_SEC = 60;
|
||||
public static final int NETWORK_TIMEOUT_MS = 3000;
|
||||
|
||||
public static final String UTF_8 = StandardCharsets.UTF_8.name();
|
||||
|
||||
public static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
public static final String HEADER_HOST = "HOST";
|
||||
public static final String HEADER_ACCEPT = "Accept";
|
||||
public static final String HEADER_CACHE_CONTROL = "Cache-Control";
|
||||
public static final String HEADER_LANGUAGE = "Accept-Language";
|
||||
public static final String HEADER_SOAPACTION = "SOAPACTION";
|
||||
public static final String HEADER_CONNECTION = "CONNECTION";
|
||||
public static final String HEADER_USER_AGENT = "USER_AGENT";
|
||||
public static final String USER_AGENT = "Darwin/16.5.0 UPnP/1.0 HUAWEI_iCOS/iCOS V1R1C00 DLNADOC/1.50";
|
||||
public static final String ACCEPT_TYPE = "*/*";
|
||||
|
||||
/**
|
||||
* OAuth authentication for Deutsche Telekom MatengaTV portal
|
||||
*/
|
||||
public static final String OAUTH_GET_CRED_URL = "https://slbedmfk11100.prod.sngtv.t-online.de";
|
||||
public static final String OAUTH_GET_CRED_PORT = "33428";
|
||||
public static final String OAUTH_GET_CRED_URI = "/EDS/JSON/Login?UserID=Guest";
|
||||
public static final String OAUTH_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_3 like Mac OS X) AppleWebKit/603.3.8 (KHTML, like Gecko) Mobile/14G60 (400962928)";
|
||||
|
||||
//
|
||||
// MR events
|
||||
//
|
||||
public static final String MR_EVENT_EIT_CHANGE = "EVENT_EIT_CHANGE";
|
||||
|
||||
public static final String MR_EVENT_CHAN_TAG = "\"channel_num\":";
|
||||
|
||||
/**
|
||||
* program Info event data
|
||||
* EVENT_EIT_CHANGE: for a complete list see
|
||||
* http://support.huawei.com/hedex/pages/DOC1100366313CEH0713H/01/DOC1100366313CEH0713H/01/resources/dsv_hdx_idp/DSV/en/en-us_topic_0094619523.html
|
||||
*/
|
||||
public static final int EV_EITCHG_RUNNING_NONE = 0;
|
||||
public static final int EV_EITCHG_RUNNING_NOT_RUNNING = 1;
|
||||
public static final int EV_EITCHG_RUNNING_STARTING = 2;
|
||||
public static final int EV_EITCHG_RUNNING_PAUSING = 3;
|
||||
public static final int EV_EITCHG_RUNNING_RUNNING = 4;
|
||||
|
||||
/**
|
||||
* playStatus event data
|
||||
* EVENT_PLAYMODE_CHANGE: for a complete list see
|
||||
* http://support.huawei.com/hedex/pages/DOC1100366313CEH0713H/01/DOC1100366313CEH0713H/01/resources/dsv_hdx_idp/DSV/en/en-us_topic_0094619231.html
|
||||
*/
|
||||
public static final int EV_PLAYCHG_STOP = 0; // STOP: stop status.
|
||||
public static final int EV_PLAYCHG_PAUSE = 1; // PAUSE: pause status.
|
||||
public static final int EV_PLAYCHG_PLAY = 2; // NORMAL_PLAY: normal playback status for non-live content
|
||||
// (including TSTV).
|
||||
public static final int EV_PLAYCHG_TRICK = 3; // TRICK_MODE: trick play mode, such as fast-forward, rewind,
|
||||
// slow-forward, and slow-rewind.
|
||||
public static final int EV_PLAYCHG_MC_PLAY = 4; // MULTICAST_CHANNEL_PLAY: live broadcast status of IPTV
|
||||
// multicast channels and DVB channels.
|
||||
public static final int EV_PLAYCHG_UC_PLAY = 5; // UNICAST_CHANNEL_PLAY: live broadcast status of IPTV unicast
|
||||
// channels and OTT channels. //
|
||||
public static final int EV_PLAYCHG_BUFFERING = 20; // BUFFERING: playback buffering status, including playing
|
||||
// cPVR content during the recording, playing content
|
||||
// during the download, playing the OTT content, and no
|
||||
// data in the buffer area.
|
||||
|
||||
//
|
||||
// MagentaTVControl SOAP requests
|
||||
//
|
||||
public static final String CHECKDEV_URI = "http://{0}:{1}{2}";
|
||||
|
||||
public static final int PAIRING_TIMEOUT_SEC = 300;
|
||||
public static final String PAIRING_CONTROL_URI = "/upnp/service/X-CTC_RemotePairing/Control";
|
||||
public static final String PAIRING_SUBSCRIBE = "SUBSCRIBE /upnp/service/X-CTC_RemotePairing/Event HTTP/1.1\r\nHOST: {0}:{1}\r\nCALLBACK: <http://{2}:{3}{4}>\r\nNT: upnp:event\r\nTIMEOUT: Second-{5}\r\nCONNECTION: close\r\n\r\n";
|
||||
public static final String CONNECTION_CLOSE = "close";
|
||||
|
||||
public static final String SOAP_ENVELOPE = "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>{0}</s:Body></s:Envelope>";
|
||||
public static final String PAIRING_SOAP_ACTION = "\"urn:schemas-upnp-org:service:X-CTC_RemotePairing:1#X-pairingRequest\"";
|
||||
public static final String PAIRING_SOAP_BODY = "<u:X-pairingRequest xmlns:u=\"urn:schemas-upnp-org:service:X-CTC_RemotePairing:1\"><pairingDeviceID>{0}</pairingDeviceID><friendlyName>{1}</friendlyName><userID>{2}</userID></u:X-pairingRequest>";
|
||||
|
||||
public static final String PAIRCHECK_URI = "/upnp/service/X-CTC_RemotePairing/Control";
|
||||
public static final String PAIRCHECK_SOAP_ACTION = "\"urn:schemas-upnp-org:service:X-CTC_RemotePairing:1#X-pairingCheck\"";
|
||||
public static final String PAIRCHECK_SOAP_BODY = "<u:X-pairingCheck xmlns:u=\"urn:schemas-upnp-org:service:X-CTC_RemotePairing:1\"><pairingDeviceID>{0}</pairingDeviceID><verificationCode>{1}</verificationCode></u:X-pairingCheck>";
|
||||
|
||||
public static final String SENDKEY_URI = "/upnp/service/X-CTC_RemoteControl/Control";
|
||||
public static final String SENDKEY_SOAP_ACTION = "\"urn:schemas-upnp-org:service:X-CTC_RemoteControl:1#X_CTC_RemoteKey\"";
|
||||
public static final String SENDKEY_SOAP_BODY = "<u:X_CTC_RemoteKey xmlns:u=\"urn:schemas-upnp-org:service:X-CTC_RemoteControl:1\"><InstanceID>0</InstanceID><KeyCode>keyCode={0}^{1}:{2}^userID:{3}</KeyCode></u:X_CTC_RemoteKey>";
|
||||
|
||||
public static final String HTTP_NOTIFY = "NOTIFY";
|
||||
public static final String NOTIFY_SID = "SID: ";
|
||||
|
||||
public static final String HASH_ALGORITHM_MD5 = "MD5";
|
||||
public static final String HASH_ALGORITHM_SHA256 = "SHA-256";
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.BINDING_ID;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.network.MagentaTVOAuth;
|
||||
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.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Console commands for interacting with the MagentaTV binding
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ConsoleCommandExtension.class)
|
||||
public class MagentaTVConsoleHandler extends AbstractConsoleCommandExtension {
|
||||
|
||||
private static final String CMD_LOGIN = "login";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVConsoleHandler.class);
|
||||
private final MagentaTVOAuth oauth = new MagentaTVOAuth();
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.OPTIONAL)
|
||||
private @Nullable ClientBuilder injectedClientBuilder;
|
||||
|
||||
@Activate
|
||||
public MagentaTVConsoleHandler() {
|
||||
super(BINDING_ID, "Interact with the " + BINDING_ID + " integration.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String[] args, Console console) {
|
||||
if (args.length > 0) {
|
||||
String subCommand = args[0];
|
||||
switch (subCommand) {
|
||||
case CMD_LOGIN:
|
||||
if (args.length == 1) {
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
|
||||
console.print("Login Name (email): ");
|
||||
String username = br.readLine();
|
||||
console.print("Password: ");
|
||||
String pwd = br.readLine();
|
||||
console.println("Attempting login...");
|
||||
login(console, username, pwd);
|
||||
} catch (IOException e) {
|
||||
console.println(e.toString());
|
||||
}
|
||||
} else if (args.length == 3) {
|
||||
login(console, args[1], args[2]);
|
||||
} else {
|
||||
printUsage(console);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.println("Unknown command '" + subCommand + "'");
|
||||
printUsage(console);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getUsages() {
|
||||
return Arrays.asList(buildCommandUsage(CMD_LOGIN + " [<email>] [<password>]",
|
||||
"Logs into the account with the provided credentials and retrieves the User ID."));
|
||||
}
|
||||
|
||||
private void login(Console console, String username, String password) {
|
||||
try {
|
||||
logger.info("Performing OAuth for user {}", username);
|
||||
String userId = oauth.getUserId(username, password);
|
||||
console.println("Login successful, returned User ID is " + userId);
|
||||
console.println(
|
||||
"Edit thing configuration and copy this value to the field User ID or use it as parameter userId for the textual configuration.");
|
||||
logger.info("Login with account {} was successful, returned User ID is {}", username, userId);
|
||||
} catch (MagentaTVException e) {
|
||||
console.println("Login with account " + username + " failed: " + e.getMessage());
|
||||
logger.warn("Unable to login with account {}, check credentials ({})", username, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVUtil.substringAfterLast;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.handler.MagentaTVHandler;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVDeviceManager} class manages the device table (shared between HandlerFactory and Thing handlers).
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = MagentaTVDeviceManager.class)
|
||||
public class MagentaTVDeviceManager {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVDeviceManager.class);
|
||||
|
||||
protected class MagentaTVDevice {
|
||||
protected String udn = "";
|
||||
protected String mac = "";
|
||||
protected String deviceId = "";
|
||||
protected String ipAddress = "";
|
||||
protected Map<String, String> properties = new HashMap<>();
|
||||
protected @Nullable MagentaTVHandler thingHandler;
|
||||
}
|
||||
|
||||
private final Map<String, MagentaTVDevice> deviceList = new HashMap<>();
|
||||
|
||||
public void registerDevice(String udn, String deviceId, String ipAddress, MagentaTVHandler handler) {
|
||||
logger.trace("Register new device, UDN={}, deviceId={}, ipAddress={}", udn, deviceId, ipAddress);
|
||||
addNewDevice(udn, deviceId, ipAddress, "", new TreeMap<String, String>(), handler);
|
||||
}
|
||||
|
||||
private void addNewDevice(String udn, String deviceId, String ipAddress, String macAddress,
|
||||
Map<String, String> discoveryProperties, @Nullable MagentaTVHandler handler) {
|
||||
String mac = "";
|
||||
if (macAddress.isEmpty()) { // build MAC from UDN
|
||||
mac = substringAfterLast(udn, "-");
|
||||
} else {
|
||||
mac = macAddress;
|
||||
}
|
||||
|
||||
boolean newDev = false;
|
||||
synchronized (deviceList) {
|
||||
MagentaTVDevice dev;
|
||||
if (deviceList.containsKey(udn.toUpperCase())) {
|
||||
dev = deviceList.get(udn.toUpperCase());
|
||||
} else {
|
||||
dev = new MagentaTVDevice();
|
||||
newDev = true;
|
||||
}
|
||||
dev.udn = udn.toUpperCase();
|
||||
dev.mac = mac.toUpperCase();
|
||||
if (!deviceId.isEmpty()) {
|
||||
dev.deviceId = deviceId.toUpperCase();
|
||||
}
|
||||
dev.ipAddress = ipAddress;
|
||||
dev.properties = discoveryProperties;
|
||||
dev.thingHandler = handler;
|
||||
if (newDev) {
|
||||
deviceList.put(dev.udn, dev);
|
||||
}
|
||||
}
|
||||
logger.debug("New device {}: (UDN={} ,deviceId={}, ipAddress={}, macAddress={}), now {} devices.",
|
||||
newDev ? "added" : "updated", udn, deviceId, ipAddress, mac, deviceList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a device from the table
|
||||
*
|
||||
* @param deviceId
|
||||
*/
|
||||
public void removeDevice(String deviceId) {
|
||||
MagentaTVDevice dev = lookupDevice(deviceId);
|
||||
if (dev != null) {
|
||||
synchronized (deviceList) {
|
||||
logger.trace("Device with UDN {} removed from table, new site={}", dev.udn, deviceList.size());
|
||||
deviceList.remove(dev.udn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a device in the table by an id (this could be the UDN, the MAC
|
||||
* address, the IP address or a unique device ID)
|
||||
*
|
||||
* @param uniqueId
|
||||
* @return
|
||||
*/
|
||||
public @Nullable MagentaTVDevice lookupDevice(String uniqueId) {
|
||||
MagentaTVDevice dev = null;
|
||||
logger.trace("Lookup device, uniqueId={}", uniqueId);
|
||||
int i = 0;
|
||||
for (String key : deviceList.keySet()) {
|
||||
synchronized (deviceList) {
|
||||
if (deviceList.containsKey(key)) {
|
||||
dev = deviceList.get(key);
|
||||
logger.trace("Devies[{}]: deviceId={}, UDN={}, ipAddress={}, macAddress={}", i++, dev.deviceId,
|
||||
dev.udn, dev.ipAddress, dev.mac);
|
||||
if (dev.udn.equalsIgnoreCase(uniqueId) || dev.ipAddress.equalsIgnoreCase(uniqueId)
|
||||
|| dev.deviceId.equalsIgnoreCase(uniqueId) || dev.mac.equalsIgnoreCase(uniqueId)) {
|
||||
return dev;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("Device with id {} was not found in table ({} entries", uniqueId, deviceList.size());
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* returned the discovered properties
|
||||
*
|
||||
* @param udn Unique ID from UPnP discovery
|
||||
* @return property map with discovered properties
|
||||
*/
|
||||
public @Nullable Map<String, String> getDiscoveredProperties(String udn) {
|
||||
if (deviceList.containsKey(udn.toUpperCase())) {
|
||||
MagentaTVDevice dev = deviceList.get(udn.toUpperCase());
|
||||
return dev.properties;
|
||||
}
|
||||
if (deviceList.size() > 0) {
|
||||
logger.debug("getDiscoveredProperties(): Unknown UDN: {}", udn);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int numberOfDevices() {
|
||||
return deviceList.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVException} class a binding specific exception class.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 6214176461907613559L;
|
||||
|
||||
public MagentaTVException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MagentaTVException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public MagentaTVException(Exception e, String message, Object... a) {
|
||||
super(MessageFormat.format(message, a) + " (" + e.getClass() + ": " + e.getMessage() + ")", e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.InstanceCreator;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVGsonDTO} class implements The MR returns event information every time the program changes. This
|
||||
* information is mapped to various Thing channels and also used to catch the power down event for MR400 (there is no
|
||||
* way to query power status). This class provides the mapping between event JSON and Java class using Gson.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
public class MagentaTVGsonDTO {
|
||||
/*
|
||||
* Program information event is send by the MR when a channel is changed.
|
||||
*
|
||||
* Sample data:
|
||||
* {"type":"EVENT_EIT_CHANGE","instance_id":26,"channel_code":"54","channel_num":"11","mediaId":"1221",
|
||||
* "program_info": [ {"start_time":"2018/10/14 10:21:59","event_id":"9581","duration":"00:26:47",
|
||||
* "free_CA_mode":false,"running_status":4, "short_event": [{"event_name":"Mysticons","language_code":"DEU",
|
||||
* "text_char":"Die Mysticons..." } ]},
|
||||
* {"start_time":"2018/10/14 10:48:46","event_id":"12204","duration":"00:23:54","free_CA_mode":false,
|
||||
* "running_status":1, "short_event": [ {"event_name":"Winx Club","language_code":"DEU", "text_char":"Daphnes Eltern
|
||||
* veranstalten...!" }]} ] }
|
||||
*/
|
||||
// The following classes are used to map the JSON data into objects using GSon.
|
||||
public static class MRProgramInfoEvent {
|
||||
@SerializedName("type")
|
||||
public String type = "";
|
||||
@SerializedName("instance_id")
|
||||
public Integer instanceId = 0;
|
||||
@SerializedName("channel_code")
|
||||
public String channelCode = "";
|
||||
@SerializedName("channel_num")
|
||||
public String channelNum = "";
|
||||
@SerializedName("mediaId")
|
||||
public String mediaId = "";
|
||||
@SerializedName("program_info")
|
||||
public ArrayList<MRProgramStatus> programInfo = new ArrayList<>();
|
||||
}
|
||||
|
||||
public static class MRProgramInfoEventInstanceCreator implements InstanceCreator<MRProgramInfoEvent> {
|
||||
@Override
|
||||
public MRProgramInfoEvent createInstance(@Nullable Type type) {
|
||||
return new MRProgramInfoEvent();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MRProgramStatus {
|
||||
@SerializedName("start_time")
|
||||
public String startTime = "";
|
||||
@SerializedName("event_id")
|
||||
public String eventId = "";
|
||||
@SerializedName("duration")
|
||||
public String duration = "";
|
||||
@SerializedName("free_CA_mode")
|
||||
public Boolean freeCAMmode = false;
|
||||
@SerializedName("running_status")
|
||||
public Integer runningStatus = EV_EITCHG_RUNNING_NONE;
|
||||
@SerializedName("short_event")
|
||||
public ArrayList<MRShortProgramInfo> shortEvent = new ArrayList<>();
|
||||
}
|
||||
|
||||
public static class MRProgramStatusInstanceCreator implements InstanceCreator<MRProgramStatus> {
|
||||
@Override
|
||||
public MRProgramStatus createInstance(@Nullable Type type) {
|
||||
return new MRProgramStatus();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MRShortProgramInfo {
|
||||
@SerializedName("event_name")
|
||||
public String eventName = "";
|
||||
@SerializedName("language_code")
|
||||
public String languageCode = "";
|
||||
@SerializedName("text_char")
|
||||
public String textChar = "";
|
||||
}
|
||||
|
||||
public static class MRShortProgramInfoInstanceCreator implements InstanceCreator<MRShortProgramInfo> {
|
||||
@Override
|
||||
public MRShortProgramInfo createInstance(@Nullable Type type) {
|
||||
return new MRShortProgramInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* playStatus event format (JSON) playContent event, for details see
|
||||
* http://support.huawei.com/hedex/pages/DOC1100366313CEH0713H/01/DOC1100366313CEH0713H/01/resources/dsv_hdx_idp/DSV/en/en-us_topic_0094619231.html
|
||||
*
|
||||
* sample 1: {"new_play_mode":4,"duration":0,"playBackState":1,"mediaType":1,"mediaCode":"3733","playPostion":0}
|
||||
* sample 2: {"new_play_mode":4, "playBackState":1,"mediaType":1,"mediaCode":"3479"}
|
||||
*/
|
||||
public static class MRPayEvent {
|
||||
@SerializedName("new_play_mode")
|
||||
public Integer newPlayMode = EV_PLAYCHG_STOP;
|
||||
public Integer duration = -1;
|
||||
public Integer playBackState = EV_PLAYCHG_STOP;
|
||||
public Integer mediaType = 0;
|
||||
public String mediaCode = "";
|
||||
public Integer playPostion = -1;
|
||||
}
|
||||
|
||||
public static class MRPayEventInstanceCreator implements InstanceCreator<MRPayEvent> {
|
||||
@Override
|
||||
public MRPayEvent createInstance(@Nullable Type type) {
|
||||
return new MRPayEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deutsche Telekom uses a OAuth-based authentication to access the EPG portal.
|
||||
* The binding automates the login incl. OAuth authentication. This class helps mapping the response to a Java
|
||||
* object (using Gson)
|
||||
*
|
||||
* Sample response:
|
||||
* { "enctytoken":"7FA9A6C05EDD873799392BBDDC5B7F34","encryptiontype":"0002",
|
||||
* "platformcode":"0200", "epgurl":"http://appepmfk20005.prod.sngtv.t-online.de:33200",
|
||||
* "version":"MEM V200R008C15B070", "epghttpsurl":"https://appepmfk20005.prod.sngtv.t-online.de:33207",
|
||||
* "rootCerAddr": "http://appepmfk20005.prod.sngtv.t-online.de:33200/EPG/CA/iptv_ca.der",
|
||||
* "upgAddr4IPTV":"https://slbedifk11100.prod.sngtv.t-online.de:33428/EDS/jsp/upgrade.jsp",
|
||||
* "upgAddr4OTT":"https://slbedmfk11100.prod.sngtv.t-online.de:33428/EDS/jsp/upgrade.jsp,https://slbedmfk11100.prod.sngtv.t-online.de:33428/EDS/jsp/upgrade.jsp",
|
||||
* "sam3Para": [
|
||||
* {"key":"SAM3ServiceURL","value":"https://accounts.login.idm.telekom.com"},
|
||||
* {"key":"OAuthClientSecret","value":"21EAB062-C4EE-489C-BC80-6A65397F3F96"},
|
||||
* {"key":"OAuthScope","value":"ngtvepg"},
|
||||
* {"key":"OAuthClientId","value":"10LIVESAM30000004901NGTV0000000000000000"} ]
|
||||
* }
|
||||
*/
|
||||
public static class OauthCredentials {
|
||||
public String epghttpsurl = "";
|
||||
public ArrayList<OauthKeyValue> sam3Para = new ArrayList<OauthKeyValue>();
|
||||
}
|
||||
|
||||
public static class OauthCredentialsInstanceCreator implements InstanceCreator<OauthCredentials> {
|
||||
@Override
|
||||
public OauthCredentials createInstance(@Nullable Type type) {
|
||||
return new OauthCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
public static class OauthKeyValue {
|
||||
public String key = "";
|
||||
public String value = "";
|
||||
}
|
||||
|
||||
public static class OAuthTokenResponse {
|
||||
@SerializedName("error_description")
|
||||
public String errorDescription = "";
|
||||
public String error = "";
|
||||
@SerializedName("access_token")
|
||||
public String accessToken = "";
|
||||
}
|
||||
|
||||
public static class OAuthTokenResponseInstanceCreator implements InstanceCreator<OAuthTokenResponse> {
|
||||
@Override
|
||||
public OAuthTokenResponse createInstance(@Nullable Type type) {
|
||||
return new OAuthTokenResponse();
|
||||
}
|
||||
}
|
||||
|
||||
public static class OAuthAuthenticateResponse {
|
||||
public String retcode = "";
|
||||
public String desc = "";
|
||||
public String epgurl = "";
|
||||
public String userID = "";
|
||||
}
|
||||
|
||||
public static class OAuthAuthenticateResponseInstanceCreator implements InstanceCreator<OAuthAuthenticateResponse> {
|
||||
@Override
|
||||
public OAuthAuthenticateResponse createInstance(@Nullable Type type) {
|
||||
return new OAuthAuthenticateResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVDeviceManager.MagentaTVDevice;
|
||||
import org.openhab.binding.magentatv.internal.handler.MagentaTVHandler;
|
||||
import org.openhab.binding.magentatv.internal.network.MagentaTVNetwork;
|
||||
import org.openhab.binding.magentatv.internal.network.MagentaTVPoweroffListener;
|
||||
import org.openhab.core.net.HttpServiceUtil;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
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.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { ThingHandlerFactory.class, MagentaTVHandlerFactory.class }, configurationPid = "binding."
|
||||
+ BINDING_ID)
|
||||
public class MagentaTVHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVHandlerFactory.class);
|
||||
|
||||
private final MagentaTVNetwork network = new MagentaTVNetwork();
|
||||
private final MagentaTVDeviceManager manager;
|
||||
private @Nullable MagentaTVPoweroffListener upnpListener;
|
||||
private boolean servletInitialized = false;
|
||||
|
||||
/**
|
||||
* Activate the bundle: save properties
|
||||
*
|
||||
* @param componentContext
|
||||
* @param configProperties set of properties from cfg (use same names as in
|
||||
* thing config)
|
||||
*/
|
||||
|
||||
@Activate
|
||||
public MagentaTVHandlerFactory(@Reference NetworkAddressService networkAddressService,
|
||||
@Reference MagentaTVDeviceManager manager, ComponentContext componentContext,
|
||||
Map<String, String> configProperties) throws IOException {
|
||||
super.activate(componentContext);
|
||||
this.manager = manager;
|
||||
|
||||
try {
|
||||
logger.debug("Initialize network access");
|
||||
System.setProperty("java.net.preferIPv4Stack", "true");
|
||||
String lip = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
Integer port = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
|
||||
if (port == -1) {
|
||||
port = 8080;
|
||||
}
|
||||
network.initLocalNet(lip != null ? lip : "", port.toString());
|
||||
upnpListener = new MagentaTVPoweroffListener(this, network.getLocalInterface());
|
||||
} catch (MagentaTVException e) {
|
||||
logger.warn("Initialization failed: {}", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (upnpListener != null) {
|
||||
upnpListener.start();
|
||||
}
|
||||
|
||||
logger.debug("Create thing type {}", thing.getThingTypeUID().getAsString());
|
||||
if (THING_TYPE_RECEIVER.equals(thingTypeUID)) {
|
||||
return new MagentaTVHandler(manager, thing, network);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a device to the device table
|
||||
*
|
||||
* @param udn UDN for the device
|
||||
* @param deviceId A unique device id
|
||||
* @param ipAddress IP address of the receiver
|
||||
* @param handler The corresponding thing handler
|
||||
*/
|
||||
|
||||
public void setNotifyServletStatus(boolean newStatus) {
|
||||
logger.debug("NotifyServlet started");
|
||||
servletInitialized = newStatus;
|
||||
}
|
||||
|
||||
public boolean getNotifyServletStatus() {
|
||||
return servletInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* We received the pairing result (by the Notify servlet)
|
||||
*
|
||||
* @param notifyDeviceId The unique device id pairing was initiated for
|
||||
* @param pairingCode Pairing code computed by the receiver
|
||||
* @return true: thing handler was called, false: failed, e.g. unknown device
|
||||
*/
|
||||
public boolean notifyPairingResult(String notifyDeviceId, String ipAddress, String pairingCode) {
|
||||
try {
|
||||
logger.trace("PairingResult: Check {} devices for id {}, ipAddress {}", manager.numberOfDevices(),
|
||||
notifyDeviceId, ipAddress);
|
||||
MagentaTVDevice dev = manager.lookupDevice(ipAddress);
|
||||
if ((dev != null) && (dev.thingHandler != null)) {
|
||||
if (dev.deviceId.isEmpty()) {
|
||||
logger.trace("deviceId {} assigned for ipAddress {}", notifyDeviceId, ipAddress);
|
||||
dev.deviceId = notifyDeviceId;
|
||||
}
|
||||
if (dev.thingHandler != null) {
|
||||
dev.thingHandler.onPairingResult(pairingCode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug("Received pairingCode {} for unregistered device {}!", pairingCode, ipAddress);
|
||||
} catch (MagentaTVException e) {
|
||||
logger.debug("Unable to process pairing result for deviceID {}: {}", notifyDeviceId, e.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A programInfo or playStatus event was received from the receiver
|
||||
*
|
||||
* @param mrMac MR MAC address (used to map the device)
|
||||
* @param jsonEvent Event data in JSON format
|
||||
* @return true: thing handler was called, false: failed, e.g. unknown device
|
||||
*/
|
||||
public boolean notifyMREvent(String mrMac, String jsonEvent) {
|
||||
try {
|
||||
logger.trace("Received MR event from MAC {}, JSON={}", mrMac, jsonEvent);
|
||||
MagentaTVDevice dev = manager.lookupDevice(mrMac);
|
||||
if ((dev != null) && (dev.thingHandler != null)) {
|
||||
dev.thingHandler.onMREvent(jsonEvent);
|
||||
return true;
|
||||
}
|
||||
logger.debug("Received event for unregistered MR: MAC address {}, JSON={}", mrMac, jsonEvent);
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Unable to process MR event! {} ({}), json={}", e.getMessage(), e.getClass(), jsonEvent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The PowerOff Listener got a byebye message. This comes in when the receiver
|
||||
* was is going to suspend mode.
|
||||
*
|
||||
* @param ipAddress receiver IP
|
||||
*/
|
||||
public void onPowerOff(String ipAddress) {
|
||||
try {
|
||||
logger.debug("ByeBye message received for IP {}", ipAddress);
|
||||
MagentaTVDevice dev = manager.lookupDevice(ipAddress);
|
||||
if ((dev != null) && (dev.thingHandler != null)) {
|
||||
dev.thingHandler.onPowerOff();
|
||||
}
|
||||
} catch (MagentaTVException e) {
|
||||
logger.debug("Unable to process SSDP message for IP {} - {}", ipAddress, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* {@link MagentaTVUtil} implements some helper functions.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVUtil {
|
||||
public static String getString(@Nullable String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
public static String substringBefore(@Nullable String string, String pattern) {
|
||||
if (string != null) {
|
||||
int pos = string.indexOf(pattern);
|
||||
if (pos > 0) {
|
||||
return string.substring(0, pos);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String substringBeforeLast(@Nullable String string, String pattern) {
|
||||
if (string != null) {
|
||||
int pos = string.lastIndexOf(pattern);
|
||||
if (pos > 0) {
|
||||
return string.substring(0, pos);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String substringAfter(@Nullable String string, String pattern) {
|
||||
if (string != null) {
|
||||
int pos = string.indexOf(pattern);
|
||||
if (pos != -1) {
|
||||
return string.substring(pos + pattern.length());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String substringAfterLast(@Nullable String string, String pattern) {
|
||||
if (string != null) {
|
||||
int pos = string.lastIndexOf(pattern);
|
||||
if (pos != -1) {
|
||||
return string.substring(pos + pattern.length());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String substringBetween(@Nullable String string, String begin, String end) {
|
||||
if (string != null) {
|
||||
int s = string.indexOf(begin);
|
||||
if (s != -1) {
|
||||
// The end tag might be included before the start tag, e.g.
|
||||
// when using "http://" and ":" to get the IP from http://192.168.1.1:8081/xxx
|
||||
// therefore make it 2 steps
|
||||
String result = string.substring(s + begin.length());
|
||||
return substringBefore(result, end);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.config;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVDynamicConfig} extends MagentaTVThingConfiguration contains additional dynamic data
|
||||
* runtime).
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVDynamicConfig extends MagentaTVThingConfiguration {
|
||||
protected String modelId = MODEL_MR400; // MR model
|
||||
protected String hardwareVersion = "";
|
||||
protected String firmwareVersion = "";
|
||||
protected String friendlyName = ""; // Receiver's Friendly Name from UPnP descriptin
|
||||
protected String descriptionUrl = MR401B_DEF_DESCRIPTION_URL; // Device description, usually from UPnP discovery
|
||||
protected String localIP = ""; // Outbound IP for pairing/communication
|
||||
protected String localMAC = ""; // used to compute the terminalID
|
||||
protected String wakeOnLAN = ""; // Device supports Wake-on-LAN
|
||||
protected String terminalID = ""; // terminalID for pairing process
|
||||
protected String pairingCode = ""; // Input to the paring process
|
||||
protected String verificationCode = ""; // Result of the paring process
|
||||
|
||||
public MagentaTVDynamicConfig() {
|
||||
}
|
||||
|
||||
public MagentaTVDynamicConfig(MagentaTVThingConfiguration config) {
|
||||
super.update(config);
|
||||
}
|
||||
|
||||
public void updateNetwork(MagentaTVDynamicConfig network) {
|
||||
this.setLocalIP(network.getLocalIP());
|
||||
this.setLocalMAC(network.getLocalMAC());
|
||||
this.setTerminalID(network.getTerminalID());
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return modelId.toUpperCase();
|
||||
}
|
||||
|
||||
public String getPort() {
|
||||
return !port.isEmpty() ? port : isMR400() ? MR400_DEF_REMOTE_PORT : MR401B_DEF_REMOTE_PORT;
|
||||
}
|
||||
|
||||
public void setPort(String port) {
|
||||
if (modelId.contains(MODEL_MR400) && port.equals("49153")) {
|
||||
// overwrite port returned by discovery (invalid for this model)
|
||||
this.port = MR400_DEF_REMOTE_PORT;
|
||||
} else {
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMR400() {
|
||||
return modelId.equals(MODEL_MR400);
|
||||
}
|
||||
|
||||
public void setModel(String modelId) {
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
public String getWakeOnLAN() {
|
||||
return wakeOnLAN;
|
||||
}
|
||||
|
||||
public void setWakeOnLAN(String wakeOnLAN) {
|
||||
this.wakeOnLAN = wakeOnLAN.toUpperCase();
|
||||
}
|
||||
|
||||
public String getDescriptionUrl() {
|
||||
if (descriptionUrl.equals(MR400_DEF_DESCRIPTION_URL)
|
||||
&& !(port.equals(MR400_DEF_REMOTE_PORT) || port.equals("49153"))) {
|
||||
// MR401B returns the wrong URL
|
||||
return MR401B_DEF_DESCRIPTION_URL;
|
||||
}
|
||||
return descriptionUrl;
|
||||
}
|
||||
|
||||
public void setDescriptionUrl(String descriptionUrl) {
|
||||
this.descriptionUrl = getString(descriptionUrl);
|
||||
}
|
||||
|
||||
public String getFriendlyName() {
|
||||
return friendlyName;
|
||||
}
|
||||
|
||||
public void setFriendlyName(String friendlyName) {
|
||||
this.friendlyName = friendlyName;
|
||||
}
|
||||
|
||||
public String getHardwareVersion() {
|
||||
return hardwareVersion;
|
||||
}
|
||||
|
||||
public void setHardwareVersion(String hardwareVersion) {
|
||||
this.hardwareVersion = hardwareVersion;
|
||||
}
|
||||
|
||||
public String getFirmwareVersion() {
|
||||
return firmwareVersion;
|
||||
}
|
||||
|
||||
public void setFirmwareVersion(String firmwareVersion) {
|
||||
this.firmwareVersion = firmwareVersion;
|
||||
}
|
||||
|
||||
public String getTerminalID() {
|
||||
return terminalID;
|
||||
}
|
||||
|
||||
public void setTerminalID(String terminalID) {
|
||||
this.terminalID = terminalID.toUpperCase();
|
||||
}
|
||||
|
||||
public String getPairingCode() {
|
||||
return pairingCode;
|
||||
}
|
||||
|
||||
public void setPairingCode(String pairingCode) {
|
||||
this.pairingCode = pairingCode;
|
||||
}
|
||||
|
||||
public String getVerificationCode() {
|
||||
return verificationCode;
|
||||
}
|
||||
|
||||
public void setVerificationCode(String verificationCode) {
|
||||
this.verificationCode = verificationCode;
|
||||
}
|
||||
|
||||
public String getLocalIP() {
|
||||
return localIP;
|
||||
}
|
||||
|
||||
public void setLocalIP(String localIP) {
|
||||
this.localIP = localIP;
|
||||
}
|
||||
|
||||
public String getLocalMAC() {
|
||||
return localMAC;
|
||||
}
|
||||
|
||||
public void setLocalMAC(String localMAC) {
|
||||
this.localMAC = localMAC.toUpperCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVThingConfiguration} contains the thing config (updated at
|
||||
* runtime).
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVThingConfiguration {
|
||||
public String ipAddress = ""; // IP Address of the MR
|
||||
public String port = ""; // Port of the remote service
|
||||
public String udn = ""; // UPnP UDN
|
||||
public String macAddress = ""; // Usually gets filled by the thing discovery (or set by .things file)
|
||||
public String accountName = ""; // Credentials: Account Name from Telekom Kundencenter (used for OAuth)
|
||||
public String accountPassword = ""; // Credentials: Account Password from Telekom Kundencenter (used for OAuth)
|
||||
public String userId = ""; // userId required for pairing (can be configured manually or gets auto-filled by the
|
||||
// binding on successful OAuth. Value is persisted so OAuth nedds only to be redone when
|
||||
// credentials change.
|
||||
|
||||
public void update(MagentaTVThingConfiguration newConfig) {
|
||||
ipAddress = newConfig.ipAddress;
|
||||
port = newConfig.port;
|
||||
udn = newConfig.udn;
|
||||
macAddress = newConfig.macAddress;
|
||||
accountName = newConfig.accountName;
|
||||
accountPassword = newConfig.accountPassword;
|
||||
userId = newConfig.userId;
|
||||
}
|
||||
|
||||
public String getUDN() {
|
||||
return udn.toUpperCase();
|
||||
}
|
||||
|
||||
public void setUDN(String udn) {
|
||||
this.udn = udn;
|
||||
}
|
||||
|
||||
public String getIpAddress() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public String getMacAddress() {
|
||||
return macAddress;
|
||||
}
|
||||
|
||||
public void setMacAddress(String macAddress) {
|
||||
this.macAddress = macAddress.toUpperCase();
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return accountName;
|
||||
}
|
||||
|
||||
public void setAccountName(String accountName) {
|
||||
this.accountName = accountName;
|
||||
}
|
||||
|
||||
public String getAccountPassword() {
|
||||
return accountPassword;
|
||||
}
|
||||
|
||||
public void setAccountPassword(String accountPassword) {
|
||||
this.accountPassword = accountPassword;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
protected String getString(@Nullable Object value) {
|
||||
return value != null ? (String) value : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*;
|
||||
import static org.openhab.core.thing.Thing.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
|
||||
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 MagentaTVDiscoveryParticipant} is responsible for discovering new
|
||||
* and removed MagentaTV receivers. It uses the central UpnpDiscoveryService.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = UpnpDiscoveryParticipant.class)
|
||||
public class MagentaTVDiscoveryParticipant implements UpnpDiscoveryParticipant {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVDiscoveryParticipant.class);
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Collections.singleton(THING_TYPE_RECEIVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* New discovered result.
|
||||
*/
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
|
||||
DiscoveryResult result = null;
|
||||
try {
|
||||
String modelName = getString(device.getDetails().getModelDetails().getModelName()).toUpperCase();
|
||||
String manufacturer = getString(device.getDetails().getManufacturerDetails().getManufacturer())
|
||||
.toUpperCase();
|
||||
logger.trace("Device discovered: {} - {}", manufacturer, modelName);
|
||||
|
||||
ThingUID uid = getThingUID(device);
|
||||
if (uid != null) {
|
||||
logger.debug("Discovered an MagentaTV Media Receiver {}, UDN: {}, Model {}.{}",
|
||||
device.getDetails().getFriendlyName(), device.getIdentity().getUdn().getIdentifierString(),
|
||||
modelName, device.getDetails().getModelDetails().getModelNumber());
|
||||
|
||||
Map<String, Object> properties = new TreeMap<>();
|
||||
String descriptorURL = device.getIdentity().getDescriptorURL().toString();
|
||||
String port = substringBefore(substringAfterLast(descriptorURL, ":"), "/");
|
||||
String hex = device.getIdentity().getUdn().getIdentifierString()
|
||||
.substring(device.getIdentity().getUdn().getIdentifierString().length() - 12);
|
||||
String mac = hex.substring(0, 2) + ":" + hex.substring(2, 4) + ":" + hex.substring(4, 6) + ":"
|
||||
+ hex.substring(6, 8) + ":" + hex.substring(8, 10) + ":" + hex.substring(10, 12);
|
||||
properties.put(PROPERTY_VENDOR, VENDOR + "(" + manufacturer + ")");
|
||||
properties.put(PROPERTY_MODEL_ID, modelName);
|
||||
properties.put(PROPERTY_HARDWARE_VERSION, device.getDetails().getModelDetails().getModelNumber());
|
||||
properties.put(PROPERTY_MAC_ADDRESS, mac);
|
||||
properties.put(PROPERTY_UDN, device.getIdentity().getUdn().getIdentifierString().toUpperCase());
|
||||
properties.put(PROPERTY_IP, substringBetween(descriptorURL, "http://", ":"));
|
||||
properties.put(PROPERTY_PORT, port);
|
||||
properties.put(PROPERTY_DESC_URL, substringAfterLast(descriptorURL, ":" + port));
|
||||
|
||||
logger.debug("Create Thing for device {} with UDN {}, Model{}", device.getDetails().getFriendlyName(),
|
||||
device.getIdentity().getUdn().getIdentifierString(), modelName);
|
||||
result = DiscoveryResultBuilder.create(uid).withLabel(device.getDetails().getFriendlyName())
|
||||
.withProperties(properties).withRepresentationProperty(PROPERTY_MAC_ADDRESS).build();
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Unable to create thing for device {}/{} - {}", device.getDetails().getFriendlyName(),
|
||||
device.getIdentity().getUdn().getIdentifierString(), e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the UID for a device
|
||||
*/
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(@Nullable RemoteDevice device) {
|
||||
if (device != null) {
|
||||
String manufacturer = getString(device.getDetails().getManufacturerDetails().getManufacturer())
|
||||
.toUpperCase();
|
||||
String model = device.getDetails().getModelDetails().getModelName().toUpperCase();
|
||||
if (manufacturer.contains(OEM_VENDOR) && ((model.contains(MODEL_MR400) || model.contains(MODEL_MR401B)
|
||||
|| model.contains(MODEL_MR601) || model.contains(MODEL_MR201)))) {
|
||||
return new ThingUID(THING_TYPE_RECEIVER, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getString(@Nullable String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.handler;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVException;
|
||||
import org.openhab.binding.magentatv.internal.config.MagentaTVDynamicConfig;
|
||||
import org.openhab.binding.magentatv.internal.network.MagentaTVHttp;
|
||||
import org.openhab.binding.magentatv.internal.network.MagentaTVNetwork;
|
||||
import org.openhab.binding.magentatv.internal.network.MagentaTVOAuth;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVControl} implements the control functions for the
|
||||
* receiver.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVControl {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVControl.class);
|
||||
private final static HashMap<String, String> KEY_MAP = new HashMap<>();
|
||||
|
||||
private final MagentaTVNetwork network;
|
||||
private final MagentaTVHttp http = new MagentaTVHttp();
|
||||
private final MagentaTVOAuth oauth = new MagentaTVOAuth();
|
||||
private final MagentaTVDynamicConfig config;
|
||||
private boolean initialized = false;
|
||||
private String thingId = "";
|
||||
|
||||
public MagentaTVControl() {
|
||||
config = new MagentaTVDynamicConfig();
|
||||
network = new MagentaTVNetwork();
|
||||
}
|
||||
|
||||
public MagentaTVControl(MagentaTVDynamicConfig config, MagentaTVNetwork network) {
|
||||
thingId = config.getFriendlyName();
|
||||
this.network = network;
|
||||
this.config = config;
|
||||
this.config.setTerminalID(computeMD5(network.getLocalMAC().toUpperCase() + config.getUDN()));
|
||||
this.config.setLocalIP(network.getLocalIP());
|
||||
this.config.setLocalMAC(network.getLocalMAC());
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thingConfig - the Control class adds various attributes of the
|
||||
* discovered device (like the MR model)
|
||||
*
|
||||
* @return thingConfig
|
||||
*/
|
||||
public MagentaTVDynamicConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate OAuth authentication
|
||||
*
|
||||
* @param accountName T-Online user id
|
||||
* @param accountPassword T-Online password
|
||||
* @return true: successful, false: failed
|
||||
*
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public String getUserId(String accountName, String accountPassword) throws MagentaTVException {
|
||||
return oauth.getUserId(accountName, accountPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries the device properties. This will result in an Exception if the device
|
||||
* is not connected.
|
||||
*
|
||||
* Response is returned in XMl format, e.g.:
|
||||
* <?xml version="1.0"?> <root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
* <specVersion><major>1</major><minor>0</minor></specVersion> <device>
|
||||
* <UDN>uuid:70dff25c-1bdf-5731-a283-XXXXXXXX</UDN>
|
||||
* <friendlyName>DMS_XXXXXXXXXXXX</friendlyName>
|
||||
* <deviceType>urn:schemas-upnp-org:device:tvdevice:1</deviceType>
|
||||
* <manufacturer>Zenterio</manufacturer> <modelName>MR401B</modelName>
|
||||
* <modelNumber>R01A5</modelNumber> <productVersionNumber>" 334
|
||||
* "</productVersionNumber> <productType>stb</productType>
|
||||
* <serialNumber></serialNumber> <X_wakeOnLan>0</X_wakeOnLan> <serviceList>
|
||||
* <service> <serviceType>urn:dial-multiscreen-org:service:dial:1</serviceType>
|
||||
* <serviceId>urn:dial-multiscreen-org:service:dial</serviceId> </service>
|
||||
* </serviceList> </device> </root>
|
||||
*
|
||||
* @return true: device is online, false: device is offline
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public boolean checkDev() throws MagentaTVException {
|
||||
logger.debug("{}: Check device {} ({}:{})", thingId, config.getTerminalID(), config.getIpAddress(),
|
||||
config.getPort());
|
||||
|
||||
String url = MessageFormat.format(CHECKDEV_URI, config.getIpAddress(), config.getPort(),
|
||||
config.getDescriptionUrl());
|
||||
String result = http.httpGet(buildHost(), url, "");
|
||||
if (result.contains("<modelName>")) {
|
||||
config.setModel(substringBetween(result, "<modelName>", "</modelName>"));
|
||||
}
|
||||
if (result.contains("<modelNumber>")) {
|
||||
config.setHardwareVersion(substringBetween(result, "<modelNumber>", "</modelNumber>"));
|
||||
}
|
||||
if (result.contains("<X_wakeOnLan>")) {
|
||||
String wol = substringBetween(result, "<X_wakeOnLan>", "</X_wakeOnLan>");
|
||||
config.setWakeOnLAN(wol);
|
||||
logger.debug("{}: Wake-on-LAN is {}", thingId, wol.equals("0") ? "disabled" : "enabled");
|
||||
}
|
||||
if (result.contains("<productVersionNumber>")) {
|
||||
String version;
|
||||
if (result.contains("<productVersionNumber>" ")) {
|
||||
version = substringBetween(result, "<productVersionNumber>" ", " "</productVersionNumber>");
|
||||
} else {
|
||||
version = substringBetween(result, "<productVersionNumber>", "</productVersionNumber>");
|
||||
}
|
||||
config.setFirmwareVersion(version);
|
||||
}
|
||||
if (result.contains("<friendlyName>")) {
|
||||
String friendlyName = result.substring(result.indexOf("<friendlyName>") + "<friendlyName>".length(),
|
||||
result.indexOf("</friendlyName>"));
|
||||
config.setFriendlyName(friendlyName);
|
||||
}
|
||||
if (result.contains("<UDN>uuid:")) {
|
||||
String udn = result.substring(result.indexOf("<UDN>uuid:") + "<UDN>uuid:".length(),
|
||||
result.indexOf("</UDN>"));
|
||||
if (config.getUDN().isEmpty()) {
|
||||
config.setUDN(udn);
|
||||
}
|
||||
}
|
||||
logger.trace("{}: Online status verified for device {}:{}, UDN={}", thingId, config.getIpAddress(),
|
||||
config.getPort(), config.getUDN());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sends a SUBSCRIBE request to the MR. This also defines the local callback url
|
||||
* used by the MR to return the pairing code and event information.
|
||||
*
|
||||
* Subscripbe to event channel a) receive the pairing code b) receive
|
||||
* programInfo and playStatus events after successful paring
|
||||
*
|
||||
* SUBSCRIBE /upnp/service/X-CTC_RemotePairing/Event HTTP/1.1\r\n HOST:
|
||||
* $remote_ip:$remote_port CALLBACK: <http://$local_ip:$local_port/>\r\n // NT:
|
||||
* upnp:event\r\n // TIMEOUT: Second-300\r\n // CONNECTION: close\r\n // \r\n
|
||||
*
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public void subscribeEventChannel() throws MagentaTVException {
|
||||
String sid = "";
|
||||
logger.debug("{}: Subscribe Event Channel (terminalID={}, {}:{}", thingId, config.getTerminalID(),
|
||||
config.getIpAddress(), config.getPort());
|
||||
String subscribe = MessageFormat.format(PAIRING_SUBSCRIBE, config.getIpAddress(), config.getPort(),
|
||||
network.getLocalIP(), network.getLocalPort(), PAIRING_NOTIFY_URI, PAIRING_TIMEOUT_SEC);
|
||||
String response = http.sendData(config.getIpAddress(), config.getPort(), subscribe);
|
||||
if (!response.contains("200 OK")) {
|
||||
response = substringBefore(response, "SERVER");
|
||||
throw new MagentaTVException("Unable to subscribe to pairing channel: " + response);
|
||||
}
|
||||
if (!response.contains(NOTIFY_SID)) {
|
||||
throw new MagentaTVException("Unable to subscribe to pairing channel, SID missing: " + response);
|
||||
}
|
||||
|
||||
StringTokenizer tokenizer = new StringTokenizer(response, "\r\n");
|
||||
while (tokenizer.hasMoreElements()) {
|
||||
String str = tokenizer.nextToken();
|
||||
if (!str.isEmpty()) {
|
||||
if (str.contains(NOTIFY_SID)) {
|
||||
sid = str.substring("SID: uuid:".length());
|
||||
logger.debug("{}: SUBSCRIBE returned SID {}", thingId, sid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Pairing Request to the Media Receiver. The method waits for the
|
||||
* response, but the pairing code will be received via the NOTIFY callback (see
|
||||
* NotifyServlet)
|
||||
*
|
||||
* XML format for Pairing Request: <s:Envelope
|
||||
* xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"
|
||||
* <s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"> <s:Body>\n
|
||||
* <u:X-pairingRequest
|
||||
* xmlns:u=\"urn:schemas-upnp-org:service:X-CTC_RemotePairing:1\">\n
|
||||
* <pairingDeviceID>$pairingDeviceID</pairingDeviceID>\n
|
||||
* <friendlyName>$friendlyName</friendlyName>\n <userID>$userID</userID>\n
|
||||
* </u:X-pairingRequest>\n </s:Body> </s:Envelope>
|
||||
*
|
||||
* @returns true: pairing successful
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public boolean sendPairingRequest() throws MagentaTVException {
|
||||
logger.debug("{}: Send Pairing Request (deviceID={}, type={}, userID={})", thingId, config.getTerminalID(),
|
||||
DEF_FRIENDLY_NAME, config.getUserId());
|
||||
resetPairing();
|
||||
|
||||
String soapBody = MessageFormat.format(PAIRING_SOAP_BODY, config.getTerminalID(), DEF_FRIENDLY_NAME,
|
||||
config.getUserId());
|
||||
String soapXml = MessageFormat.format(SOAP_ENVELOPE, soapBody);
|
||||
String response = http.httpPOST(buildHost(), buildReceiverUrl(PAIRING_CONTROL_URI), soapXml,
|
||||
PAIRING_SOAP_ACTION, CONNECTION_CLOSE);
|
||||
|
||||
// pairingCode will be received by the Servlet, is calls onPairingResult()
|
||||
// Exception if request failed (response code != HTTP_OK)
|
||||
if (!response.contains("X-pairingRequestResponse") || !response.contains("<result>")) {
|
||||
throw new MagentaTVException("Unexpected result for pairing response: " + response);
|
||||
}
|
||||
|
||||
String result = substringBetween(response, "<result>", "</result>");
|
||||
if (!result.equals("0")) {
|
||||
throw new MagentaTVException("Pairing failed, result=" + result);
|
||||
}
|
||||
|
||||
logger.debug("{}: Pairing initiated (deviceID={}).", thingId, config.getTerminalID());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the verifificationCode to complete pairing. This will be triggered
|
||||
* as a result after receiving the pairing code provided by the MR. The
|
||||
* verification code is the MD5 hash of <Pairing Code><Terminal-ID><User ID>
|
||||
*
|
||||
* @param pairingCode Pairing code received from the MR
|
||||
* @return true: a new code has been generated, false: the code matches a
|
||||
* previous pairing
|
||||
*/
|
||||
public boolean generateVerificationCode(String pairingCode) {
|
||||
if (config.getPairingCode().equals(pairingCode) && !config.getVerificationCode().isEmpty()) {
|
||||
logger.debug("{}: Pairing code ({}) refreshed, verificationCode={}", thingId, pairingCode,
|
||||
config.getVerificationCode());
|
||||
return false;
|
||||
}
|
||||
config.setPairingCode(pairingCode);
|
||||
String md5Input = pairingCode + config.getTerminalID() + config.getUserId();
|
||||
config.setVerificationCode(computeMD5(md5Input).toUpperCase());
|
||||
logger.debug("{}: VerificationCode({}): Input={}, code={}", thingId, config.getTerminalID(), md5Input,
|
||||
config.getVerificationCode());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a pairing verification request to the receiver. This is important to
|
||||
* complete the pairing process. You should see a message like "Connected to
|
||||
* openHAB" on your TV screen.
|
||||
*
|
||||
* @return true: successful, false: a non-critical error occured, caller handles
|
||||
* this
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public boolean verifyPairing() throws MagentaTVException {
|
||||
logger.debug("{}: Verify pairing (id={}, code={}", thingId, config.getTerminalID(),
|
||||
config.getVerificationCode());
|
||||
String soapBody = MessageFormat.format(PAIRCHECK_SOAP_BODY, config.getTerminalID(),
|
||||
config.getVerificationCode());
|
||||
String soapXml = MessageFormat.format(SOAP_ENVELOPE, soapBody);
|
||||
String response = http.httpPOST(buildHost(), buildReceiverUrl(PAIRCHECK_URI), soapXml, PAIRCHECK_SOAP_ACTION,
|
||||
CONNECTION_CLOSE);
|
||||
|
||||
// Exception if request failed (response code != HTTP_OK)
|
||||
if (!response.contains("<pairingResult>")) {
|
||||
throw new MagentaTVException("Unexpected result for pairing verification: " + response);
|
||||
}
|
||||
|
||||
String result = getXmlValue(response, "pairingResult");
|
||||
if (!result.equals("0")) {
|
||||
logger.debug("{}: Pairing failed or pairing no longer valid, result={}", thingId, result);
|
||||
resetPairing();
|
||||
// let the caller decide how to proceed
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.isMR400()) {
|
||||
String enable4K = getXmlValue(response, "Enable4K");
|
||||
String enableSAT = getXmlValue(response, "EnableSAT");
|
||||
logger.debug("{}: Features: Enable4K:{}, EnableSAT:{}", thingId, enable4K, enableSAT);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return true if pairing is completed (verification code was generated)
|
||||
*/
|
||||
public boolean isPaired() {
|
||||
// pairing was completed successful if we have the verification code
|
||||
return !config.getVerificationCode().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset pairing information (e.g. when verification failed)
|
||||
*/
|
||||
public void resetPairing() {
|
||||
// pairing no longer valid
|
||||
config.setPairingCode("");
|
||||
config.setVerificationCode("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send key code to the MR (via SOAP request). A key code could be send by it's
|
||||
* code (0x.... notation) or with a symbolic namne, which will first be mapped
|
||||
* to the key code
|
||||
*
|
||||
* XML format for Send Key
|
||||
*
|
||||
* <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"
|
||||
* s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"> <s:Body>\n
|
||||
* <u:X_CTC_RemoteKey
|
||||
* xmlns:u=\"urn:schemas-upnp-org:service:X-CTC_RemoteControl:1\">\n
|
||||
* <InstanceID>0</InstanceID>\n
|
||||
* <KeyCode>keyCode=$keyCode^$pairingDeviceID:$verificationCode^userID:$userID</KeyCode>\n
|
||||
* </u:X_CTC_RemoteKey>\n </s:Body></s:Envelope>
|
||||
*
|
||||
* @param keyName
|
||||
* @return true: successful, false: failed, e.g. unkown key code
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public boolean sendKey(String keyName) throws MagentaTVException {
|
||||
String keyCode = getKeyCode(keyName);
|
||||
logger.debug("{}: Send Key {} (keyCode={}, tid={})", thingId, keyName, keyCode, config.getTerminalID());
|
||||
if (keyCode.length() <= "0x".length()) {
|
||||
logger.debug("{}: Key {} is unkown!", thingId, keyCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
String soapBody = MessageFormat.format(SENDKEY_SOAP_BODY, keyCode, config.getTerminalID(),
|
||||
config.getVerificationCode(), config.getUserId());
|
||||
String soapXml = MessageFormat.format(SOAP_ENVELOPE, soapBody);
|
||||
logger.debug("{}: send keyCode={} to {}:{}", thingId, keyCode, config.getIpAddress(), config.getPort());
|
||||
logger.trace("{}: sendKey terminalid={}, pairingCode={}, verificationCode={}, userId={}", thingId,
|
||||
config.getTerminalID(), config.getPairingCode(), config.getVerificationCode(), config.getUserId());
|
||||
http.httpPOST(buildHost(), buildReceiverUrl(SENDKEY_URI), soapXml, SENDKEY_SOAP_ACTION, CONNECTION_CLOSE);
|
||||
// Exception if request failed (response code != HTTP_OK)
|
||||
// pairingCode will be received by the Servlet, is calls onPairingResult()
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select channel for TV
|
||||
*
|
||||
* @param channel new channel (a sequence of numbers, which will be send one by one)
|
||||
* @return true:ok, false: failed
|
||||
*/
|
||||
public boolean selectChannel(String channel) throws MagentaTVException {
|
||||
logger.debug("{}: Select channel {}", thingId, channel);
|
||||
for (int i = 0; i < channel.length(); i++) {
|
||||
if (!sendKey("" + channel.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key code to send to receiver
|
||||
*
|
||||
* @param key Key for which to get the key code
|
||||
* @return
|
||||
*/
|
||||
private String getKeyCode(String key) {
|
||||
if (key.contains("0x")) {
|
||||
// direct key code
|
||||
return key;
|
||||
}
|
||||
if (KEY_MAP.containsKey(key)) {
|
||||
return KEY_MAP.get(key);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Map playStatus code to string for a list of codes see
|
||||
* http://support.huawei.com/hedex/pages/DOC1100366313CEH0713H/01/DOC1100366313CEH0713H/01/resources/dsv_hdx_idp/DSV/en/en-us_topic_0094619231.html
|
||||
*
|
||||
* @param playStatus Integer code parsed form json (see EV_PLAYCHG_XXX)
|
||||
* @return playStatus as String
|
||||
*/
|
||||
public String getPlayStatus(int playStatus) {
|
||||
switch (playStatus) {
|
||||
case EV_PLAYCHG_PLAY:
|
||||
return "playing";
|
||||
case EV_PLAYCHG_STOP:
|
||||
return "stopped";
|
||||
case EV_PLAYCHG_PAUSE:
|
||||
return "paused";
|
||||
case EV_PLAYCHG_TRICK:
|
||||
return "tricking";
|
||||
case EV_PLAYCHG_MC_PLAY:
|
||||
return "playing (MC)";
|
||||
case EV_PLAYCHG_UC_PLAY:
|
||||
return "playing (UC)";
|
||||
case EV_PLAYCHG_BUFFERING:
|
||||
return "buffering";
|
||||
default:
|
||||
return Integer.toString(playStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map runningStatus code to string for a list of codes see
|
||||
* http://support.huawei.com/hedex/pages/DOC1100366313CEH0713H/01/DOC1100366313CEH0713H/01/resources/dsv_hdx_idp/DSV/en/en-us_topic_0094619523.html
|
||||
*
|
||||
* @param runStatus Integer code parsed form json (see EV_EITCHG_RUNNING_XXX)
|
||||
* @return runningStatus as String
|
||||
*/
|
||||
public String getRunStatus(int runStatus) {
|
||||
switch (runStatus) {
|
||||
case EV_EITCHG_RUNNING_NOT_RUNNING:
|
||||
return "stopped";
|
||||
case EV_EITCHG_RUNNING_STARTING:
|
||||
return "starting";
|
||||
case EV_EITCHG_RUNNING_PAUSING:
|
||||
return "paused";
|
||||
case EV_EITCHG_RUNNING_RUNNING:
|
||||
return "running";
|
||||
default:
|
||||
return Integer.toString(runStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* builds url from the discovered IP address/port and the requested uri
|
||||
*
|
||||
* @param uri requested URI
|
||||
* @return the complete URL
|
||||
*/
|
||||
public String buildReceiverUrl(String uri) {
|
||||
return MessageFormat.format("http://{0}:{1}{2}", config.getIpAddress(), config.getPort(), uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* build host string
|
||||
*
|
||||
* @return formatted string (<ip_address>:<port>)
|
||||
*/
|
||||
private String buildHost() {
|
||||
return config.getIpAddress() + ":" + config.getPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, return the MD5 hash of the String.
|
||||
*
|
||||
* @param unhashed The string contents to be hashed.
|
||||
* @return MD5 Hashed value of the String. Null if there is a problem hashing
|
||||
* the String.
|
||||
*/
|
||||
public static String computeMD5(String unhashed) {
|
||||
try {
|
||||
byte[] bytesOfMessage = unhashed.getBytes(UTF_8);
|
||||
|
||||
MessageDigest md5 = MessageDigest.getInstance(HASH_ALGORITHM_MD5);
|
||||
byte[] hash = md5.digest(bytesOfMessage);
|
||||
StringBuilder sb = new StringBuilder(2 * hash.length);
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b & 0xff));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
} catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to parse a Xml tag value from string without using a complex XML class
|
||||
*
|
||||
* @param xml Input string in the format <tag>value</tag>
|
||||
* @param tagName The tag to find
|
||||
* @return Tag value (between <tag> and </tag>)
|
||||
*/
|
||||
public static String getXmlValue(String xml, String tagName) {
|
||||
String open = "<" + tagName + ">";
|
||||
String close = "</" + tagName + ">";
|
||||
if (xml.contains(open) && xml.contains(close)) {
|
||||
return substringBetween(xml, open, close);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize key map (key name -> key code)
|
||||
* "
|
||||
* for a list of valid key codes see
|
||||
* http://support.huawei.com/hedex/pages/DOC1100366313CEH0713H/01/DOC1100366313CEH0713H/01/resources/dsv_hdx_idp/DSV/en/en-us_topic_0094619112.html
|
||||
*/
|
||||
static {
|
||||
KEY_MAP.put("POWER", "0x0100");
|
||||
KEY_MAP.put("MENU", "0x0110");
|
||||
KEY_MAP.put("EPG", "0x0111");
|
||||
KEY_MAP.put("TVMENU", "0x0454");
|
||||
KEY_MAP.put("VODMENU", "0x0455");
|
||||
KEY_MAP.put("TVODMENU", "0x0456");
|
||||
KEY_MAP.put("NVODMENU", "0x0458");
|
||||
KEY_MAP.put("INFO", "0x010C");
|
||||
KEY_MAP.put("TTEXT", "0x0560");
|
||||
KEY_MAP.put("0", "0x0030");
|
||||
KEY_MAP.put("1", "0x0031");
|
||||
KEY_MAP.put("2", "0x0032");
|
||||
KEY_MAP.put("3", "0x0033");
|
||||
KEY_MAP.put("4", "0x0034");
|
||||
KEY_MAP.put("5", "0x0035");
|
||||
KEY_MAP.put("6", "0x0036");
|
||||
KEY_MAP.put("7", "0x0037");
|
||||
KEY_MAP.put("8", "0x0038");
|
||||
KEY_MAP.put("9", "0x0039");
|
||||
KEY_MAP.put("SPACE", "0x0020");
|
||||
KEY_MAP.put("POUND", "0x0069");
|
||||
KEY_MAP.put("STAR", "0x006A");
|
||||
KEY_MAP.put("UP", "0x0026");
|
||||
KEY_MAP.put("DOWN", "0x0028");
|
||||
KEY_MAP.put("LEFT", "0x0025");
|
||||
KEY_MAP.put("RIGHT", "0x0027");
|
||||
KEY_MAP.put("PGUP", "0x0021");
|
||||
KEY_MAP.put("PGDOWN", "0x0022");
|
||||
KEY_MAP.put("DELETE", "0x0008");
|
||||
KEY_MAP.put("ENTER", "0x000D");
|
||||
KEY_MAP.put("SEARCH", "0x0451");
|
||||
KEY_MAP.put("RED", "0x0113");
|
||||
KEY_MAP.put("GREEN", "0x0114");
|
||||
KEY_MAP.put("YELLOW", "0x0115");
|
||||
KEY_MAP.put("BLUE", "0x0116");
|
||||
KEY_MAP.put("OPTION", "0x0460");
|
||||
KEY_MAP.put("OK", "0x000D");
|
||||
KEY_MAP.put("BACK", "0x0008");
|
||||
KEY_MAP.put("EXIT", "0x045D");
|
||||
KEY_MAP.put("PORTAL", "0x0110");
|
||||
KEY_MAP.put("VOLUP", "0x0103");
|
||||
KEY_MAP.put("VOLDOWN", "0x0104");
|
||||
KEY_MAP.put("INTER", "0x010D");
|
||||
KEY_MAP.put("HELP", "0x011C");
|
||||
KEY_MAP.put("SETTINGS", "0x011D");
|
||||
KEY_MAP.put("MUTE", "0x0105");
|
||||
KEY_MAP.put("CHUP", "0x0101");
|
||||
KEY_MAP.put("CHDOWN", "0x0102");
|
||||
KEY_MAP.put("REWIND", "0x0109");
|
||||
KEY_MAP.put("PLAY", "0x0107");
|
||||
KEY_MAP.put("PAUSE", "0x0107");
|
||||
KEY_MAP.put("FORWARD", "0x0108");
|
||||
KEY_MAP.put("TRACK", "0x0106");
|
||||
KEY_MAP.put("LASTCH", "0x045E");
|
||||
KEY_MAP.put("PREVCH", "0x010B");
|
||||
KEY_MAP.put("NEXTCH", "0x0107");
|
||||
KEY_MAP.put("RECORD", "0x0461");
|
||||
KEY_MAP.put("STOP", "0x010E");
|
||||
KEY_MAP.put("BEGIN", "0x010B");
|
||||
KEY_MAP.put("END", "0x010A");
|
||||
KEY_MAP.put("REPLAY", "0x045B");
|
||||
KEY_MAP.put("SKIP", "0x045C");
|
||||
KEY_MAP.put("SUBTITLE", "0x236");
|
||||
KEY_MAP.put("RECORDINGS", "0x045F");
|
||||
KEY_MAP.put("FAV", "0x0119");
|
||||
KEY_MAP.put("SOURCE", "0x0083");
|
||||
KEY_MAP.put("SWITCH", "0x0118");
|
||||
KEY_MAP.put("IPTV", "0x0081");
|
||||
KEY_MAP.put("PC", "0x0082");
|
||||
KEY_MAP.put("PIP", "0x0084");
|
||||
KEY_MAP.put("MULTIVIEW", "0x0562");
|
||||
KEY_MAP.put("F1", "0x0070");
|
||||
KEY_MAP.put("F2", "0x0071");
|
||||
KEY_MAP.put("F3", "0x0072");
|
||||
KEY_MAP.put("F4", "0x0073");
|
||||
KEY_MAP.put("F5", "0x0074");
|
||||
KEY_MAP.put("F6", "0x0075");
|
||||
KEY_MAP.put("F7", "0x0076");
|
||||
KEY_MAP.put("F8", "0x0077");
|
||||
KEY_MAP.put("F9", "0x0078");
|
||||
KEY_MAP.put("F10", "0x0079");
|
||||
KEY_MAP.put("F11", "0x007A");
|
||||
KEY_MAP.put("F12", "0x007B");
|
||||
KEY_MAP.put("F13", "0x007C");
|
||||
KEY_MAP.put("F14", "0x007D");
|
||||
KEY_MAP.put("F15", "0x007E");
|
||||
KEY_MAP.put("F16", "0x007F");
|
||||
|
||||
KEY_MAP.put("PVR", "0x0461");
|
||||
KEY_MAP.put("RADIO", "0x0462");
|
||||
|
||||
// Those key codes are missing and not included in the spec
|
||||
// KEY_MAP.put("TV", "0x");
|
||||
// KEY_MAP.put("RADIO", "0x");
|
||||
// KEY_MAP.put("MOVIES", "0x");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.handler;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVDeviceManager;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVException;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRPayEvent;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRPayEventInstanceCreator;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramInfoEvent;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramInfoEventInstanceCreator;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramStatus;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRProgramStatusInstanceCreator;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRShortProgramInfo;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.MRShortProgramInfoInstanceCreator;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponse;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthTokenResponse;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentials;
|
||||
import org.openhab.binding.magentatv.internal.config.MagentaTVDynamicConfig;
|
||||
import org.openhab.binding.magentatv.internal.config.MagentaTVThingConfiguration;
|
||||
import org.openhab.binding.magentatv.internal.network.MagentaTVNetwork;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.NextPreviousType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PlayPauseType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.RewindFastforwardType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
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.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVHandler extends BaseThingHandler implements MagentaTVListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVHandler.class);
|
||||
|
||||
protected MagentaTVDynamicConfig config = new MagentaTVDynamicConfig();
|
||||
private final Gson gson;
|
||||
protected final MagentaTVNetwork network;
|
||||
protected final MagentaTVDeviceManager manager;
|
||||
protected MagentaTVControl control = new MagentaTVControl();
|
||||
|
||||
private String thingId = "";
|
||||
private volatile int idRefresh = 0;
|
||||
private @Nullable ScheduledFuture<?> initializeJob;
|
||||
private @Nullable ScheduledFuture<?> pairingWatchdogJob;
|
||||
private @Nullable ScheduledFuture<?> renewEventJob;
|
||||
|
||||
/**
|
||||
* Constructor, save bindingConfig (services as default for thingConfig)
|
||||
*
|
||||
* @param thing
|
||||
* @param bindingConfig
|
||||
*/
|
||||
public MagentaTVHandler(MagentaTVDeviceManager manager, Thing thing, MagentaTVNetwork network) {
|
||||
super(thing);
|
||||
this.manager = manager;
|
||||
this.network = network;
|
||||
gson = new GsonBuilder().registerTypeAdapter(OauthCredentials.class, new MRProgramInfoEventInstanceCreator())
|
||||
.registerTypeAdapter(OAuthTokenResponse.class, new MRProgramStatusInstanceCreator())
|
||||
.registerTypeAdapter(OAuthAuthenticateResponse.class, new MRShortProgramInfoInstanceCreator())
|
||||
.registerTypeAdapter(OAuthAuthenticateResponse.class, new MRPayEventInstanceCreator()).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Thing initialization:
|
||||
* - initialize thing status from UPnP discovery, thing config, local network settings
|
||||
* - initiate OAuth if userId is not configured and credentials are available
|
||||
* - wait for NotifyServlet to initialize (solves timing issues on fast startup)
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
// The framework requires you to return from this method quickly. For that the initialization itself is executed
|
||||
// asynchronously
|
||||
String label = getThing().getLabel();
|
||||
thingId = label != null ? label : getThing().getUID().toString();
|
||||
resetEventChannels();
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
config = new MagentaTVDynamicConfig(getConfigAs(MagentaTVThingConfiguration.class));
|
||||
try {
|
||||
initializeJob = scheduler.schedule(this::initializeThing, 5, TimeUnit.SECONDS);
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Unable to schedule thing initialization", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeThing() {
|
||||
String errorMessage = "";
|
||||
try {
|
||||
if (config.getUDN().isEmpty()) {
|
||||
// get UDN from device name
|
||||
String uid = this.getThing().getUID().getAsString();
|
||||
config.setUDN(substringAfterLast(uid, ":"));
|
||||
}
|
||||
if (config.getMacAddress().isEmpty()) {
|
||||
// get MAC address from UDN (last 12 digits)
|
||||
String macAddress = substringAfterLast(config.getUDN(), "_");
|
||||
if (macAddress.isEmpty()) {
|
||||
macAddress = substringAfterLast(config.getUDN(), "-");
|
||||
}
|
||||
config.setMacAddress(macAddress);
|
||||
}
|
||||
control = new MagentaTVControl(config, network);
|
||||
config.updateNetwork(control.getConfig()); // get network parameters from control
|
||||
|
||||
// Check for emoty credentials (e.g. missing in .things file)
|
||||
String account = config.getAccountName();
|
||||
if (config.getUserId().isEmpty()) {
|
||||
if (account.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Credentials missing or invalid! Fill credentials into thing configuration or generate UID on the openHAB console - see README");
|
||||
return;
|
||||
}
|
||||
|
||||
getUserId();
|
||||
}
|
||||
|
||||
connectReceiver(); // throws MagentaTVException on error
|
||||
|
||||
// setup background device check
|
||||
renewEventJob = scheduler.scheduleWithFixedDelay(this::renewEventSubscription, 2, 5, TimeUnit.MINUTES);
|
||||
|
||||
// change to ThingStatus.ONLINE will be done when the pairing result is received
|
||||
// (see onPairingResult())
|
||||
} catch (MagentaTVException e) {
|
||||
errorMessage = e.toString();
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("{}: Exception on initialization", thingId, e);
|
||||
} finally {
|
||||
if (!errorMessage.isEmpty()) {
|
||||
logger.debug("{}: {}", thingId, errorMessage);
|
||||
setOnlineStatus(ThingStatus.OFFLINE, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This routine is called every time the Thing configuration has been changed (e.g. PaperUI)
|
||||
*/
|
||||
@Override
|
||||
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
|
||||
logger.debug("{}: Thing config updated, re-initialize", thingId);
|
||||
cancelAllJobs();
|
||||
if (configurationParameters.containsKey(PROPERTY_ACCT_NAME)) {
|
||||
@Nullable
|
||||
String newAccount = (String) configurationParameters.get(PROPERTY_ACCT_NAME);
|
||||
if ((newAccount != null) && !newAccount.isEmpty()) {
|
||||
// new account info, need to renew userId
|
||||
config.setUserId("");
|
||||
}
|
||||
}
|
||||
|
||||
super.handleConfigurationUpdate(configurationParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle channel commands
|
||||
*
|
||||
* @param channelUID - the channel, which received the command
|
||||
* @param command - the actual command (could be instance of StringType,
|
||||
* DecimalType or OnOffType)
|
||||
*/
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command == RefreshType.REFRESH) {
|
||||
// currently no channels to be refreshed
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isOnline() || command.toString().equalsIgnoreCase("PAIR")) {
|
||||
logger.debug("{}: Receiver {} is offline, try to (re-)connect", thingId, deviceName());
|
||||
connectReceiver(); // reconnect to MR, throws an exception if this fails
|
||||
}
|
||||
|
||||
logger.debug("{}: Channel command for device {}: {} for channel {}", thingId, config.getFriendlyName(),
|
||||
command, channelUID.getId());
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_POWER: // toggle power
|
||||
logger.debug("{}: Toggle power, new state={}", thingId, command);
|
||||
control.sendKey("POWER");
|
||||
break;
|
||||
case CHANNEL_PLAYER:
|
||||
logger.debug("{}: Player command: {}", thingId, command);
|
||||
if (command instanceof OnOffType) {
|
||||
control.sendKey("POWER");
|
||||
} else if (command instanceof PlayPauseType) {
|
||||
if (command == PlayPauseType.PLAY) {
|
||||
control.sendKey("PLAY");
|
||||
} else if (command == PlayPauseType.PAUSE) {
|
||||
control.sendKey("PAUSE");
|
||||
}
|
||||
} else if (command instanceof NextPreviousType) {
|
||||
if (command == NextPreviousType.NEXT) {
|
||||
control.sendKey("NEXTCH");
|
||||
} else if (command == NextPreviousType.PREVIOUS) {
|
||||
control.sendKey("PREVCH");
|
||||
}
|
||||
} else if (command instanceof RewindFastforwardType) {
|
||||
if (command == RewindFastforwardType.FASTFORWARD) {
|
||||
control.sendKey("FORWARD");
|
||||
} else if (command == RewindFastforwardType.REWIND) {
|
||||
control.sendKey("REWIND");
|
||||
}
|
||||
} else {
|
||||
logger.debug("{}: Unknown media command: {}", thingId, command);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_CHANNEL:
|
||||
String chan = command.toString();
|
||||
control.selectChannel(chan);
|
||||
break;
|
||||
case CHANNEL_MUTE:
|
||||
if (command == OnOffType.ON) {
|
||||
control.sendKey("MUTE");
|
||||
} else {
|
||||
control.sendKey("VOLUP");
|
||||
}
|
||||
break;
|
||||
case CHANNEL_KEY:
|
||||
if (command.toString().equalsIgnoreCase("PAIR")) { // special key to re-pair receiver (already done
|
||||
// above)
|
||||
logger.debug("{}: PAIRing key received, reconnect receiver {}", thingId, deviceName());
|
||||
} else {
|
||||
control.sendKey(command.toString());
|
||||
mapKeyToMediateState(command.toString());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("{}: Command {} for unknown channel {}", thingId, command, channelUID.getAsString());
|
||||
}
|
||||
} catch (MagentaTVException e) {
|
||||
String errorMessage = MessageFormat.format("Channel operation failed (command={0}, value={1}): {2}",
|
||||
command, channelUID.getId(), e.getMessage());
|
||||
logger.debug("{}: {}", thingId, errorMessage);
|
||||
setOnlineStatus(ThingStatus.OFFLINE, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void mapKeyToMediateState(String key) {
|
||||
State state = null;
|
||||
switch (key.toUpperCase()) {
|
||||
case "PLAY":
|
||||
state = PlayPauseType.PLAY;
|
||||
break;
|
||||
case "PAUSE":
|
||||
state = PlayPauseType.PAUSE;
|
||||
break;
|
||||
case "FORWARD":
|
||||
state = RewindFastforwardType.FASTFORWARD;
|
||||
break;
|
||||
case "REWIND":
|
||||
updateState(CHANNEL_PLAYER, RewindFastforwardType.REWIND);
|
||||
break;
|
||||
}
|
||||
if (state != null) {
|
||||
logger.debug("{}: Setting Player state to {}", thingId, state);
|
||||
updateState(CHANNEL_PLAYER, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the receiver
|
||||
*
|
||||
* @throws MagentaTVException something failed
|
||||
*/
|
||||
protected void connectReceiver() throws MagentaTVException {
|
||||
if (control.checkDev()) {
|
||||
updateThingProperties();
|
||||
manager.registerDevice(config.getUDN(), config.getTerminalID(), config.getIpAddress(), this);
|
||||
control.subscribeEventChannel();
|
||||
control.sendPairingRequest();
|
||||
|
||||
// check for pairing timeout
|
||||
final int iRefresh = ++idRefresh;
|
||||
pairingWatchdogJob = scheduler.schedule(() -> {
|
||||
if (iRefresh == idRefresh) { // Make a best effort to not run multiple deferred refresh
|
||||
if (config.getVerificationCode().isEmpty()) {
|
||||
setOnlineStatus(ThingStatus.OFFLINE, "Timeout on pairing request!");
|
||||
}
|
||||
}
|
||||
}, 15, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If userId is empty and credentials are given the Telekom OAuth service is
|
||||
* used to query the userId
|
||||
*
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
private void getUserId() throws MagentaTVException {
|
||||
String userId = config.getUserId();
|
||||
if (userId.isEmpty()) {
|
||||
// run OAuth authentication, this finally provides the userId
|
||||
logger.debug("{}: Login with account {}", thingId, config.getAccountName());
|
||||
userId = control.getUserId(config.getAccountName(), config.getAccountPassword());
|
||||
|
||||
// Update thing configuration (persistent) - remove credentials, add userId
|
||||
Configuration configuration = this.getConfig();
|
||||
configuration.remove(PROPERTY_ACCT_NAME);
|
||||
configuration.remove(PROPERTY_ACCT_PWD);
|
||||
configuration.remove(PROPERTY_USERID);
|
||||
configuration.put(PROPERTY_ACCT_NAME, "");
|
||||
configuration.put(PROPERTY_ACCT_PWD, "");
|
||||
configuration.put(PROPERTY_USERID, userId);
|
||||
this.updateConfiguration(configuration);
|
||||
config.setAccountName("");
|
||||
config.setAccountPassword("");
|
||||
} else {
|
||||
logger.debug("{}: Skip OAuth, use existing userId {}", thingId, config.getUserId());
|
||||
}
|
||||
if (!userId.isEmpty()) {
|
||||
config.setUserId(userId);
|
||||
} else {
|
||||
logger.warn("{}: Unable to obtain userId from OAuth", thingId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update thing status
|
||||
*
|
||||
* @param mode new thing status
|
||||
* @return ON = power on, OFF=power off
|
||||
*/
|
||||
public void setOnlineStatus(ThingStatus newStatus, String errorMessage) {
|
||||
ThingStatus status = this.getThing().getStatus();
|
||||
if (status != newStatus) {
|
||||
if (newStatus == ThingStatus.ONLINE) {
|
||||
updateStatus(newStatus);
|
||||
updateState(CHANNEL_POWER, OnOffType.ON);
|
||||
} else {
|
||||
if (!errorMessage.isEmpty()) {
|
||||
logger.debug("{}: Communication Error - {}, switch Thing offline", thingId, errorMessage);
|
||||
updateStatus(newStatus, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
|
||||
} else {
|
||||
updateStatus(newStatus);
|
||||
}
|
||||
updateState(CHANNEL_POWER, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wakeup of the MR was detected (e.g. UPnP received)
|
||||
*
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
@Override
|
||||
public void onWakeup(Map<String, String> discoveredProperties) throws MagentaTVException {
|
||||
if ((this.getThing().getStatus() == ThingStatus.OFFLINE) || config.getVerificationCode().isEmpty()) {
|
||||
// Device sent a UPnP discovery information, trigger to reconnect
|
||||
connectReceiver();
|
||||
} else {
|
||||
logger.debug("{}: Refesh device status for {} (UDN={}", thingId, deviceName(), config.getUDN());
|
||||
setOnlineStatus(ThingStatus.ONLINE, "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The pairing result has been received. The pairing code will be used to generate the verification code and
|
||||
* complete pairing with the MR. Finally if pairing was completed successful the thing status will change to ONLINE
|
||||
*
|
||||
* @param pairingCode pairing code received from MR (NOTIFY event data)
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
@Override
|
||||
public void onPairingResult(String pairingCode) throws MagentaTVException {
|
||||
if (control.isInitialized()) {
|
||||
if (control.generateVerificationCode(pairingCode)) {
|
||||
config.setPairingCode(pairingCode);
|
||||
logger.debug(
|
||||
"{}: Pairing code received (UDN {}, terminalID {}, pairingCode={}, verificationCode={}, userId={})",
|
||||
thingId, config.getUDN(), config.getTerminalID(), config.getPairingCode(),
|
||||
config.getVerificationCode(), config.getUserId());
|
||||
|
||||
// verify pairing completes the pairing process
|
||||
if (control.verifyPairing()) {
|
||||
logger.debug("{}: Pairing completed for device {} ({}), Thing now ONLINE", thingId,
|
||||
config.getFriendlyName(), config.getTerminalID());
|
||||
setOnlineStatus(ThingStatus.ONLINE, "");
|
||||
cancelPairingCheck(); // stop timeout check
|
||||
}
|
||||
}
|
||||
updateThingProperties(); // persist pairing and verification code
|
||||
} else {
|
||||
logger.debug("{}: control not yet initialized!", thingId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMREvent(String jsonInput) {
|
||||
logger.trace("{}: Process MR event for device {}, json={}", thingId, deviceName(), jsonInput);
|
||||
boolean flUpdatePower = false;
|
||||
String jsonEvent = fixEventJson(jsonInput);
|
||||
if (jsonEvent.contains(MR_EVENT_EIT_CHANGE)) {
|
||||
logger.debug("{}: EVENT_EIT_CHANGE event received.", thingId);
|
||||
|
||||
MRProgramInfoEvent pinfo = gson.fromJson(jsonEvent, MRProgramInfoEvent.class);
|
||||
if (!pinfo.channelNum.isEmpty()) {
|
||||
logger.debug("{}: EVENT_EIT_CHANGE for channel {}/{}", thingId, pinfo.channelNum, pinfo.channelCode);
|
||||
updateState(CHANNEL_CHANNEL, new DecimalType(pinfo.channelNum));
|
||||
updateState(CHANNEL_CHANNEL_CODE, new DecimalType(pinfo.channelCode));
|
||||
}
|
||||
if (pinfo.programInfo != null) {
|
||||
int i = 0;
|
||||
for (MRProgramStatus ps : pinfo.programInfo) {
|
||||
if ((ps.startTime == null) || ps.startTime.isEmpty()) {
|
||||
logger.debug("{}: EVENT_EIT_CHANGE: empty event data = {}", thingId, jsonEvent);
|
||||
continue; // empty program_info
|
||||
}
|
||||
updateState(CHANNEL_RUN_STATUS, new StringType(control.getRunStatus(ps.runningStatus)));
|
||||
|
||||
if (ps.shortEvent != null) {
|
||||
for (MRShortProgramInfo se : ps.shortEvent) {
|
||||
if ((ps.startTime == null) || ps.startTime.isEmpty()) {
|
||||
logger.debug("{}: EVENT_EIT_CHANGE: empty program info", thingId);
|
||||
continue;
|
||||
}
|
||||
// Convert UTC to local time
|
||||
// 2018/11/04 21:45:00 -> "2018-11-04T10:15:30.00Z"
|
||||
String tsLocal = ps.startTime.replace('/', '-').replace(" ", "T") + "Z";
|
||||
Instant timestamp = Instant.parse(tsLocal);
|
||||
ZonedDateTime localTime = timestamp.atZone(ZoneId.of("Europe/Berlin"));
|
||||
tsLocal = substringBeforeLast(localTime.toString(), "[");
|
||||
tsLocal = substringBefore(tsLocal.replace('-', '/').replace('T', ' '), "+");
|
||||
|
||||
logger.debug("{}: Info for channel {} / {} - {} {}.{}, start time={}, duration={}", thingId,
|
||||
pinfo.channelNum, pinfo.channelCode, control.getRunStatus(ps.runningStatus),
|
||||
se.eventName, se.textChar, tsLocal, ps.duration);
|
||||
if (ps.runningStatus != EV_EITCHG_RUNNING_NOT_RUNNING) {
|
||||
updateState(CHANNEL_PROG_TITLE, new StringType(se.eventName));
|
||||
updateState(CHANNEL_PROG_TEXT, new StringType(se.textChar));
|
||||
updateState(CHANNEL_PROG_START, new DateTimeType(localTime));
|
||||
|
||||
try {
|
||||
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
Date date = dateFormat.parse(ps.duration);
|
||||
long minutes = date.getTime() / 1000L / 60l;
|
||||
updateState(CHANNEL_PROG_DURATION, toQuantityType(minutes, SmartHomeUnits.MINUTE));
|
||||
} catch (ParseException e) {
|
||||
logger.debug("{}: Unable to parse programDuration: {}", thingId, ps.duration);
|
||||
}
|
||||
|
||||
if (i++ == 0) {
|
||||
flUpdatePower = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (jsonEvent.contains("new_play_mode")) {
|
||||
MRPayEvent event = gson.fromJson(jsonEvent, MRPayEvent.class);
|
||||
if (event.duration == null) {
|
||||
event.duration = -1;
|
||||
}
|
||||
if (event.playPostion == null) {
|
||||
event.playPostion = -1;
|
||||
}
|
||||
logger.debug("{}: STB event playContent: playMode={}, duration={}, playPosition={}", thingId,
|
||||
control.getPlayStatus(event.newPlayMode), event.duration, event.playPostion);
|
||||
|
||||
// If we get a playConfig event there MR must be online. However it also sends a
|
||||
// plyMode stop before powering off the device, so we filter this.
|
||||
if ((event.newPlayMode != EV_PLAYCHG_STOP) && this.isInitialized()) {
|
||||
flUpdatePower = true;
|
||||
}
|
||||
if (event.newPlayMode != -1) {
|
||||
String playMode = control.getPlayStatus(event.newPlayMode);
|
||||
updateState(CHANNEL_PLAY_MODE, new StringType(playMode));
|
||||
mapPlayModeToMediaControl(playMode);
|
||||
}
|
||||
if (event.duration > 0) {
|
||||
updateState(CHANNEL_PROG_DURATION, new StringType(event.duration.toString()));
|
||||
}
|
||||
if (event.playPostion != -1) {
|
||||
updateState(CHANNEL_PROG_POS, toQuantityType(event.playPostion / 6, SmartHomeUnits.MINUTE));
|
||||
}
|
||||
} else {
|
||||
logger.debug("{}: Unknown MR event, JSON={}", thingId, jsonEvent);
|
||||
}
|
||||
if (flUpdatePower) {
|
||||
// We received a non-stopped event -> MR must be on
|
||||
updateState(CHANNEL_POWER, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
|
||||
private void mapPlayModeToMediaControl(String playMode) {
|
||||
switch (playMode) {
|
||||
case "playing":
|
||||
case "playing (MC)":
|
||||
case "playing (UC)":
|
||||
case "buffering":
|
||||
logger.debug("{}: Setting Player state to PLAY", thingId);
|
||||
updateState(CHANNEL_PLAYER, PlayPauseType.PLAY);
|
||||
break;
|
||||
case "paused":
|
||||
case "stopped":
|
||||
logger.debug("{}: Setting Player state to PAUSE", thingId);
|
||||
updateState(CHANNEL_PLAYER, PlayPauseType.PAUSE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the MR powers off it send a UPnP message, which is catched by the binding.
|
||||
*/
|
||||
@Override
|
||||
public void onPowerOff() throws MagentaTVException {
|
||||
logger.debug("{}: Power-Off received for device {}", thingId, deviceName());
|
||||
// MR was powered off -> update power status, reset items
|
||||
resetEventChannels();
|
||||
}
|
||||
|
||||
private void resetEventChannels() {
|
||||
updateState(CHANNEL_POWER, OnOffType.OFF);
|
||||
updateState(CHANNEL_PROG_TITLE, StringType.EMPTY);
|
||||
updateState(CHANNEL_PROG_TEXT, StringType.EMPTY);
|
||||
updateState(CHANNEL_PROG_START, StringType.EMPTY);
|
||||
updateState(CHANNEL_PROG_DURATION, DecimalType.ZERO);
|
||||
updateState(CHANNEL_PROG_POS, DecimalType.ZERO);
|
||||
updateState(CHANNEL_CHANNEL, DecimalType.ZERO);
|
||||
updateState(CHANNEL_CHANNEL_CODE, DecimalType.ZERO);
|
||||
}
|
||||
|
||||
private String fixEventJson(String jsonEvent) {
|
||||
// MR401: channel_num is a string -> ok
|
||||
// MR201: channel_num is an int -> fix JSON formatting to String
|
||||
if (jsonEvent.contains(MR_EVENT_CHAN_TAG) && !jsonEvent.contains(MR_EVENT_CHAN_TAG + "\"")) {
|
||||
// hack: reformat the JSON string to make it compatible with the GSON parsing
|
||||
logger.trace("{}: malformed JSON->fix channel_num", thingId);
|
||||
String start = substringBefore(jsonEvent, MR_EVENT_CHAN_TAG); // up to "channel_num":
|
||||
String end = substringAfter(jsonEvent, MR_EVENT_CHAN_TAG); // behind "channel_num":
|
||||
String chan = substringBetween(jsonEvent, MR_EVENT_CHAN_TAG, ",").trim();
|
||||
return start + "\"channel_num\":" + "\"" + chan + "\"" + end;
|
||||
}
|
||||
return jsonEvent;
|
||||
}
|
||||
|
||||
private boolean isOnline() {
|
||||
return this.getThing().getStatus() == ThingStatus.ONLINE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew the event subscription. The periodic refresh is required, otherwise the receive will stop sending events.
|
||||
* Reconnect if nessesary.
|
||||
*/
|
||||
private void renewEventSubscription() {
|
||||
if (!control.isInitialized()) {
|
||||
return;
|
||||
}
|
||||
logger.debug("{}: Check receiver status, current state {}/{}", thingId,
|
||||
this.getThing().getStatusInfo().getStatus(), this.getThing().getStatusInfo().getStatusDetail());
|
||||
|
||||
try {
|
||||
// when pairing is completed re-new event channel subscription
|
||||
if ((this.getThing().getStatus() != ThingStatus.OFFLINE) && !config.getVerificationCode().isEmpty()) {
|
||||
logger.debug("{}: Renew MR event subscription for device {}", thingId, deviceName());
|
||||
control.subscribeEventChannel();
|
||||
}
|
||||
} catch (MagentaTVException e) {
|
||||
logger.warn("{}: Re-new event subscription failed: {}", deviceName(), e.toString());
|
||||
}
|
||||
|
||||
// another try: if the above SUBSCRIBE fails, try a re-connect immediatly
|
||||
try {
|
||||
if ((this.getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR)
|
||||
&& !config.getUserId().isEmpty()) {
|
||||
// if we have no userId the OAuth is not completed or pairing process got stuck
|
||||
logger.debug("{}: Reconnect media receiver", deviceName());
|
||||
connectReceiver(); // throws MagentaTVException on error
|
||||
}
|
||||
} catch (MagentaTVException | RuntimeException e) {
|
||||
logger.debug("{}: Re-connect to receiver failed: {}", deviceName(), e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public void updateThingProperties() {
|
||||
Map<String, String> properties = new HashMap<String, String>();
|
||||
properties.put(PROPERTY_FRIENDLYNAME, config.getFriendlyName());
|
||||
properties.put(PROPERTY_MODEL_NUMBER, config.getModel());
|
||||
properties.put(PROPERTY_DESC_URL, config.getDescriptionUrl());
|
||||
properties.put(PROPERTY_PAIRINGCODE, config.getPairingCode());
|
||||
properties.put(PROPERTY_VERIFICATIONCODE, config.getVerificationCode());
|
||||
properties.put(PROPERTY_LOCAL_IP, config.getLocalIP());
|
||||
properties.put(PROPERTY_TERMINALID, config.getLocalIP());
|
||||
properties.put(PROPERTY_LOCAL_MAC, config.getLocalMAC());
|
||||
properties.put(PROPERTY_WAKEONLAN, config.getWakeOnLAN());
|
||||
updateProperties(properties);
|
||||
}
|
||||
|
||||
public static State toQuantityType(@Nullable Number value, Unit<?> unit) {
|
||||
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
|
||||
}
|
||||
|
||||
private String deviceName() {
|
||||
return config.getFriendlyName() + "(" + config.getTerminalID() + ")";
|
||||
}
|
||||
|
||||
private void cancelJob(@Nullable ScheduledFuture<?> job) {
|
||||
if ((job != null) && !job.isCancelled()) {
|
||||
job.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected void cancelInitialize() {
|
||||
cancelJob(initializeJob);
|
||||
}
|
||||
|
||||
protected void cancelPairingCheck() {
|
||||
cancelJob(pairingWatchdogJob);
|
||||
}
|
||||
|
||||
protected void cancelRenewEvent() {
|
||||
cancelJob(renewEventJob);
|
||||
}
|
||||
|
||||
private void cancelAllJobs() {
|
||||
cancelInitialize();
|
||||
cancelPairingCheck();
|
||||
cancelRenewEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
cancelAllJobs();
|
||||
manager.removeDevice(config.getTerminalID());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.handler;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVException;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVListener} defines the interface to pass back the pairing
|
||||
* code and device events to the listener
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MagentaTVListener {
|
||||
/**
|
||||
* Device returned pairing code
|
||||
*
|
||||
* @param pairingCode Code to be used for pairing process
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
void onPairingResult(String pairingCode) throws MagentaTVException;
|
||||
|
||||
/**
|
||||
* Device woke up (UPnP)
|
||||
*
|
||||
* @param discoveredProperties Properties from UPnP discovery
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
void onWakeup(Map<String, String> discoveredProperties) throws MagentaTVException;
|
||||
|
||||
/**
|
||||
* An event has been received from the MR
|
||||
*
|
||||
* @param playContent event information
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
void onMREvent(String playContent) throws MagentaTVException;
|
||||
|
||||
/**
|
||||
* A power-off was detected (SSDN message received)
|
||||
*
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
void onPowerOff() throws MagentaTVException;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.network;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.ws.rs.HttpMethod;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVException;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVHttp} supplies network functions.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVHttp {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVHttp.class);
|
||||
|
||||
public String httpGet(String host, String urlBase, String urlParameters) throws MagentaTVException {
|
||||
String url = "";
|
||||
String response = "";
|
||||
try {
|
||||
url = !urlParameters.isEmpty() ? urlBase + "?" + urlParameters : urlBase;
|
||||
Properties httpHeader = new Properties();
|
||||
httpHeader.setProperty(HEADER_USER_AGENT, USER_AGENT);
|
||||
httpHeader.setProperty(HEADER_HOST, host);
|
||||
httpHeader.setProperty(HEADER_ACCEPT, "*/*");
|
||||
response = HttpUtil.executeUrl(HttpMethod.GET, url, httpHeader, null, null, NETWORK_TIMEOUT_MS);
|
||||
logger.trace("GET {} - Response={}", url, response);
|
||||
return response;
|
||||
} catch (IOException e) {
|
||||
throw new MagentaTVException(e, "HTTP GET {0} failed: {1}", url, response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL and a set parameters, send a HTTP POST request to the URL
|
||||
* location created by the URL and parameters.
|
||||
*
|
||||
* @param url The URL to send a POST request to.
|
||||
* @param urlParameters List of parameters to use in the URL for the POST
|
||||
* request. Null if no parameters.
|
||||
* @param soapAction Header attribute for SOAP ACTION: xxx
|
||||
* @param connection Header attribut for CONNECTION: xxx
|
||||
* @return String contents of the response for the POST request.
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public String httpPOST(String host, String url, String postData, String soapAction, String connection)
|
||||
throws MagentaTVException {
|
||||
String httpResponse = "";
|
||||
try {
|
||||
Properties httpHeader = new Properties();
|
||||
httpHeader.setProperty(HEADER_CONTENT_TYPE, CONTENT_TYPE_XML);
|
||||
httpHeader.setProperty(HEADER_ACCEPT, "");
|
||||
httpHeader.setProperty(HEADER_USER_AGENT, USER_AGENT);
|
||||
httpHeader.setProperty(HEADER_HOST, host);
|
||||
if (!soapAction.isEmpty()) {
|
||||
httpHeader.setProperty(HEADER_SOAPACTION, soapAction);
|
||||
}
|
||||
if (!connection.isEmpty()) {
|
||||
httpHeader.setProperty(HEADER_CONNECTION, connection);
|
||||
}
|
||||
|
||||
logger.trace("POST {} - SoapAction={}, Data = {}", url, postData, soapAction);
|
||||
InputStream dataStream = new ByteArrayInputStream(postData.getBytes(StandardCharsets.UTF_8));
|
||||
httpResponse = HttpUtil.executeUrl(HttpMethod.POST, url, httpHeader, dataStream, null, NETWORK_TIMEOUT_MS);
|
||||
logger.trace("POST {} - Response = {}", url, httpResponse);
|
||||
return httpResponse;
|
||||
} catch (IOException e) {
|
||||
throw new MagentaTVException(e, "HTTP POST {0} failed, response={1}", url, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send raw TCP data (SUBSCRIBE command)
|
||||
*
|
||||
* @param remoteIp receiver's IP
|
||||
* @param remotePort destination port
|
||||
* @param data data to send
|
||||
* @return received response
|
||||
* @throws IOException
|
||||
*/
|
||||
public String sendData(String remoteIp, String remotePort, String data) throws MagentaTVException {
|
||||
|
||||
String errorMessage = "";
|
||||
StringBuffer response = new StringBuffer();
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.setSoTimeout(NETWORK_TIMEOUT_MS); // set read timeout
|
||||
socket.connect(new InetSocketAddress(remoteIp, Integer.parseInt(remotePort)), NETWORK_TIMEOUT_MS);
|
||||
|
||||
OutputStream out = socket.getOutputStream();
|
||||
PrintStream ps = new PrintStream(out, true);
|
||||
ps.println(data);
|
||||
|
||||
InputStream in = socket.getInputStream();
|
||||
|
||||
// wait until somthing to read is available or socket I/O fails (IOException)
|
||||
BufferedReader buff = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
do {
|
||||
String line = buff.readLine();
|
||||
response.append(line);
|
||||
response.append("\r\n");
|
||||
} while (buff.ready());
|
||||
} catch (UnknownHostException e) {
|
||||
errorMessage = "Unknown host!";
|
||||
} catch (IOException /* | InterruptedException */ e) {
|
||||
errorMessage = MessageFormat.format("{0} ({1})", e.getMessage(), e.getClass());
|
||||
}
|
||||
|
||||
if (!errorMessage.isEmpty()) {
|
||||
throw new MagentaTVException(
|
||||
MessageFormat.format("Network I/O failed for {0}:{1}: {2}", remoteIp, remotePort, errorMessage));
|
||||
}
|
||||
return response.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.network;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.apache.commons.net.util.SubnetUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVNetwork} supplies network functions.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVNetwork {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVNetwork.class);
|
||||
|
||||
private String localIP = "";
|
||||
private String localPort = "";
|
||||
private String localMAC = "";
|
||||
private @Nullable NetworkInterface localInterface;
|
||||
|
||||
/**
|
||||
* Init local network interface, determine local IP and MAC address
|
||||
*
|
||||
* @param networkAddressService
|
||||
* @return
|
||||
*/
|
||||
public void initLocalNet(String localIP, String localPort) throws MagentaTVException {
|
||||
try {
|
||||
if (localIP.isEmpty() || localIP.equals("0.0.0.0") || localIP.equals("127.0.0.1")) {
|
||||
throw new MagentaTVException("Unable to detect local IP address!");
|
||||
}
|
||||
this.localPort = localPort;
|
||||
this.localIP = localIP;
|
||||
|
||||
// get MAC address
|
||||
InetAddress ip = InetAddress.getByName(localIP);
|
||||
localInterface = NetworkInterface.getByInetAddress(ip);
|
||||
if (localInterface != null) {
|
||||
byte[] mac = localInterface.getHardwareAddress();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? ":" : ""));
|
||||
}
|
||||
localMAC = sb.toString().toUpperCase();
|
||||
logger.debug("Local IP address={}, Local MAC address = {}", localIP, localMAC);
|
||||
return;
|
||||
}
|
||||
} catch (UnknownHostException | SocketException e) {
|
||||
throw new MagentaTVException(e);
|
||||
}
|
||||
|
||||
throw new MagentaTVException(
|
||||
"Unable to get local IP / MAC address, check network settings in openHAB system configuration!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if client ip equals or is in range of ip networks provided by
|
||||
* semicolon separated list
|
||||
*
|
||||
* @param clientIp in numeric form like "192.168.0.10"
|
||||
* @param ipList like "127.0.0.1;192.168.0.0/24;10.0.0.0/8"
|
||||
* @return true if client ip from the list os ips and networks
|
||||
*/
|
||||
public static boolean isIpInSubnet(String clientIp, String ipList) {
|
||||
if (ipList.isEmpty()) {
|
||||
// No ip address provided
|
||||
return true;
|
||||
}
|
||||
String[] subnetMasks = ipList.split(";");
|
||||
for (String subnetMask : subnetMasks) {
|
||||
subnetMask = subnetMask.trim();
|
||||
if (clientIp.equals(subnetMask)) {
|
||||
return true;
|
||||
}
|
||||
if (subnetMask.contains("/")) {
|
||||
if (new SubnetUtils(subnetMask).getInfo().isInRange(clientIp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public NetworkInterface getLocalInterface() {
|
||||
return localInterface;
|
||||
}
|
||||
|
||||
public String getLocalIP() {
|
||||
return localIP;
|
||||
}
|
||||
|
||||
public String getLocalPort() {
|
||||
return localPort;
|
||||
}
|
||||
|
||||
public String getLocalMAC() {
|
||||
return localMAC;
|
||||
}
|
||||
|
||||
public static final int WOL_PORT = 9;
|
||||
|
||||
/**
|
||||
* Send a Wake-on-LAN packet
|
||||
*
|
||||
* @param ipAddr destination ip
|
||||
* @param macAddress destination MAC address
|
||||
* @throws MagentaTVException
|
||||
*/
|
||||
public void sendWakeOnLAN(String ipAddr, String macAddress) throws MagentaTVException {
|
||||
try {
|
||||
byte[] macBytes = getMacBytes(macAddress);
|
||||
byte[] bytes = new byte[6 + 16 * macBytes.length];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
bytes[i] = (byte) 0xff;
|
||||
}
|
||||
for (int i = 6; i < bytes.length; i += macBytes.length) {
|
||||
System.arraycopy(macBytes, 0, bytes, i, macBytes.length);
|
||||
}
|
||||
|
||||
InetAddress address = InetAddress.getByName(ipAddr);
|
||||
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, WOL_PORT);
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.send(packet);
|
||||
}
|
||||
|
||||
logger.debug("Wake-on-LAN packet sent to {} / {}", ipAddr, macAddress);
|
||||
} catch (IOException e) {
|
||||
throw new MagentaTVException(e, "Unable to send Wake-on-LAN packet to {} / {}", ipAddr, macAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MAC address from string to byte array
|
||||
*
|
||||
* @param macStr MAC address as string
|
||||
* @return MAC address as byte array
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
private static byte[] getMacBytes(String macStr) throws IllegalArgumentException {
|
||||
byte[] bytes = new byte[6];
|
||||
String[] hex = macStr.split("(\\:|\\-)");
|
||||
if (hex.length != 6) {
|
||||
throw new IllegalArgumentException("Invalid MAC address.");
|
||||
}
|
||||
try {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
bytes[i] = (byte) Integer.parseInt(hex[i], 16);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid hex digit in MAC address.", e);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.network;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVUtil.substringBetween;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ConfigurationPolicy;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Main OSGi service and HTTP servlet for MagentaTV NOTIFY.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
|
||||
public class MagentaTVNotifyServlet extends HttpServlet {
|
||||
private static final long serialVersionUID = 2119809008606371618L;
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVNotifyServlet.class);
|
||||
|
||||
private final MagentaTVHandlerFactory handlerFactory;
|
||||
|
||||
@Activate
|
||||
public MagentaTVNotifyServlet(@Reference MagentaTVHandlerFactory handlerFactory, @Reference HttpService httpService,
|
||||
Map<String, Object> config) {
|
||||
this.handlerFactory = handlerFactory;
|
||||
try {
|
||||
httpService.registerServlet(PAIRING_NOTIFY_URI, this, null, httpService.createDefaultHttpContext());
|
||||
logger.debug("Servlet started at {}", PAIRING_NOTIFY_URI);
|
||||
if (!handlerFactory.getNotifyServletStatus()) {
|
||||
handlerFactory.setNotifyServletStatus(true);
|
||||
}
|
||||
} catch (ServletException | NamespaceException e) {
|
||||
logger.warn("Could not start MagentaTVNotifyServlet: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify servlet handler (will be called by jetty
|
||||
*
|
||||
* Format of SOAP message:
|
||||
* <e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"> <e:property>
|
||||
* <uniqueDeviceID>1C18548DAF7DE9BC231249DB28D2A650</uniqueDeviceID>
|
||||
* </e:property> <e:property> <messageBody>X-pairingCheck:5218C0AA</messageBody>
|
||||
* </e:property> </e:propertyset>
|
||||
*
|
||||
* Format of event message: <?xml version="1.0"?>
|
||||
* <e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"> <e:property>
|
||||
* <STB_Mac>AC6FBB61B1E5</STB_Mac> </e:property> <e:property>
|
||||
* <STB_playContent>{"new_play_mode":0,"playBackState":1,&
|
||||
* quot;mediaType":1,"mediaCode":"3682"}</
|
||||
* STB_playContent> </e:property> </e:propertyset>
|
||||
*
|
||||
* @param request
|
||||
* @param resp
|
||||
*
|
||||
* @throws ServletException, IOException
|
||||
*/
|
||||
@Override
|
||||
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String data = inputStreamToString(request);
|
||||
try {
|
||||
if ((request == null) || (response == null)) {
|
||||
return;
|
||||
}
|
||||
String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
if (ipAddress == null) {
|
||||
ipAddress = request.getRemoteAddr();
|
||||
}
|
||||
String path = request.getRequestURI();
|
||||
logger.trace("Reqeust from {}:{}{} ({}, {})", ipAddress, request.getRemotePort(), path,
|
||||
request.getRemoteHost(), request.getProtocol());
|
||||
if (!path.equalsIgnoreCase(PAIRING_NOTIFY_URI)) {
|
||||
logger.debug("Invalid request received - path = {}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.contains(NOTIFY_PAIRING_CODE)) {
|
||||
String deviceId = data.substring(data.indexOf("<uniqueDeviceID>") + "<uniqueDeviceID>".length(),
|
||||
data.indexOf("</uniqueDeviceID>"));
|
||||
String pairingCode = data.substring(data.indexOf(NOTIFY_PAIRING_CODE) + NOTIFY_PAIRING_CODE.length(),
|
||||
data.indexOf("</messageBody>"));
|
||||
logger.debug("Pairing code {} received for deviceID {}", pairingCode, deviceId);
|
||||
if (!handlerFactory.notifyPairingResult(deviceId, ipAddress, pairingCode)) {
|
||||
logger.trace("Pairing data={}", data);
|
||||
}
|
||||
} else {
|
||||
if (data.contains("STB_")) {
|
||||
data = data.replaceAll(""", "\"");
|
||||
String stbMac = substringBetween(data, "<STB_Mac>", "</STB_Mac>");
|
||||
String stbEvent = "";
|
||||
if (data.contains("<STB_playContent>")) {
|
||||
stbEvent = substringBetween(data, "<STB_playContent>", "</STB_playContent>");
|
||||
} else if (data.contains("<STB_EitChanged>")) {
|
||||
stbEvent = substringBetween(data, "<STB_EitChanged>", "</STB_EitChanged>");
|
||||
} else {
|
||||
logger.debug("Unknown STB event: {}", data);
|
||||
}
|
||||
if (!stbEvent.isEmpty()) {
|
||||
if (!handlerFactory.notifyMREvent(stbMac, stbEvent)) {
|
||||
logger.debug("Event not processed, data={}", data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Unable to process http request, data={}", data != null ? data : "<empty>");
|
||||
} finally {
|
||||
// send response
|
||||
if (response != null) {
|
||||
response.setCharacterEncoding(UTF_8);
|
||||
response.getWriter().write("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private String inputStreamToString(@Nullable HttpServletRequest request) throws IOException {
|
||||
if (request == null) {
|
||||
return "";
|
||||
}
|
||||
Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\\A");
|
||||
return scanner.hasNext() ? scanner.next() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.network;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVUtil.substringAfterLast;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.ws.rs.HttpMethod;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVException;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponse;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponseInstanceCreator;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthTokenResponse;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthTokenResponseInstanceCreator;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentials;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentialsInstanceCreator;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthKeyValue;
|
||||
import org.openhab.binding.magentatv.internal.handler.MagentaTVControl;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVOAuth} class implements the OAuth authentication, which
|
||||
* is used to query the userID from the Telekom platform.
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*
|
||||
* Deutsche Telekom uses a OAuth-based authentication to access the EPG portal. The
|
||||
* communication between the MR and the remote app requires a pairing before the receiver could be
|
||||
* controlled by sending keys etc. The so called userID is not directly derived from any local parameters
|
||||
* (like terminalID as a has from the mac address), but will be returned as a result from the OAuth
|
||||
* authentication. This will be performed in 3 steps
|
||||
* 1. Get OAuth credentials -> Service URL, Scope, Secret, Client ID
|
||||
* 2. Get OAth Token -> authentication token for step 3
|
||||
* 3. Authenticate, which then provides the userID (beside other parameters)
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVOAuth {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVOAuth.class);
|
||||
final Gson gson;
|
||||
|
||||
public MagentaTVOAuth() {
|
||||
gson = new GsonBuilder().registerTypeAdapter(OauthCredentials.class, new OauthCredentialsInstanceCreator())
|
||||
.registerTypeAdapter(OAuthTokenResponse.class, new OAuthTokenResponseInstanceCreator())
|
||||
.registerTypeAdapter(OAuthAuthenticateResponse.class, new OAuthAuthenticateResponseInstanceCreator())
|
||||
.create();
|
||||
}
|
||||
|
||||
public String getUserId(String accountName, String accountPassword) throws MagentaTVException {
|
||||
logger.debug("Authenticate with account {}", accountName);
|
||||
if (accountName.isEmpty() || accountPassword.isEmpty()) {
|
||||
throw new MagentaTVException("Credentials for OAuth missing, check thing config!");
|
||||
}
|
||||
|
||||
String step = "initialize";
|
||||
String url = "";
|
||||
Properties httpHeader;
|
||||
String postData = "";
|
||||
String httpResponse = "";
|
||||
InputStream dataStream = null;
|
||||
|
||||
// OAuth autentication results
|
||||
String oAuthScope = "";
|
||||
String oAuthService = "";
|
||||
String epghttpsurl = "";
|
||||
String retcode = "";
|
||||
String retmsg = "";
|
||||
|
||||
try {
|
||||
step = "get credentials";
|
||||
httpHeader = initHttpHeader();
|
||||
url = OAUTH_GET_CRED_URL + ":" + OAUTH_GET_CRED_PORT + OAUTH_GET_CRED_URI;
|
||||
httpHeader.setProperty(HEADER_HOST, substringAfterLast(OAUTH_GET_CRED_URL, "/"));
|
||||
logger.trace("{} from {}", step, url);
|
||||
httpResponse = HttpUtil.executeUrl(HttpMethod.GET, url, httpHeader, null, null, NETWORK_TIMEOUT_MS);
|
||||
logger.trace("http response = {}", httpResponse);
|
||||
OauthCredentials cred = gson.fromJson(httpResponse, OauthCredentials.class);
|
||||
epghttpsurl = getString(cred.epghttpsurl);
|
||||
if (epghttpsurl.isEmpty()) {
|
||||
throw new MagentaTVException("Unable to determine EPG url");
|
||||
}
|
||||
if (!epghttpsurl.contains("/EPG")) {
|
||||
epghttpsurl = epghttpsurl + "/EPG";
|
||||
}
|
||||
logger.debug("epghttpsurl = {}", epghttpsurl);
|
||||
|
||||
// get OAuth data from response
|
||||
if (cred.sam3Para != null) {
|
||||
for (OauthKeyValue si : cred.sam3Para) {
|
||||
logger.trace("sam3Para.{} = {}", si.key, si.value);
|
||||
if (si.key.equalsIgnoreCase("oAuthScope")) {
|
||||
oAuthScope = si.value;
|
||||
} else if (si.key.equalsIgnoreCase("SAM3ServiceURL")) {
|
||||
oAuthService = si.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oAuthScope.isEmpty() || oAuthService.isEmpty()) {
|
||||
throw new MagentaTVException("OAuth failed: Can't get Scope and Service: " + httpResponse);
|
||||
}
|
||||
|
||||
// Get OAuth token
|
||||
step = "get token";
|
||||
url = oAuthService + "/oauth2/tokens";
|
||||
logger.debug("{} from {}", step, url);
|
||||
|
||||
String userId = "";
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
String cnonce = MagentaTVControl.computeMD5(uuid);
|
||||
// New flow based on WebTV
|
||||
postData = MessageFormat.format(
|
||||
"password={0}&scope={1}+offline_access&grant_type=password&username={2}&x_telekom.access_token.format=CompactToken&x_telekom.access_token.encoding=text%2Fbase64&client_id=10LIVESAM30000004901NGTVWEB0000000000000",
|
||||
URLEncoder.encode(accountPassword, UTF_8), oAuthScope, URLEncoder.encode(accountName, UTF_8));
|
||||
url = oAuthService + "/oauth2/tokens";
|
||||
dataStream = new ByteArrayInputStream(postData.getBytes(Charset.forName("UTF-8")));
|
||||
httpResponse = HttpUtil.executeUrl(HttpMethod.POST, url, httpHeader, dataStream, null, NETWORK_TIMEOUT_MS);
|
||||
logger.trace("http response={}", httpResponse);
|
||||
OAuthTokenResponse resp = gson.fromJson(httpResponse, OAuthTokenResponse.class);
|
||||
if (resp.accessToken.isEmpty()) {
|
||||
String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} ({2})",
|
||||
accountName, getString(resp.errorDescription), getString(resp.error));
|
||||
logger.warn("{}", errorMessage);
|
||||
throw new MagentaTVException(errorMessage);
|
||||
}
|
||||
|
||||
uuid = "t_" + MagentaTVControl.computeMD5(accountName);
|
||||
url = "https://web.magentatv.de/EPG/JSON/DTAuthenticate?SID=user&T=Mac_chrome_81";
|
||||
postData = "{\"userType\":1,\"terminalid\":\"" + uuid + "\",\"mac\":\"" + uuid + "\""
|
||||
+ ",\"terminaltype\":\"MACWEBTV\",\"utcEnable\":1,\"timezone\":\"Europe/Berlin\","
|
||||
+ "\"terminalDetail\":[{\"key\":\"GUID\",\"value\":\"" + uuid + "\"},"
|
||||
+ "{\"key\":\"HardwareSupplier\",\"value\":\"\"},{\"key\":\"DeviceClass\",\"value\":\"PC\"},"
|
||||
+ "{\"key\":\"DeviceStorage\",\"value\":\"1\"},{\"key\":\"DeviceStorageSize\",\"value\":\"\"}],"
|
||||
+ "\"softwareVersion\":\"\",\"osversion\":\"\",\"terminalvendor\":\"Unknown\","
|
||||
+ "\"caDeviceInfo\":[{\"caDeviceType\":6,\"caDeviceId\":\"" + uuid + "\"}]," + "\"accessToken\":\""
|
||||
+ resp.accessToken + "\",\"preSharedKeyID\":\"PC01P00002\",\"cnonce\":\"" + cnonce + "\"}";
|
||||
dataStream = new ByteArrayInputStream(postData.getBytes(Charset.forName("UTF-8")));
|
||||
logger.debug("HTTP POST {}, postData={}", url, postData);
|
||||
httpResponse = HttpUtil.executeUrl(HttpMethod.POST, url, httpHeader, dataStream, null, NETWORK_TIMEOUT_MS);
|
||||
|
||||
logger.trace("http response={}", httpResponse);
|
||||
OAuthAuthenticateResponse authResp = gson.fromJson(httpResponse, OAuthAuthenticateResponse.class);
|
||||
if (authResp.userID.isEmpty()) {
|
||||
String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} {2}",
|
||||
accountName, getString(authResp.retcode), getString(authResp.desc));
|
||||
logger.warn("{}", errorMessage);
|
||||
throw new MagentaTVException(errorMessage);
|
||||
}
|
||||
userId = getString(authResp.userID);
|
||||
if (userId.isEmpty()) {
|
||||
throw new MagentaTVException("No userID received!");
|
||||
}
|
||||
String hashedUserID = MagentaTVControl.computeMD5(userId).toUpperCase();
|
||||
logger.trace("done, userID = {}", hashedUserID);
|
||||
return hashedUserID;
|
||||
} catch (IOException e) {
|
||||
throw new MagentaTVException(e,
|
||||
"Unable to authenticate {0}: {1} failed; serviceURL={2}, rc={3}/{4}, response={5}", accountName,
|
||||
step, oAuthService, retcode, retmsg, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private Properties initHttpHeader() {
|
||||
Properties httpHeader = new Properties();
|
||||
httpHeader.setProperty(HEADER_USER_AGENT, OAUTH_USER_AGENT);
|
||||
httpHeader.setProperty(HEADER_ACCEPT, "*/*");
|
||||
httpHeader.setProperty(HEADER_LANGUAGE, "de-de");
|
||||
httpHeader.setProperty(HEADER_CACHE_CONTROL, "no-cache");
|
||||
return httpHeader;
|
||||
}
|
||||
|
||||
private String getString(@Nullable String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.magentatv.internal.network;
|
||||
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
|
||||
import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.MulticastSocket;
|
||||
import java.net.NetworkInterface;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.magentatv.internal.MagentaTVHandlerFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MagentaTVPoweroffListener} implements a UPnP listener to detect
|
||||
* power-off of the receiver
|
||||
*
|
||||
* @author Markus Michels - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MagentaTVPoweroffListener extends Thread {
|
||||
private final Logger logger = LoggerFactory.getLogger(MagentaTVPoweroffListener.class);
|
||||
|
||||
private final MagentaTVHandlerFactory handlerFactory;
|
||||
|
||||
public static final String UPNP_MULTICAST_ADDRESS = "239.255.255.250";
|
||||
public static final int UPNP_PORT = 1900;
|
||||
public static final String UPNP_BYEBYE_MESSAGE = "ssdp:byebye";
|
||||
|
||||
protected final MulticastSocket socket;
|
||||
protected @Nullable NetworkInterface networkInterface;
|
||||
protected byte[] buf = new byte[256];
|
||||
|
||||
public MagentaTVPoweroffListener(MagentaTVHandlerFactory handlerFactory,
|
||||
@Nullable NetworkInterface networkInterface) throws IOException {
|
||||
setName("OH-Binding-magentatv-upnp-listener");
|
||||
setDaemon(true);
|
||||
|
||||
this.handlerFactory = handlerFactory;
|
||||
this.networkInterface = networkInterface;
|
||||
socket = new MulticastSocket(UPNP_PORT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (!isStarted()) {
|
||||
logger.debug("Listening to SSDP shutdown messages");
|
||||
super.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listening thread. Receive SSDP multicast packets and filter for byebye If
|
||||
* such a packet is received the handlerFactory is called, which then dispatches
|
||||
* the event to the thing handler.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
logger.debug("SSDP listener started");
|
||||
socket.setReceiveBufferSize(1024);
|
||||
socket.setReuseAddress(true);
|
||||
|
||||
// Join the Multicast group on the selected network interface
|
||||
socket.setNetworkInterface(networkInterface);
|
||||
InetAddress group = InetAddress.getByName(UPNP_MULTICAST_ADDRESS);
|
||||
socket.joinGroup(group);
|
||||
|
||||
// read the SSDP messages
|
||||
while (!socket.isClosed()) {
|
||||
DatagramPacket packet = new DatagramPacket(buf, buf.length);
|
||||
socket.receive(packet);
|
||||
String message = new String(packet.getData(), 0, packet.getLength());
|
||||
try {
|
||||
String ipAddress = substringAfter(packet.getAddress().toString(), "/");
|
||||
if (message.contains("NTS: ")) {
|
||||
String ssdpMsg = substringBetween(message, "NTS: ", "\r");
|
||||
if (ssdpMsg != null) {
|
||||
if (message.contains(MR400_DEF_DESCRIPTION_URL)
|
||||
|| message.contains(MR401B_DEF_DESCRIPTION_URL)) {
|
||||
if (ssdpMsg.contains(UPNP_BYEBYE_MESSAGE)) {
|
||||
handlerFactory.onPowerOff(ipAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Unable to process SSDP message: {}", message);
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException e) {
|
||||
logger.debug("Poweroff listener failure: {}", e.getMessage());
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return socket.isBound();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the socket gets closed
|
||||
*/
|
||||
public void close() {
|
||||
if (isStarted()) {
|
||||
logger.debug("No longer listening to SSDP messages");
|
||||
if (!socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the socket gets closed
|
||||
*/
|
||||
public void dispose() {
|
||||
logger.debug("SSDP listener terminated");
|
||||
close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="magentatv" 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>MagentaTV Binding</name>
|
||||
<description>This is the binding for MagentaTV receivers</description>
|
||||
<author>Markus Michels</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,107 @@
|
||||
# binding
|
||||
binding.magentatv.name = MagentaTV Binding
|
||||
binding.magentatv.description = Dieses Binding integriert Telekom MagentaTV Receiver.
|
||||
|
||||
# thing types
|
||||
thing-type.magentatv.receiver.label = Media Receiver
|
||||
thing-type.magentatv.receiver.description = Media Receiver zum Epmfang von MagentaTV
|
||||
|
||||
# Thing configuration
|
||||
thing-type.config.magentatv.receiver.ipAddress.label = IP-Adresse
|
||||
thing-type.config.magentatv.ipAddress.description = IP Adresse des Media Receivers
|
||||
thing-type.config.magentatv.receiver.userId.label = User ID
|
||||
thing-type.config.magentatv.receiver.userId.description = Technische Benutzerkennung, siehe Dokumentation
|
||||
thing-type.config.magentatv.receiver.accountName.label = Login-Name
|
||||
thing-type.config.magentatv.receiver.accountName.description = Login-Name (E-Mail) zur Anmeldung im Telekom Kundencenter
|
||||
thing-type.config.magentatv.receiver.accountPassword.label = Passwort
|
||||
thing-type.config.magentatv.receiver.accountPassword.description = Passwort für den Zugang zum Telekom Kundencenter
|
||||
thing-type.config.magentatv.receiver.udn.label = UDN
|
||||
thing-type.config.magentatv.receiver.udn.description = Unique Device Number des Receivers (UPnP UDN)
|
||||
thing-type.config.magentatv.receiver.port.label = IP Port
|
||||
thing-type.config.magentatv.receiver.port.description = Ziel IP-Port für Fernzugriff
|
||||
|
||||
|
||||
# channel-groups
|
||||
thing-type.magentatv.receiver.group.control.label = Steuerung
|
||||
thing-type.magentatv.receiver.group.control.description = Funktionen zur Steuerung des Receivers
|
||||
thing-type.magentatv.receiver.group.program.label = Programm
|
||||
thing-type.magentatv.receiver.group.program.description = Informationen zum laufenden Programm
|
||||
thing-type.magentatv.receiver.group.status.label = Status
|
||||
thing-type.magentatv.receiver.group.status.description = Weitere Statusinformationen
|
||||
|
||||
# channels
|
||||
channel-type.magentatv.channelNumber.label = Kanal
|
||||
channel-type.magentatv.channelNumber.description = Programmkanal
|
||||
channel-type.magentatv.player.label = Fernbedienung
|
||||
channel-type.magentatv.player.description = Steuerung der Abspielfunktion des Receivers
|
||||
channel-type.magentatv.programText.label = Beschreibung
|
||||
channel-type.magentatv.programText.description = Programmbeschreibung, wie vom Sender ausgestrahlt.
|
||||
channel-type.magentatv.programStart.label = Start
|
||||
channel-type.magentatv.programStart.description = Startzeitpunkt der Sendung
|
||||
channel-type.magentatv.programDuration.label = Spieldauer
|
||||
channel-type.magentatv.programDuration.description = Spieldauer, sofern bekannt.
|
||||
channel-type.magentatv.programPosition.label = Position
|
||||
channel-type.magentatv.programPosition.description = Position innerhalb der Sendung.
|
||||
channel-type.magentatv.channelCode.label = Kanalcode
|
||||
channel-type.magentatv.channelCode.description = Kanalcode
|
||||
channel-type.magentatv.runStatus.label = Abspielstatus
|
||||
channel-type.magentatv.runStatus.description = Status der Abspielung.
|
||||
channel-type.magentatv.playMode.label = Abspielmodus
|
||||
channel-type.magentatv.playMode.description = Modus der Übertragung.
|
||||
channel-type.magentatv.key.command.option.POWER = Betrieb
|
||||
channel-type.magentatv.key.command.option.INFO = Info
|
||||
channel-type.magentatv.key.command.option.MENU = Menü
|
||||
channel-type.magentatv.key.command.option.EPG = EPG
|
||||
channel-type.magentatv.key.command.option.TTEXT = Teletext
|
||||
channel-type.magentatv.key.command.option.PORTAL = Portal
|
||||
channel-type.magentatv.key.command.option.STAR = *
|
||||
channel-type.magentatv.key.command.option.POUND = #
|
||||
channel-type.magentatv.key.command.option.SPACE = Leertaste
|
||||
channel-type.magentatv.key.command.option.OK = Ok
|
||||
channel-type.magentatv.key.command.option.ENTER = Enter
|
||||
channel-type.magentatv.key.command.option.BACK = Zurück
|
||||
channel-type.magentatv.key.command.option.DELETE = Löschen
|
||||
channel-type.magentatv.key.command.option.EXIT = Exit
|
||||
channel-type.magentatv.key.command.option.OPTION = Opt
|
||||
channel-type.magentatv.key.command.option.SETTINGS = Einstellungen
|
||||
channel-type.magentatv.key.command.option.UP = Hoch
|
||||
channel-type.magentatv.key.command.option.DOWN = Runter
|
||||
channel-type.magentatv.key.command.option.LEFT = Links
|
||||
channel-type.magentatv.key.command.option.RIGHT = Rechts
|
||||
channel-type.magentatv.key.command.option.PGUP = Seite hoch
|
||||
channel-type.magentatv.key.command.option.PGDOWN = Seite ab
|
||||
channel-type.magentatv.key.command.option.FAV = Favoriten
|
||||
channel-type.magentatv.key.command.option.RED = rot
|
||||
channel-type.magentatv.key.command.option.GREEN = grün
|
||||
channel-type.magentatv.key.command.option.BLUE = blau
|
||||
channel-type.magentatv.key.command.option.YELLOW = gelb
|
||||
channel-type.magentatv.key.command.option.SEARCH = Suche
|
||||
channel-type.magentatv.key.command.option.NEXT = Weiter
|
||||
channel-type.magentatv.key.command.option.VOLUP = Lauter
|
||||
channel-type.magentatv.key.command.option.VOLDOWN = Leister
|
||||
channel-type.magentatv.key.command.option.MUTE = Stumm
|
||||
channel-type.magentatv.key.command.option.CHUP = Kanal auf
|
||||
channel-type.magentatv.key.command.option.CHDOWN = Kanal ab
|
||||
channel-type.magentatv.key.command.option.LASTCH = Letzter Kanal
|
||||
channel-type.magentatv.key.command.option.NEXTCH = Nächster Kanal
|
||||
channel-type.magentatv.key.command.option.PREVSH = Vorh. Kanal
|
||||
channel-type.magentatv.key.command.option.BEGIN = Beginn
|
||||
channel-type.magentatv.key.command.option.END = Ende
|
||||
channel-type.magentatv.key.command.option.PLAY = Abspielen
|
||||
channel-type.magentatv.key.command.option.PAUSE = Pause
|
||||
channel-type.magentatv.key.command.option.REWIND = Zurückspuelen
|
||||
channel-type.magentatv.key.command.option.FORWARD = Vorspulen
|
||||
channel-type.magentatv.key.command.option.TRACK = Spur
|
||||
channel-type.magentatv.key.command.option.REPLAY = Wiederholen
|
||||
channel-type.magentatv.key.command.option.SKIP = Überspringen
|
||||
channel-type.magentatv.key.command.option.STOP = Stop
|
||||
channel-type.magentatv.key.command.option.RECORD = Aufnahme
|
||||
channel-type.magentatv.key.command.option.SUBTITLES = Untertitel
|
||||
channel-type.magentatv.key.command.option.MEDIA = Media
|
||||
channel-type.magentatv.key.command.option.INTER = Interaktion
|
||||
channel-type.magentatv.key.command.option.SOURCE = Quelle
|
||||
channel-type.magentatv.key.command.option.SWITCH = IPTV/DVB
|
||||
channel-type.magentatv.key.command.option.IPTV = IPTV
|
||||
channel-type.magentatv.key.command.option.PIP = PIP
|
||||
channel-type.magentatv.key.command.option.MULTIVIEW = Multi View
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="magentatv"
|
||||
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="receiver">
|
||||
<label>MagentaTV Media Receiver</label>
|
||||
<description>Represents a Telekom Media Receiver for MagentaTV</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="control" typeId="control">
|
||||
<label>Control</label>
|
||||
</channel-group>
|
||||
<channel-group id="program" typeId="program">
|
||||
<label>Program Information</label>
|
||||
</channel-group>
|
||||
<channel-group id="status" typeId="status">
|
||||
<label>Play Status</label>
|
||||
</channel-group>
|
||||
</channel-groups>
|
||||
|
||||
|
||||
<config-description uri="thing-type:magentatv:receiver">
|
||||
<parameter name="ipAddress" type="text">
|
||||
<label>Device IP Address</label>
|
||||
<description>IP address of the receiver</description>
|
||||
<required>true</required>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="userId" type="text">
|
||||
<label>User ID</label>
|
||||
<description>Technical User ID required for pairing process</description>
|
||||
</parameter>
|
||||
<parameter name="accountName" type="text">
|
||||
<label>Account Name</label>
|
||||
<description>Credentials: Login name (e.g. xxx@t-online.de, same as for the Telekom Kundencenter)</description>
|
||||
</parameter>
|
||||
<parameter name="accountPassword" type="text">
|
||||
<label>Account Password</label>
|
||||
<description>Credentials: Account Password (same as for the Telekom Kundencenter)</description>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the Media Receiver</description>
|
||||
<required>true</required>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="port" type="text">
|
||||
<label>Port</label>
|
||||
<description>Port address for UPnP</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-group-type id="control">
|
||||
<label>Control</label>
|
||||
<description>Control function for your Media Receiver</description>
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
<channel id="channel" typeId="channelNumber"/>
|
||||
<channel id="player" typeId="system.media-control"/>
|
||||
<channel id="mute" typeId="system.mute"/>
|
||||
<channel id="key" typeId="key"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="program">
|
||||
<label>Program Information</label>
|
||||
<description>Information on the running program</description>
|
||||
<channels>
|
||||
<channel id="title" typeId="system.media-title"/>
|
||||
<channel id="text" typeId="programText"/>
|
||||
<channel id="start" typeId="programStart"/>
|
||||
<channel id="duration" typeId="programDuration"/>
|
||||
<channel id="position" typeId="programPosition"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="status">
|
||||
<label>Play Status</label>
|
||||
<description>Status information on media play</description>
|
||||
<channels>
|
||||
<channel id="channelCode" typeId="channelCode"/>
|
||||
<channel id="runStatus" typeId="runStatus"/>
|
||||
<channel id="playMode" typeId="playMode"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="channelNumber">
|
||||
<item-type>Number</item-type>
|
||||
<label>Channel</label>
|
||||
<description>Send channel number to switch program</description>
|
||||
<state min="1" max="999" step="1"></state>
|
||||
</channel-type>
|
||||
<channel-type id="channelCode" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Channel Code</label>
|
||||
<description>Channel code in the channel list</description>
|
||||
<state readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="runStatus" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Status</label>
|
||||
<description>Run status</description>
|
||||
<state readOnly="true" pattern="%s">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="playMode">
|
||||
<item-type>String</item-type>
|
||||
<label>Play Mode</label>
|
||||
<description>Play Mode for running program</description>
|
||||
<state readOnly="true" pattern="%s">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="programTitle">
|
||||
<item-type>String</item-type>
|
||||
<label>Program</label>
|
||||
<description>Running program</description>
|
||||
<state readOnly="true" pattern="%s">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="programText">
|
||||
<item-type>String</item-type>
|
||||
<label>Description</label>
|
||||
<description>Some info on the running program</description>
|
||||
<state readOnly="true" pattern="%s">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="programStart">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Start</label>
|
||||
<description>Program start time</description>
|
||||
<state readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="programDuration">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Duration</label>
|
||||
<description>Duration of the program</description>
|
||||
<state pattern="%d %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="programPosition">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Play Position</label>
|
||||
<description>Play Position since program started</description>
|
||||
<state pattern="%d %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="key">
|
||||
<item-type>String</item-type>
|
||||
<label>Key</label>
|
||||
<description>Send Key to the Media Receive (POWER/MENU/INFO... - see documentation)</description>
|
||||
<command>
|
||||
<options>
|
||||
<option value="POWER">POWER</option>
|
||||
<option value="HELP">Help</option>
|
||||
<option value="INFO">Info</option>
|
||||
<option value="MENU">Menu</option>
|
||||
<option value="EPG">EPG</option>
|
||||
<option value="TTEXT">TeleText</option>
|
||||
<option value="PORTAL">Portal</option>
|
||||
<option value="STAR">*</option>
|
||||
<option value="POUND">#</option>
|
||||
<option value="SPACE">Space</option>
|
||||
<option value="OK">Ok</option>
|
||||
<option value="ENTER ">Enter</option>
|
||||
<option value="BACK">Back</option>
|
||||
<option value="DELETE">Delete</option>
|
||||
<option value="EXIT">Exit</option>
|
||||
<option value="OPTION">Opt</option>
|
||||
<option value="SETTINGS">Settings</option>
|
||||
<option value="UP">Up</option>
|
||||
<option value="DOWN">Down</option>
|
||||
<option value="LEFT">Left</option>
|
||||
<option value="RIGHT">Right</option>
|
||||
<option value="PGUP">Page Up</option>
|
||||
<option value="PGDOWN">Page Down</option>
|
||||
<option value="FAV">Favorites</option>
|
||||
<option value="RED">red</option>
|
||||
<option value="GREEN">green</option>
|
||||
<option value="YELLOW">yellow</option>
|
||||
<option value="BLUE">blue</option>
|
||||
<option value="SEARCH">Search</option>
|
||||
<option value="NEXT">Next</option>
|
||||
<option value="VOLUP">VolUp</option>
|
||||
<option value="VOLDOWN">VolDown</option>
|
||||
<option value="MUTE">Mute</option>
|
||||
<option value="CHUP">ChanUp</option>
|
||||
<option value="CHDOWN">ChanDown</option>
|
||||
<option value="LASTCH">Last Channel</option>
|
||||
<option value="NEXTCH">Next Channel</option>
|
||||
<option value="PREVCH">Prev Channel</option>
|
||||
<option value="BEGIN">Go Begin</option>
|
||||
<option value="END">Go End</option>
|
||||
<option value="PLAY">Play</option>
|
||||
<option value="PAUSE">Pause</option>
|
||||
<option value="REWIND">Rewind</option>
|
||||
<option value="FORWARD">Forward</option>
|
||||
<option value="PREVCHAP">Prev Chapter</option>
|
||||
<option value="NEXTCHAP">Next Chapter</option>
|
||||
<option value="TRACK">Track</option>
|
||||
<option value="REPLAY">Replay</option>
|
||||
<option value="SKIP">Skip</option>
|
||||
<option value="STOP">Stop</option>
|
||||
<option value="RECORD">Record</option>
|
||||
<option value="SUBTITLES">Sub Titles</option>
|
||||
<option value="MEDIA">Media</option>
|
||||
<option value="INTER">Interaction</option>
|
||||
<option value="SOURCE">Source</option>
|
||||
<option value="SWITCH">Switch IPTV/DVB</option>
|
||||
<option value="IPTV">IPTV</option>
|
||||
<option value="PIP">PIP</option>
|
||||
<option value="MULTIVIEW">Multi View</option>
|
||||
</options>
|
||||
</command>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user