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