added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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>

View File

@@ -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";
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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 "";
}
}

View File

@@ -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();
}
}

View File

@@ -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 : "";
}
}

View File

@@ -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 : "";
}
}

View File

@@ -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>&quot; 334
* &quot;</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>&quot; ")) {
version = substringBetween(result, "<productVersionNumber>&quot; ", " &quot;</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");
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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>{&quot;new_play_mode&quot;:0,&quot;playBackState&quot;:1,&
* quot;mediaType&quot;:1,&quot;mediaCode&quot;:&quot;3682&quot;}</
* 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("&quot;", "\"");
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() : "";
}
}

View File

@@ -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 : "";
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>