[androidtv] AndroidTV Binding initial contribution (#14282)

Signed-off-by: Ben Rosenblum <rosenblumb@gmail.com>
This commit is contained in:
morph166955 2023-06-26 01:49:42 -05:00 committed by GitHub
parent 5a1daa252e
commit 46039efd0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 5749 additions and 0 deletions

View File

@ -24,6 +24,7 @@
/bundles/org.openhab.binding.ambientweather/ @mhilbush
/bundles/org.openhab.binding.amplipi/ @kaikreuzer
/bundles/org.openhab.binding.androiddebugbridge/ @GiviMAD
/bundles/org.openhab.binding.androidtv/ @morph166955
/bundles/org.openhab.binding.anel/ @paphko
/bundles/org.openhab.binding.anthem/ @mhilbush
/bundles/org.openhab.binding.astro/ @gerrieg

View File

@ -111,6 +111,11 @@
<artifactId>org.openhab.binding.androiddebugbridge</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.androidtv</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.anel</artifactId>

View File

@ -0,0 +1,13 @@
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/openhab-addons

View File

@ -0,0 +1,484 @@
# AndroidTV Binding
This binding is designed to emulate different protocols to interact with the AndroidTV platform.
Currently it emulates both the Google Video App to interact with a variety of AndroidTVs for purposes of remote control.
It also currently emulates the Nvidia ShieldTV Android App to interact with an Nvidia ShieldTV for purposes of remote control.
## Supported Things
This binding supports two thing types:
- **googletv** - An AndroidTV running Google Video
- **shieldtv** - An Nvidia ShieldTV
## Discovery
Both GoogleTVs and ShieldTVs should be added automatically to the inbox through the mDNS discovery process.
In the case of the ShieldTV, openHAB will likely create an inbox entry for both a GoogleTV and a ShieldTV device.
Only the ShieldTV device should be configured, the GoogleTV can be ignored.
There is no benefit to configuring two things for a ShieldTV device.
This could cause undesired effects.
## Binding Configuration
This binding does not require any special configuration files.
This binding does require a PIN login process (documented below) upon first connection.
This binding requires GoogleTV to be installed on the device (https://play.google.com/store/apps/details?id=com.google.android.videos)
## Thing Configuration
There are three required fields to connect successfully to a ShieldTV.
| Name | Type | Description | Default | Required | Advanced |
|------------------|---------|---------------------------------------|---------|----------|----------|
| ipAddress | text | IP address of the device | N/A | yes | no |
| keystore | text | Location of the Java Keystore | N/A | no | no |
| keystorePassword | text | Password of the Java Keystore | N/A | no | no |
```java
Thing androidtv:shieldtv:livingroom [ ipAddress="192.168.1.2" ]
Thing androidtv:googletv:theater [ ipAddress="192.168.1.3" ]
```
## Channels
| Channel | Type | Description | GoogleTV | ShieldTV |
|------------|--------|-----------------------------|----------|----------|
| keyboard | String | Keyboard Data Entry | RW | RW |
| keypress | String | Manual Key Press Entry | RW | RW |
| keycode | String | Direct KEYCODE Entry | RW | RW |
| pincode | String | PIN Code Entry | RW | RW |
| app | String | App Control | RO | RW |
| appname | String | App Name | N/A | RW |
| appurl | String | App URL | N/A | RW |
| player | Player | Player Control | RW | RW |
| power | Switch | Power Control | RW | RW |
| volume | Dimmer | Volume Control | RO | RO |
| mute | Switch | Mute Control | RW | RW |
```java
String ShieldTV_KEYBOARD "KEYBOARD [%s]" { channel = "androidtv:shieldtv:livingroom:keyboard" }
String ShieldTV_KEYPRESS "KEYPRESS [%s]" { channel = "androidtv:shieldtv:livingroom:keypress" }
String ShieldTV_KEYCODE "KEYCODE [%s]" { channel = "androidtv:shieldtv:livingroom:keycode" }
String ShieldTV_PINCODE "PINCODE [%s]" { channel = "androidtv:shieldtv:livingroom:pincode" }
String ShieldTV_APP "APP [%s]" { channel = "androidtv:shieldtv:livingroom:app" }
String ShieldTV_APPNAME "APPNAME [%s]" { channel = "androidtv:shieldtv:livingroom:appname" }
String ShieldTV_APPURL "APPURL [%s]" { channel = "androidtv:shieldtv:livingroom:appurl" }
Player ShieldTV_PLAYER "PLAYER [%s]" { channel = "androidtv:shieldtv:livingroom:player" }
Switch ShieldTV_POWER "POWER [%s]" { channel = "androidtv:shieldtv:livingroom:power" }
Dimmer ShieldTV_VOLUME "VOLUME [%s]" { channel = "androidtv:shieldtv:livingroom:volume" }
Switch ShieldTV_MUTE "MUTE [%s]" { channel = "androidtv:shieldtv:livingroom:mute" }
String GoogleTV_KEYBOARD "KEYBOARD [%s]" { channel = "androidtv:googletv:theater:keyboard" }
String GoogleTV_KEYPRESS "KEYPRESS [%s]" { channel = "androidtv:googletv:theater:keypress" }
String GoogleTV_KEYCODE "KEYCODE [%s]" { channel = "androidtv:googletv:theater:keycode" }
String GoogleTV_PINCODE "PINCODE [%s]" { channel = "androidtv:googletv:theater:pincode" }
String GoogleTV_APP "APP [%s]" { channel = "androidtv:googletv:theater:app" }
Player GoogleTV_PLAYER "PLAYER [%s]" { channel = "androidtv:googletv:theater:player" }
Switch GoogleTV_POWER "POWER [%s]" { channel = "androidtv:googletv:theater:power" }
Dimmer GoogleTV_VOLUME "VOLUME [%s]" { channel = "androidtv:googletv:theater:volume" }
Switch GoogleTV_MUTE "MUTE [%s]" { channel = "androidtv:googletv:theater:mute" }
```
KEYPRESS will accept the following commands as strings (case sensitive):
- KEY_UP
- KEY_DOWN
- KEY_RIGHT
- KEY_LEFT
- KEY_ENTER
- KEY_HOME
- KEY_BACK
- KEY_MENU
- KEY_PLAY
- KEY_PAUSE
- KEY_PLAYPAUSE
- KEY_STOP
- KEY_NEXT
- KEY_PREVIOUS
- KEY_REWIND
- KEY_FORWARD
- KEY_POWER
- KEY_GOOGLE
- KEY_VOLUP
- KEY_VOLDOWN
- KEY_MUTE
- KEY_SUBMIT
The list above causes an instantanious "press and release" of each button.
If you would like to manually control the press and release of each you may append _PRESS and _RELEASE to the end of each.
(e.g. KEY_FORWARD_PRESS or KEY_FORWARD_RELEASE)
You may also send an ASCII character as a single letter to simulate a key entry (e.g KEY_A, KEY_1, KEY_z).
Use KEY_SUBMIT when full text entry is complete to tell the shield to process the line.
KEY_SUBMIT is automatically sent by KEYBOARD when a command is sent to the channel.
APP will display the currently active app as presented by the AndroidTV.
You may also send it a command of the app package name (e.g. com.google.android.youtube.tv) to start/change-to that app.
KEYCODE values are listed at the bottom of this README.
NOTE: Not all KEYCODES work on all devices. Keycodes above 255 have not been tested.
## Pin Code Process
For the AndroidTV to be successfully accessed an on-screen PIN authentication is required on the first connection.
To begin the PIN process, send the text "REQUEST" to the pincode channel while watching your AndroidTV.
A 6 digit PIN should be displayed on the screen.
To complete the PIN process, send the PIN displayed to the pincode channel.
The display should return back to where it was originally.
If you are on a ShieldTV you must run that process a second time to authenticate the GoogleTV protocol stack.
This completes the PIN process.
Upon reconnection (either from reconfiguration or a restart of OpenHAB), you should now see a message of "Login Successful" in openhab.log
## Full Example
```java
Thing androidtv:shieldtv:livingroom [ ipAddress="192.168.1.2" ]
Thing androidtv:googletv:theater [ ipAddress="192.168.1.3" ]
```
```java
String ShieldTV_KEYBOARD "KEYBOARD [%s]" { channel = "androidtv:shieldtv:livingroom:keyboard" }
String ShieldTV_KEYPRESS "KEYPRESS [%s]" { channel = "androidtv:shieldtv:livingroom:keypress" }
String ShieldTV_KEYCODE "KEYCODE [%s]" { channel = "androidtv:shieldtv:livingroom:keycode" }
String ShieldTV_PINCODE "PINCODE [%s]" { channel = "androidtv:shieldtv:livingroom:pincode" }
String ShieldTV_APP "APP [%s]" { channel = "androidtv:shieldtv:livingroom:app" }
String ShieldTV_APPNAME "APPNAME [%s]" { channel = "androidtv:shieldtv:livingroom:appname" }
String ShieldTV_APPURL "APPURL [%s]" { channel = "androidtv:shieldtv:livingroom:appurl" }
Player ShieldTV_PLAYER "PLAYER [%s]" { channel = "androidtv:shieldtv:livingroom:player" }
Switch ShieldTV_POWER "POWER [%s]" { channel = "androidtv:shieldtv:livingroom:power" }
Dimmer ShieldTV_VOLUME "VOLUME [%s]" { channel = "androidtv:shieldtv:livingroom:volume" }
Switch ShieldTV_MUTE "MUTE [%s]" { channel = "androidtv:shieldtv:livingroom:mute" }
String GoogleTV_KEYBOARD "KEYBOARD [%s]" { channel = "androidtv:googletv:theater:keyboard" }
String GoogleTV_KEYPRESS "KEYPRESS [%s]" { channel = "androidtv:googletv:theater:keypress" }
String GoogleTV_KEYCODE "KEYCODE [%s]" { channel = "androidtv:googletv:theater:keycode" }
String GoogleTV_PINCODE "PINCODE [%s]" { channel = "androidtv:googletv:theater:pincode" }
String GoogleTV_APP "APP [%s]" { channel = "androidtv:googletv:theater:app" }
Player GoogleTV_PLAYER "PLAYER [%s]" { channel = "androidtv:googletv:theater:player" }
Switch GoogleTV_POWER "POWER [%s]" { channel = "androidtv:googletv:theater:power" }
Dimmer GoogleTV_VOLUME "VOLUME [%s]" { channel = "androidtv:googletv:theater:volume" }
Switch GoogleTV_MUTE "MUTE [%s]" { channel = "androidtv:googletv:theater:mute" }
```
## Google Keycodes
| CODE | BUTTON |
|------|--------|
| 0 | KEYCODE_UNKNOWN |
| 1 | KEYCODE_SOFT_LEFT |
| 2 | KEYCODE_SOFT_RIGHT |
| 3 | KEYCODE_HOME |
| 4 | KEYCODE_BACK |
| 5 | KEYCODE_CALL |
| 6 | KEYCODE_ENDCALL |
| 7 | KEYCODE_0 |
| 8 | KEYCODE_1 |
| 9 | KEYCODE_2 |
| 10 | KEYCODE_3 |
| 11 | KEYCODE_4 |
| 12 | KEYCODE_5 |
| 13 | KEYCODE_6 |
| 14 | KEYCODE_7 |
| 15 | KEYCODE_8 |
| 16 | KEYCODE_9 |
| 17 | KEYCODE_STAR |
| 18 | KEYCODE_POUND |
| 19 | KEYCODE_DPAD_UP |
| 20 | KEYCODE_DPAD_DOWN |
| 21 | KEYCODE_DPAD_LEFT |
| 22 | KEYCODE_DPAD_RIGHT |
| 23 | KEYCODE_DPAD_CENTER |
| 24 | KEYCODE_VOLUME_UP |
| 25 | KEYCODE_VOLUME_DOWN |
| 26 | KEYCODE_POWER |
| 27 | KEYCODE_CAMERA |
| 28 | KEYCODE_CLEAR |
| 29 | KEYCODE_A |
| 30 | KEYCODE_B |
| 31 | KEYCODE_C |
| 32 | KEYCODE_D |
| 33 | KEYCODE_E |
| 34 | KEYCODE_F |
| 35 | KEYCODE_G |
| 36 | KEYCODE_H |
| 37 | KEYCODE_I |
| 38 | KEYCODE_J |
| 39 | KEYCODE_K |
| 40 | KEYCODE_L |
| 41 | KEYCODE_M |
| 42 | KEYCODE_N |
| 43 | KEYCODE_O |
| 44 | KEYCODE_P |
| 45 | KEYCODE_Q |
| 46 | KEYCODE_R |
| 47 | KEYCODE_S |
| 48 | KEYCODE_T |
| 49 | KEYCODE_U |
| 50 | KEYCODE_V |
| 51 | KEYCODE_W |
| 52 | KEYCODE_X |
| 53 | KEYCODE_Y |
| 54 | KEYCODE_Z |
| 55 | KEYCODE_COMMA |
| 56 | KEYCODE_PERIOD |
| 57 | KEYCODE_ALT_LEFT |
| 58 | KEYCODE_ALT_RIGHT |
| 59 | KEYCODE_SHIFT_LEFT |
| 60 | KEYCODE_SHIFT_RIGHT |
| 61 | KEYCODE_TAB |
| 62 | KEYCODE_SPACE |
| 63 | KEYCODE_SYM |
| 64 | KEYCODE_EXPLORER |
| 65 | KEYCODE_ENVELOPE |
| 66 | KEYCODE_ENTER |
| 67 | KEYCODE_DEL |
| 68 | KEYCODE_GRAVE |
| 69 | KEYCODE_MINUS |
| 70 | KEYCODE_EQUALS |
| 71 | KEYCODE_LEFT_BRACKET |
| 72 | KEYCODE_RIGHT_BRACKET |
| 73 | KEYCODE_BACKSLASH |
| 74 | KEYCODE_SEMICOLON |
| 75 | KEYCODE_APOSTROPHE |
| 76 | KEYCODE_SLASH |
| 77 | KEYCODE_AT |
| 78 | KEYCODE_NUM |
| 79 | KEYCODE_HEADSETHOOK |
| 80 | KEYCODE_FOCUS |
| 81 | KEYCODE_PLUS |
| 82 | KEYCODE_MENU |
| 83 | KEYCODE_NOTIFICATION |
| 84 | KEYCODE_SEARCH |
| 85 | KEYCODE_MEDIA_PLAY_PAUSE |
| 86 | KEYCODE_MEDIA_STOP |
| 87 | KEYCODE_MEDIA_NEXT |
| 88 | KEYCODE_MEDIA_PREVIOUS |
| 89 | KEYCODE_MEDIA_REWIND |
| 90 | KEYCODE_MEDIA_FAST_FORWARD |
| 91 | KEYCODE_MUTE |
| 92 | KEYCODE_PAGE_UP |
| 93 | KEYCODE_PAGE_DOWN |
| 94 | KEYCODE_PICTSYMBOLS |
| 95 | KEYCODE_SWITCH_CHARSET |
| 96 | KEYCODE_BUTTON_A |
| 97 | KEYCODE_BUTTON_B |
| 98 | KEYCODE_BUTTON_C |
| 99 | KEYCODE_BUTTON_X |
| 100 | KEYCODE_BUTTON_Y |
| 101 | KEYCODE_BUTTON_Z |
| 102 | KEYCODE_BUTTON_L1 |
| 103 | KEYCODE_BUTTON_R1 |
| 104 | KEYCODE_BUTTON_L2 |
| 105 | KEYCODE_BUTTON_R2 |
| 106 | KEYCODE_BUTTON_THUMBL |
| 107 | KEYCODE_BUTTON_THUMBR |
| 108 | KEYCODE_BUTTON_START |
| 109 | KEYCODE_BUTTON_SELECT |
| 110 | KEYCODE_BUTTON_MODE |
| 111 | KEYCODE_ESCAPE |
| 112 | KEYCODE_FORWARD_DEL |
| 113 | KEYCODE_CTRL_LEFT |
| 114 | KEYCODE_CTRL_RIGHT |
| 115 | KEYCODE_CAPS_LOCK |
| 116 | KEYCODE_SCROLL_LOCK |
| 117 | KEYCODE_META_LEFT |
| 118 | KEYCODE_META_RIGHT |
| 119 | KEYCODE_FUNCTION |
| 120 | KEYCODE_SYSRQ |
| 121 | KEYCODE_BREAK |
| 122 | KEYCODE_MOVE_HOME |
| 123 | KEYCODE_MOVE_END |
| 124 | KEYCODE_INSERT |
| 125 | KEYCODE_FORWARD |
| 126 | KEYCODE_MEDIA_PLAY |
| 127 | KEYCODE_MEDIA_PAUSE |
| 128 | KEYCODE_MEDIA_CLOSE |
| 129 | KEYCODE_MEDIA_EJECT |
| 130 | KEYCODE_MEDIA_RECORD |
| 131 | KEYCODE_F1 |
| 132 | KEYCODE_F2 |
| 133 | KEYCODE_F3 |
| 134 | KEYCODE_F4 |
| 135 | KEYCODE_F5 |
| 136 | KEYCODE_F6 |
| 137 | KEYCODE_F7 |
| 138 | KEYCODE_F8 |
| 139 | KEYCODE_F9 |
| 140 | KEYCODE_F10 |
| 141 | KEYCODE_F11 |
| 142 | KEYCODE_F12 |
| 143 | KEYCODE_NUM_LOCK |
| 144 | KEYCODE_NUMPAD_0 |
| 145 | KEYCODE_NUMPAD_1 |
| 146 | KEYCODE_NUMPAD_2 |
| 147 | KEYCODE_NUMPAD_3 |
| 148 | KEYCODE_NUMPAD_4 |
| 149 | KEYCODE_NUMPAD_5 |
| 150 | KEYCODE_NUMPAD_6 |
| 151 | KEYCODE_NUMPAD_7 |
| 152 | KEYCODE_NUMPAD_8 |
| 153 | KEYCODE_NUMPAD_9 |
| 154 | KEYCODE_NUMPAD_DIVIDE |
| 155 | KEYCODE_NUMPAD_MULTIPLY |
| 156 | KEYCODE_NUMPAD_SUBTRACT |
| 157 | KEYCODE_NUMPAD_ADD |
| 158 | KEYCODE_NUMPAD_DOT |
| 159 | KEYCODE_NUMPAD_COMMA |
| 160 | KEYCODE_NUMPAD_ENTER |
| 161 | KEYCODE_NUMPAD_EQUALS |
| 162 | KEYCODE_NUMPAD_LEFT_PAREN |
| 163 | KEYCODE_NUMPAD_RIGHT_PAREN |
| 164 | KEYCODE_VOLUME_MUTE |
| 165 | KEYCODE_INFO |
| 166 | KEYCODE_CHANNEL_UP |
| 167 | KEYCODE_CHANNEL_DOWN |
| 168 | KEYCODE_ZOOM_IN |
| 169 | KEYCODE_ZOOM_OUT |
| 170 | KEYCODE_TV |
| 171 | KEYCODE_WINDOW |
| 172 | KEYCODE_GUIDE |
| 173 | KEYCODE_DVR |
| 174 | KEYCODE_BOOKMARK |
| 175 | KEYCODE_CAPTIONS |
| 176 | KEYCODE_SETTINGS |
| 177 | KEYCODE_TV_POWER |
| 178 | KEYCODE_TV_INPUT |
| 179 | KEYCODE_STB_POWER |
| 180 | KEYCODE_STB_INPUT |
| 181 | KEYCODE_AVR_POWER |
| 182 | KEYCODE_AVR_INPUT |
| 183 | KEYCODE_PROG_RED |
| 184 | KEYCODE_PROG_GREEN |
| 185 | KEYCODE_PROG_YELLOW |
| 186 | KEYCODE_PROG_BLUE |
| 187 | KEYCODE_APP_SWITCH |
| 188 | KEYCODE_BUTTON_1 |
| 189 | KEYCODE_BUTTON_2 |
| 190 | KEYCODE_BUTTON_3 |
| 191 | KEYCODE_BUTTON_4 |
| 192 | KEYCODE_BUTTON_5 |
| 193 | KEYCODE_BUTTON_6 |
| 194 | KEYCODE_BUTTON_7 |
| 195 | KEYCODE_BUTTON_8 |
| 196 | KEYCODE_BUTTON_9 |
| 197 | KEYCODE_BUTTON_10 |
| 198 | KEYCODE_BUTTON_11 |
| 199 | KEYCODE_BUTTON_12 |
| 200 | KEYCODE_BUTTON_13 |
| 201 | KEYCODE_BUTTON_14 |
| 202 | KEYCODE_BUTTON_15 |
| 203 | KEYCODE_BUTTON_16 |
| 204 | KEYCODE_LANGUAGE_SWITCH |
| 205 | KEYCODE_MANNER_MODE |
| 206 | KEYCODE_3D_MODE |
| 207 | KEYCODE_CONTACTS |
| 208 | KEYCODE_CALENDAR |
| 209 | KEYCODE_MUSIC |
| 210 | KEYCODE_CALCULATOR |
| 211 | KEYCODE_ZENKAKU_HANKAKU |
| 212 | KEYCODE_EISU |
| 213 | KEYCODE_MUHENKAN |
| 214 | KEYCODE_HENKAN |
| 215 | KEYCODE_KATAKANA_HIRAGANA |
| 216 | KEYCODE_YEN |
| 217 | KEYCODE_RO |
| 218 | KEYCODE_KANA |
| 219 | KEYCODE_ASSIST |
| 220 | KEYCODE_BRIGHTNESS_DOWN |
| 221 | KEYCODE_BRIGHTNESS_UP |
| 222 | KEYCODE_MEDIA_AUDIO_TRACK |
| 223 | KEYCODE_SLEEP |
| 224 | KEYCODE_WAKEUP |
| 225 | KEYCODE_PAIRING |
| 226 | KEYCODE_MEDIA_TOP_MENU |
| 227 | KEYCODE_11 |
| 228 | KEYCODE_12 |
| 229 | KEYCODE_LAST_CHANNEL |
| 230 | KEYCODE_TV_DATA_SERVICE |
| 231 | KEYCODE_VOICE_ASSIST |
| 232 | KEYCODE_TV_RADIO_SERVICE |
| 233 | KEYCODE_TV_TELETEXT |
| 234 | KEYCODE_TV_NUMBER_ENTRY |
| 235 | KEYCODE_TV_TERRESTRIAL_ANALOG |
| 236 | KEYCODE_TV_TERRESTRIAL_DIGITAL |
| 237 | KEYCODE_TV_SATELLITE |
| 238 | KEYCODE_TV_SATELLITE_BS |
| 239 | KEYCODE_TV_SATELLITE_CS |
| 240 | KEYCODE_TV_SATELLITE_SERVICE |
| 241 | KEYCODE_TV_NETWORK |
| 242 | KEYCODE_TV_ANTENNA_CABLE |
| 243 | KEYCODE_TV_INPUT_HDMI_1 |
| 244 | KEYCODE_TV_INPUT_HDMI_2 |
| 245 | KEYCODE_TV_INPUT_HDMI_3 |
| 246 | KEYCODE_TV_INPUT_HDMI_4 |
| 247 | KEYCODE_TV_INPUT_COMPOSITE_1 |
| 248 | KEYCODE_TV_INPUT_COMPOSITE_2 |
| 249 | KEYCODE_TV_INPUT_COMPONENT_1 |
| 250 | KEYCODE_TV_INPUT_COMPONENT_2 |
| 251 | KEYCODE_TV_INPUT_VGA_1 |
| 252 | KEYCODE_TV_AUDIO_DESCRIPTION |
| 253 | KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP |
| 254 | KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN |
| 255 | KEYCODE_TV_ZOOM_MODE |
| 256 | KEYCODE_TV_CONTENTS_MENU |
| 257 | KEYCODE_TV_MEDIA_CONTEXT_MENU |
| 258 | KEYCODE_TV_TIMER_PROGRAMMING |
| 259 | KEYCODE_HELP |
| 260 | KEYCODE_NAVIGATE_PREVIOUS |
| 261 | KEYCODE_NAVIGATE_NEXT |
| 262 | KEYCODE_NAVIGATE_IN |
| 263 | KEYCODE_NAVIGATE_OUT |
| 264 | KEYCODE_STEM_PRIMARY |
| 265 | KEYCODE_STEM_1 |
| 266 | KEYCODE_STEM_2 |
| 267 | KEYCODE_STEM_3 |
| 268 | KEYCODE_DPAD_UP_LEFT |
| 269 | KEYCODE_DPAD_DOWN_LEFT |
| 270 | KEYCODE_DPAD_UP_RIGHT |
| 271 | KEYCODE_DPAD_DOWN_RIGHT |
| 272 | KEYCODE_MEDIA_SKIP_FORWARD |
| 273 | KEYCODE_MEDIA_SKIP_BACKWARD |
| 274 | KEYCODE_MEDIA_STEP_FORWARD |
| 275 | KEYCODE_MEDIA_STEP_BACKWARD |
| 276 | KEYCODE_SOFT_SLEEP |
| 277 | KEYCODE_CUT |
| 278 | KEYCODE_COPY |
| 279 | KEYCODE_PASTE |
| 280 | KEYCODE_SYSTEM_NAVIGATION_UP |
| 281 | KEYCODE_SYSTEM_NAVIGATION_DOWN |
| 282 | KEYCODE_SYSTEM_NAVIGATION_LEFT |
| 283 | KEYCODE_SYSTEM_NAVIGATION_RIGHT |
| 284 | KEYCODE_ALL_APPS |
| 285 | KEYCODE_REFRESH |
| 286 | KEYCODE_THUMBS_UP |
| 287 | KEYCODE_THUMBS_DOWN |
| 288 | KEYCODE_PROFILE_SWITCH |
| 289 | KEYCODE_VIDEO_APP_1 |
| 290 | KEYCODE_VIDEO_APP_2 |
| 291 | KEYCODE_VIDEO_APP_3 |
| 292 | KEYCODE_VIDEO_APP_4 |
| 293 | KEYCODE_VIDEO_APP_5 |
| 294 | KEYCODE_VIDEO_APP_6 |
| 295 | KEYCODE_VIDEO_APP_7 |
| 296 | KEYCODE_VIDEO_APP_8 |
| 297 | KEYCODE_FEATURED_APP_1 |
| 298 | KEYCODE_FEATURED_APP_2 |
| 299 | KEYCODE_FEATURED_APP_3 |
| 300 | KEYCODE_FEATURED_APP_4 |
| 301 | KEYCODE_DEMO_APP_1 |
| 302 | KEYCODE_DEMO_APP_2 |
| 303 | KEYCODE_DEMO_APP_3 |
| 304 | KEYCODE_DEMO_APP_4 |

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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>4.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.androidtv</artifactId>
<name>openHAB Add-ons :: Bundles :: AndroidTV Binding</name>
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.52</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.52</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link AndroidTVBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class AndroidTVBindingConstants {
private static final String BINDING_ID = "androidtv";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_GOOGLETV = new ThingTypeUID(BINDING_ID, "googletv");
public static final ThingTypeUID THING_TYPE_SHIELDTV = new ThingTypeUID(BINDING_ID, "shieldtv");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GOOGLETV, THING_TYPE_SHIELDTV);
// List of all Channel ids
public static final String CHANNEL_DEBUG = "debug";
public static final String CHANNEL_KEYBOARD = "keyboard";
public static final String CHANNEL_KEYPRESS = "keypress";
public static final String CHANNEL_KEYCODE = "keycode";
public static final String CHANNEL_PINCODE = "pincode";
public static final String CHANNEL_APP = "app";
public static final String CHANNEL_APPNAME = "appname";
public static final String CHANNEL_APPURL = "appurl";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_MUTE = "mute";
public static final String CHANNEL_PLAYER = "player";
// List of all config properties
public static final String PROPERTY_IP_ADDRESS = "ipAddress";
// List of all static String literals
public static final String PIN_REQUEST = "REQUEST";
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of command options.
*
* @author Ben Rosenblum - Initial contribution
*
* Originally written for ADB by Christoph Weitkamp - Initial contribution
*/
@Component(service = { DynamicCommandDescriptionProvider.class, AndroidTVDynamicCommandDescriptionProvider.class })
@NonNullByDefault
public class AndroidTVDynamicCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider {
@Activate
public AndroidTVDynamicCommandDescriptionProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@ -0,0 +1,261 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConfiguration;
import org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConnectionManager;
import org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConfiguration;
import org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConnectionManager;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandOption;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AndroidTVHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* Significant portions reused from Lutron binding with permission from Bob A.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class AndroidTVHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(AndroidTVHandler.class);
private @Nullable ShieldTVConnectionManager shieldtvConnectionManager;
private @Nullable GoogleTVConnectionManager googletvConnectionManager;
private @Nullable ScheduledFuture<?> monitorThingStatusJob;
private final Object monitorThingStatusJobLock = new Object();
private static final int THING_STATUS_FREQUENCY = 250;
private final AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider;
private final ThingTypeUID thingTypeUID;
private final String thingID;
public AndroidTVHandler(Thing thing, AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider,
ThingTypeUID thingTypeUID) {
super(thing);
this.commandDescriptionProvider = commandDescriptionProvider;
this.thingTypeUID = thingTypeUID;
this.thingID = this.getThing().getUID().getId();
}
public void setThingProperty(String property, String value) {
thing.setProperty(property, value);
}
public String getThingID() {
return this.thingID;
}
public void updateChannelState(String channel, State state) {
updateState(channel, state);
}
public ScheduledExecutorService getScheduler() {
return scheduler;
}
public void updateCDP(String channelName, Map<String, String> cdpMap) {
logger.trace("{} - Updating CDP for {}", this.thingID, channelName);
List<CommandOption> commandOptions = new ArrayList<CommandOption>();
cdpMap.forEach((key, value) -> commandOptions.add(new CommandOption(key, value)));
logger.trace("{} - CDP List: {}", this.thingID, commandOptions);
commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), channelName), commandOptions);
}
private void monitorThingStatus() {
synchronized (monitorThingStatusJobLock) {
checkThingStatus();
monitorThingStatusJob = scheduler.schedule(this::monitorThingStatus, THING_STATUS_FREQUENCY,
TimeUnit.MILLISECONDS);
}
}
public void checkThingStatus() {
String statusMessage = "";
boolean failed = false;
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
if (googletvConnectionManager != null) {
if (!googletvConnectionManager.getLoggedIn()) {
statusMessage = "GoogleTV: " + googletvConnectionManager.getStatusMessage();
failed = true;
} else {
statusMessage = "GoogleTV: ONLINE";
}
}
if (THING_TYPE_SHIELDTV.equals(thingTypeUID)) {
if (shieldtvConnectionManager != null) {
if (!shieldtvConnectionManager.getLoggedIn()) {
statusMessage = statusMessage + " | ShieldTV: " + shieldtvConnectionManager.getStatusMessage();
failed = true;
} else {
statusMessage = statusMessage + " | ShieldTV: ONLINE";
}
}
}
if (failed) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, statusMessage);
} else {
updateStatus(ThingStatus.ONLINE);
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.protocols-starting");
GoogleTVConfiguration googletvConfig = getConfigAs(GoogleTVConfiguration.class);
String ipAddress = googletvConfig.ipAddress;
if (ipAddress.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.googletv-address-not-specified");
return;
}
googletvConnectionManager = new GoogleTVConnectionManager(this, googletvConfig);
if (THING_TYPE_SHIELDTV.equals(thingTypeUID)) {
ShieldTVConfiguration shieldtvConfig = getConfigAs(ShieldTVConfiguration.class);
ipAddress = shieldtvConfig.ipAddress;
if (ipAddress.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.shieldtv-address-not-specified");
return;
}
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
}
monitorThingStatusJob = scheduler.schedule(this::monitorThingStatus, THING_STATUS_FREQUENCY,
TimeUnit.MILLISECONDS);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("{} - Command received at handler: {} {}", this.thingID, channelUID.getId(), command);
if (command.toString().equals("REFRESH")) {
// REFRESH causes issues on some channels. Block for now until implemented.
return;
}
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
if (CHANNEL_DEBUG.equals(channelUID.getId())) {
if (command instanceof StringType) {
if (command.toString().equals("GOOGLETV_HALT") && (googletvConnectionManager != null)) {
googletvConnectionManager.dispose();
googletvConnectionManager = null;
} else if (command.toString().equals("GOOGLETV_START")) {
GoogleTVConfiguration googletvConfig = getConfigAs(GoogleTVConfiguration.class);
googletvConnectionManager = new GoogleTVConnectionManager(this, googletvConfig);
} else if (command.toString().equals("GOOGLETV_SHIM") && (googletvConnectionManager == null)) {
GoogleTVConfiguration googletvConfig = getConfigAs(GoogleTVConfiguration.class);
googletvConfig.shim = true;
googletvConnectionManager = new GoogleTVConnectionManager(this, googletvConfig);
} else if (command.toString().equals("SHIELDTV_HALT") && (shieldtvConnectionManager != null)) {
shieldtvConnectionManager.dispose();
shieldtvConnectionManager = null;
} else if (command.toString().equals("SHIELDTV_START")) {
ShieldTVConfiguration shieldtvConfig = getConfigAs(ShieldTVConfiguration.class);
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
} else if (command.toString().equals("SHIELDTV_SHIM") && (shieldtvConnectionManager == null)) {
ShieldTVConfiguration shieldtvConfig = getConfigAs(ShieldTVConfiguration.class);
shieldtvConfig.shim = true;
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
} else if (command.toString().startsWith("GOOGLETV") && (googletvConnectionManager != null)) {
googletvConnectionManager.handleCommand(channelUID, command);
} else if (command.toString().startsWith("SHIELDTV") && (shieldtvConnectionManager != null)) {
shieldtvConnectionManager.handleCommand(channelUID, command);
}
}
return;
}
if (THING_TYPE_SHIELDTV.equals(thingTypeUID) && (shieldtvConnectionManager != null)) {
if (CHANNEL_PINCODE.equals(channelUID.getId())) {
if (command instanceof StringType) {
if (!shieldtvConnectionManager.getLoggedIn()) {
shieldtvConnectionManager.handleCommand(channelUID, command);
return;
}
}
} else if (CHANNEL_APP.equals(channelUID.getId())) {
if (command instanceof StringType) {
shieldtvConnectionManager.handleCommand(channelUID, command);
return;
}
}
}
if (googletvConnectionManager != null) {
googletvConnectionManager.handleCommand(channelUID, command);
return;
}
logger.warn("{} - Commands All Failed. Please report this as a bug. {} {}", thingID, channelUID.getId(),
command);
}
@Override
public void dispose() {
synchronized (monitorThingStatusJobLock) {
ScheduledFuture<?> monitorThingStatusJob = this.monitorThingStatusJob;
if (monitorThingStatusJob != null) {
monitorThingStatusJob.cancel(true);
}
}
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
if (shieldtvConnectionManager != null) {
shieldtvConnectionManager.dispose();
}
if (googletvConnectionManager != null) {
googletvConnectionManager.dispose();
}
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link AndroidTVHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.androidtv", service = ThingHandlerFactory.class)
public class AndroidTVHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GOOGLETV,
THING_TYPE_SHIELDTV);
private final AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider;
@Activate
public AndroidTVHandlerFactory(
final @Reference AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider) {
this.commandDescriptionProvider = commandDescriptionProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
return new AndroidTVHandler(thing, commandDescriptionProvider, thingTypeUID);
}
}

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.discovery;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.net.InetAddress;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link MDNSDiscoveryParticipant} that will discover GOOGLETV(s).
*
* @author Ben Rosenblum - initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, immediate = true, configurationPid = "discovery.googletv")
public class GoogleTVDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(GoogleTVDiscoveryParticipant.class);
private static final String GOOGLETV_MDNS_SERVICE_TYPE = "_androidtvremote2._tcp.local.";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES;
}
@Override
public String getServiceType() {
return GOOGLETV_MDNS_SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(@Nullable ServiceInfo service) {
if ((service == null) || !service.hasData()) {
return null;
}
InetAddress[] ipAddresses = service.getInet4Addresses();
if (ipAddresses.length > 0) {
String ipAddress = ipAddresses[0].getHostAddress();
String macAddress = service.getPropertyString("bt");
if (logger.isDebugEnabled()) {
String nice = service.getNiceTextString();
String qualifiedName = service.getQualifiedName();
logger.debug("GoogleTV mDNS discovery notified of GoogleTV mDNS service: {}", nice);
logger.trace("GoogleTV mDNS service qualifiedName: {}", qualifiedName);
logger.trace("GoogleTV mDNS service ipAddresses: {} ({})", ipAddresses, ipAddresses.length);
logger.trace("GoogleTV mDNS service selected ipAddress: {}", ipAddress);
logger.trace("GoogleTV mDNS service property macAddress: {}", macAddress);
}
final ThingUID uid = getThingUID(service);
if (uid != null) {
final String id = uid.getId();
final String label = service.getName() + " (" + id + ")";
final Map<String, Object> properties = Map.of(PROPERTY_IP_ADDRESS, ipAddress);
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label).build();
} else {
return null;
}
} else {
return null;
}
}
@Override
public @Nullable ThingUID getThingUID(@Nullable ServiceInfo service) {
if ((service == null) || !service.hasData() || (service.getPropertyString("bt") == null)) {
return null;
}
return new ThingUID(THING_TYPE_GOOGLETV, service.getPropertyString("bt").replace(":", ""));
}
}

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.discovery;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import java.net.InetAddress;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link MDNSDiscoveryParticipant} that will discover SHIELDTV(s).
*
* @author Ben Rosenblum - initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, immediate = true, configurationPid = "discovery.shieldtv")
public class ShieldTVDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(ShieldTVDiscoveryParticipant.class);
private static final String SHIELDTV_MDNS_SERVICE_TYPE = "_nv_shield_remote._tcp.local.";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES;
}
@Override
public String getServiceType() {
return SHIELDTV_MDNS_SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(@Nullable ServiceInfo service) {
if (service == null || !service.hasData()) {
return null;
}
InetAddress[] ipAddresses = service.getInet4Addresses();
if (ipAddresses.length > 0) {
String ipAddress = ipAddresses[0].getHostAddress();
String serverId = service.getPropertyString("SERVER");
String serverCapability = service.getPropertyString("SERVER_CAPABILITY");
if (logger.isDebugEnabled()) {
String nice = service.getNiceTextString();
String qualifiedName = service.getQualifiedName();
logger.debug("ShieldTV mDNS discovery notified of ShieldTV mDNS service: {}", nice);
logger.trace("ShieldTV mDNS service qualifiedName: {}", qualifiedName);
logger.trace("ShieldTV mDNS service ipAddresses: {} ({})", ipAddresses, ipAddresses.length);
logger.trace("ShieldTV mDNS service selected ipAddress: {}", ipAddress);
logger.trace("ShieldTV mDNS service property SERVER: {}", serverId);
logger.trace("ShieldTV mDNS service property SERVER_CAPABILITY: {}", serverCapability);
}
final ThingUID uid = getThingUID(service);
if (uid != null) {
final String id = uid.getId();
final String label = service.getName() + " (" + id + ")";
final Map<String, Object> properties = Map.of(PROPERTY_IP_ADDRESS, ipAddress);
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label).build();
} else {
return null;
}
} else {
return null;
}
}
@Override
public @Nullable ThingUID getThingUID(@Nullable ServiceInfo service) {
if (service == null || !service.hasData() || (service.getPropertyString("SERVER") == null)) {
return null;
}
return new ThingUID(THING_TYPE_SHIELDTV, service.getPropertyString("SERVER").substring(8));
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.googletv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* GoogleTVCommand represents a GoogleTV protocol command
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVCommand {
private String command;
public GoogleTVCommand(String command) {
this.command = command;
}
@Override
public String toString() {
return command;
}
public boolean isEmpty() {
return command.isEmpty();
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.googletv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GoogleTVConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVConfiguration {
public String ipAddress = "";
public int port = 6466;
public int reconnect;
public int heartbeat;
public String keystoreFileName = "";
public String keystorePassword = "";
public int delay = 0;
public boolean shim;
public boolean shimNewKeys;
public String mode = "";
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.googletv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GoogleTVConstants} class defines common constants, which are
* used across the googletv protocol.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVConstants {
// List of all static String literals
public static final String DELIMITER_00 = "00";
public static final String DELIMITER_01 = "01";
public static final String DELIMITER_02 = "02";
public static final String DELIMITER_08 = "08";
public static final String DELIMITER_0A = "0a";
public static final String DELIMITER_10 = "10";
public static final String DELIMITER_12 = "12";
public static final String DELIMITER_1A = "1a";
public static final String DELIMITER_42 = "42";
public static final String DELIMITER_92 = "92";
public static final String DELIMITER_A2 = "a2";
public static final String DELIMITER_C2 = "c2";
public static final String MESSAGE_POWEROFF = "c202020800";
public static final String MESSAGE_POWERON = "c202020801";
public static final String MESSAGE_PINSUCCESS = "080210c801ca02";
public static final String HARD_DROP = "ffffffff";
}

View File

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

View File

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

View File

@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.googletv;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.interfaces.RSAPublicKey;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* GoogleTVCommand represents a GoogleTV protocol command
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class GoogleTVUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(GoogleTVUtils.class);
private static String processMag(final byte[] magnitude) {
final int length = magnitude.length;
if (length != 0) {
final BigInteger bigInteger = new BigInteger(1, magnitude);
final StringBuilder sb = new StringBuilder();
sb.append("%0");
sb.append(length + length);
sb.append("x");
return String.format(sb.toString(), bigInteger);
}
return "";
}
private static final byte[] processDigestArray(final byte[] array) {
int n = 0;
int length;
while (true) {
length = array.length;
if (n >= length || array[n] != 0) {
break;
}
++n;
}
final int n2 = length - n;
final byte[] array2 = new byte[n2];
System.arraycopy(array, n, array2, 0, n2);
return array2;
}
public static final byte[] processDigest(byte[] digest, Certificate clientCert, Certificate serverCert) {
final PublicKey clientPublicKey = clientCert.getPublicKey();
final PublicKey serverPublicKey = serverCert.getPublicKey();
processMag(digest);
if (clientPublicKey instanceof RSAPublicKey && serverPublicKey instanceof RSAPublicKey) {
final RSAPublicKey clientRSAPublicKey = (RSAPublicKey) clientPublicKey;
final RSAPublicKey serverRSAPublicKey = (RSAPublicKey) serverPublicKey;
try {
final MessageDigest instance = MessageDigest.getInstance("SHA-256");
final byte[] byteArray1 = clientRSAPublicKey.getModulus().abs().toByteArray();
final byte[] byteArray2 = clientRSAPublicKey.getPublicExponent().abs().toByteArray();
final byte[] byteArray3 = serverRSAPublicKey.getModulus().abs().toByteArray();
final byte[] byteArray4 = serverRSAPublicKey.getPublicExponent().abs().toByteArray();
final byte[] r1 = processDigestArray(byteArray1);
final byte[] r2 = processDigestArray(byteArray2);
final byte[] r3 = processDigestArray(byteArray3);
final byte[] r4 = processDigestArray(byteArray4);
processMag(r1);
processMag(r2);
processMag(r3);
processMag(r4);
processMag(digest);
instance.update(r1);
instance.update(r2);
instance.update(r3);
instance.update(r4);
instance.update(digest);
digest = instance.digest();
processMag(digest);
} catch (NoSuchAlgorithmException e) {
LOGGER.warn("NoSuchAlgorithmException Exception", e);
}
}
return digest;
}
public static String byteArrayToString(byte[] array) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.length; i++) {
sb.append((char) (array[i] & 0xFF));
}
return sb.toString();
}
public static String validatePIN(String pin, Certificate clientCert, Certificate serverCert) {
char[] charArray = pin.toCharArray();
String s1 = "" + charArray[0] + charArray[1];
String s2 = "" + charArray[2] + charArray[3];
String s3 = "" + charArray[4] + charArray[5];
int si1 = Integer.parseInt(s1, 16);
int si2 = Integer.parseInt(s2, 16);
int si3 = Integer.parseInt(s3, 16);
byte[] sb123 = new byte[] { (byte) si1, (byte) si2, (byte) si3 };
byte[] sb23 = new byte[] { (byte) si2, (byte) si3 };
byte[] digest = processDigest(sb23, clientCert, serverCert);
String digestString = GoogleTVRequest.decodeMessage(byteArrayToString(digest));
byte[] validPinB = new byte[] { digest[0], (byte) si2, (byte) si3 };
String validPin = GoogleTVRequest.decodeMessage(byteArrayToString(validPinB));
LOGGER.trace("validatePIN {} {} {} {} {} {}", sb123, digest[0], sb23, validPinB, validPin, digestString);
return digestString;
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.shieldtv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* ShieldTVCommand represents a ShieldTV protocol command
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVCommand {
private String command;
public ShieldTVCommand(String command) {
this.command = command;
}
@Override
public String toString() {
return command;
}
public boolean isEmpty() {
return command.isEmpty();
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.shieldtv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShieldTVConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVConfiguration {
public String ipAddress = "";
public int port = 8987;
public int reconnect;
public int heartbeat;
public String keystoreFileName = "";
public String keystorePassword = "";
public int delay = 0;
public boolean shim;
public boolean shimNewKeys;
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.shieldtv;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShieldTVConstants} class defines common constants, which are
* used across the shieldtv protocol.
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVConstants {
// List of all static String literals
public static final String DELIMITER_0 = "0";
public static final String DELIMITER_00 = "00";
public static final String DELIMITER_08 = "08";
public static final String DELIMITER_0A = "0a";
public static final String DELIMITER_12 = "12";
public static final String DELIMITER_18 = "18";
public static final String DELIMITER_22 = "22";
public static final String DELIMITER_2A = "2a";
public static final String DELIMITER_E9 = "e9";
public static final String DELIMITER_EC = "ec";
public static final String DELIMITER_F0 = "f0";
public static final String DELIMITER_F1 = "f1";
public static final String DELIMITER_F3 = "f3";
public static final String HARD_DROP = "ffffffff";
public static final String APP_START_SUCCESS = "08f1071202080318f107";
public static final String APP_START_FAILED = "08f107120608031202080118f107";
public static final String KEEPALIVE_REPLY = "080028fae0a6c0d130";
public static final String TIMEOUT = "080a121108b510120c0804120854696d65206f7574180a";
public static final String MESSAGE_LOWPRIV = "080a12";
public static final String MESSAGE_HOSTNAME = "080b12";
public static final String MESSAGE_APPDB = "08f10712";
public static final String MESSAGE_GOOD_COMMAND = "08f30712";
public static final String MESSAGE_PINSTART = "0308cf08";
public static final String MESSAGE_CERT_COMING = "20";
public static final String MESSAGE_SUCCESS = "08f007";
public static final String MESSAGE_APP_SUCCESS = "08ec07";
public static final String MESSAGE_APP_GET_SUCCESS = "0803";
public static final String MESSAGE_APP_CURRENT = "0807";
public static final String MESSAGE_SHORTNAME = "08e807";
public static final String MESSAGE_CERT = "08b510";
public static final String MESSAGE_CERT_PAYLOAD = "0753756363657373";
}

View File

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

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.androidtv.internal.protocol.shieldtv;
import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains static methods for constructing LEAP messages
*
* @author Ben Rosenblum - Initial contribution
*/
@NonNullByDefault
public class ShieldTVRequest {
public static String encodeMessage(String message) {
StringBuilder reply = new StringBuilder();
char[] charArray = message.toCharArray();
for (int i = 0; i < charArray.length; i = i + 2) {
String st = "" + charArray[i] + "" + charArray[i + 1];
char ch = (char) Integer.parseInt(st, 16);
reply.append(ch);
}
return reply.toString();
}
public static String decodeMessage(String message) {
StringBuilder sb = new StringBuilder();
char ch[] = message.toCharArray();
for (int i = 0; i < ch.length; i++) {
String hexString = Integer.toHexString(ch[i]);
if (hexString.length() % 2 > 0) {
sb.append('0');
}
sb.append(hexString);
}
return sb.toString();
}
public static String pinRequest(String pin) {
if (PIN_REQUEST.equals(pin)) {
String message = "080a120308cd08";
return message;
} else {
String prefix = "080a121f08d108121a0a06";
String encodedPin = decodeMessage(pin);
String suffix = "121036646564646461326639366635646261";
return prefix + encodedPin + suffix;
}
}
public static String loginRequest() {
String message = "0801121a0801121073616d73756e6720534d2d4739393855180128fbff04";
return message;
}
public static String keepAlive() {
String message = "080028fae0a6c0d130";
return message;
}
private static String fixMessage(String tempMsg) {
if (tempMsg.length() % 2 > 0) {
tempMsg = "0" + tempMsg;
}
return tempMsg;
}
public static String startApp(String message) {
int length = message.length();
String len1 = fixMessage(Integer.toHexString(length + 6));
String len2 = fixMessage(Integer.toHexString(length + 2));
String len3 = fixMessage(Integer.toHexString(length));
String reply = "08f10712" + len1 + "080212" + len2 + "0a" + len3 + decodeMessage(message);
return reply;
}
// 080b120308cd08 - Longer Hostname Reply
// 08f30712020805 - Unknown
// 08f10712020800 - Get all apps
// 08ec0712020806 - Get current app
public static String keyboardEntry(String entry) {
// 08ec07120d08081205616263646532020a0a
// 08ec0712 0d 0808 12 05 6162636465 3202 0a0a
int length = entry.length();
String len1 = fixMessage(Integer.toHexString(length + 8));
String len2 = fixMessage(Integer.toHexString(length));
String len3 = fixMessage(Integer.toHexString(length * 2));
String reply = "08ec0712" + len1 + "080812" + len2 + decodeMessage(entry) + "3202" + len3 + len3;
return reply;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@
<module>org.openhab.binding.ambientweather</module>
<module>org.openhab.binding.amplipi</module>
<module>org.openhab.binding.androiddebugbridge</module>
<module>org.openhab.binding.androidtv</module>
<module>org.openhab.binding.anel</module>
<module>org.openhab.binding.anthem</module>
<module>org.openhab.binding.astro</module>