diff --git a/bundles/org.openhab.binding.androidtv/NOTICE b/bundles/org.openhab.binding.androidtv/NOTICE
index 38d625e349232..489fa7b4b6821 100644
--- a/bundles/org.openhab.binding.androidtv/NOTICE
+++ b/bundles/org.openhab.binding.androidtv/NOTICE
@@ -11,3 +11,21 @@ https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
+
+== Third-party Content
+
+httpclient
+* License: Apache 2.0 License
+* Project: https://hc.apache.org/httpcomponents-client-ga
+* Source: https://hc.apache.org/httpcomponents-client-ga
+
+httpcore
+* License: Apache 2.0 License
+* Project: http://hc.apache.org/httpcomponents-core-ga
+* Source: http://hc.apache.org/httpcomponents-core-ga
+
+jackson
+* License: Apache 2.0 License
+* Project: https://github.com/FasterXML/jackson
+* Source: https://github.com/FasterXML/jackson
+
diff --git a/bundles/org.openhab.binding.androidtv/README.md b/bundles/org.openhab.binding.androidtv/README.md
index 1fce2128de78b..30c6660dc55ee 100644
--- a/bundles/org.openhab.binding.androidtv/README.md
+++ b/bundles/org.openhab.binding.androidtv/README.md
@@ -1,8 +1,9 @@
# 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.
+Currently it emulates 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.
+It also currently emulates the PhilipsTV App to interact with a 2016+ PhilipsTV for purposes of remote control.
## Supported Things
@@ -10,14 +11,15 @@ This binding supports two thing types:
- **googletv** - An AndroidTV running Google Video
- **shieldtv** - An Nvidia ShieldTV
+- **philipstv** - A 2016+ Philips TV
## Discovery
-Both GoogleTVs and ShieldTVs should be added automatically to the inbox through the mDNS discovery process.
+All relevant thing types 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.
+In the case of the ShieldTV or PhilipsTV, openHAB will likely create an inbox entry for both a GoogleTV and a ShieldTV or PhilipsTV device.
+Only the ShieldTV or PhilipsTV device should be configured, the GoogleTV can be ignored.
+There is no benefit to configuring two things for a ShieldTV or PhilipsTV device.
This could cause undesired effects.
## Binding Configuration
@@ -30,37 +32,58 @@ This binding requires GoogleTV to be installed on the device (https://play.googl
## Thing Configuration
-There are three required fields to connect successfully to a ShieldTV.
+The is one required field to connect to the devices. All other fields are optional.
| Name | Type | Description | Default | Required | Advanced |
|------------------|---------|---------------------------------------|---------|----------|----------|
| ipAddress | text | IP address of the device | N/A | yes | no |
-| googletvPort | text | TCP Port for GoogleTV | 6466 | no | no |
-| shieldtvPort | text | TCP Port for ShieldTV | 8987 | no | no |
-| keystore | text | Location of the Java Keystore | N/A | no | no |
-| keystorePassword | text | Password of the Java Keystore | N/A | no | no |
-| gtvEnabled | boolean | Enable/Disable the GoogleTV protocol | true | no | no |
+| googletvPort | text | TCP Port for GoogleTV | 6466 | no | yes |
+| shieldtvPort | text | TCP Port for ShieldTV | 8987 | no | yes |
+| philipstvPort | text | TCP Port for PhilipsTV | 1926 | no | yes |
+| keystoreFileName | text | Location of the Java Keystore | N/A | no | yes |
+| keystorePassword | text | Password of the Java Keystore | N/A | no | yes |
+| reconnect | text | Delay between reconnections | 60 | no | yes |
+| heartbeat | text | Frequency of heartbeats | 5 | no | yes |
+| delay | text | Delay between messages | 0 | no | yes |
+| refreshRate | text | Refresh interval of PhilipsTV | 10 | no | yes |
+| useUpnpDiscovery | boolean | Enables UPnP Discovery for PhilipsTV | true | no | yes |
+| gtvEnabled | boolean | Enable/Disable the GoogleTV protocol | true | no | yes |
```java
Thing androidtv:shieldtv:livingroom [ ipAddress="192.168.1.2" ]
Thing androidtv:googletv:theater [ ipAddress="192.168.1.3" ]
+Thing androidtv:philipstv:bedroom [ ipAddress="192.168.1.4" ]
```
## 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 |
+| Channel | Type | Description | GoogleTV | ShieldTV | PhilipsTV |
+|----------------------|--------|--------------------------------------|----------|----------|-----------|
+| keyboard | String | Keyboard Data Entry | RW | RW | RW |
+| keypress | String | Manual Key Press Entry | RW | RW | RW |
+| keycode | String | Direct KEYCODE Entry | RW | RW | RW |
+| pincode | String | PIN Code Entry | RW | RW | RW |
+| app | String | App Control | RO | RW | RW |
+| appname | String | App Name | N/A | RW | RW |
+| appurl | String | App URL | N/A | RO | N/A |
+| appicon | Image | App Icon | N/A | N/A | RO |
+| player | Player | Player Control | RW | RW | RW |
+| power | Switch | Power Control | RW | RW | RW |
+| volume | Dimmer | Volume Control | RO | RO | RW |
+| mute | Switch | Mute Control | RW | RW | RW |
+| tvChannel | String | TV Channel Control | N/A | N/A | RW |
+| brightness | Dimmer | Brightness Control | N/A | N/A | RW |
+| contrast | Dimmer | Contrast Control | N/A | N/A | RW |
+| sharpness | Dimmer | Sharpness Control | N/A | N/A | RW |
+| searchContent | String | Google Assistant search | N/A | N/A | RW |
+| ambilightPower | Switch | Ambilight power control | N/A | N/A | RW |
+| ambilightHuePower | Switch | Ambilight + Hue power control | N/A | N/A | RW |
+| ambilightStyle | String | Ambilight Style plus algorithm used | N/A | N/A | RW |
+| ambilightColor | Color | Color for all Ambilight Sides | N/A | N/A | RW |
+| ambilightLeftColor | Color | Color for left Ambilight Side | N/A | N/A | RW |
+| ambilightRightColor | Color | Color for right Ambilight Side | N/A | N/A | RW |
+| ambilightTopColor | Color | Color for top Ambilight Side | N/A | N/A | RW |
+| ambilightBottomColor | Color | Color for bottom Ambilight Side | N/A | N/A | RW |
```java
@@ -76,6 +99,32 @@ Switch ShieldTV_POWER "POWER [%s]" { channel = "androidtv:shieldtv:livingroom:po
Dimmer ShieldTV_VOLUME "VOLUME [%s]" { channel = "androidtv:shieldtv:livingroom:volume" }
Switch ShieldTV_MUTE "MUTE [%s]" { channel = "androidtv:shieldtv:livingroom:mute" }
+String PhilipsTV_KEYBOARD "KEYBOARD [%s]" { channel = "androidtv:philipstv:bedroom:keyboard" }
+String PhilipsTV_KEYPRESS "KEYPRESS [%s]" { channel = "androidtv:philipstv:bedroom:keypress" }
+String PhilipsTV_KEYCODE "KEYCODE [%s]" { channel = "androidtv:philipstv:bedroom:keycode" }
+String PhilipsTV_PINCODE "PINCODE [%s]" { channel = "androidtv:philipstv:bedroom:pincode" }
+String PhilipsTV_APP "APP [%s]" { channel = "androidtv:philipstv:bedroom:app" }
+String PhilipsTV_APPNAME "APPNAME [%s]" { channel = "androidtv:philipstv:bedroom:appname" }
+Image PhilipsTV_APPICON "APPICON [%s]" { channel = "androidtv:philipstv:bedroom:appicon" }
+Player PhilipsTV_PLAYER "PLAYER [%s]" { channel = "androidtv:philipstv:bedroom:player" }
+Switch PhilipsTV_POWER "POWER" { channel = "androidtv:philipstv:bedroom:power" }
+Dimmer PhilipsTV_VOLUME "VOLUME [%s]" { channel = "androidtv:philipstv:bedroom:volume" }
+Switch PhilipsTV_MUTE "MUTE [%s]" { channel = "androidtv:philipstv:bedroom:mute" }
+String PhilipsTV_TVCHANNEL "TVCHANNEL [%s]" { channel = "androidtv:philipstv:bedroom:tvChannel" }
+String PhilipsTV_SEARCHCONTENT "SEARCH CONTENT [%s]" { channel = "androidtv:philipstv:bedroom:searchContent" }
+Switch PhilipsTV_AMBILIGHTPOWER "AMBILIGHT POWER [%s]" { channel = "androidtv:philipstv:bedroom:ambilightPower" }
+Switch PhilipsTV_AMBILIGHTHUEPOWER "AMBILIGHT HUE POWER [%s]" { channel = "androidtv:philipstv:bedroom:ambilightHuePower" }
+Switch PhilipsTV_AMBILIGHTLOUNGEPOWER "AMBILIGHT LOUNGE POWER" { channel = "androidtv:philipstv:bedroom:ambilightLoungePower" }
+String PhilipsTV_AMBILIGHTSTYLE "AMBILIGHT STYLE" { channel = "androidtv:philipstv:bedroom:ambilightStyle" }
+Color PhilipsTV_AMBILIGHTALLCOLOR "ALL SIDES AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightColor" }
+Color PhilipsTV_AMBILIGHTLEFTCOLOR "LEFT SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightLeftColor" }
+Color PhilipsTV_AMBILIGHTRIGHTCOLOR "RIGHT SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightRightColor" }
+Color PhilipsTV_AMBILIGHTTOPCOLOR "TOP SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightTopColor" }
+Color PhilipsTV_AMBILIGHTBOTTOMCOLOR "BOTTOM SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightBottomColor" }
+Dimmer PhilipsTV_BRIGHTNESS "BRIGHTNESS [%s]" { channel = "androidtv:philipstv:bedroom:brightness" }
+Dimmer PhilipsTV_CONTRAST "CONTRAST [%s]" { channel = "androidtv:philipstv:bedroom:contrast" }
+Dimmer PhilipsTV_SHARPNESS "SHARPNESS [%s]" { channel = "androidtv:philipstv:bedroom:sharpness" }
+
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" }
@@ -168,7 +217,7 @@ openhab> openhab:androidtv androidtv:googletv:theater pincode abc123
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.
+If you are on a ShieldTV or PhilipsTV you must run that process a second time to authenticate the GoogleTV protocol stack.
This completes the PIN process.
@@ -179,6 +228,7 @@ Upon reconnection (either from reconfiguration or a restart of OpenHAB), you sho
```java
Thing androidtv:shieldtv:livingroom [ ipAddress="192.168.1.2" ]
Thing androidtv:googletv:theater [ ipAddress="192.168.1.3" ]
+Thing androidtv:philipstv:bedroom [ ipAddress="192.168.1.4" ]
```
```java
@@ -194,6 +244,32 @@ Switch ShieldTV_POWER "POWER [%s]" { channel = "androidtv:shieldtv:livingroom:po
Dimmer ShieldTV_VOLUME "VOLUME [%s]" { channel = "androidtv:shieldtv:livingroom:volume" }
Switch ShieldTV_MUTE "MUTE [%s]" { channel = "androidtv:shieldtv:livingroom:mute" }
+String PhilipsTV_KEYBOARD "KEYBOARD [%s]" { channel = "androidtv:philipstv:bedroom:keyboard" }
+String PhilipsTV_KEYPRESS "KEYPRESS [%s]" { channel = "androidtv:philipstv:bedroom:keypress" }
+String PhilipsTV_KEYCODE "KEYCODE [%s]" { channel = "androidtv:philipstv:bedroom:keycode" }
+String PhilipsTV_PINCODE "PINCODE [%s]" { channel = "androidtv:philipstv:bedroom:pincode" }
+String PhilipsTV_APP "APP [%s]" { channel = "androidtv:philipstv:bedroom:app" }
+String PhilipsTV_APPNAME "APPNAME [%s]" { channel = "androidtv:philipstv:bedroom:appname" }
+Image PhilipsTV_APPICON "APPICON [%s]" { channel = "androidtv:philipstv:bedroom:appicon" }
+Player PhilipsTV_PLAYER "PLAYER [%s]" { channel = "androidtv:philipstv:bedroom:player" }
+Switch PhilipsTV_POWER "POWER" { channel = "androidtv:philipstv:bedroom:power" }
+Dimmer PhilipsTV_VOLUME "VOLUME [%s]" { channel = "androidtv:philipstv:bedroom:volume" }
+Switch PhilipsTV_MUTE "MUTE [%s]" { channel = "androidtv:philipstv:bedroom:mute" }
+String PhilipsTV_TVCHANNEL "TVCHANNEL [%s]" { channel = "androidtv:philipstv:bedroom:tvChannel" }
+String PhilipsTV_SEARCHCONTENT "SEARCH CONTENT [%s]" { channel = "androidtv:philipstv:bedroom:searchContent" }
+Switch PhilipsTV_AMBILIGHTPOWER "AMBILIGHT POWER [%s]" { channel = "androidtv:philipstv:bedroom:ambilightPower" }
+Switch PhilipsTV_AMBILIGHTHUEPOWER "AMBILIGHT HUE POWER [%s]" { channel = "androidtv:philipstv:bedroom:ambilightHuePower" }
+Switch PhilipsTV_AMBILIGHTLOUNGEPOWER "AMBILIGHT LOUNGE POWER" { channel = "androidtv:philipstv:bedroom:ambilightLoungePower" }
+String PhilipsTV_AMBILIGHTSTYLE "AMBILIGHT STYLE" { channel = "androidtv:philipstv:bedroom:ambilightStyle" }
+Color PhilipsTV_AMBILIGHTALLCOLOR "ALL SIDES AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightColor" }
+Color PhilipsTV_AMBILIGHTLEFTCOLOR "LEFT SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightLeftColor" }
+Color PhilipsTV_AMBILIGHTRIGHTCOLOR "RIGHT SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightRightColor" }
+Color PhilipsTV_AMBILIGHTTOPCOLOR "TOP SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightTopColor" }
+Color PhilipsTV_AMBILIGHTBOTTOMCOLOR "BOTTOM SIDE AMBILIGHT COLOR [%s]" { channel = "androidtv:philipstv:bedroom:ambilightBottomColor" }
+Dimmer PhilipsTV_BRIGHTNESS "BRIGHTNESS [%s]" { channel = "androidtv:philipstv:bedroom:brightness" }
+Dimmer PhilipsTV_CONTRAST "CONTRAST [%s]" { channel = "androidtv:philipstv:bedroom:contrast" }
+Dimmer PhilipsTV_SHARPNESS "SHARPNESS [%s]" { channel = "androidtv:philipstv:bedroom:sharpness" }
+
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" }
diff --git a/bundles/org.openhab.binding.androidtv/pom.xml b/bundles/org.openhab.binding.androidtv/pom.xml
index 662c89ad21446..b4379e2613424 100644
--- a/bundles/org.openhab.binding.androidtv/pom.xml
+++ b/bundles/org.openhab.binding.androidtv/pom.xml
@@ -14,6 +14,10 @@
openHAB Add-ons :: Bundles :: AndroidTV Binding
+
+ !net.sf.ehcache.*,!net.spy.*
+
+
org.bouncycastle
@@ -33,6 +37,36 @@
1.75compile
+
+ org.apache.httpcomponents
+ httpclient-osgi
+ 4.5.14
+ compile
+
+
+ org.apache.httpcomponents
+ httpcore-osgi
+ 4.4.16
+ compile
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+ compile
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+ compile
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+ compile
+
diff --git a/bundles/org.openhab.binding.androidtv/src/main/feature/feature.xml b/bundles/org.openhab.binding.androidtv/src/main/feature/feature.xml
index 4704774878262..1249665cd8c92 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/feature/feature.xml
+++ b/bundles/org.openhab.binding.androidtv/src/main/feature/feature.xml
@@ -4,6 +4,7 @@
openhab-runtime-base
+ openhab-transport-upnpmvn:org.openhab.addons.bundles/org.openhab.binding.androidtv/${project.version}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVBindingConstants.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVBindingConstants.java
index 776122e5f8669..e5ad0e34dbd6b 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVBindingConstants.java
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVBindingConstants.java
@@ -31,8 +31,9 @@ public class AndroidTVBindingConstants {
// 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 SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GOOGLETV, THING_TYPE_SHIELDTV);
+ public static final ThingTypeUID THING_TYPE_PHILIPSTV = new ThingTypeUID(BINDING_ID, "philipstv");
+ public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GOOGLETV, THING_TYPE_SHIELDTV,
+ THING_TYPE_PHILIPSTV);
// List of all Channel ids
public static final String CHANNEL_DEBUG = "debug";
@@ -43,15 +44,32 @@ public class AndroidTVBindingConstants {
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_APP_ICON = "appicon";
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";
+ public static final String CHANNEL_BRIGHTNESS = "brightness";
+ public static final String CHANNEL_CONTRAST = "contrast";
+ public static final String CHANNEL_SHARPNESS = "sharpness";
+ public static final String CHANNEL_TV_CHANNEL = "tvChannel";
+ public static final String CHANNEL_SEARCH_CONTENT = "searchContent";
+ public static final String CHANNEL_AMBILIGHT = "ambilight";
+ public static final String CHANNEL_AMBILIGHT_POWER = "ambilightPower";
+ public static final String CHANNEL_AMBILIGHT_HUE_POWER = "ambilightHuePower";
+ public static final String CHANNEL_AMBILIGHT_LOUNGE_POWER = "ambilightLoungePower";
+ public static final String CHANNEL_AMBILIGHT_STYLE = "ambilightStyle";
+ public static final String CHANNEL_AMBILIGHT_COLOR = "ambilightColor";
+ public static final String CHANNEL_AMBILIGHT_LEFT_COLOR = "ambilightLeftColor";
+ public static final String CHANNEL_AMBILIGHT_RIGHT_COLOR = "ambilightRightColor";
+ public static final String CHANNEL_AMBILIGHT_TOP_COLOR = "ambilightTopColor";
+ public static final String CHANNEL_AMBILIGHT_BOTTOM_COLOR = "ambilightBottomColor";
// List of all config properties
public static final String PARAMETER_IP_ADDRESS = "ipAddress";
public static final String PARAMETER_GOOGLETV_PORT = "googletvPort";
public static final String PARAMETER_SHIELDTV_PORT = "shieldtvPort";
+ public static final String PARAMETER_PHILIPSTV_PORT = "philipstvPort";
public static final String PARAMETER_GTV_ENABLED = "gtvEnabled";
// List of all static String literals
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVDynamicStateDescriptionProvider.java
new file mode 100644
index 0000000000000..3cf1557891cea
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVDynamicStateDescriptionProvider.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2024 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.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.openhab.core.types.StateDescription;
+import org.openhab.core.types.StateDescriptionFragmentBuilder;
+import org.openhab.core.types.StateOption;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+
+/**
+ * Dynamic provider of state options while leaving other state description fields as original.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class,
+ AndroidTVDynamicStateDescriptionProvider.class }, immediate = true)
+@NonNullByDefault
+public class AndroidTVDynamicStateDescriptionProvider implements DynamicStateDescriptionProvider {
+ private final Map> channelOptionsMap = new ConcurrentHashMap<>();
+
+ public void setStateOptions(ChannelUID channelUID, List options) {
+ channelOptionsMap.put(channelUID, options);
+ }
+
+ @Override
+ public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
+ @Nullable Locale locale) {
+ List options = channelOptionsMap.get(channel.getUID());
+
+ if (options == null) {
+ return null;
+ }
+ if (original != null) {
+ return StateDescriptionFragmentBuilder.create(original).withOptions(options).build().toStateDescription();
+ }
+
+ return StateDescriptionFragmentBuilder.create().withOptions(options).build().toStateDescription();
+ }
+
+ @Deactivate
+ public void deactivate() {
+ channelOptionsMap.clear();
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandler.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandler.java
index dd2660b3cc123..c29aba647fe42 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandler.java
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandler.java
@@ -25,14 +25,19 @@
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.philipstv.PhilipsTVConfiguration;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
import org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConfiguration;
import org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConnectionManager;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
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.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandOption;
@@ -55,6 +60,7 @@ public class AndroidTVHandler extends BaseThingHandler {
private @Nullable ShieldTVConnectionManager shieldtvConnectionManager;
private @Nullable GoogleTVConnectionManager googletvConnectionManager;
+ private @Nullable PhilipsTVConnectionManager philipstvConnectionManager;
private @Nullable ScheduledFuture> monitorThingStatusJob;
private final Object monitorThingStatusJobLock = new Object();
@@ -68,13 +74,20 @@ public class AndroidTVHandler extends BaseThingHandler {
private String currentThingStatus = "";
private boolean currentThingFailed = false;
+ private DiscoveryServiceRegistry discoveryServiceRegistry;
+
+ private AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider;
+
public AndroidTVHandler(Thing thing, AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider,
- AndroidTVTranslationProvider translationProvider, ThingTypeUID thingTypeUID) {
+ AndroidTVTranslationProvider translationProvider, DiscoveryServiceRegistry discoveryServiceRegistry,
+ AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider, ThingTypeUID thingTypeUID) {
super(thing);
this.commandDescriptionProvider = commandDescriptionProvider;
this.translationProvider = translationProvider;
this.thingTypeUID = thingTypeUID;
this.thingID = this.getThing().getUID().getId();
+ this.discoveryServiceRegistry = discoveryServiceRegistry;
+ this.stateDescriptionProvider = stateDescriptionProvider;
}
public void setThingProperty(String property, String value) {
@@ -89,6 +102,22 @@ public String getThingID() {
return this.thingID;
}
+ public Configuration getThingConfig() {
+ return getConfig();
+ }
+
+ public DiscoveryServiceRegistry getDiscoveryServiceRegistry() {
+ return discoveryServiceRegistry;
+ }
+
+ public AndroidTVDynamicStateDescriptionProvider getStateDescriptionProvider() {
+ return stateDescriptionProvider;
+ }
+
+ public ThingUID getThingUID() {
+ return getThing().getUID();
+ }
+
public void updateChannelState(String channel, State state) {
updateState(channel, state);
}
@@ -122,6 +151,7 @@ public void checkThingStatus() {
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
+ PhilipsTVConnectionManager philipstvConnectionManager = this.philipstvConnectionManager;
if (googletvConnectionManager != null) {
if (!googletvConnectionManager.getLoggedIn()) {
@@ -143,6 +173,15 @@ public void checkThingStatus() {
}
}
+ if (THING_TYPE_PHILIPSTV.equals(thingTypeUID)) {
+ if (philipstvConnectionManager != null) {
+ if (!philipstvConnectionManager.getLoggedIn()) {
+ failed = true;
+ }
+ statusMessage = statusMessage + "PhilipsTV: " + philipstvConnectionManager.getStatusMessage();
+ }
+ }
+
if (!currentThingStatus.equals(statusMessage) || (currentThingFailed != failed)) {
if (failed) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, statusMessage);
@@ -186,14 +225,30 @@ public void initialize() {
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
}
+ if (THING_TYPE_PHILIPSTV.equals(thingTypeUID)) {
+ PhilipsTVConfiguration philipstvConfig = getConfigAs(PhilipsTVConfiguration.class);
+ ipAddress = philipstvConfig.ipAddress;
+
+ if (ipAddress.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.philipstv-address-not-specified");
+ return;
+ }
+
+ philipstvConnectionManager = new PhilipsTVConnectionManager(this, philipstvConfig);
+ }
+
monitorThingStatusJob = scheduler.schedule(this::monitorThingStatus, THING_STATUS_FREQUENCY,
TimeUnit.MILLISECONDS);
}
public void sendCommandToProtocol(ChannelUID channelUID, Command command) {
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
+ PhilipsTVConnectionManager philipstvConnectionManager = this.philipstvConnectionManager;
if (THING_TYPE_SHIELDTV.equals(thingTypeUID) && (shieldtvConnectionManager != null)) {
shieldtvConnectionManager.handleCommand(channelUID, command);
+ } else if (THING_TYPE_PHILIPSTV.equals(thingTypeUID) && (philipstvConnectionManager != null)) {
+ philipstvConnectionManager.handleCommand(channelUID, command);
}
}
@@ -208,6 +263,7 @@ public void handleCommand(ChannelUID channelUID, Command command) {
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
+ PhilipsTVConnectionManager philipstvConnectionManager = this.philipstvConnectionManager;
if (CHANNEL_DEBUG.equals(channelUID.getId())) {
if (command instanceof StringType) {
@@ -231,10 +287,18 @@ public void handleCommand(ChannelUID channelUID, Command command) {
ShieldTVConfiguration shieldtvConfig = getConfigAs(ShieldTVConfiguration.class);
shieldtvConfig.shim = true;
shieldtvConnectionManager = new ShieldTVConnectionManager(this, shieldtvConfig);
+ } else if (command.toString().equals("PHILIPSTV_HALT") && (philipstvConnectionManager != null)) {
+ philipstvConnectionManager.dispose();
+ philipstvConnectionManager = null;
+ } else if (command.toString().equals("PHILIPSTV_START")) {
+ PhilipsTVConfiguration philipstvConfig = getConfigAs(PhilipsTVConfiguration.class);
+ philipstvConnectionManager = new PhilipsTVConnectionManager(this, philipstvConfig);
} else if (command.toString().startsWith("GOOGLETV") && (googletvConnectionManager != null)) {
googletvConnectionManager.handleCommand(channelUID, command);
} else if (command.toString().startsWith("SHIELDTV") && (shieldtvConnectionManager != null)) {
shieldtvConnectionManager.handleCommand(channelUID, command);
+ } else if (command.toString().startsWith("PHILIPSTV") && (philipstvConnectionManager != null)) {
+ philipstvConnectionManager.handleCommand(channelUID, command);
}
}
return;
@@ -259,6 +323,33 @@ public void handleCommand(ChannelUID channelUID, Command command) {
}
}
+ if (THING_TYPE_PHILIPSTV.equals(thingTypeUID) && (philipstvConnectionManager != null)) {
+ if (googletvConnectionManager != null) {
+ if (CHANNEL_PINCODE.equals(channelUID.getId())) {
+ if (command instanceof StringType) {
+ if (!philipstvConnectionManager.getLoggedIn()) {
+ philipstvConnectionManager.handleCommand(channelUID, command);
+ return;
+ }
+ }
+ } else if (CHANNEL_POWER.equals(channelUID.getId()) && !googletvConnectionManager.getLoggedIn()) {
+ philipstvConnectionManager.handleCommand(channelUID, command);
+ return;
+ } else if (CHANNEL_APP.equals(channelUID.getId()) || CHANNEL_APPNAME.equals(channelUID.getId())
+ || CHANNEL_TV_CHANNEL.equals(channelUID.getId()) || CHANNEL_VOLUME.equals(channelUID.getId())
+ || CHANNEL_MUTE.equals(channelUID.getId()) || CHANNEL_SEARCH_CONTENT.equals(channelUID.getId())
+ || CHANNEL_BRIGHTNESS.equals(channelUID.getId()) || CHANNEL_SHARPNESS.equals(channelUID.getId())
+ || CHANNEL_CONTRAST.equals(channelUID.getId())
+ || channelUID.getId().startsWith(CHANNEL_AMBILIGHT)) {
+ philipstvConnectionManager.handleCommand(channelUID, command);
+ return;
+ }
+ } else {
+ philipstvConnectionManager.handleCommand(channelUID, command);
+ return;
+ }
+ }
+
if (googletvConnectionManager != null) {
googletvConnectionManager.handleCommand(channelUID, command);
return;
@@ -279,11 +370,16 @@ public void dispose() {
GoogleTVConnectionManager googletvConnectionManager = this.googletvConnectionManager;
ShieldTVConnectionManager shieldtvConnectionManager = this.shieldtvConnectionManager;
+ PhilipsTVConnectionManager philipstvConnectionManager = this.philipstvConnectionManager;
if (shieldtvConnectionManager != null) {
shieldtvConnectionManager.dispose();
}
+ if (philipstvConnectionManager != null) {
+ philipstvConnectionManager.dispose();
+ }
+
if (googletvConnectionManager != null) {
googletvConnectionManager.dispose();
}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandlerFactory.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandlerFactory.java
index 4b2803b3ec649..6abf7346c9f2d 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandlerFactory.java
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/AndroidTVHandlerFactory.java
@@ -18,6 +18,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Thing;
@@ -39,18 +40,24 @@
@Component(configurationPid = "binding.androidtv", service = ThingHandlerFactory.class)
public class AndroidTVHandlerFactory extends BaseThingHandlerFactory {
- private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GOOGLETV,
- THING_TYPE_SHIELDTV);
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GOOGLETV, THING_TYPE_SHIELDTV,
+ THING_TYPE_PHILIPSTV);
private final AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider;
private final AndroidTVTranslationProvider translationProvider;
+ private final DiscoveryServiceRegistry discoveryServiceRegistry;
+ private final AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider;
@Activate
public AndroidTVHandlerFactory(
final @Reference AndroidTVDynamicCommandDescriptionProvider commandDescriptionProvider,
- final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider) {
+ final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider,
+ final @Reference DiscoveryServiceRegistry discoveryServiceRegistry,
+ final @Reference AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider) {
this.commandDescriptionProvider = commandDescriptionProvider;
this.translationProvider = new AndroidTVTranslationProvider(i18nProvider, localeProvider);
+ this.discoveryServiceRegistry = discoveryServiceRegistry;
+ this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
@@ -61,6 +68,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
- return new AndroidTVHandler(thing, commandDescriptionProvider, translationProvider, thingTypeUID);
+ return new AndroidTVHandler(thing, commandDescriptionProvider, translationProvider, discoveryServiceRegistry,
+ stateDescriptionProvider, thingTypeUID);
}
}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/ConnectionManager.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/ConnectionManager.java
new file mode 100644
index 0000000000000..f282666fdf0c5
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/ConnectionManager.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.HttpHostConnectException;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * The {@link ConnectionManager} is responsible for handling https GETs and POSTs to the Philips
+ * TVs.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class ConnectionManager {
+
+ private static final String TARGET_URI_MSG = "Target Uri is: {}";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ // Cannot use jetty in OH2.4 due to 9.4.11.v20180605 version with digest auth bug
+ // https://github.com/eclipse/jetty.project/issues/1555
+ private final CloseableHttpClient httpClient;
+
+ private final HttpHost httpHost;
+
+ public ConnectionManager(CloseableHttpClient httpClient, HttpHost httpHost) {
+ this.httpClient = httpClient;
+ this.httpHost = httpHost;
+ OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
+
+ public String doHttpsGet(String path) throws IOException {
+ String uri = httpHost.toURI() + path;
+ logger.debug(TARGET_URI_MSG, uri);
+ HttpGet httpGet = new HttpGet(uri);
+ String jsonContent = "";
+ try (CloseableHttpClient client = httpClient; //
+ CloseableHttpResponse response = client.execute(httpHost, httpGet)) {
+ validateResponse(response, uri);
+ jsonContent = getJsonFromResponse(response);
+ } catch (HttpHostConnectException e) {
+ logger.debug("HttpHostConnectException when getting {}", uri);
+ }
+ return jsonContent;
+ }
+
+ public String doHttpsPost(String path, String json) throws IOException {
+ String uri = httpHost.toURI() + path;
+ logger.debug(TARGET_URI_MSG, uri);
+ HttpPost httpPost = new HttpPost(uri);
+ httpPost.setHeader("Content-type", "application/json");
+ httpPost.setEntity(new StringEntity(json));
+ String jsonContent = "";
+ try (CloseableHttpClient client = httpClient; //
+ CloseableHttpResponse response = client.execute(httpHost, httpPost)) {
+ validateResponse(response, uri);
+ jsonContent = getJsonFromResponse(response);
+ } catch (HttpHostConnectException e) {
+ logger.debug("HttpHostConnectException when getting {}", uri);
+ }
+ return jsonContent;
+ }
+
+ private void validateResponse(CloseableHttpResponse response, String uri) throws HttpResponseException {
+ if (response == null) {
+ throw new HttpResponseException(0, String.format("The response for the request to %s was empty.", uri));
+ } else if (response.getStatusLine().getStatusCode() == 401) {
+ throw new HttpResponseException(401, "The given username/password combination is invalid.");
+ }
+ }
+
+ private String getJsonFromResponse(HttpResponse response) throws IOException {
+ String jsonContent = EntityUtils.toString(response.getEntity());
+ logger.trace("----------------------------------------");
+ logger.trace("{}", response.getStatusLine());
+ logger.trace("{}", jsonContent);
+ return jsonContent;
+ }
+
+ public byte[] doHttpsGetForImage(String path) throws IOException {
+ String uri = httpHost.toURI() + path;
+ logger.debug(TARGET_URI_MSG, uri);
+ HttpGet httpGet = new HttpGet(uri);
+ try (CloseableHttpClient client = httpClient;
+ CloseableHttpResponse response = client.execute(httpHost, httpGet)) {
+ if ((response != null) && (response.getStatusLine().getStatusCode() == 401)) {
+ throw new HttpResponseException(401, "The given username/password combination is invalid.");
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ if (response != null) {
+ response.getEntity().writeTo(baos);
+ }
+ return baos.toByteArray();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/ConnectionManagerUtil.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/ConnectionManagerUtil.java
new file mode 100644
index 0000000000000..49a31e7186c39
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/ConnectionManagerUtil.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.CONNECT_TIMEOUT_MILLISECONDS;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.HTTPS;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.MAX_REQUEST_RETRIES;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.SOCKET_TIMEOUT_MILLISECONDS;
+
+import java.net.NoRouteToHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.HttpHostConnectException;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.ssl.SSLContextBuilder;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ConnectionManagerUtil} is offering methods for connection specific processes.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public final class ConnectionManagerUtil {
+
+ private ConnectionManagerUtil() {
+ }
+
+ public static CloseableHttpClient createSharedHttpClient(HttpHost target, String username, String password)
+ throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
+ CredentialsProvider credProvider = new BasicCredentialsProvider();
+ credProvider.setCredentials(new AuthScope(target.getHostName(), target.getPort()),
+ new UsernamePasswordCredentials(username, password));
+
+ RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT_MILLISECONDS)
+ .setSocketTimeout(SOCKET_TIMEOUT_MILLISECONDS).build();
+
+ SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(getSslConnectionWithoutCertValidation(),
+ NoopHostnameVerifier.INSTANCE);
+
+ Registry socketFactoryRegistry = RegistryBuilder. create()
+ .register(HTTPS, sslsf).build();
+
+ PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
+
+ HttpRequestRetryHandler requestRetryHandler = (exception, executionCount, context) -> {
+ if (exception instanceof NoRouteToHostException) {
+ return false;
+ }
+ String message = Optional.ofNullable(exception.getMessage()).orElse("");
+ if (!message.isEmpty()) {
+ if ((exception instanceof HttpHostConnectException) && message.contains("Connection refused")) {
+ return false;
+ }
+ }
+ return executionCount < MAX_REQUEST_RETRIES;
+ };
+
+ return HttpClients.custom().setDefaultRequestConfig(requestConfig).setSSLSocketFactory(sslsf)
+ .setDefaultCredentialsProvider(credProvider).setConnectionManager(connManager)
+ .setRetryHandler(requestRetryHandler).setConnectionManagerShared(true).build();
+ }
+
+ private static SSLContext getSslConnectionWithoutCertValidation()
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
+ return new SSLContextBuilder().loadTrustMaterial(null, (certificate, authType) -> true).build();
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVBindingConstants.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVBindingConstants.java
new file mode 100644
index 0000000000000..d57bb924d0fb7
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVBindingConstants.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PhilipsTVBindingConstants} class defines common constants, which are used across the
+ * whole binding.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class PhilipsTVBindingConstants {
+
+ // Config Parameters
+ public static final String HOST = "host";
+
+ public static final String PORT = "philipstvPort";
+
+ public static final String MAC_ADDRESS = "macAddress";
+
+ public static final String USERNAME = "username";
+
+ public static final String PASSWORD = "password";
+
+ public static final String HTTPS = "https";
+
+ // Connection specific values
+ static final int CONNECT_TIMEOUT_MILLISECONDS = 3 * 1000;
+
+ static final int SOCKET_TIMEOUT_MILLISECONDS = 1000;
+
+ static final int MAX_REQUEST_RETRIES = 3;
+
+ // Default port for jointspace v6
+ public static final int DEFAULT_PORT = 1926;
+
+ // Powerstates
+ public static final String POWER_ON = "On";
+
+ public static final String POWER_OFF = "Off";
+
+ public static final String STANDBY = "Standby";
+
+ public static final String STANDBYKEEP = "StandbyKeep";
+
+ public static final String STANDBY_MSG = "online.standby";
+
+ public static final String EMPTY = "";
+
+ // REST Paths
+ public static final String SLASH = "/";
+
+ private static final String API_VERSION = "6";
+
+ public static final String BASE_PATH = SLASH + API_VERSION + SLASH;
+
+ public static final String VOLUME_PATH = BASE_PATH + "audio" + SLASH + "volume";
+
+ public static final String KEY_CODE_PATH = BASE_PATH + "input" + SLASH + "key";
+
+ public static final String TV_POWERSTATE_PATH = BASE_PATH + "powerstate";
+
+ public static final String GET_AVAILABLE_APP_LIST_PATH = BASE_PATH + "applications";
+
+ public static final String GET_NETWORK_DEVICES_PATH = BASE_PATH + "network" + SLASH + "devices";
+
+ private static final String ACTIVITIES_BASE_PATH = BASE_PATH + "activities" + SLASH;
+
+ public static final String GET_AVAILABLE_TV_CHANNEL_LIST_PATH = BASE_PATH + "channeldb" + SLASH + "tv" + SLASH
+ + "channelLists" + SLASH + "all";
+
+ public static final String TV_CHANNEL_PATH = ACTIVITIES_BASE_PATH + "tv";
+ public static final String GET_CURRENT_APP_PATH = ACTIVITIES_BASE_PATH + "current";
+ public static final String LAUNCH_APP_PATH = ACTIVITIES_BASE_PATH + "launch";
+
+ private static final String AMBILIGHT_BASE_PATH = BASE_PATH + "ambilight" + SLASH;
+ public static final String AMBILIGHT_POWERSTATE_PATH = AMBILIGHT_BASE_PATH + "power";
+ public static final String AMBILIGHT_CONFIG_PATH = AMBILIGHT_BASE_PATH + "currentconfiguration";
+ public static final String AMBILIGHT_MODE_PATH = AMBILIGHT_BASE_PATH + "mode";
+ public static final String AMBILIGHT_CACHED_PATH = AMBILIGHT_BASE_PATH + "cached";
+ public static final String AMBILIGHT_TOPOLOGY_PATH = AMBILIGHT_BASE_PATH + "topology";
+ public static final String AMBILIGHT_LOUNGE_PATH = AMBILIGHT_BASE_PATH + "lounge";
+
+ private static final String SETTINGS_BASE_PATH = BASE_PATH + "menuitems" + SLASH + "settings" + SLASH;
+ public static final String UPDATE_SETTINGS_PATH = SETTINGS_BASE_PATH + "update";
+ public static final String CURRENT_SETTINGS_PATH = SETTINGS_BASE_PATH + "current";
+ public static final String STRUCTURE_SETTINGS_PATH = SETTINGS_BASE_PATH + "structure";
+
+ // Logging messages
+ public static final String TV_OFFLINE_MSG = "offline.tv-is-not-reachable-and-should-therefore-be-off";
+ public static final String TV_NOT_LISTENING_MSG = "offline.tv-does-not-accept-commands-at-the-moment";
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVConfiguration.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVConfiguration.java
new file mode 100644
index 0000000000000..e29bad8434531
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVConfiguration.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PhilipsTVConfiguration} class contains fields for mapping thing configuration parameters.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class PhilipsTVConfiguration {
+
+ public String ipAddress = "";
+ public Integer philipstvPort = 1926;
+ public Integer refreshRate = 10;
+ public boolean useUpnpDiscovery = true;
+ public String pairingCode = "";
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVConnectionManager.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVConnectionManager.java
new file mode 100644
index 0000000000000..30c49311eacce
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/PhilipsTVConnectionManager.java
@@ -0,0 +1,696 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv;
+
+import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.NoRouteToHostException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketTimeoutException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Predicate;
+
+import org.apache.http.HttpHost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.androidtv.internal.AndroidTVDynamicStateDescriptionProvider;
+import org.openhab.binding.androidtv.internal.AndroidTVHandler;
+import org.openhab.binding.androidtv.internal.AndroidTVTranslationProvider;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.pairing.PhilipsTVPairing;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.AmbilightService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.AppService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.KeyPressService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.PowerService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.SearchContentService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.TvChannelService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.TvPictureService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.VolumeService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.core.OpenHAB;
+import org.openhab.core.config.discovery.DiscoveryListener;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * The {@link PhilipsTVHandler} is responsible for handling commands, which are sent to one of the
+ * channels.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class PhilipsTVConnectionManager implements DiscoveryListener {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private AndroidTVHandler handler;
+
+ public PhilipsTVConfiguration config;
+
+ private ScheduledExecutorService scheduler;
+
+ private final AndroidTVTranslationProvider translationProvider;
+
+ private DiscoveryServiceRegistry discoveryServiceRegistry;
+
+ private AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider;
+
+ private @Nullable ThingUID upnpThingUID;
+
+ private @Nullable ScheduledFuture> refreshScheduler;
+
+ private final Predicate> isRefreshSchedulerRunning = r -> (r != null) && !r.isCancelled();
+
+ private final ReentrantLock lock = new ReentrantLock();
+
+ private boolean isLoggedIn = false;
+
+ private String statusMessage = "";
+
+ private HttpHost target;
+
+ private String username = "";
+ private String password = "";
+ private String macAddress = "";
+
+ private @Nullable ScheduledFuture> deviceHealthJob;
+ private boolean isOnline = true;
+ private boolean pendingPowerOn = false;
+
+ public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ /* Philips TV services */
+ private Map channelServices = new HashMap<>();
+
+ public PhilipsTVConnectionManager(AndroidTVHandler handler, PhilipsTVConfiguration config) {
+ logger.debug("Create a Philips TV Handler for thing '{}'", handler.getThingUID());
+ this.handler = handler;
+ this.config = config;
+ this.scheduler = handler.getScheduler();
+ this.translationProvider = handler.getTranslationProvider();
+ this.discoveryServiceRegistry = handler.getDiscoveryServiceRegistry();
+ this.stateDescriptionProvider = handler.getStateDescriptionProvider();
+ this.target = new HttpHost(config.ipAddress, config.philipstvPort, HTTPS);
+ initialize();
+ }
+
+ private void setStatus(boolean isLoggedIn) {
+ if (isLoggedIn) {
+ setStatus(isLoggedIn, "online.online");
+ } else {
+ setStatus(isLoggedIn, "offline.unknown");
+ }
+ }
+
+ private void setStatus(boolean isLoggedIn, String statusMessage) {
+ String translatedMessage = translationProvider.getText(statusMessage);
+ logger.trace("setStatus to {} {} {}", isLoggedIn, statusMessage, translatedMessage);
+ if ((this.isLoggedIn != isLoggedIn) || (!this.statusMessage.equals(translatedMessage))) {
+ this.isLoggedIn = isLoggedIn;
+ this.statusMessage = translatedMessage;
+ handler.checkThingStatus();
+ }
+ }
+
+ public String getStatusMessage() {
+ return statusMessage;
+ }
+
+ public void setLoggedIn(boolean isLoggedIn) {
+ if (this.isLoggedIn != isLoggedIn) {
+ setStatus(isLoggedIn);
+ }
+ }
+
+ public boolean getLoggedIn() {
+ return isLoggedIn;
+ }
+
+ public void updateStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail, String thingStatusMessage) {
+ if (thingStatus == ThingStatus.ONLINE) {
+ setLoggedIn(true);
+ } else {
+ logger.trace("Updating status to {} {} {}", thingStatus, thingStatusDetail, thingStatusMessage);
+ setStatus(false, thingStatusMessage);
+ }
+ }
+
+ public String getMacAddress() {
+ return this.macAddress;
+ }
+
+ public void saveConfigs() {
+ String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
+ File folder = new File(folderName);
+
+ if (!folder.exists()) {
+ logger.debug("Creating directory {}", folderName);
+ folder.mkdirs();
+ }
+
+ String fileName = folderName + "/philipstv." + handler.getThing().getUID().getId() + ".config";
+
+ Map configMap = new HashMap<>();
+ configMap.put("username", username);
+ configMap.put("password", password);
+ configMap.put("macAddress", macAddress);
+
+ try {
+ String configJson = OBJECT_MAPPER.writeValueAsString(configMap);
+ logger.debug("Writing configJson \"{}\" to {}", configJson, fileName);
+ Files.write(Paths.get(fileName), configJson.getBytes());
+ } catch (JsonProcessingException e) {
+ logger.warn("JsonProcessingException trying to save configMap: {}", e.getMessage(), e);
+ } catch (IOException ex) {
+ logger.debug("IOException when writing configJson to file {}", ex.getMessage());
+ }
+ }
+
+ private void readConfigs() {
+ String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
+ String fileName = folderName + "/philipstv." + handler.getThing().getUID().getId() + ".config";
+ File file = new File(fileName);
+ if (!file.exists()) {
+ return;
+ }
+ try {
+ final byte[] contents = Files.readAllBytes(Paths.get(fileName));
+ String configJson = new String(contents);
+ logger.debug("Read configJson \"{}\" from {}", configJson, fileName);
+ Map configMap = OBJECT_MAPPER.readValue(configJson,
+ new TypeReference>() {
+ });
+ this.username = Optional.ofNullable(configMap.get("username")).orElse("");
+ this.password = Optional.ofNullable(configMap.get("password")).orElse("");
+ this.macAddress = Optional.ofNullable(configMap.get("macAddress")).orElse("");
+ logger.debug("Processed configJson as {} {} {}", this.username, this.password, this.macAddress);
+ } catch (IOException ex) {
+ logger.debug("IOException when reading configJson from file {}", ex.getMessage());
+ }
+ }
+
+ public void setCreds(String username, String password) {
+ this.username = username;
+ this.password = password;
+ saveConfigs();
+ }
+
+ private boolean servicePing() {
+ int timeout = 500;
+
+ SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.philipstvPort);
+ try (Socket socket = new Socket()) {
+ socket.connect(socketAddress, timeout);
+ return true;
+ } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
+ return false;
+ } catch (IOException ignored) {
+ // IOException is thrown by automatic close() of the socket.
+ // This should actually never return a value as we should return true above already
+ return true;
+ }
+ }
+
+ private void checkHealth() {
+ boolean isOnline = servicePing();
+ logger.debug("{} - Device Health - Online: {} - Logged In: {}", handler.getThingID(), isOnline, isLoggedIn);
+ if (isOnline != this.isOnline) {
+ this.isOnline = isOnline;
+ if (isOnline) {
+ logger.debug("{} - Device is back online. Attempting reconnection.", handler.getThingID());
+ connect();
+ } else {
+ logger.debug("{} - Device is offline.", handler.getThingID());
+ postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "offline.communication-error-will-try-to-reconnect");
+ }
+ }
+ }
+
+ public void checkPendingPowerOn() {
+ if (pendingPowerOn) {
+ @Nullable
+ PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
+ if (powerService != null) {
+ powerService.handleCommand(CHANNEL_POWER, OnOffType.ON);
+ }
+ pendingPowerOn = false;
+ startDeviceHealthJob(5, TimeUnit.SECONDS);
+ }
+ }
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("Received channel: {}, command: {}", channelUID, command);
+ String username = this.username;
+ String password = this.password;
+
+ if (channelUID.getId().equals(CHANNEL_PINCODE)) {
+ if (command instanceof StringType) {
+ HttpHost target = new HttpHost(config.ipAddress, config.philipstvPort, HTTPS);
+ if (command.toString().equals("REQUEST")) {
+ try {
+ initPairingCodeRetrieval(target);
+ } catch (IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "offline.error-occured-while-presenting-pairing-code");
+ }
+ } else {
+ boolean hasFailed = initCredentialsRetrieval(target, command.toString());
+ if (hasFailed) {
+ postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "offline.error-occured-during-retrieval-of-credentials");
+ return;
+ }
+ readConfigs();
+ username = this.username;
+ password = this.password;
+
+ if ((username.isEmpty()) || (password.isEmpty())) {
+ postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "offline.pairing-was-unsuccessful");
+ return;
+ }
+
+ }
+ }
+ return;
+ }
+
+ if ((username.isEmpty()) || (password.isEmpty())) {
+ return; // pairing process is not finished
+ }
+
+ boolean isLoggedIn = this.isLoggedIn;
+ Map channelServices = this.channelServices;
+
+ if ((!isLoggedIn) && (!channelUID.getId().equals(CHANNEL_POWER)
+ & !channelUID.getId().equals(CHANNEL_AMBILIGHT_LOUNGE_POWER))) {
+ // Check if tv turned on meanwhile
+ @Nullable
+ PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
+ if (powerService != null) {
+ powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
+ }
+ isLoggedIn = this.isLoggedIn;
+ if (!isLoggedIn) {
+ // still offline
+ logger.warn(
+ "Cannot execute command {} for channel {}: PowerState of TV was checked and resolved to offline.",
+ command, channelUID.getId());
+ return;
+ }
+ }
+
+ String channel = channelUID.getId();
+ long startTime = System.currentTimeMillis();
+ // Delegate the other commands to correct channel service
+ @Nullable
+ PhilipsTVService philipsTvService = channelServices.get(channel);
+
+ if (philipsTvService == null) {
+ logger.warn("Unknown channel for Philips TV Binding: {}", channel);
+ return;
+ }
+
+ if ((!isLoggedIn) && (channelUID.getId().equals(CHANNEL_POWER)) && (command.equals(OnOffType.ON))) {
+ startDeviceHealthJob(1, TimeUnit.SECONDS);
+ pendingPowerOn = true;
+ }
+
+ philipsTvService.handleCommand(channel, command);
+ long stopTime = System.currentTimeMillis();
+ long elapsedTime = stopTime - startTime;
+ logger.trace("The command {} took : {} nanoseconds", command.toFullString(), elapsedTime);
+ }
+
+ public void initialize() {
+ logger.debug("Init of handler for Thing: {}", handler.getThingID());
+
+ readConfigs();
+ String username = this.username;
+ String password = this.password;
+
+ if ((username.isEmpty()) || (password.isEmpty())) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ "offline.pairing-is-not-configured-yet");
+ return;
+ }
+
+ connect();
+ startDeviceHealthJob(5, TimeUnit.SECONDS);
+ }
+
+ private void startDeviceHealthJob(int interval, TimeUnit unit) {
+ ScheduledFuture> deviceHealthJob = this.deviceHealthJob;
+ if (deviceHealthJob != null) {
+ deviceHealthJob.cancel(true);
+ }
+ this.deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, interval, interval, unit);
+ }
+
+ private void connect() {
+ HttpHost target = this.target;
+ String username = this.username;
+ String password = this.password;
+ String macAddress = this.macAddress;
+ logger.debug("Starting connection to {} {} {}", username, password, macAddress);
+
+ if (!config.useUpnpDiscovery && isSchedulerInitializable()) {
+ logger.debug("connect starting refresh scheduler");
+ startRefreshScheduler();
+ }
+
+ CloseableHttpClient httpClient;
+
+ try {
+ httpClient = ConnectionManagerUtil.createSharedHttpClient(target, username, password);
+ } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
+ postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ String.format("offline.error-occurred-during-creation-of-http-client: %s", e.getMessage()));
+ return;
+ }
+
+ ConnectionManager connectionManager = new ConnectionManager(httpClient, target);
+
+ if (macAddress.isEmpty()) {
+ try {
+ Optional wolAddress = WakeOnLanUtil.getMacFromEnabledInterface(connectionManager);
+ if (wolAddress.isPresent()) {
+ this.macAddress = wolAddress.get();
+ saveConfigs();
+ } else {
+ logger.debug("MAC Address could not be determined for Wake-On-LAN support, "
+ + "because Wake-On-LAN is not enabled on the TV.");
+ }
+ } catch (IOException e) {
+ logger.debug("Error occurred during retrieval of MAC Address: {}", e.getMessage());
+ }
+ }
+
+ startServices(connectionManager);
+
+ discoveryServiceRegistry.addDiscoveryListener(this);
+
+ // Thing is initialized, check power state and available communication of the TV and set ONLINE or OFFLINE
+ postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.online");
+
+ Map channelServices = this.channelServices;
+ @Nullable
+ PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
+ if (powerService != null) {
+ powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
+ }
+ }
+
+ private void startServices(ConnectionManager connectionManager) {
+ Map services = new HashMap<>();
+
+ PhilipsTVService volumeService = new VolumeService(this, connectionManager);
+ services.put(CHANNEL_VOLUME, volumeService);
+ services.put(CHANNEL_MUTE, volumeService);
+
+ PhilipsTVService tvPictureService = new TvPictureService(this, connectionManager);
+ services.put(CHANNEL_BRIGHTNESS, tvPictureService);
+ services.put(CHANNEL_SHARPNESS, tvPictureService);
+ services.put(CHANNEL_CONTRAST, tvPictureService);
+
+ PhilipsTVService keyPressService = new KeyPressService(this, connectionManager);
+ services.put(CHANNEL_KEYPRESS, keyPressService);
+ services.put(CHANNEL_PLAYER, keyPressService);
+
+ PhilipsTVService appService = new AppService(this, connectionManager);
+ services.put(CHANNEL_APP, appService);
+ services.put(CHANNEL_APPNAME, appService);
+ services.put(CHANNEL_APP_ICON, appService);
+
+ PhilipsTVService ambilightService = new AmbilightService(this, connectionManager);
+ services.put(CHANNEL_AMBILIGHT_POWER, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_HUE_POWER, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_LOUNGE_POWER, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_STYLE, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_COLOR, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_LEFT_COLOR, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_RIGHT_COLOR, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_TOP_COLOR, ambilightService);
+ services.put(CHANNEL_AMBILIGHT_BOTTOM_COLOR, ambilightService);
+
+ services.put(CHANNEL_TV_CHANNEL, new TvChannelService(this, connectionManager));
+ services.put(CHANNEL_POWER, new PowerService(this, connectionManager));
+ services.put(CHANNEL_SEARCH_CONTENT, new SearchContentService(this, connectionManager));
+ channelServices = Collections.unmodifiableMap(services);
+ }
+
+ /**
+ * Starts the pairing Process with the TV, which results in a Pairing Code shown on TV.
+ */
+ private void initPairingCodeRetrieval(HttpHost target)
+ throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
+ logger.info("Pairing code for tv authentication is missing. "
+ + "Starting initial pairing process. Please provide manually the pairing code shown on the tv at the configuration of the tv thing.");
+ PhilipsTVPairing pairing = new PhilipsTVPairing();
+ pairing.requestPairingPin(target);
+ }
+
+ private boolean initCredentialsRetrieval(HttpHost target, String pincode) {
+ boolean hasFailed = false;
+ logger.info(
+ "Pairing code is available, but username and/or password is missing. Therefore we try to grant authorization and retrieve username and password.");
+ PhilipsTVPairing pairing = new PhilipsTVPairing();
+ try {
+ if (pincode.isEmpty()) {
+ pairing.finishPairingWithTv(config.pairingCode, this, target);
+ } else {
+ pairing.finishPairingWithTv(pincode, this, target);
+ }
+ postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ "offline.authentication-with-philips-tv-device-was-successful-continuing-initialization-of-the-tv");
+ } catch (Exception e) {
+ postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
+ "offline.could-not-successfully-finish-pairing-process-with-the-tv");
+ logger.warn("Error during finishing pairing process with the TV: {}", e.getMessage(), e);
+ hasFailed = true;
+ }
+ return hasFailed;
+ }
+
+ // callback methods for channel services
+ public void postUpdateChannel(String channelUID, State state) {
+ handler.updateChannelState(channelUID, state);
+ }
+
+ public synchronized void postUpdateThing(ThingStatus status, ThingStatusDetail statusDetail, String msg) {
+ logger.trace("postUpdateThing {} {} {}", status, statusDetail, msg);
+ if (status == ThingStatus.ONLINE) {
+ if (msg.equalsIgnoreCase(STANDBY_MSG)) {
+ handler.updateChannelState(CHANNEL_POWER, OnOffType.OFF);
+ } else {
+ handler.updateChannelState(CHANNEL_POWER, OnOffType.ON);
+ startDeviceHealthJob(5, TimeUnit.SECONDS);
+ pendingPowerOn = false;
+ }
+ if (isSchedulerInitializable()) { // Init refresh scheduler only, if pairing is completed
+ startRefreshScheduler();
+ }
+ } else if (status == ThingStatus.OFFLINE) {
+ handler.updateChannelState(CHANNEL_POWER, OnOffType.OFF);
+ if (!TV_NOT_LISTENING_MSG.equals(msg)) { // avoid cancelling refresh if TV is temporarily not available
+ ScheduledFuture> refreshScheduler = this.refreshScheduler;
+ if (refreshScheduler != null) {
+ if (config.useUpnpDiscovery && isRefreshSchedulerRunning.test(refreshScheduler)) {
+ stopRefreshScheduler();
+ }
+ }
+ // Reset app and channel list (if existing) for new retrieval during next startup
+ Map channelServices = this.channelServices;
+ @Nullable
+ PhilipsTVService appnameService = channelServices.get(CHANNEL_APPNAME);
+ if (appnameService != null) {
+ ((AppService) appnameService).clearAvailableAppList();
+ }
+ @Nullable
+ PhilipsTVService tvchannelService = channelServices.get(CHANNEL_TV_CHANNEL);
+ if (tvchannelService != null) {
+ ((TvChannelService) tvchannelService).clearAvailableTvChannelList();
+ }
+ }
+ }
+ updateStatus(status, statusDetail, msg);
+ }
+
+ private boolean isSchedulerInitializable() {
+ String username = this.username;
+ String password = this.password;
+ boolean schedulerIsDone = false;
+ ScheduledFuture> refreshScheduler = this.refreshScheduler;
+ if (refreshScheduler != null) {
+ schedulerIsDone = refreshScheduler.isDone();
+ }
+ return (!username.isEmpty()) && (!password.isEmpty()) && ((refreshScheduler == null) || schedulerIsDone);
+ }
+
+ private void startRefreshScheduler() {
+ int configuredRefreshRateOrDefault = Optional.ofNullable(config.refreshRate).orElse(10);
+ if (configuredRefreshRateOrDefault > 0) { // If value equals zero, refreshing should not be scheduled
+ ScheduledFuture> refreshScheduler = this.refreshScheduler;
+ if (refreshScheduler != null) {
+ logger.debug("Refresh Scheduler already started for Philips TV {}, terminating.", handler.getThingID());
+ if (isRefreshSchedulerRunning.test(refreshScheduler)) {
+ stopRefreshScheduler();
+ }
+ }
+ logger.debug("Starting Refresh Scheduler for Philips TV {} with refresh rate of {}.", handler.getThingID(),
+ configuredRefreshRateOrDefault);
+ this.refreshScheduler = scheduler.scheduleWithFixedDelay(this::refreshTvProperties, 10,
+ configuredRefreshRateOrDefault, TimeUnit.SECONDS);
+ }
+ }
+
+ private void stopRefreshScheduler() {
+ logger.debug("Stopping Refresh Scheduler for Philips TV: {}", handler.getThingID());
+ ScheduledFuture> refreshScheduler = this.refreshScheduler;
+ if (refreshScheduler != null) {
+ refreshScheduler.cancel(true);
+ }
+ }
+
+ private void refreshTvProperties() {
+ try {
+ boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
+ if (isLockAcquired) {
+ try {
+ if (isOnline) {
+ Map channelServices = this.channelServices;
+ @Nullable
+ PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
+ if (powerService != null) {
+ powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
+ }
+ @Nullable
+ PhilipsTVService volumeService = channelServices.get(CHANNEL_VOLUME);
+ if (volumeService != null) {
+ volumeService.handleCommand(CHANNEL_VOLUME, RefreshType.REFRESH);
+ }
+ @Nullable
+ PhilipsTVService appnameService = channelServices.get(CHANNEL_APPNAME);
+ if (appnameService != null) {
+ appnameService.handleCommand(CHANNEL_APPNAME, RefreshType.REFRESH);
+ }
+ @Nullable
+ PhilipsTVService tvchannelService = channelServices.get(CHANNEL_TV_CHANNEL);
+ if (tvchannelService != null) {
+ tvchannelService.handleCommand(CHANNEL_TV_CHANNEL, RefreshType.REFRESH);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+ } catch (InterruptedException e) {
+ logger.warn("Exception occurred during refreshing the tv properties: {}", e.getMessage());
+ }
+ }
+
+ public void updateChannelStateDescription(final String channelId, Map values) {
+ AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider = this.stateDescriptionProvider;
+ List options = new ArrayList<>();
+ if (!values.isEmpty()) {
+ values.forEach((key, value) -> options.add(new StateOption(key, value)));
+ stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThingUID(), channelId), options);
+ }
+ }
+
+ @Override
+ public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
+ logger.debug("thingDiscovered: {}", result);
+
+ if (config.useUpnpDiscovery && config.ipAddress.equals(result.getProperties().get(HOST))) {
+ upnpThingUID = result.getThingUID();
+ logger.debug("thingDiscovered, thingUID={}, discoveredUID={}", handler.getThingUID(), upnpThingUID);
+ Map channelServices = this.channelServices;
+ @Nullable
+ PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
+ if (powerService != null) {
+ powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
+ }
+ }
+ }
+
+ @Override
+ public void thingRemoved(DiscoveryService discoveryService, ThingUID thingUID) {
+ logger.debug("thingRemoved: {}", thingUID);
+
+ if (thingUID.equals(upnpThingUID)) {
+ postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.standby");
+ }
+ }
+
+ @Override
+ public @Nullable Collection removeOlderResults(DiscoveryService discoveryService, long l,
+ @Nullable Collection collection, @Nullable ThingUID thingUID) {
+ return Collections.emptyList();
+ }
+
+ public void dispose() {
+ discoveryServiceRegistry.removeDiscoveryListener(this);
+ ScheduledFuture> refreshScheduler = this.refreshScheduler;
+ if (refreshScheduler != null) {
+ if (isRefreshSchedulerRunning.test(refreshScheduler)) {
+ stopRefreshScheduler();
+ }
+ }
+ ScheduledFuture> deviceHealthJob = this.deviceHealthJob;
+ if (deviceHealthJob != null) {
+ deviceHealthJob.cancel(true);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/WakeOnLanUtil.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/WakeOnLanUtil.java
new file mode 100644
index 0000000000000..c157244e7b33e
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/WakeOnLanUtil.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.GET_NETWORK_DEVICES_PATH;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * The {@link WakeOnLanUtil} is offering methods for powering on TVs via Wake-On-LAN.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public final class WakeOnLanUtil {
+
+ private static final int MAX_WOL_RETRIES = 10;
+
+ private static final int WOL_PORT = 9;
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WakeOnLanUtil.class);
+
+ private static final Pattern MAC_PATTERN = Pattern.compile("([:\\-])");
+
+ private static final Predicate IS_WOL_ENABLED = j -> j.get("wake-on-lan").asText()
+ .equalsIgnoreCase("Enabled");
+
+ private static final Predicate IS_NOT_LOOPBACK = ni -> {
+ try {
+ return !ni.isLoopback();
+ } catch (SocketException e) {
+ return false;
+ }
+ };
+
+ private WakeOnLanUtil() {
+ }
+
+ public static Optional getMacFromEnabledInterface(ConnectionManager connectionManager) throws IOException {
+ String jsonContent = connectionManager.doHttpsGet(GET_NETWORK_DEVICES_PATH);
+ List jsonNode = OBJECT_MAPPER.readValue(jsonContent, new TypeReference>() {
+ });
+
+ return jsonNode.stream().filter(IS_WOL_ENABLED).map(j -> j.get("mac").asText())
+ .peek(m -> LOGGER.debug("Mac identified as: {}", m)).findFirst();
+ }
+
+ public static void wakeOnLan(String ip, String mac) throws IOException, InterruptedException {
+ for (int i = 0; i < MAX_WOL_RETRIES; i++) {
+ if (isReachable(ip)) {
+ Thread.sleep(2000);
+ return;
+ } else {
+ Thread.sleep(100);
+ sendWakeOnLanPackage(mac);
+ }
+ }
+ }
+
+ private static void sendWakeOnLanPackage(String mac) throws IOException {
+ byte[] macBytes = getMacBytes(mac);
+ byte[] bytes = new byte[6 + (16 * macBytes.length)];
+ for (int i = 0; i < 6; i++) {
+ bytes[i] = (byte) 0xff;
+ }
+ for (int i = 6; i < bytes.length; i += macBytes.length) {
+ System.arraycopy(macBytes, 0, bytes, i, macBytes.length);
+ }
+
+ List broadcastAddresses = Collections.list(NetworkInterface.getNetworkInterfaces()).stream()
+ .filter(IS_NOT_LOOPBACK).map(NetworkInterface::getInterfaceAddresses).flatMap(Collection::stream)
+ .map(InterfaceAddress::getBroadcast).filter(Objects::nonNull).collect(Collectors.toList());
+
+ for (InetAddress broadcast : broadcastAddresses) {
+ DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcast, WOL_PORT);
+ try (DatagramSocket socket = new DatagramSocket()) {
+ LOGGER.debug("WOL sent to Broadcast-IP {} with MAC {}", broadcast, mac);
+ socket.send(packet);
+ }
+ }
+ }
+
+ private static byte[] getMacBytes(String mac) {
+ byte[] bytes = new byte[6];
+ String[] hex = MAC_PATTERN.split(mac);
+ if (hex.length != 6) {
+ throw new IllegalArgumentException("Invalid MAC address.");
+ }
+ try {
+ for (int i = 0; i < 6; i++) {
+ bytes[i] = (byte) Integer.parseInt(hex[i], 16);
+ }
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid hex digit in MAC address.");
+ }
+ return bytes;
+ }
+
+ public static boolean isReachable(String ipAddress) throws IOException {
+ InetAddress inetAddress = InetAddress.getByName(ipAddress);
+ return inetAddress.isReachable(1000);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/discovery/PhilipsTVDiscoveryParticipant.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/discovery/PhilipsTVDiscoveryParticipant.java
new file mode 100644
index 0000000000000..632dc50520583
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/discovery/PhilipsTVDiscoveryParticipant.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.discovery;
+
+import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.jupnp.model.meta.DeviceDetails;
+import org.jupnp.model.meta.ModelDetails;
+import org.jupnp.model.meta.RemoteDevice;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PhilipsTVDiscoveryParticipant} is responsible for discovering Philips TV devices through UPnP.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class PhilipsTVDiscoveryParticipant implements UpnpDiscoveryParticipant {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Override
+ public Set getSupportedThingTypeUIDs() {
+ return Collections.singleton(THING_TYPE_PHILIPSTV);
+ }
+
+ @Override
+ public @Nullable DiscoveryResult createResult(RemoteDevice device) {
+ final ThingUID uid = getThingUID(device);
+ if (uid == null) {
+ return null;
+ }
+
+ final Map properties = new HashMap<>(2);
+ String ipAddress = device.getIdentity().getDescriptorURL().getHost();
+ properties.put(PARAMETER_IP_ADDRESS, ipAddress);
+ properties.put(PARAMETER_PHILIPSTV_PORT, DEFAULT_PORT);
+ logger.debug("Philips TV Found: {}, using default port {}", ipAddress, DEFAULT_PORT);
+ String friendlyName = device.getDetails().getFriendlyName();
+ if (friendlyName.length() > 0 && Character.isDigit(friendlyName.charAt(0))) {
+ friendlyName = "_" + friendlyName; // label must not start with a digit
+ }
+
+ return DiscoveryResultBuilder.create(uid).withThingType(THING_TYPE_PHILIPSTV).withProperties(properties)
+ .withLabel(friendlyName).build();
+ }
+
+ @Override
+ public @Nullable ThingUID getThingUID(RemoteDevice device) {
+ DeviceDetails details = device.getDetails();
+ if (details != null) {
+ ModelDetails modelDetails = details.getModelDetails();
+ if (modelDetails != null) {
+ String modelName = modelDetails.getModelName();
+ String modelDescription = modelDetails.getModelDescription();
+ if (modelName != null && modelDescription != null) {
+ if (modelName.contains("Philips TV")) {
+ logger.debug("Device found: {} with desc {}", modelName, modelDescription);
+ // One Philips TV contains several UPnP devices.
+ // Create unique Philips TV thing for every Media Renderer
+ // device and ignore rest of the UPnP devices.
+ if (modelDescription.contains("Media")) {
+ // UDN shouldn't contain '-' characters.
+ String udn = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
+ logger.debug("Discovered a Philips TV '{}' model '{}' thing with UDN '{}'",
+ device.getDetails().getFriendlyName(), modelName, udn);
+
+ return new ThingUID(THING_TYPE_PHILIPSTV, udn);
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/PhilipsTVPairing.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/PhilipsTVPairing.java
new file mode 100644
index 0000000000000..8b22a977f5c2e
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/PhilipsTVPairing.java
@@ -0,0 +1,204 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.pairing;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.BASE_PATH;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.EMPTY;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.SLASH;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Formatter;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.MalformedChallengeException;
+import org.apache.http.client.AuthCache;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.auth.DigestScheme;
+import org.apache.http.impl.client.BasicAuthCache;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManagerUtil;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.pairing.model.AuthDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.pairing.model.DeviceDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.pairing.model.FinishPairingDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.pairing.model.PairingDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.pairing.model.RequestCodeDTO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PhilipsTVPairing} is responsible for the initial pairing process with the Philips TV.
+ * The outcome of this one-time pairing is a registered user with password, which will be used for
+ * controlling the tv.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class PhilipsTVPairing {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private static String authTimestamp = "";
+
+ private static String authKey = "";
+
+ private static String deviceId = "";
+
+ private final String pairingBasePath = BASE_PATH + "pair" + SLASH;
+
+ public void requestPairingPin(HttpHost target)
+ throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
+ RequestCodeDTO requestCodeDTO = new RequestCodeDTO(
+ Stream.of("read", "write", "control").collect(Collectors.toList()), createDeviceSpecification());
+
+ CloseableHttpClient httpClient = ConnectionManagerUtil.createSharedHttpClient(target, EMPTY, EMPTY);
+ ConnectionManager connectionManager = new ConnectionManager(httpClient, target);
+ String requestCodeJson = OBJECT_MAPPER.writeValueAsString(requestCodeDTO);
+ String requestPairingCodePath = pairingBasePath + "request";
+ logger.debug("Request pairing code with json: {}", requestCodeJson);
+ PairingDTO pairingDTO = OBJECT_MAPPER
+ .readValue(connectionManager.doHttpsPost(requestPairingCodePath, requestCodeJson), PairingDTO.class);
+
+ authTimestamp = pairingDTO.getTimestamp();
+ authKey = pairingDTO.getAuthKey();
+
+ logger.info("The pairing code is valid for {} seconds.", pairingDTO.getTimeout());
+ }
+
+ public void finishPairingWithTv(String pairingCode, PhilipsTVConnectionManager handler, HttpHost target)
+ throws NoSuchAlgorithmException, InvalidKeyException, IOException, KeyStoreException,
+ KeyManagementException {
+ AuthDTO authDTO = new AuthDTO();
+ authDTO.setAuthAppId("1");
+ authDTO.setAuthSignature(calculateRFC2104HMAC(authTimestamp + pairingCode));
+ authDTO.setAuthTimestamp(authTimestamp);
+ authDTO.setPin(pairingCode);
+
+ FinishPairingDTO finishPairingDTO = new FinishPairingDTO(createDeviceSpecification(), authDTO);
+ String grantPairingJson = OBJECT_MAPPER.writeValueAsString(finishPairingDTO);
+
+ Header challengeHeader = null;
+ try (CloseableHttpClient httpClient = ConnectionManagerUtil.createSharedHttpClient(target, EMPTY, EMPTY)) {
+ CloseableHttpResponse response = httpClient
+ .execute(new HttpGet(target.toURI() + pairingBasePath + "grant"));
+ challengeHeader = response.getFirstHeader("WWW-Authenticate");
+ } catch (IOException e) {
+ logger.debug("finishPairingWithTv: {}", e.getMessage());
+ throw e;
+ }
+
+ try (CloseableHttpClient client = ConnectionManagerUtil.createSharedHttpClient(target, deviceId, authKey)) {
+ logger.debug("{} and device id: {} and auth_key: {}", grantPairingJson, deviceId, authKey);
+
+ String grantPairingCodePath = pairingBasePath + "grant";
+ HttpPost httpPost = new HttpPost(grantPairingCodePath);
+ httpPost.setHeader("Content-type", "application/json");
+ httpPost.setEntity(new StringEntity(grantPairingJson));
+
+ DigestScheme digestAuth = new DigestScheme();
+ digestAuth.processChallenge(challengeHeader);
+
+ AuthCache authCache = new BasicAuthCache();
+ authCache.put(target, digestAuth);
+
+ HttpClientContext localContext = HttpClientContext.create();
+ localContext.setAuthCache(authCache);
+
+ try (CloseableHttpResponse response = client.execute(target, httpPost, localContext)) {
+ String jsonContent = EntityUtils.toString(response.getEntity());
+ logger.debug("----------------------------------------");
+ logger.debug("{}", response.getStatusLine());
+ logger.debug("{}", jsonContent);
+ if (response.getStatusLine().getStatusCode() != 200) {
+ throw new IOException("Pairing grant failed");
+ }
+ if (jsonContent.contains("INVALID_PIN")) {
+ throw new IOException("Invalid PIN");
+ }
+ }
+ handler.setCreds(deviceId, authKey);
+ } catch (MalformedChallengeException e) {
+ logger.debug("finishPairingWithTv: {}", e.getMessage());
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ private String createDeviceId() {
+ StringBuilder deviceIdBuilder = new StringBuilder();
+ String chars = "abcdefghkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789";
+ for (int i = 0; i < 16; i++) {
+ int index = (int) Math.floor(Math.random() * chars.length());
+ deviceIdBuilder.append(chars.charAt(index));
+ }
+ return deviceIdBuilder.toString();
+ }
+
+ private DeviceDTO createDeviceSpecification() {
+ DeviceDTO deviceDTO = new DeviceDTO();
+ deviceDTO.setAppName("openHAB");
+ deviceDTO.setAppId("app.id");
+ deviceDTO.setDeviceName("heliotrope");
+ deviceDTO.setDeviceOs("Android");
+ deviceDTO.setType("native");
+ if (deviceId.isEmpty()) {
+ deviceId = createDeviceId();
+ }
+ deviceDTO.setId(deviceId);
+ return deviceDTO;
+ }
+
+ private String toHexString(byte[] bytes) {
+ try (Formatter formatter = new Formatter()) {
+ for (byte b : bytes) {
+ formatter.format("%02x", b);
+ }
+
+ return formatter.toString();
+ }
+ }
+
+ private String calculateRFC2104HMAC(String data)
+ throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
+ String hmacSHA1 = "HmacSHA1";
+ // Key used for generated the HMAC signature
+ String secretKey = "ZmVay1EQVFOaZhwQ4Kv81ypLAZNczV9sG4KkseXWn1NEk6cXmPKO/MCa9sryslvLCFMnNe4Z4CPXzToowvhHvA==";
+ Key signingKey = new SecretKeySpec(Base64.getDecoder().decode(secretKey), hmacSHA1);
+ Mac mac = Mac.getInstance(hmacSHA1);
+ mac.init(signingKey);
+ return Base64.getEncoder().encodeToString(toHexString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8.name())))
+ .getBytes(StandardCharsets.UTF_8.name()));
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/AuthDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/AuthDTO.java
new file mode 100644
index 0000000000000..47f7e4ae1ad77
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/AuthDTO.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.pairing.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link FinishPairingDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class AuthDTO {
+
+ @JsonProperty("auth_signature")
+ private String authSignature = "";
+
+ @JsonProperty("auth_timestamp")
+ private String authTimestamp = "";
+
+ @JsonProperty("pin")
+ private String pin = "";
+
+ @JsonProperty("auth_AppId")
+ private String authAppId = "";
+
+ public void setAuthSignature(String authSignature) {
+ this.authSignature = authSignature;
+ }
+
+ public String getAuthSignature() {
+ return authSignature;
+ }
+
+ public void setAuthTimestamp(String authTimestamp) {
+ this.authTimestamp = authTimestamp;
+ }
+
+ public String getAuthTimestamp() {
+ return authTimestamp;
+ }
+
+ public void setPin(String pin) {
+ this.pin = pin;
+ }
+
+ public String getPin() {
+ return pin;
+ }
+
+ public void setAuthAppId(String authAppId) {
+ this.authAppId = authAppId;
+ }
+
+ public String getAuthAppId() {
+ return authAppId;
+ }
+
+ @Override
+ public String toString() {
+ return "Auth{" + "auth_signature = '" + authSignature + '\'' + ",auth_timestamp = '" + authTimestamp + '\''
+ + ",pin = '" + pin + '\'' + ",auth_AppId = '" + authAppId + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/DeviceDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/DeviceDTO.java
new file mode 100644
index 0000000000000..a63ffcc6cf2b7
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/DeviceDTO.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.pairing.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link RequestCodeDTO} and {@link FinishPairingDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class DeviceDTO {
+
+ @JsonProperty("app_name")
+ private String appName = "";
+
+ @JsonProperty("device_name")
+ private String deviceName = "";
+
+ @JsonProperty("id")
+ private String id = "";
+
+ @JsonProperty("type")
+ private String type = "";
+
+ @JsonProperty("app_id")
+ private String appId = "";
+
+ @JsonProperty("device_os")
+ private String deviceOs = "";
+
+ public void setAppName(String appName) {
+ this.appName = appName;
+ }
+
+ public String getAppName() {
+ return appName;
+ }
+
+ public void setDeviceName(String deviceName) {
+ this.deviceName = deviceName;
+ }
+
+ public String getDeviceName() {
+ return deviceName;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public void setDeviceOs(String deviceOs) {
+ this.deviceOs = deviceOs;
+ }
+
+ public String getDeviceOs() {
+ return deviceOs;
+ }
+
+ @Override
+ public String toString() {
+ return "Device{" + "app_name = '" + appName + '\'' + ",device_name = '" + deviceName + '\'' + ",id = '" + id
+ + '\'' + ",type = '" + type + '\'' + ",app_id = '" + appId + '\'' + ",device_os = '" + deviceOs + '\''
+ + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/FinishPairingDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/FinishPairingDTO.java
new file mode 100644
index 0000000000000..c28ea0989d291
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/FinishPairingDTO.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.pairing.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link FinishPairingDTO} class defines the Data Transfer Object
+ * for the Philips TV API /pair/grant endpoint to finish pairing.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class FinishPairingDTO {
+
+ @JsonProperty("auth")
+ private AuthDTO auth;
+
+ @JsonProperty("device")
+ private DeviceDTO device;
+
+ public FinishPairingDTO(DeviceDTO device, AuthDTO auth) {
+ this.device = device;
+ this.auth = auth;
+ }
+
+ public void setAuth(AuthDTO auth) {
+ this.auth = auth;
+ }
+
+ public AuthDTO getAuth() {
+ return auth;
+ }
+
+ public void setDevice(DeviceDTO device) {
+ this.device = device;
+ }
+
+ public DeviceDTO getDevice() {
+ return device;
+ }
+
+ @Override
+ public String toString() {
+ return "FinishPairingDTO{" + "auth = '" + auth + '\'' + ",device = '" + device + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/PairingDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/PairingDTO.java
new file mode 100644
index 0000000000000..bfe1175d83728
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/PairingDTO.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.pairing.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Response Data Transfer Object of {@link RequestCodeDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class PairingDTO {
+
+ @JsonProperty("auth_key")
+ private String authKey = "";
+
+ @JsonProperty("timestamp")
+ private String timestamp = "";
+
+ @JsonProperty("timeout")
+ private String timeout = "";
+
+ public String getTimeout() {
+ return timeout;
+ }
+
+ public void setTimeout(String timeout) {
+ this.timeout = timeout;
+ }
+
+ public void setAuthKey(String authKey) {
+ this.authKey = authKey;
+ }
+
+ public String getAuthKey() {
+ return authKey;
+ }
+
+ public void setTimestamp(String timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public String toString() {
+ return "PairingCodeDTO{" + "auth_key = '" + authKey + '\'' + ",timestamp = '" + timestamp + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/RequestCodeDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/RequestCodeDTO.java
new file mode 100644
index 0000000000000..e72a0b47d0712
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/pairing/model/RequestCodeDTO.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.pairing.model;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link RequestCodeDTO} class defines the Data Transfer Object
+ * for the Philips TV API /pair/request endpoint to request a pairing code.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class RequestCodeDTO {
+
+ @JsonProperty("scope")
+ private List scope;
+
+ @JsonProperty("device")
+ private DeviceDTO device;
+
+ public RequestCodeDTO(List scope, DeviceDTO device) {
+ this.scope = scope;
+ this.device = device;
+ }
+
+ public void setScope(List scope) {
+ this.scope = scope;
+ }
+
+ public List getScope() {
+ return scope;
+ }
+
+ public void setDevice(DeviceDTO device) {
+ this.device = device;
+ }
+
+ public DeviceDTO getDevice() {
+ return device;
+ }
+
+ @Override
+ public String toString() {
+ return "RequestPinDTO{" + "scope = '" + scope + '\'' + ",device = '" + device + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/AmbilightService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/AmbilightService.java
new file mode 100644
index 0000000000000..3cf2f5c97e015
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/AmbilightService.java
@@ -0,0 +1,316 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.WakeOnLanUtil;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.DataDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.TvSettingsUpdateDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ValueDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ValuesDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightColorDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightColorDeltaDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightColorSettingsDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightConfigDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightLoungeDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightModeDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightPowerDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ambilight.AmbilightTopologyDTO;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Service for handling commands regarding Ambilight settings of the TV
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class AmbilightService implements PhilipsTVService {
+
+ private static final List AMBILIGHT_COLOR_CHANNELS = Stream
+ .of(CHANNEL_AMBILIGHT_COLOR, CHANNEL_AMBILIGHT_LEFT_COLOR, CHANNEL_AMBILIGHT_RIGHT_COLOR,
+ CHANNEL_AMBILIGHT_TOP_COLOR, CHANNEL_AMBILIGHT_BOTTOM_COLOR)
+ .collect(Collectors.toList());
+ private static final int AMBILIGHT_HUE_NODE_ID = 2131230774;
+ private static final int AMBILIGHT_BRIGHTNESS_NODE_ID = 2131230769;
+ private static final String AMBILIGHT_MODE_MANUAL = "manual";
+ private static final String AMBILIGHT_STYLE_FOLLOW_VIDEO = "FOLLOW_VIDEO";
+ private static final String AMBILIGHT_STYLE_FOLLOW_COLOR = "FOLLOW_COLOR";
+ private static final String AMBILIGHT_ALGORITHM_MANUAL_HUE = "MANUAL HUE";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final boolean isWakeOnLanEnabled;
+
+ private @Nullable AmbilightTopologyDTO ambilightTopology;
+
+ private final ConnectionManager connectionManager;
+
+ public AmbilightService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ this.isWakeOnLanEnabled = handler.getMacAddress().isEmpty() ? false : true;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ try {
+ if (CHANNEL_AMBILIGHT_POWER.equals(channel) && (command instanceof OnOffType)) {
+ setAmbilightPowerState(command);
+ } else if (CHANNEL_AMBILIGHT_POWER.equals(channel) && (command instanceof RefreshType)) {
+ AmbilightPowerDTO ambilightPowerDTO = getAmbilightPowerState();
+ handler.postUpdateChannel(CHANNEL_AMBILIGHT_POWER,
+ ambilightPowerDTO.isPoweredOn() ? OnOffType.ON : OnOffType.OFF);
+ } else if (CHANNEL_AMBILIGHT_HUE_POWER.equals(channel) && (command instanceof OnOffType)) {
+ setAmbilightHuePowerState(command);
+ } else if (CHANNEL_AMBILIGHT_LOUNGE_POWER.equals(channel) && (command instanceof OnOffType)) {
+ setAmbilightLoungePowerState(command);
+ } else if (CHANNEL_AMBILIGHT_STYLE.equals(channel) && (command instanceof StringType)) {
+ setAmbilightStyle(command.toString());
+ } else if (CHANNEL_AMBILIGHT_STYLE.equals(channel) && (command instanceof RefreshType)) {
+ AmbilightConfigDTO config = getAmbilightConfig();
+ String styleWithAlgorithm = String.format("%s %s", config.getStyleName(), config.getMenuSetting());
+ handler.postUpdateChannel(CHANNEL_AMBILIGHT_STYLE, new StringType(styleWithAlgorithm));
+ } else if (CHANNEL_AMBILIGHT_COLOR.equals(channel) && (command instanceof HSBType)) {
+ setAllAmbilightColors((HSBType) command);
+ } else if ((CHANNEL_AMBILIGHT_LEFT_COLOR.equals(channel) || CHANNEL_AMBILIGHT_RIGHT_COLOR.equals(channel)
+ || CHANNEL_AMBILIGHT_TOP_COLOR.equals(channel) || CHANNEL_AMBILIGHT_BOTTOM_COLOR.equals(channel))
+ && (command instanceof HSBType)) {
+ setAmbilightPixel((HSBType) command, channel);
+ } else if (AMBILIGHT_COLOR_CHANNELS.contains(channel) && (command instanceof PercentType)) {
+ setAmbilightBrightness(((PercentType) command).intValue());
+ } else {
+ if (!(command instanceof RefreshType)) {
+ logger.warn("Unknown command: {} for Channel {}", command, channel);
+ }
+ }
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Error during handling the Ambilight command {} for Channel {}: {}", command, channel,
+ e.getMessage(), e);
+ }
+ }
+ }
+
+ private AmbilightPowerDTO getAmbilightPowerState() throws IOException {
+ return OBJECT_MAPPER.readValue(connectionManager.doHttpsGet(AMBILIGHT_POWERSTATE_PATH),
+ AmbilightPowerDTO.class);
+ }
+
+ private void setAmbilightPowerState(Command command) throws IOException {
+ if (command.equals(OnOffType.OFF)) {
+ AmbilightPowerDTO ambilightPower = new AmbilightPowerDTO();
+ ambilightPower.setPower(POWER_OFF);
+ String powerStateJson = OBJECT_MAPPER.writeValueAsString(ambilightPower);
+ logger.debug("Post Ambilight power state json: {}", powerStateJson);
+ connectionManager.doHttpsPost(AMBILIGHT_POWERSTATE_PATH, powerStateJson);
+ } else { // power on via setting FOLLOW_VIDEO instead through POWERSTATE_PATH which sets FOLLOW_COLOR
+ setAmbilightStyle(String.format("%s %s", AMBILIGHT_STYLE_FOLLOW_VIDEO, "STANDARD"));
+ }
+ }
+
+ private void setAmbilightHuePowerState(Command command) throws IOException {
+ DataDTO data = new DataDTO((command.equals(OnOffType.ON) ? "true" : "false"));
+
+ ValueDTO value = new ValueDTO(data);
+ value.setNodeid(AMBILIGHT_HUE_NODE_ID);
+ value.setAvailable("true");
+ value.setControllable("true");
+
+ ValuesDTO values = new ValuesDTO(value);
+ TvSettingsUpdateDTO ambilightHuePower = new TvSettingsUpdateDTO(Collections.singletonList(values));
+
+ String ambilightHuePowerJson = OBJECT_MAPPER.writeValueAsString(ambilightHuePower);
+ logger.debug("Post Ambilight hue power state json: {}", ambilightHuePowerJson);
+ connectionManager.doHttpsPost(UPDATE_SETTINGS_PATH, ambilightHuePowerJson);
+ }
+
+ private void setAmbilightLoungePowerState(Command command) throws IOException, InterruptedException {
+ AmbilightColorDTO ambilightColorDTO = new AmbilightColorDTO();
+ if (command.equals(OnOffType.ON)) {
+ if (isWakeOnLanEnabled && !WakeOnLanUtil.isReachable(handler.config.ipAddress)) {
+ WakeOnLanUtil.wakeOnLan(handler.config.ipAddress, handler.getMacAddress());
+ }
+ ambilightColorDTO.setHue(0);
+ } else {
+ ambilightColorDTO.setHue(255);
+ }
+ AmbilightLoungeDTO ambilightLoungeDTO = new AmbilightLoungeDTO(ambilightColorDTO);
+
+ String setAmbilightLoungeJson = OBJECT_MAPPER.writeValueAsString(ambilightLoungeDTO);
+ logger.debug("Setting ambilight lounge power state json: {}", setAmbilightLoungeJson);
+ connectionManager.doHttpsPost(AMBILIGHT_LOUNGE_PATH, setAmbilightLoungeJson);
+ }
+
+ private void setAmbilightStyle(String styleToSet) throws IOException {
+ String[] styleWithAlgorithm = styleToSet.split(" ");
+ if (styleWithAlgorithm.length != 2) {
+ throw new IllegalStateException("Style and/or algorithm is missing.");
+ }
+ String style = styleWithAlgorithm[0];
+ String algorithm = styleWithAlgorithm[1];
+ AmbilightConfigDTO ambilightConfig = new AmbilightConfigDTO(
+ new AmbilightColorSettingsDTO(new AmbilightColorDTO(), new AmbilightColorDeltaDTO()));
+ ambilightConfig.setStyleName(style);
+ ambilightConfig.setMenuSetting(algorithm);
+ if (style.equals(AMBILIGHT_STYLE_FOLLOW_COLOR) && algorithm.equals(AMBILIGHT_ALGORITHM_MANUAL_HUE)) {
+ ambilightConfig.setAlgorithm(algorithm);
+ ambilightConfig.setIsExpert(true);
+ AmbilightColorDeltaDTO ambilightColorDeltaDTO = new AmbilightColorDeltaDTO();
+ ambilightColorDeltaDTO.setHue(0);
+ ambilightColorDeltaDTO.setBrightness(0);
+ ambilightColorDeltaDTO.setSaturation(0);
+ AmbilightColorSettingsDTO ambilightColorSettingsDTO = new AmbilightColorSettingsDTO(new AmbilightColorDTO(),
+ ambilightColorDeltaDTO);
+ ambilightColorSettingsDTO.setSpeed(255);
+ ambilightConfig.setColorSettings(ambilightColorSettingsDTO);
+ }
+ String ambilightConfigJson = OBJECT_MAPPER.writeValueAsString(ambilightConfig);
+ logger.debug("Post config for Ambilight style json: {}", ambilightConfigJson);
+ connectionManager.doHttpsPost(AMBILIGHT_CONFIG_PATH, ambilightConfigJson);
+ }
+
+ private AmbilightConfigDTO getAmbilightConfig() throws IOException {
+ return OBJECT_MAPPER.readValue(connectionManager.doHttpsGet(AMBILIGHT_CONFIG_PATH), AmbilightConfigDTO.class);
+ }
+
+ private void setAmbilightMode(String mode) throws IOException {
+ AmbilightModeDTO ambilightMode = new AmbilightModeDTO();
+ ambilightMode.setCurrent(mode);
+ String ambilightModeJson = OBJECT_MAPPER.writeValueAsString(ambilightMode);
+ logger.debug("Post ambilight mode json: {}", ambilightModeJson);
+ connectionManager.doHttpsPost(AMBILIGHT_MODE_PATH, ambilightModeJson);
+ }
+
+ // private AmbilightModeDTO getAmbilightMode() throws IOException {
+ // return OBJECT_MAPPER.readValue(connectionManager.doHttpsGet(AMBILIGHT_MODE_PATH), AmbilightModeDTO.class);
+ // }
+
+ private void setAmbilightBrightness(int brightnessToSet) throws IOException {
+ String ambilightBrightnessJson = ServiceUtil.createTvSettingsUpdateJson(AMBILIGHT_BRIGHTNESS_NODE_ID,
+ brightnessToSet / 10);
+ logger.debug("Post Ambilight brightness json: {}", ambilightBrightnessJson);
+ connectionManager.doHttpsPost(UPDATE_SETTINGS_PATH, ambilightBrightnessJson);
+ }
+
+ private void setAmbilightPixel(HSBType hsb, String channel) throws IOException {
+ if (ambilightTopology == null) {
+ ambilightTopology = getAmbilightTopology();
+ }
+ setAmbilightMode(AMBILIGHT_MODE_MANUAL); // activates the usage of cached values
+ String sideToSet = determineAmbilightSide(channel);
+ int pixelSize = ambilightTopology.getPixelSizeForGivenSide(sideToSet);
+
+ ObjectNode rootNode = OBJECT_MAPPER.createObjectNode();
+
+ ObjectNode pixel = OBJECT_MAPPER.createObjectNode();
+ pixel.put("r", hsb.getRed().intValue());
+ pixel.put("g", hsb.getGreen().intValue());
+ pixel.put("b", hsb.getBlue().intValue());
+
+ ObjectNode sidePixels = OBJECT_MAPPER.createObjectNode();
+ // pixel declaration in json start with 0
+ IntStream.range(0, pixelSize).forEach(i -> sidePixels.set(Integer.toString(i), pixel));
+
+ IntStream.rangeClosed(1, ambilightTopology.getLayers()).forEach(i -> {
+ ObjectNode layerX = OBJECT_MAPPER.createObjectNode();
+ layerX.set(sideToSet, sidePixels);
+
+ rootNode.set("layer" + i, layerX);
+ });
+
+ String ambilightPixelJson = OBJECT_MAPPER.writeValueAsString(rootNode);
+ logger.debug("Sending {} Ambilight pixel json: {}", sideToSet, ambilightPixelJson);
+ connectionManager.doHttpsPost(AMBILIGHT_CACHED_PATH, ambilightPixelJson);
+ }
+
+ private AmbilightTopologyDTO getAmbilightTopology() throws IOException {
+ return OBJECT_MAPPER.readValue(connectionManager.doHttpsGet(AMBILIGHT_TOPOLOGY_PATH),
+ AmbilightTopologyDTO.class);
+ }
+
+ private String determineAmbilightSide(String channel) {
+ String sideToSet;
+ switch (channel) {
+ case CHANNEL_AMBILIGHT_LEFT_COLOR:
+ sideToSet = "left";
+ break;
+ case CHANNEL_AMBILIGHT_RIGHT_COLOR:
+ sideToSet = "right";
+ break;
+ case CHANNEL_AMBILIGHT_TOP_COLOR:
+ sideToSet = "top";
+ break;
+ case CHANNEL_AMBILIGHT_BOTTOM_COLOR:
+ sideToSet = "bottom";
+ break;
+ default:
+ throw new IllegalStateException("Unexpected channel for ambilight pixel set: " + channel);
+ }
+ return sideToSet;
+ }
+
+ private void setAllAmbilightColors(HSBType hsb) throws IOException {
+ AmbilightColorDTO ambilightColor = new AmbilightColorDTO(hsb);
+ AmbilightColorDeltaDTO ambilightColorDelta = new AmbilightColorDeltaDTO();
+ ambilightColorDelta.setHue(0);
+ ambilightColorDelta.setSaturation(0);
+ ambilightColorDelta.setBrightness(0);
+
+ AmbilightColorSettingsDTO ambilightColorSettings = new AmbilightColorSettingsDTO(ambilightColor,
+ ambilightColorDelta);
+ ambilightColorSettings.setSpeed(255);
+
+ AmbilightConfigDTO ambilightConfig = new AmbilightConfigDTO(ambilightColorSettings);
+ ambilightConfig.setIsExpert(true);
+ ambilightConfig.setStyleName("FOLLOW_COLOR");
+ ambilightConfig.setAlgorithm("MANUAL_HUE");
+
+ String setAmbilightColorsJson = OBJECT_MAPPER.writeValueAsString(ambilightConfig);
+ logger.debug("Setting ambilight colors json: {}", setAmbilightColorsJson);
+ connectionManager.doHttpsPost(AMBILIGHT_CONFIG_PATH, setAmbilightColorsJson);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/AppService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/AppService.java
new file mode 100644
index 0000000000000..8e893e29d65df
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/AppService.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.IOException;
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.http.ParseException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.ApplicationsDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.AvailableAppsDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.ComponentDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.CurrentAppDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.ExtrasDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.IntentDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.LaunchAppDTO;
+import org.openhab.core.library.types.RawType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AppService} is responsible for handling key code commands, which emulate a button
+ * press on a remote control.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class AppService implements PhilipsTVService {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ // Label , Entry of App
+ private @Nullable Map> availableApps;
+
+ private String currentPackageName = "";
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final ConnectionManager connectionManager;
+
+ public AppService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ try {
+ synchronized (this) {
+ if (isAvailableAppListEmpty()) {
+ getAvailableAppListFromTv();
+ handler.updateChannelStateDescription(CHANNEL_APPNAME, availableApps.keySet().stream()
+ .collect(Collectors.toMap(Function.identity(), Function.identity())));
+ }
+ }
+ if (command instanceof RefreshType) {
+ // Get current App name
+ String packageName = getCurrentApp();
+ if (currentPackageName.equals(packageName)) {
+ return;
+ } else {
+ currentPackageName = packageName;
+ }
+ Optional>> app = availableApps.entrySet()
+ .stream().filter(e -> e.getValue().getKey().equalsIgnoreCase(packageName)).findFirst();
+ if (app.isPresent()) {
+ handler.postUpdateChannel(CHANNEL_APP, new StringType(packageName));
+ Map.Entry> appEntry = app.get();
+ handler.postUpdateChannel(CHANNEL_APPNAME, new StringType(appEntry.getKey()));
+ // Get icon for current App
+ RawType image = getIconForApp(appEntry.getValue().getKey(), appEntry.getValue().getValue());
+ handler.postUpdateChannel(CHANNEL_APP_ICON, (image != null) ? image : UnDefType.UNDEF);
+ } else { // NA
+ handler.postUpdateChannel(CHANNEL_APP, new StringType(packageName));
+ handler.postUpdateChannel(CHANNEL_APPNAME, new StringType(packageName));
+ handler.postUpdateChannel(CHANNEL_APP_ICON, UnDefType.UNDEF);
+ }
+ } else if (command instanceof StringType) {
+ String appName = "";
+ if (CHANNEL_APPNAME.equals(channel) && availableApps.containsKey(command.toString())) {
+ launchApp(command.toString());
+ } else if (CHANNEL_APP.equals(channel)) {
+ launchDNApp(command.toString());
+ } else {
+ logger.warn("The given App with Name: {} {} couldn't be found in the local App List from the tv.",
+ command, appName);
+ }
+ } else {
+ logger.warn("Unknown command: {} for Channel {}", command, channel);
+ }
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ logger.debug("Could not execute command for apps, the TV is offline.");
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Error occurred during handling of command for apps: {}", e.getMessage(), e);
+ }
+ }
+ }
+
+ private boolean isAvailableAppListEmpty() {
+ return (availableApps == null) || availableApps.isEmpty();
+ }
+
+ private void launchDNApp(String appName) throws IOException {
+ for (Map.Entry> entry : availableApps.entrySet()) {
+ Map.Entry app = entry.getValue();
+ if (app.getKey().equals(appName)) {
+ logger.debug("Found app by dn: {} {} {}", entry.getKey(), app.getKey(), app.getValue());
+ launchApp(entry.getKey());
+ return;
+ }
+ }
+ logger.warn("The given App with DN: {} couldn't be found in the local App List from the tv.", appName);
+ }
+
+ private void launchApp(String appName) throws IOException {
+ Map.Entry app = availableApps.get(appName);
+
+ ComponentDTO componentDTO = new ComponentDTO();
+ componentDTO.setPackageName(app.getKey());
+ componentDTO.setClassName(app.getValue());
+
+ IntentDTO intentDTO = new IntentDTO(componentDTO, new ExtrasDTO());
+ intentDTO.setAction("empty");
+ LaunchAppDTO launchAppDTO = new LaunchAppDTO(intentDTO);
+ String appLaunchJson = OBJECT_MAPPER.writeValueAsString(launchAppDTO);
+
+ logger.debug("App Launch json: {}", appLaunchJson);
+ connectionManager.doHttpsPost(LAUNCH_APP_PATH, appLaunchJson);
+ }
+
+ private String getCurrentApp() throws IOException, ParseException {
+ CurrentAppDTO currentAppDTO = OBJECT_MAPPER.readValue(connectionManager.doHttpsGet(GET_CURRENT_APP_PATH),
+ CurrentAppDTO.class);
+ return currentAppDTO.getComponent().getPackageName();
+ }
+
+ private @Nullable RawType getIconForApp(String packageName, String className) throws IOException {
+ String pathForIcon = String.format("%s%s-%s%sicon", SLASH, className, packageName, SLASH);
+ byte[] icon = connectionManager
+ .doHttpsGetForImage(String.format("%s%s", GET_AVAILABLE_APP_LIST_PATH, pathForIcon));
+ if ((icon != null) && (icon.length > 0)) {
+ return new RawType(icon, "image/png");
+ } else {
+ return null;
+ }
+ }
+
+ private void getAvailableAppListFromTv() throws IOException {
+ AvailableAppsDTO availableAppsDTO = OBJECT_MAPPER
+ .readValue(connectionManager.doHttpsGet(GET_AVAILABLE_APP_LIST_PATH), AvailableAppsDTO.class);
+
+ ConcurrentMap> appsMap = availableAppsDTO.getApplications()
+ .stream()
+ .collect(Collectors.toConcurrentMap(ApplicationsDTO::getLabel,
+ a -> new AbstractMap.SimpleEntry<>(a.getIntent().getComponent().getPackageName(),
+ a.getIntent().getComponent().getClassName()),
+ (a1, a2) -> a1));
+
+ logger.debug("appsMap - Apps added: {}", appsMap.size());
+ if (logger.isTraceEnabled()) {
+ appsMap.keySet().forEach(app -> logger.trace("appsMap - App found: {}", app));
+ }
+
+ this.availableApps = appsMap;
+ }
+
+ public void clearAvailableAppList() {
+ if (availableApps != null) {
+ availableApps.clear();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/KeyPress.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/KeyPress.java
new file mode 100644
index 0000000000000..96fd973cfce78
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/KeyPress.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The {@link KeyPress} presents all available key codes of Philips TV.
+ *
+ * @see http://jointspace.sourceforge.net/projectdata/documentation/jasonApi/1/doc/API-Method-input-key-POST.html
+ *
+ *
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public enum KeyPress {
+
+ KEY_STANDBY("Standby"),
+ KEY_BACK("Back"),
+ KEY_FIND("Find"),
+ KEY_RED_COLOR("RedColour"),
+ KEY_GREEN_COLOR("GreenColour"),
+ KEY_YELLOW_COLOR("YellowColour"),
+ KEY_BLUE_COLOR("BlueColour"),
+ KEY_HOME("Home"),
+ KEY_VOLUME_UP("VolumeUp"),
+ KEY_VOLUME_DOWN("VolumeDown"),
+ KEY_MUTE("Mute"),
+ KEY_OPTIONS("Options"),
+ KEY_DOT("Dot"),
+ KEY_0("Digit0"),
+ KEY_1("Digit1"),
+ KEY_2("Digit2"),
+ KEY_3("Digit3"),
+ KEY_4("Digit4"),
+ KEY_5("Digit5"),
+ KEY_6("Digit6"),
+ KEY_7("Digit7"),
+ KEY_8("Digit8"),
+ KEY_9("Digit9"),
+ KEY_INFO("Info"),
+ KEY_CURSOR_UP("CursorUp"),
+ KEY_CURSOR_DOWN("CursorDown"),
+ KEY_CURSOR_LEFT("CursorLeft"),
+ KEY_CURSOR_RIGHT("CursorRight"),
+ KEY_CONFIRM("Confirm"),
+ KEY_NEXT("Next"),
+ KEY_PREVIOUS("Previous"),
+ KEY_ADJUST("Adjust"),
+ KEY_WATCH_TV("WatchTV"),
+ KEY_VIEW_MODE("Viewmode"),
+ KEY_TELETEXT("Teletext"),
+ KEY_SUBTITLE("Subtitle"),
+ KEY_CHANNEL_STEP_UP("ChannelStepUp"),
+ KEY_CHANNEL_STEP_DOWN("ChannelStepDown"),
+ KEY_SOURCE("Source"),
+ KEY_AMBILIGHT_ON_OFF("AmbilightOnOff"),
+ KEY_PLAY("Play"),
+ KEY_PAUSE("Pause"),
+ KEY_FAST_FORWARD("FastForward"),
+ KEY_STOP("Stop"),
+ KEY_REWIND("Rewind"),
+ KEY_RECORD("Record"),
+ KEY_ONLINE("Online");
+
+ private final String value;
+
+ KeyPress(String value) {
+ this.value = value;
+ }
+
+ public static KeyPress getKeyPressForValue(String value) throws IllegalArgumentException {
+ return Arrays.stream(values()).filter(v -> v.value.equalsIgnoreCase(value)).findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Key code could not be recognized: " + value));
+ }
+
+ @JsonValue
+ @Override
+ public String toString() {
+ return this.value;
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/KeyPressService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/KeyPressService.java
new file mode 100644
index 0000000000000..fb61c3844d3c7
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/KeyPressService.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.keypress.KeyPressDTO;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link KeyPressService} is responsible for handling key code commands, which emulate a button
+ * press on a remote control.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class KeyPressService implements PhilipsTVService {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final ConnectionManager connectionManager;
+
+ public KeyPressService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ KeyPress keyPress = null;
+ if (isSupportedCommand(command)) {
+ // Three approaches to resolve the KEY_CODE
+ try {
+ keyPress = KeyPress.valueOf(command.toString().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ try {
+ keyPress = KeyPress.valueOf("KEY_" + command.toString().toUpperCase());
+ } catch (IllegalArgumentException e2) {
+ try {
+ keyPress = KeyPress.getKeyPressForValue(command.toString());
+ } catch (IllegalArgumentException e3) {
+ logger.trace("KeyPress threw IllegalArgumentException", e3);
+ }
+ }
+ }
+
+ if (keyPress != null) {
+ try {
+ sendKeyPress(keyPress);
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ logger.debug("Could not execute command for key code, the TV is offline.");
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Unknown error occurred while sending keyPress code {}: {}", keyPress,
+ e.getMessage(), e);
+ }
+ }
+ } else {
+ logger.warn("Command '{}' not a supported keyPress code.", command);
+ }
+ }
+ }
+
+ private static boolean isSupportedCommand(Command command) {
+ return (command instanceof StringType) || (command instanceof NextPreviousType)
+ || (command instanceof PlayPauseType) || (command instanceof RewindFastforwardType);
+ }
+
+ private void sendKeyPress(KeyPress key) throws IOException {
+ KeyPressDTO keyPressDTO = new KeyPressDTO(key);
+ String keyPressJson = OBJECT_MAPPER.writeValueAsString(keyPressDTO);
+ logger.debug("KeyPress Json sent: {}", keyPressJson);
+ connectionManager.doHttpsPost(KEY_CODE_PATH, keyPressJson);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/PowerService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/PowerService.java
new file mode 100644
index 0000000000000..fcf9e211f441c
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/PowerService.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.IOException;
+
+import org.apache.http.ParseException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.WakeOnLanUtil;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.power.PowerStateDTO;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PowerService} is responsible for handling power states commands, which are sent to the
+ * power channel.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class PowerService implements PhilipsTVService {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final ConnectionManager connectionManager;
+
+ private final boolean isWakeOnLanEnabled;
+
+ public PowerService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ this.isWakeOnLanEnabled = handler.getMacAddress().isEmpty() ? false : true;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ try {
+ if (command instanceof RefreshType) {
+ PowerStateDTO powerStateDTO = getPowerState();
+ if (powerStateDTO.isPoweredOn()) {
+ handler.postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.online");
+ } else if (powerStateDTO.isStandby()) {
+ handler.postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.standby");
+ if (powerStateDTO.isStandbyKeep()) {
+ handler.checkPendingPowerOn();
+ }
+ } else {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, EMPTY);
+ }
+ } else if (command instanceof OnOffType) {
+ setPowerState((OnOffType) command);
+ if (command == OnOffType.ON) {
+ handler.postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.online");
+ } else {
+ handler.postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.standby");
+ }
+ } else {
+ logger.warn("Unknown command: {} for Channel {}", command, channel);
+ }
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Unexpected Error handling the PowerState command {} for Channel {}: {}", command, channel,
+ e.getMessage());
+ }
+ }
+ }
+
+ private PowerStateDTO getPowerState() throws IOException, ParseException {
+ return OBJECT_MAPPER.readValue(connectionManager.doHttpsGet(TV_POWERSTATE_PATH), PowerStateDTO.class);
+ }
+
+ private void setPowerState(OnOffType onOffType) throws IOException, InterruptedException {
+ PowerStateDTO powerStateDTO = new PowerStateDTO();
+ if (onOffType == OnOffType.ON) {
+ if (isWakeOnLanEnabled && !WakeOnLanUtil.isReachable(handler.config.ipAddress)) {
+ WakeOnLanUtil.wakeOnLan(handler.config.ipAddress, handler.getMacAddress());
+ }
+ powerStateDTO.setPowerState(POWER_ON);
+ } else {
+ powerStateDTO.setPowerState(STANDBY);
+ }
+
+ String powerStateJson = OBJECT_MAPPER.writeValueAsString(powerStateDTO);
+ logger.debug("PowerState Json sent: {}", powerStateJson);
+ connectionManager.doHttpsPost(TV_POWERSTATE_PATH, powerStateJson);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/SearchContentService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/SearchContentService.java
new file mode 100644
index 0000000000000..344d1222d56d0
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/SearchContentService.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.ComponentDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.ExtrasDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.IntentDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.application.LaunchAppDTO;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for toggling the Google Assistant on the Philips TV
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class SearchContentService implements PhilipsTVService {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final ConnectionManager connectionManager;
+
+ public SearchContentService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ if (command instanceof StringType) {
+ try {
+ searchForContentOnTv(command.toString());
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ logger.warn("Could not search content on Philips TV: TV is offline.");
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Error during the launch of search content on Philips TV: {}", e.getMessage(), e);
+ }
+ }
+ } else if (!(command instanceof RefreshType)) {
+ logger.warn("Unknown command: {} for Channel {}", command, channel);
+ }
+ }
+
+ private void searchForContentOnTv(String searchContent) throws IOException {
+ ExtrasDTO extrasDTO = new ExtrasDTO();
+ extrasDTO.setQuery(searchContent);
+
+ IntentDTO intentDTO = new IntentDTO(new ComponentDTO(), extrasDTO);
+ intentDTO.setAction("android.search.action.GLOBAL_SEARCH");
+ LaunchAppDTO launchAppDTO = new LaunchAppDTO(intentDTO);
+
+ String searchContentLaunch = OBJECT_MAPPER.writeValueAsString(launchAppDTO);
+
+ logger.debug("Search Content Launch json: {}", searchContentLaunch);
+ connectionManager.doHttpsPost(LAUNCH_APP_PATH, searchContentLaunch);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/ServiceUtil.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/ServiceUtil.java
new file mode 100644
index 0000000000000..a2b62f4a45f53
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/ServiceUtil.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+
+import java.util.Collections;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.DataDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.NodesDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.TvSettingsCurrentDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.TvSettingsUpdateDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ValueDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.ValuesDTO;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+/**
+ * Util class for common used methods from philips tv services
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+final class ServiceUtil {
+
+ private ServiceUtil() {
+ }
+
+ static String createTvSettingsRetrievalJson(int nodeId) throws JsonProcessingException {
+ NodesDTO nodes = new NodesDTO();
+ nodes.setNodeid(nodeId);
+ TvSettingsCurrentDTO tvSettingCurrent = new TvSettingsCurrentDTO(Collections.singletonList(nodes));
+ return OBJECT_MAPPER.writeValueAsString(tvSettingCurrent);
+ }
+
+ static String createTvSettingsUpdateJson(int nodeId, int valueToSet) throws JsonProcessingException {
+ DataDTO data = new DataDTO(valueToSet);
+ ValueDTO value = new ValueDTO(data);
+ value.setNodeid(nodeId);
+ ValuesDTO values = new ValuesDTO(value);
+ values.setValue(value);
+ TvSettingsUpdateDTO tvSetting = new TvSettingsUpdateDTO(Collections.singletonList(values));
+ return OBJECT_MAPPER.writeValueAsString(tvSetting);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/TvChannelService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/TvChannelService.java
new file mode 100644
index 0000000000000..53ae7fb3c39aa
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/TvChannelService.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.channel.AvailableTvChannelsDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.channel.ChannelDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.channel.ChannelListDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.channel.TvChannelDTO;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for handling commands regarding setting or retrieving the TV channel
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class TvChannelService implements PhilipsTVService {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ // Name , ccid of TV Channel
+ private @Nullable Map availableTvChannels;
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final ConnectionManager connectionManager;
+
+ public TvChannelService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ try {
+ synchronized (this) {
+ if (isTvChannelListEmpty()) {
+ availableTvChannels = getAvailableTvChannelListFromTv();
+ handler.updateChannelStateDescription(CHANNEL_TV_CHANNEL, availableTvChannels.keySet().stream()
+ .collect(Collectors.toMap(Function.identity(), Function.identity())));
+ }
+ }
+ if (command instanceof RefreshType) {
+ // Get current tv channel name
+ String tvChannelName = getCurrentTvChannel();
+ handler.postUpdateChannel(CHANNEL_TV_CHANNEL, new StringType(tvChannelName));
+ } else if (command instanceof StringType) {
+ if (availableTvChannels.containsKey(command.toString())) {
+ switchTvChannel(command);
+ } else {
+ logger.warn(
+ "The given TV Channel with Name: {} couldn't be found in the local Channel List from the TV.",
+ command);
+ }
+ } else {
+ logger.warn("Unknown command: {} for Channel {}", command, channel);
+ }
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ logger.warn("Could not execute command for TV Channels, the TV is offline.");
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Error occurred during handling of command for TV Channels: {}", e.getMessage(), e);
+ }
+ }
+ }
+
+ private boolean isTvChannelListEmpty() {
+ return (availableTvChannels == null) || availableTvChannels.isEmpty();
+ }
+
+ private Map getAvailableTvChannelListFromTv() throws IOException {
+ AvailableTvChannelsDTO availableTvChannelsDTO = OBJECT_MAPPER.readValue(
+ connectionManager.doHttpsGet(GET_AVAILABLE_TV_CHANNEL_LIST_PATH), AvailableTvChannelsDTO.class);
+
+ ConcurrentMap tvChannelsMap = availableTvChannelsDTO.getChannel().stream()
+ .collect(Collectors.toConcurrentMap(ChannelDTO::getName, ChannelDTO::getCcid, (c1, c2) -> c1));
+
+ logger.debug("TV Channels added: {}", tvChannelsMap.size());
+ if (logger.isTraceEnabled()) {
+ tvChannelsMap.keySet().forEach(app -> logger.trace("TV Channel found: {}", app));
+ }
+ return tvChannelsMap;
+ }
+
+ private String getCurrentTvChannel() throws IOException {
+ TvChannelDTO tvChannelDTO = OBJECT_MAPPER.readValue(connectionManager.doHttpsGet(TV_CHANNEL_PATH),
+ TvChannelDTO.class);
+ return Optional.ofNullable(tvChannelDTO.getChannel()).map(ChannelDTO::getName).orElse("NA");
+ }
+
+ private void switchTvChannel(Command command) throws IOException {
+ ChannelDTO channelDTO = new ChannelDTO();
+ channelDTO.setCcid(availableTvChannels.get(command.toString()));
+
+ ChannelListDTO channelListDTO = new ChannelListDTO();
+ channelListDTO.setId("allter");
+ channelListDTO.setVersion("30");
+
+ TvChannelDTO tvChannelDTO = new TvChannelDTO(channelDTO, channelListDTO);
+ String switchTvChannelJson = OBJECT_MAPPER.writeValueAsString(tvChannelDTO);
+ logger.debug("Switch TV Channel json: {}", switchTvChannelJson);
+ connectionManager.doHttpsPost(TV_CHANNEL_PATH, switchTvChannelJson);
+ }
+
+ public void clearAvailableTvChannelList() {
+ if (availableTvChannels != null) {
+ availableTvChannels.clear();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/TvPictureService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/TvPictureService.java
new file mode 100644
index 0000000000000..214d843a2d426
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/TvPictureService.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.TvSettingsUpdateDTO;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for handling commands regarding the TV picture settings
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class TvPictureService implements PhilipsTVService {
+
+ private static final int SHARPNESS_NODE_ID = 2131230851;
+ private static final int CONTRAST_NODE_ID = 2131230850;
+ private static final int BRIGHTNESS_NODE_ID = 2131230852;
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final ConnectionManager connectionManager;
+
+ public TvPictureService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ try {
+ if (CHANNEL_BRIGHTNESS.equals(channel) && command instanceof PercentType) {
+ setBrightness(((PercentType) command).intValue());
+ } else if (CHANNEL_BRIGHTNESS.equals(channel) && command instanceof RefreshType) {
+ int currentBrightness = getBrightness();
+ handler.postUpdateChannel(CHANNEL_BRIGHTNESS, new PercentType(currentBrightness));
+ } else if (CHANNEL_CONTRAST.equals(channel) && command instanceof PercentType) {
+ setContrast(((PercentType) command).intValue());
+ } else if (CHANNEL_CONTRAST.equals(channel) && command instanceof RefreshType) {
+ int currentContrast = getContrast();
+ handler.postUpdateChannel(CHANNEL_CONTRAST, new PercentType(currentContrast));
+ } else if (CHANNEL_SHARPNESS.equals(channel) && command instanceof PercentType) {
+ setSharpness(((PercentType) command).intValue());
+ } else if (CHANNEL_SHARPNESS.equals(channel) && command instanceof RefreshType) {
+ int currentSharpness = getSharpness();
+ handler.postUpdateChannel(CHANNEL_SHARPNESS, new PercentType(currentSharpness));
+ } else {
+ if (!(command instanceof RefreshType)) {
+ logger.warn("Unknown command: {} for Channel {}", command, channel);
+ }
+ }
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Error during handling the TvPicture command {} for Channel {}: {}", command, channel,
+ e.getMessage(), e);
+ }
+ }
+ }
+
+ private int getBrightness() throws IOException {
+ String getBrightnessJson = ServiceUtil.createTvSettingsRetrievalJson(BRIGHTNESS_NODE_ID);
+ logger.debug("Post Tv Picture retrieval brightness json: {}", getBrightnessJson);
+ return (int) OBJECT_MAPPER.readValue(connectionManager.doHttpsPost(CURRENT_SETTINGS_PATH, getBrightnessJson),
+ TvSettingsUpdateDTO.class).getValues().get(0).getValue().getData().getValue();
+ }
+
+ private void setBrightness(int brightness) throws IOException {
+ String tvPictureBrightnessJson = ServiceUtil.createTvSettingsUpdateJson(BRIGHTNESS_NODE_ID, brightness);
+ logger.debug("Post Tv Picture brightness json: {}", tvPictureBrightnessJson);
+ connectionManager.doHttpsPost(UPDATE_SETTINGS_PATH, tvPictureBrightnessJson);
+ }
+
+ private int getContrast() throws IOException {
+ String getContrastJson = ServiceUtil.createTvSettingsRetrievalJson(CONTRAST_NODE_ID);
+ logger.debug("Post Tv Picture retrieval contrast json: {}", getContrastJson);
+ return (int) OBJECT_MAPPER.readValue(connectionManager.doHttpsPost(CURRENT_SETTINGS_PATH, getContrastJson),
+ TvSettingsUpdateDTO.class).getValues().get(0).getValue().getData().getValue();
+ }
+
+ private void setContrast(int contrast) throws IOException {
+ String tvPictureContrastJson = ServiceUtil.createTvSettingsUpdateJson(CONTRAST_NODE_ID, contrast);
+ logger.debug("Post Tv Picture contrast json: {}", tvPictureContrastJson);
+ connectionManager.doHttpsPost(UPDATE_SETTINGS_PATH, tvPictureContrastJson);
+ }
+
+ private int getSharpness() throws IOException {
+ String getSharpnessJson = ServiceUtil.createTvSettingsRetrievalJson(SHARPNESS_NODE_ID);
+ logger.debug("Post Tv Picture retrieval sharpness json: {}", getSharpnessJson);
+ return (int) OBJECT_MAPPER.readValue(connectionManager.doHttpsPost(CURRENT_SETTINGS_PATH, getSharpnessJson),
+ TvSettingsUpdateDTO.class).getValues().get(0).getValue().getData().getValue();
+ }
+
+ private void setSharpness(int sharpness) throws IOException {
+ String tvPictureSharpnessJson = ServiceUtil.createTvSettingsUpdateJson(SHARPNESS_NODE_ID, sharpness / 10);
+ logger.debug("Post Tv Picture brightness json: {}", tvPictureSharpnessJson);
+ connectionManager.doHttpsPost(UPDATE_SETTINGS_PATH, tvPictureSharpnessJson);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/VolumeService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/VolumeService.java
new file mode 100644
index 0000000000000..7fa2859415ff7
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/VolumeService.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service;
+
+import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager.OBJECT_MAPPER;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.service.KeyPress.KEY_MUTE;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.ConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVConnectionManager;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.keypress.KeyPressDTO;
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.model.volume.VolumeDTO;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link VolumeService} is responsible for handling volume commands, which are sent to the
+ * volume channel or mute channel.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public class VolumeService implements PhilipsTVService {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final PhilipsTVConnectionManager handler;
+
+ private final ConnectionManager connectionManager;
+
+ public VolumeService(PhilipsTVConnectionManager handler, ConnectionManager connectionManager) {
+ this.handler = handler;
+ this.connectionManager = connectionManager;
+ }
+
+ @Override
+ public void handleCommand(String channel, Command command) {
+ try {
+ if (command instanceof RefreshType) {
+ VolumeDTO volumeDTO = getVolume();
+ handler.postUpdateChannel(CHANNEL_VOLUME, new PercentType(volumeDTO.getCurrentVolume()));
+ handler.postUpdateChannel(CHANNEL_MUTE, volumeDTO.isMuted() ? OnOffType.ON : OnOffType.OFF);
+ } else if (CHANNEL_VOLUME.equals(channel) && command instanceof PercentType) {
+ setVolume((PercentType) command);
+ handler.postUpdateChannel(CHANNEL_VOLUME, (PercentType) command);
+ } else if (CHANNEL_MUTE.equals(channel) && command instanceof OnOffType) {
+ setMute();
+ } else {
+ logger.warn("Unknown command: {} for Channel {}", command, channel);
+ }
+ } catch (Exception e) {
+ if (isTvOfflineException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.NONE, TV_OFFLINE_MSG);
+ } else if (isTvNotListeningException(e)) {
+ handler.postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ TV_NOT_LISTENING_MSG);
+ } else {
+ logger.warn("Error during handling the VolumeService command {} for Channel {}: {}", command, channel,
+ e.getMessage(), e);
+ }
+ }
+ }
+
+ private VolumeDTO getVolume() throws IOException {
+ String jsonContent = connectionManager.doHttpsGet(VOLUME_PATH);
+ return OBJECT_MAPPER.readValue(jsonContent, VolumeDTO.class);
+ }
+
+ private void setVolume(PercentType volumeToSet) throws IOException {
+ VolumeDTO volumeDTO = new VolumeDTO();
+ volumeDTO.setMuted(false);
+ volumeDTO.setCurrentVolume(volumeToSet.intValue());
+ String volumeJson = OBJECT_MAPPER.writeValueAsString(volumeDTO);
+ logger.debug("Set json volume: {}", volumeJson);
+ connectionManager.doHttpsPost(VOLUME_PATH, volumeJson);
+ }
+
+ private void setMute() throws IOException {
+ // We just sent the KEY_MUTE and dont bother what was actually requested
+ KeyPressDTO keyPressDTO = new KeyPressDTO(KEY_MUTE);
+ String muteJson = OBJECT_MAPPER.writeValueAsString(keyPressDTO);
+ logger.debug("Set json mute state: {}", muteJson);
+ connectionManager.doHttpsPost(KEY_CODE_PATH, muteJson);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/api/PhilipsTVService.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/api/PhilipsTVService.java
new file mode 100644
index 0000000000000..20b900b32d67d
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/api/PhilipsTVService.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.api;
+
+import java.net.NoRouteToHostException;
+import java.util.Optional;
+
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.conn.HttpHostConnectException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.types.Command;
+
+/**
+ * Interface for Philips TV services.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+@NonNullByDefault
+public interface PhilipsTVService {
+
+ /**
+ * Procedure for sending command.
+ *
+ * @param channel the channel to which the command applies
+ * @param command the command to be handled
+ */
+ void handleCommand(String channel, Command command);
+
+ default boolean isTvOfflineException(Exception exception) {
+ String message = Optional.ofNullable(exception.getMessage()).orElse("");
+ if (!message.isEmpty()) {
+ if ((exception instanceof NoRouteToHostException) && message.contains("Host unreachable")) {
+ return true;
+ } else {
+ return (exception instanceof ConnectTimeoutException) && message.contains("timed out");
+ }
+ } else {
+ return false;
+ }
+ }
+
+ default boolean isTvNotListeningException(Exception exception) {
+ String message = Optional.ofNullable(exception.getMessage()).orElse("");
+ if (!message.isEmpty()) {
+ return (exception instanceof HttpHostConnectException) && message.contains("Connection refused");
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/DataDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/DataDTO.java
new file mode 100644
index 0000000000000..b065f44d94c07
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/DataDTO.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link TvSettingsUpdateDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class DataDTO {
+
+ @JsonProperty
+ private Object value; // can be int or string
+
+ public DataDTO() {
+ }
+
+ public DataDTO(Object value) {
+ this.value = value;
+ }
+
+ public void setValue(Object value) {
+ this.value = value;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return "Data{" + "value = '" + value + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/NodesDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/NodesDTO.java
new file mode 100644
index 0000000000000..feb909ae51e4d
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/NodesDTO.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link TvSettingsCurrentDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class NodesDTO {
+
+ @JsonProperty("nodeid")
+ private int nodeid;
+
+ public void setNodeid(int nodeid) {
+ this.nodeid = nodeid;
+ }
+
+ public int getNodeid() {
+ return nodeid;
+ }
+
+ @Override
+ public String toString() {
+ return "NodesItem{" + "nodeid = '" + nodeid + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/TvSettingsCurrentDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/TvSettingsCurrentDTO.java
new file mode 100644
index 0000000000000..321252394f2eb
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/TvSettingsCurrentDTO.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link TvSettingsCurrentDTO} class defines the Data Transfer Object
+ * for the POST Request to Philips TV API /menuitems/settings/current endpoint to retrieve current settings of the tv,
+ * e.g. the tv picture brightness.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class TvSettingsCurrentDTO {
+
+ @JsonProperty("nodes")
+ private List nodes;
+
+ public TvSettingsCurrentDTO() {
+ }
+
+ public TvSettingsCurrentDTO(List nodes) {
+ this.nodes = nodes;
+ }
+
+ public void setNodes(List nodes) {
+ this.nodes = nodes;
+ }
+
+ public List getNodes() {
+ return nodes;
+ }
+
+ @Override
+ public String toString() {
+ return "TvSettingsCurrentDTO{" + "nodes = '" + nodes + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/TvSettingsUpdateDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/TvSettingsUpdateDTO.java
new file mode 100644
index 0000000000000..b6210cdf2a29b
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/TvSettingsUpdateDTO.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link TvSettingsUpdateDTO} class defines the Data Transfer Object
+ * for the Philips TV API /menuitems/settings/update endpoint to update settings of the tv, e.g. turning on/off
+ * ambilight hue power.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class TvSettingsUpdateDTO {
+
+ @JsonProperty
+ private List values;
+
+ public TvSettingsUpdateDTO() {
+ }
+
+ public TvSettingsUpdateDTO(List values) {
+ this.values = values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+
+ public List getValues() {
+ return values;
+ }
+
+ @Override
+ public String toString() {
+ return "TvSettingsDTO{" + "values = '" + values + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ValueDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ValueDTO.java
new file mode 100644
index 0000000000000..e3113fc879e3e
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ValueDTO.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link TvSettingsUpdateDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class ValueDTO {
+
+ @JsonProperty("Controllable")
+ private String controllable = "";
+
+ @JsonProperty
+ private DataDTO data;
+
+ @JsonProperty("Nodeid")
+ private int nodeid;
+
+ @JsonProperty("Available")
+ private String available = "";
+
+ public ValueDTO() {
+ }
+
+ public ValueDTO(DataDTO data) {
+ this.data = data;
+ }
+
+ public void setControllable(String controllable) {
+ this.controllable = controllable;
+ }
+
+ public String getControllable() {
+ return controllable;
+ }
+
+ public void setData(DataDTO data) {
+ this.data = data;
+ }
+
+ public DataDTO getData() {
+ return data;
+ }
+
+ public void setNodeid(int nodeid) {
+ this.nodeid = nodeid;
+ }
+
+ public int getNodeid() {
+ return nodeid;
+ }
+
+ public void setAvailable(String available) {
+ this.available = available;
+ }
+
+ public String getAvailable() {
+ return available;
+ }
+
+ @Override
+ public String toString() {
+ return "Value{" + "controllable = '" + controllable + '\'' + ",data = '" + data + '\'' + ",nodeid = '" + nodeid
+ + '\'' + ",available = '" + available + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ValuesDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ValuesDTO.java
new file mode 100644
index 0000000000000..e4a2e8d72db08
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ValuesDTO.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link TvSettingsUpdateDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class ValuesDTO {
+
+ @JsonProperty
+ private ValueDTO value;
+
+ public ValuesDTO() {
+ }
+
+ public ValuesDTO(ValueDTO value) {
+ this.value = value;
+ }
+
+ public void setValue(ValueDTO value) {
+ this.value = value;
+ }
+
+ public ValueDTO getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return "ValuesItem{" + "value = '" + value + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorDTO.java
new file mode 100644
index 0000000000000..f1d9b885441b8
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorDTO.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import org.openhab.core.library.types.HSBType;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link AmbilightColorSettingsDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class AmbilightColorDTO {
+
+ @JsonProperty("saturation")
+ private int saturation;
+
+ @JsonProperty("brightness")
+ private int brightness;
+
+ @JsonProperty("hue")
+ private int hue;
+
+ public AmbilightColorDTO() {
+ }
+
+ public AmbilightColorDTO(HSBType hsb) {
+ hue = hsb.getHue().intValue() * 255 / 360;
+ saturation = hsb.getSaturation().intValue() * 255 / 100;
+ brightness = hsb.getBrightness().intValue() * 255 / 100;
+ }
+
+ public void setSaturation(int saturation) {
+ this.saturation = saturation;
+ }
+
+ public int getSaturation() {
+ return saturation;
+ }
+
+ public void setBrightness(int brightness) {
+ this.brightness = brightness;
+ }
+
+ public int getBrightness() {
+ return brightness;
+ }
+
+ public void setHue(int hue) {
+ this.hue = hue;
+ }
+
+ public int getHue() {
+ return hue;
+ }
+
+ @Override
+ public String toString() {
+ return "Color{" + "saturation = '" + saturation + '\'' + ",brightness = '" + brightness + '\'' + ",hue = '"
+ + hue + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorDeltaDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorDeltaDTO.java
new file mode 100644
index 0000000000000..963150114302a
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorDeltaDTO.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link AmbilightColorSettingsDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class AmbilightColorDeltaDTO {
+
+ @JsonProperty("saturation")
+ private int saturation;
+
+ @JsonProperty("brightness")
+ private int brightness;
+
+ @JsonProperty("hue")
+ private int hue;
+
+ public void setSaturation(int saturation) {
+ this.saturation = saturation;
+ }
+
+ public int getSaturation() {
+ return saturation;
+ }
+
+ public void setBrightness(int brightness) {
+ this.brightness = brightness;
+ }
+
+ public int getBrightness() {
+ return brightness;
+ }
+
+ public void setHue(int hue) {
+ this.hue = hue;
+ }
+
+ public int getHue() {
+ return hue;
+ }
+
+ @Override
+ public String toString() {
+ return "ColorDelta{" + "saturation = '" + saturation + '\'' + ",brightness = '" + brightness + '\'' + ",hue = '"
+ + hue + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorSettingsDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorSettingsDTO.java
new file mode 100644
index 0000000000000..fdae3bc3eee64
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightColorSettingsDTO.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link AmbilightConfigDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class AmbilightColorSettingsDTO {
+
+ @JsonProperty("color")
+ private AmbilightColorDTO color;
+
+ @JsonProperty("colorDelta")
+ private AmbilightColorDeltaDTO colorDelta;
+
+ @JsonProperty("speed")
+ private int speed;
+
+ public AmbilightColorSettingsDTO(AmbilightColorDTO color, AmbilightColorDeltaDTO colorDelta) {
+ this.color = color;
+ this.colorDelta = colorDelta;
+ }
+
+ public void setColor(AmbilightColorDTO color) {
+ this.color = color;
+ }
+
+ public AmbilightColorDTO getColor() {
+ return color;
+ }
+
+ public void setColorDelta(AmbilightColorDeltaDTO colorDelta) {
+ this.colorDelta = colorDelta;
+ }
+
+ public AmbilightColorDeltaDTO getColorDelta() {
+ return colorDelta;
+ }
+
+ public void setSpeed(int speed) {
+ this.speed = speed;
+ }
+
+ public int getSpeed() {
+ return speed;
+ }
+
+ @Override
+ public String toString() {
+ return "ColorSettings{" + "color = '" + color + '\'' + ",colorDelta = '" + colorDelta + '\'' + ",speed = '"
+ + speed + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightConfigDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightConfigDTO.java
new file mode 100644
index 0000000000000..584a3358e244c
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightConfigDTO.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link AmbilightConfigDTO} class defines the Data Transfer Object
+ * for the Philips TV API /ambilight/currentconfiguration endpoint to retrieve or set the current ambilight style.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class AmbilightConfigDTO {
+
+ @JsonProperty("isExpert")
+ private boolean isExpert;
+
+ @JsonProperty("menuSetting")
+ private String menuSetting = "";
+
+ @JsonProperty("styleName")
+ private String styleName = "";
+
+ @JsonProperty("colorSettings")
+ private AmbilightColorSettingsDTO colorSettings;
+
+ @JsonProperty("algorithm")
+ private String algorithm = "";
+
+ public AmbilightConfigDTO() {
+ }
+
+ public AmbilightConfigDTO(AmbilightColorSettingsDTO colorSettings) {
+ this.colorSettings = colorSettings;
+ }
+
+ public void setMenuSetting(String menuSetting) {
+ this.menuSetting = menuSetting;
+ }
+
+ public String getMenuSetting() {
+ return menuSetting;
+ }
+
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ }
+
+ public String getStyleName() {
+ return styleName;
+ }
+
+ public boolean isIsExpert() {
+ return isExpert;
+ }
+
+ public void setIsExpert(boolean isExpert) {
+ this.isExpert = isExpert;
+ }
+
+ public AmbilightColorSettingsDTO getColorSettings() {
+ return colorSettings;
+ }
+
+ public void setColorSettings(AmbilightColorSettingsDTO colorSettings) {
+ this.colorSettings = colorSettings;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ @Override
+ public String toString() {
+ return "AmbilightConfigDTO{" + "isExpert = '" + isExpert + '\'' + ",menuSetting = '" + menuSetting + '\''
+ + ",styleName = '" + styleName + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightLoungeDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightLoungeDTO.java
new file mode 100644
index 0000000000000..8f0fbf36429da
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightLoungeDTO.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link AmbilightLoungeDTO} class defines the Data Transfer Object
+ * for the Philips TV API /ambilight/lounge endpoint to power on or off the ambilight lounge mode.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class AmbilightLoungeDTO {
+
+ @JsonProperty("color")
+ private AmbilightColorDTO color;
+
+ public AmbilightLoungeDTO(AmbilightColorDTO color) {
+ this.color = color;
+ }
+
+ public void setColor(AmbilightColorDTO color) {
+ this.color = color;
+ }
+
+ public AmbilightColorDTO getColor() {
+ return color;
+ }
+
+ @Override
+ public String toString() {
+ return "AmbilightLoungeDTO{" + "color = '" + color + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightModeDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightModeDTO.java
new file mode 100644
index 0000000000000..bc4ef0d5f1e91
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightModeDTO.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link AmbilightModeDTO} class defines the Data Transfer Object
+ * for the Philips TV API /ambilight/mode endpoint to retrieve or set the ambilight mode.
+ *
+ * current (string): One of following values:
+ *
+ * internal: The internal ambilight algorithm is used to calculate the ambilight colours.
+ *
+ * manual: The cached ambilight colours are shown.
+ *
+ * expert: The cached ambilight colours are used as input for the internal ambilight algorithm
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class AmbilightModeDTO {
+
+ @JsonProperty("current")
+ private String current = "";
+
+ public AmbilightModeDTO() {
+ }
+
+ public void setCurrent(String current) {
+ this.current = current;
+ }
+
+ public String getCurrent() {
+ return current;
+ }
+
+ @Override
+ public String toString() {
+ return "AmbilightModeDTO{" + "current = '" + current + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightPowerDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightPowerDTO.java
new file mode 100644
index 0000000000000..21793b7082e9a
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightPowerDTO.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.POWER_ON;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link AmbilightPowerDTO} class defines the Data Transfer Object
+ * for the Philips TV API /ambilight/power endpoint to retrieve or set the current power state.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class AmbilightPowerDTO {
+
+ @JsonProperty("power")
+ private String power = "";
+
+ public String getPower() {
+ return power;
+ }
+
+ public void setPower(String power) {
+ this.power = power;
+ }
+
+ @JsonIgnore
+ public boolean isPoweredOn() {
+ return power.equalsIgnoreCase(POWER_ON);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightTopologyDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightTopologyDTO.java
new file mode 100644
index 0000000000000..4393d9fa1a0fe
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/ambilight/AmbilightTopologyDTO.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.ambilight;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link AmbilightTopologyDTO} class defines the Data Transfer Object
+ * for the Philips TV API /ambilight/topology endpoint to retrieve the ambilight topology information.
+ *
+ * Endpoint returns:
+ *
+ * layers (integer): The number of layers.
+ *
+ * left (integer): The number of pixels on the left.
+ *
+ * top (integer): The number of pixels on the top.
+ *
+ * right (integer): The number of pixels on the right.
+ *
+ * bottom (integer): The number of pixels on the bottom.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class AmbilightTopologyDTO {
+
+ @JsonProperty("top")
+ private int top;
+
+ @JsonProperty("left")
+ private int left;
+
+ @JsonProperty("bottom")
+ private int bottom;
+
+ @JsonProperty("layers")
+ private int layers;
+
+ @JsonProperty("right")
+ private int right;
+
+ public AmbilightTopologyDTO() {
+ }
+
+ public void setTop(int top) {
+ this.top = top;
+ }
+
+ public int getTop() {
+ return top;
+ }
+
+ public void setLeft(int left) {
+ this.left = left;
+ }
+
+ public int getLeft() {
+ return left;
+ }
+
+ public void setBottom(int bottom) {
+ this.bottom = bottom;
+ }
+
+ public int getBottom() {
+ return bottom;
+ }
+
+ public void setLayers(int layers) {
+ this.layers = layers;
+ }
+
+ public int getLayers() {
+ return layers;
+ }
+
+ public void setRight(int right) {
+ this.right = right;
+ }
+
+ public int getRight() {
+ return right;
+ }
+
+ @JsonIgnore
+ public int getPixelSizeForGivenSide(String side) {
+ int value;
+ switch (side) {
+ case "left":
+ value = left;
+ break;
+ case "right":
+ value = right;
+ break;
+ case "top":
+ value = top;
+ break;
+ case "bottom":
+ value = bottom;
+ break;
+ default:
+ throw new IllegalStateException("Unexpected side: " + side);
+ }
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return "AmbilightTopologyDTO{" + "top = '" + top + '\'' + ",left = '" + left + '\'' + ",bottom = '" + bottom
+ + '\'' + ",layers = '" + layers + '\'' + ",right = '" + right + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ApplicationsDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ApplicationsDTO.java
new file mode 100644
index 0000000000000..b30b6408d981b
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ApplicationsDTO.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.application;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link AvailableAppsDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class ApplicationsDTO {
+
+ @JsonProperty("label")
+ private String label = "";
+
+ @JsonProperty("id")
+ private String id = "";
+
+ @JsonProperty("type")
+ private String type = "";
+
+ @JsonProperty("intent")
+ private IntentDTO intent;
+
+ @JsonProperty("order")
+ private int order;
+
+ public ApplicationsDTO() {
+ }
+
+ public ApplicationsDTO(IntentDTO intent) {
+ this.intent = intent;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setIntent(IntentDTO intent) {
+ this.intent = intent;
+ }
+
+ public IntentDTO getIntent() {
+ return intent;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ @Override
+ public String toString() {
+ return "ApplicationsItem{" + "label = '" + label + '\'' + ",id = '" + id + '\'' + ",type = '" + type + '\''
+ + ",intent = '" + intent + '\'' + ",order = '" + order + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/AvailableAppsDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/AvailableAppsDTO.java
new file mode 100644
index 0000000000000..6097790c6204f
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/AvailableAppsDTO.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.application;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link AvailableAppsDTO} class defines the Data Transfer Object
+ * for the Philips TV API /applications endpoint for retrieving all installed apps.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class AvailableAppsDTO {
+
+ @JsonProperty("version")
+ private int version;
+
+ @JsonProperty("applications")
+ private @Nullable List applications;
+
+ public AvailableAppsDTO() {
+ }
+
+ public void setVersion(int version) {
+ this.version = version;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public void setApplications(List applications) {
+ this.applications = applications;
+ }
+
+ public @Nullable List getApplications() {
+ return applications;
+ }
+
+ @Override
+ public String toString() {
+ return "AvailableAppsDTO{" + "version = '" + version + '\'' + ",applications = '" + applications + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ComponentDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ComponentDTO.java
new file mode 100644
index 0000000000000..bddad4304df28
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ComponentDTO.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.application;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link LaunchAppDTO} and {@link CurrentAppDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class ComponentDTO {
+
+ @JsonProperty("className")
+ private String className = "";
+
+ @JsonProperty("packageName")
+ private String packageName = "";
+
+ public void setClassName(String className) {
+ this.className = className;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ @Override
+ public String toString() {
+ return "Component{" + "className = '" + className + '\'' + ",packageName = '" + packageName + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/CurrentAppDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/CurrentAppDTO.java
new file mode 100644
index 0000000000000..e5dae87ae8781
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/CurrentAppDTO.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.application;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link LaunchAppDTO} class defines the Data Transfer Object
+ * for the Philips TV API /activities/current endpoint for retrieving the current running TV app.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class CurrentAppDTO {
+
+ @JsonProperty("component")
+ private ComponentDTO component;
+
+ public CurrentAppDTO() {
+ }
+
+ public CurrentAppDTO(ComponentDTO component) {
+ this.component = component;
+ }
+
+ public void setComponent(ComponentDTO component) {
+ this.component = component;
+ }
+
+ public ComponentDTO getComponent() {
+ return component;
+ }
+
+ @Override
+ public String toString() {
+ return "Intent{" + "component = '" + component + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ExtrasDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ExtrasDTO.java
new file mode 100644
index 0000000000000..5019c33193d9e
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/ExtrasDTO.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.application;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link IntentDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class ExtrasDTO {
+
+ @JsonProperty("query")
+ private String query = "";
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ @Override
+ public String toString() {
+ return "Extras{" + "query = '" + query + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/IntentDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/IntentDTO.java
new file mode 100644
index 0000000000000..53ac1fed71fec
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/IntentDTO.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.application;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link LaunchAppDTO} and {@link LaunchAppDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class IntentDTO {
+
+ @JsonProperty("component")
+ private ComponentDTO component;
+
+ @JsonProperty("action")
+ private String action = "";
+
+ @JsonProperty("extras")
+ private ExtrasDTO extras;
+
+ public IntentDTO() {
+ }
+
+ public IntentDTO(ComponentDTO component, ExtrasDTO extras) {
+ this.component = component;
+ this.extras = extras;
+ }
+
+ public void setComponent(ComponentDTO component) {
+ this.component = component;
+ }
+
+ public ComponentDTO getComponent() {
+ return component;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setExtras(ExtrasDTO extras) {
+ this.extras = extras;
+ }
+
+ public ExtrasDTO getExtras() {
+ return extras;
+ }
+
+ @Override
+ public String toString() {
+ return "Intent{" + "component = '" + component + '\'' + ",action = '" + action + '\'' + ",extras = '" + extras
+ + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/LaunchAppDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/LaunchAppDTO.java
new file mode 100644
index 0000000000000..67fda9c421fda
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/application/LaunchAppDTO.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.application;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link LaunchAppDTO} class defines the Data Transfer Object
+ * for the Philips TV API /activities/launch endpoint for launching TV apps and launching search for content.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class LaunchAppDTO {
+
+ @JsonProperty("intent")
+ private IntentDTO intent;
+
+ public LaunchAppDTO() {
+ }
+
+ public LaunchAppDTO(IntentDTO intent) {
+ this.intent = intent;
+ }
+
+ public void setIntent(IntentDTO intent) {
+ this.intent = intent;
+ }
+
+ public IntentDTO getIntent() {
+ return intent;
+ }
+
+ @Override
+ public String toString() {
+ return "LaunchAppDTO{" + "intent = '" + intent + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/AvailableTvChannelsDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/AvailableTvChannelsDTO.java
new file mode 100644
index 0000000000000..fc10fd820cf76
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/AvailableTvChannelsDTO.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.channel;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link AvailableTvChannelsDTO} class defines the Data Transfer Object
+ * for the Philips TV API channeldb/tv/channelLists/all endpoint for retrieving all tv channels.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class AvailableTvChannelsDTO {
+
+ @JsonProperty("Channel")
+ private @Nullable List channel;
+
+ @JsonProperty("id")
+ private String id = "";
+
+ @JsonProperty("medium")
+ private String medium = "";
+
+ @JsonProperty("version")
+ private int version;
+
+ @JsonProperty("listType")
+ private String listType = "";
+
+ @JsonProperty("operator")
+ private String operator = "";
+
+ @JsonProperty("installCountry")
+ private String installCountry = "";
+
+ public AvailableTvChannelsDTO() {
+ }
+
+ public void setChannel(List channel) {
+ this.channel = channel;
+ }
+
+ public @Nullable List getChannel() {
+ return channel;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setMedium(String medium) {
+ this.medium = medium;
+ }
+
+ public String getMedium() {
+ return medium;
+ }
+
+ public void setVersion(int version) {
+ this.version = version;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public void setListType(String listType) {
+ this.listType = listType;
+ }
+
+ public String getListType() {
+ return listType;
+ }
+
+ public void setOperator(String operator) {
+ this.operator = operator;
+ }
+
+ public String getOperator() {
+ return operator;
+ }
+
+ public void setInstallCountry(String installCountry) {
+ this.installCountry = installCountry;
+ }
+
+ public String getInstallCountry() {
+ return installCountry;
+ }
+
+ @Override
+ public String toString() {
+ return "AvailableTvChannelsDTO{" + "channel = '" + channel + '\'' + ",id = '" + id + '\'' + ",medium = '"
+ + medium + '\'' + ",version = '" + version + '\'' + ",listType = '" + listType + '\'' + ",operator = '"
+ + operator + '\'' + ",installCountry = '" + installCountry + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/ChannelDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/ChannelDTO.java
new file mode 100644
index 0000000000000..0ef476fdab57f
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/ChannelDTO.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.channel;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link TvChannelDTO} and {@link AvailableTvChannelsDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class ChannelDTO {
+
+ @JsonProperty("serviceType")
+ private String serviceType = "";
+
+ @JsonProperty("logoVersion")
+ private int logoVersion;
+
+ @JsonProperty("ccid")
+ private String ccid = "";
+
+ @JsonProperty("name")
+ private String name = "";
+
+ @JsonProperty("preset")
+ private String preset = "";
+
+ @JsonProperty("tsid")
+ private int tsid;
+
+ @JsonProperty("type")
+ private String type = "";
+
+ @JsonProperty("onid")
+ private int onid;
+
+ @JsonProperty("sid")
+ private int sid;
+
+ public void setServiceType(String serviceType) {
+ this.serviceType = serviceType;
+ }
+
+ public String getServiceType() {
+ return serviceType;
+ }
+
+ public void setLogoVersion(int logoVersion) {
+ this.logoVersion = logoVersion;
+ }
+
+ public int getLogoVersion() {
+ return logoVersion;
+ }
+
+ public void setCcid(@Nullable String ccid) {
+ if (!ccid.isEmpty()) {
+ this.ccid = ccid;
+ }
+ }
+
+ public String getCcid() {
+ return ccid;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setPreset(String preset) {
+ this.preset = preset;
+ }
+
+ public String getPreset() {
+ return preset;
+ }
+
+ public void setTsid(int tsid) {
+ this.tsid = tsid;
+ }
+
+ public int getTsid() {
+ return tsid;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setOnid(int onid) {
+ this.onid = onid;
+ }
+
+ public int getOnid() {
+ return onid;
+ }
+
+ public void setSid(int sid) {
+ this.sid = sid;
+ }
+
+ public int getSid() {
+ return sid;
+ }
+
+ @Override
+ public String toString() {
+ return "ChannelItem{" + "serviceType = '" + serviceType + '\'' + ",logoVersion = '" + logoVersion + '\''
+ + ",ccid = '" + ccid + '\'' + ",name = '" + name + '\'' + ",preset = '" + preset + '\'' + ",tsid = '"
+ + tsid + '\'' + ",type = '" + type + '\'' + ",onid = '" + onid + '\'' + ",sid = '" + sid + '\'' + "}";
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/ChannelListDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/ChannelListDTO.java
new file mode 100644
index 0000000000000..db6f13e060f39
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/ChannelListDTO.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.channel;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Part of {@link TvChannelDTO}
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class ChannelListDTO {
+
+ @JsonProperty("id")
+ private String id = "";
+
+ @JsonProperty("version")
+ private String version = "";
+
+ public String getId() {
+ return id;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/TvChannelDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/TvChannelDTO.java
new file mode 100644
index 0000000000000..b025452e6a5cd
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/channel/TvChannelDTO.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.channel;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link TvChannelDTO} class defines the Data Transfer Object
+ * for the Philips TV API /activities/tv endpoint to get and switch tv channels.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class TvChannelDTO {
+
+ @JsonProperty("channel")
+ private ChannelDTO channel;
+
+ @JsonProperty("channelList")
+ private ChannelListDTO channelList;
+
+ public TvChannelDTO() {
+ }
+
+ public TvChannelDTO(ChannelDTO channel, ChannelListDTO channelList) {
+ this.channel = channel;
+ this.channelList = channelList;
+ }
+
+ public ChannelDTO getChannel() {
+ return channel;
+ }
+
+ public ChannelListDTO getChannelList() {
+ return channelList;
+ }
+
+ public void setChannel(ChannelDTO channelDTO) {
+ this.channel = channelDTO;
+ }
+
+ public void setChannelList(ChannelListDTO channelList) {
+ this.channelList = channelList;
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/keypress/KeyPressDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/keypress/KeyPressDTO.java
new file mode 100644
index 0000000000000..655fb265f74fa
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/keypress/KeyPressDTO.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.keypress;
+
+import org.openhab.binding.androidtv.internal.protocol.philipstv.service.KeyPress;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link KeyPressDTO} class defines the Data Transfer Object
+ * for the Philips TV API /input/key endpoint for remote controller emulation.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+
+public class KeyPressDTO {
+
+ @JsonProperty("key")
+ private KeyPress key;
+
+ public KeyPressDTO() {
+ }
+
+ public KeyPressDTO(KeyPress key) {
+ this.key = key;
+ }
+
+ public KeyPress getKey() {
+ return key;
+ }
+
+ public void setKey(KeyPress key) {
+ this.key = key;
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/power/PowerStateDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/power/PowerStateDTO.java
new file mode 100644
index 0000000000000..621379fa3b4c9
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/power/PowerStateDTO.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.power;
+
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.POWER_ON;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.STANDBY;
+import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.STANDBYKEEP;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link PowerStateDTO} class defines the Data Transfer Object
+ * for the Philips TV API /powerstate endpoint to retrieve or set the current power state.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class PowerStateDTO {
+
+ @JsonProperty("powerstate")
+ private String powerState = "";
+
+ public PowerStateDTO() {
+ }
+
+ public String getPowerState() {
+ return powerState;
+ }
+
+ public void setPowerState(String powerState) {
+ this.powerState = powerState;
+ }
+
+ @JsonIgnore
+ public boolean isPoweredOn() {
+ return powerState.equalsIgnoreCase(POWER_ON);
+ }
+
+ @JsonIgnore
+ public boolean isStandby() {
+ return (powerState.equalsIgnoreCase(STANDBY) || powerState.equalsIgnoreCase(STANDBYKEEP));
+ }
+
+ @JsonIgnore
+ public boolean isStandbyKeep() {
+ return powerState.equalsIgnoreCase(STANDBYKEEP);
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/volume/VolumeDTO.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/volume/VolumeDTO.java
new file mode 100644
index 0000000000000..74dad6504e128
--- /dev/null
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/philipstv/service/model/volume/VolumeDTO.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2024 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.philipstv.service.model.volume;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link VolumeDTO} class defines the Data Transfer Object
+ * for the Philips TV API /audio/volume endpoint.
+ *
+ * @author Benjamin Meyer - Initial contribution
+ * @author Ben Rosenblum - Merged into AndroidTV
+ */
+public class VolumeDTO {
+
+ @JsonProperty("current")
+ private int currentVolume;
+
+ @JsonProperty("muted")
+ private boolean muted;
+
+ public VolumeDTO() {
+ }
+
+ public int getCurrentVolume() {
+ return currentVolume;
+ }
+
+ public void setCurrentVolume(int currentVolume) {
+ this.currentVolume = currentVolume;
+ }
+
+ public boolean isMuted() {
+ return muted;
+ }
+
+ public void setMuted(boolean muted) {
+ this.muted = muted;
+ }
+}
diff --git a/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/i18n/androidtv.properties b/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/i18n/androidtv.properties
index 3e644d9af3fd5..516cb24bbe439 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/i18n/androidtv.properties
+++ b/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/i18n/androidtv.properties
@@ -7,15 +7,17 @@ addon.androidtv.description = This is the add-on for AndroidTV.
thing-type.androidtv.googletv.label = GoogleTV
thing-type.androidtv.googletv.description = GoogleTV
+thing-type.androidtv.philipstv.label = Philips TV
+thing-type.androidtv.philipstv.description = A Philips TV device
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.label = Message Delay
thing-type.config.androidtv.googletv.delay.description = Delay between messages
-thing-type.config.androidtv.googletv.gtvEnabled.label = Enable GoogleTV
-thing-type.config.androidtv.googletv.gtvEnabled.description = Enable the GoogleTV Protocol
+thing-type.config.androidtv.googletv.googletvPort.label = GoogleTV Port
+thing-type.config.androidtv.googletv.googletvPort.description = Port to connect to
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
@@ -24,12 +26,34 @@ 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.googletvPort.label = GoogleTV Port
-thing-type.config.androidtv.googletv.googletvPort.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.philipstv.delay.label = Delay
+thing-type.config.androidtv.philipstv.delay.description = Delay between messages
+thing-type.config.androidtv.philipstv.googletvPort.label = GoogleTV Port
+thing-type.config.androidtv.philipstv.googletvPort.description = Port to connect to
+thing-type.config.androidtv.philipstv.gtvEnabled.label = Enable GoogleTV
+thing-type.config.androidtv.philipstv.gtvEnabled.description = Enable the GoogleTV Protocol
+thing-type.config.androidtv.philipstv.heartbeat.label = Heartbeat Frequency
+thing-type.config.androidtv.philipstv.heartbeat.description = Frequency of heartbeats
+thing-type.config.androidtv.philipstv.ipAddress.label = Hostname
+thing-type.config.androidtv.philipstv.ipAddress.description = Hostname or IP address of the device
+thing-type.config.androidtv.philipstv.keystoreFileName.label = Keystore File Name
+thing-type.config.androidtv.philipstv.keystoreFileName.description = Java keystore containing key and certs
+thing-type.config.androidtv.philipstv.keystorePassword.label = Keystore Password
+thing-type.config.androidtv.philipstv.keystorePassword.description = Password for the keystore file
+thing-type.config.androidtv.philipstv.philipstvPort.label = PhilipsTV Port
+thing-type.config.androidtv.philipstv.philipstvPort.description = Port to connect to
+thing-type.config.androidtv.philipstv.reconnect.label = Reconnect Delay
+thing-type.config.androidtv.philipstv.reconnect.description = Delay between reconnection attempts
+thing-type.config.androidtv.philipstv.refreshRate.label = Refresh Rate
+thing-type.config.androidtv.philipstv.refreshRate.description = How often the Philips TV status details get refreshed. Value in seconds. '0' deactives refreshing.
+thing-type.config.androidtv.philipstv.useUpnpDiscovery.label = Use UPnP Discovery
+thing-type.config.androidtv.philipstv.useUpnpDiscovery.description = Enables UPnP Discovery. If disabled, constant HTTPS polling will happen.
thing-type.config.androidtv.shieldtv.delay.label = Delay
thing-type.config.androidtv.shieldtv.delay.description = Delay between messages
+thing-type.config.androidtv.shieldtv.googletvPort.label = GoogleTV Port
+thing-type.config.androidtv.shieldtv.googletvPort.description = Port to connect to
thing-type.config.androidtv.shieldtv.gtvEnabled.label = Enable GoogleTV
thing-type.config.androidtv.shieldtv.gtvEnabled.description = Enable the GoogleTV Protocol
thing-type.config.androidtv.shieldtv.heartbeat.label = Heartbeat Frequency
@@ -40,21 +64,65 @@ 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.googletvPort.label = GoogleTV Port
-thing-type.config.androidtv.shieldtv.googletvPort.description = Port to connect to
-thing-type.config.androidtv.shieldtv.shieldtvPort.label = ShieldTV Port
-thing-type.config.androidtv.shieldtv.shieldtvPort.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
+thing-type.config.androidtv.shieldtv.shieldtvPort.label = ShieldTV Port
+thing-type.config.androidtv.shieldtv.shieldtvPort.description = Port to connect to
# channel types
+channel-type.androidtv.ambilightBottomColor.label = Bottom Ambilight
+channel-type.androidtv.ambilightBottomColor.description = Sets the Ambilight color for the bottom.
+channel-type.androidtv.ambilightColor.label = All Ambilight
+channel-type.androidtv.ambilightColor.description = Sets the Ambilight color for all sides.
+channel-type.androidtv.ambilightHuePower.label = Ambilight + Hue Power
+channel-type.androidtv.ambilightHuePower.description = Ambilight + Hue power. Turns ambilight with connected Philips Hue Lamps on or off.
+channel-type.androidtv.ambilightLeftColor.label = Left Ambilight
+channel-type.androidtv.ambilightLeftColor.description = Sets the Ambilight color for the left side.
+channel-type.androidtv.ambilightLoungePower.label = Ambilight Lounge Power
+channel-type.androidtv.ambilightLoungePower.description = Ambilight lounge power. Turns ambilight lounge on or off.
+channel-type.androidtv.ambilightPower.label = Ambilight Power
+channel-type.androidtv.ambilightPower.description = Ambilight power. Turns ambilight on or off.
+channel-type.androidtv.ambilightRightColor.label = Right Ambilight
+channel-type.androidtv.ambilightRightColor.description = Sets the Ambilight color for the right side.
+channel-type.androidtv.ambilightStyle.label = Ambilight Style
+channel-type.androidtv.ambilightStyle.description = Current ambilight style. Changing this to a value from the List, switches the ambilight style.
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_VIDEO\ STANDARD = FOLLOW_VIDEO STANDARD
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_VIDEO\ NATURAL = FOLLOW_VIDEO NATURAL
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_VIDEO\ IMMERSIVE = FOLLOW_VIDEO IMMERSIVE
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_VIDEO\ VIVID = FOLLOW_VIDEO VIVID
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_VIDEO\ GAME = FOLLOW_VIDEO GAME
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_VIDEO\ COMFORT = FOLLOW_VIDEO COMFORT
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_VIDEO\ RELAX = FOLLOW_VIDEO RELAX
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ ENERGY_ADAPTIVE_BRIGHTNESS = FOLLOW_AUDIO ENERGY_ADAPTIVE_BRIGHTNESS
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ ENERGY_ADAPTIVE_COLORS = FOLLOW_AUDIO ENERGY_ADAPTIVE_COLORS
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ VU_METER = FOLLOW_AUDIO VU_METER
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ SPECTRUM_ANALYZER = FOLLOW_AUDIO SPECTRUM_ANALYZER
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ KNIGHT_RIDER_CLOCKWISE = FOLLOW_AUDIO KNIGHT_RIDER_CLOCKWISE
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ KNIGHT_RIDER_ALTERNATING = FOLLOW_AUDIO KNIGHT_RIDER_ALTERNATING
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ RANDOM_PIXEL_FLASH = FOLLOW_AUDIO RANDOM_PIXEL_FLASH
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ PARTY = FOLLOW_AUDIO PARTY
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_AUDIO\ MODE_RANDOM = FOLLOW_AUDIO MODE_RANDOM
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_COLOR\ MANUAL_HUE = FOLLOW_COLOR MANUAL_HUE
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_COLOR\ HOT_LAVA = FOLLOW_COLOR HOT_LAVA
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_COLOR\ DEEP_WATER = FOLLOW_COLOR DEEP_WATER
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_COLOR\ FRESH_NATURE = FOLLOW_COLOR FRESH_NATURE
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_COLOR\ ISF = FOLLOW_COLOR ISF
+channel-type.androidtv.ambilightStyle.state.option.FOLLOW_COLOR\ PTA_LOUNGE = FOLLOW_COLOR PTA_LOUNGE
+channel-type.androidtv.ambilightTopColor.label = Top Ambilight
+channel-type.androidtv.ambilightTopColor.description = Sets the Ambilight color for the top.
channel-type.androidtv.app.label = App
channel-type.androidtv.app.description = App Control
+channel-type.androidtv.appicon.label = App Icon
+channel-type.androidtv.appicon.description = App Icon
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.brightness.label = Brightness
+channel-type.androidtv.brightness.description = Brightness of the TV picture.
+channel-type.androidtv.contrast.label = Contrast
+channel-type.androidtv.contrast.description = Contrast of the TV picture.
channel-type.androidtv.debug.label = DEBUG Command
channel-type.androidtv.debug.description = Binding control (for debugging)
channel-type.androidtv.keyboard.label = Keyboard
@@ -67,6 +135,17 @@ 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
+channel-type.androidtv.searchContent.label = Search Content
+channel-type.androidtv.searchContent.description = Keyword(s) to search for on TV via Google Assistant
+channel-type.androidtv.sharpness.label = Sharpness
+channel-type.androidtv.sharpness.description = Sharpness of the TV picture.
+channel-type.androidtv.tvChannel.label = TV Channel
+channel-type.androidtv.tvChannel.description = Name of the currently running TV Channel. Changing this to a value from the List, switches the channel.
+
+# thing types config
+
+thing-type.config.androidtv.googletv.gtvEnabled.label = Enable GoogleTV
+thing-type.config.androidtv.googletv.gtvEnabled.description = Enable the GoogleTV Protocol
# custom thing status
@@ -86,5 +165,18 @@ offline.interrupted = Interrupted
offline.io-error = I/O Error
offline.runtime-exception = Runtime exception
offline.user-forced-pin-process = User Forced PIN Process
+offline.error-occured-while-presenting-pairing-code = Error occurred while trying to present a Pairing Code on TV
+offline.error-occured-during-retrieval-of-credentials = Error occurred during retrieval of credentials
+offline.pairing-is-not-configured-yet = Pairing is not configured yet
+offline.error-occurred-while-trying-to-present-a-pairing-code-on-tv = Error occurred while trying to present a Pairing Code on TV
+offline.pairing-code-is-available-but-credentials-missing = Pairing Code is available, but credentials missing. Trying to retrieve them
+offline.error-occurred-during-retrieval-of-credentials = Error occurred during retrieval of credentials
+offline.pairing-was-unsuccessful = Pairing was unsuccessful
+offline.error-occurred-during-creation-of-http-client = Error occurred during creation of HTTP client
+offline.authentication-with-philips-tv-device-was-successful-continuing-initialization-of-the-tv = Authentication with Philips TV device was successful. Continuing initialization of the tv.
+offline.could-not-successfully-finish-pairing-process-with-the-tv = Could not successfully finish pairing process with the TV
+offline.tv-is-not-reachable-and-should-therefore-be-off = TV is not reachable and should therefore be off
+offline.tv-does-not-accept-commands-at-the-moment = TV does not accept commands at the moment
offline.unknown = Unknown
+online.standby = Standby
online.online = Online
diff --git a/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/thing/thing-types.xml
index 8e5c032c0dadd..9fd3c643df189 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.androidtv/src/main/resources/OH-INF/thing/thing-types.xml
@@ -157,6 +157,106 @@
5true
+
+
+ Delay between messages
+ 0
+ true
+
+
+
+
+
+
+
+ A Philips TV device
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+
+ Hostname or IP address of the device
+
+
+
+ Port to connect to
+ 6466
+ true
+
+
+
+ Port to connect to
+ 1926
+ true
+
+
+
+
+ How often the Philips TV status details get refreshed. Value in seconds. '0' deactives refreshing.
+
+ true
+ 10
+
+
+
+
+ Enables UPnP Discovery. If disabled, constant HTTPS polling will happen.
+
+ true
+ true
+
+
+
+ Java keystore containing key and certs
+ true
+
+
+ password
+
+ Password for the keystore file
+ true
+
+
+
+ Delay between reconnection attempts
+ 60
+ true
+
+
+
+ Frequency of heartbeats
+ 5
+ true
+ Delay between messages
@@ -197,6 +297,12 @@
App URL
+
+ Image
+
+ App Icon
+
+
String
@@ -227,4 +333,131 @@
Player Control
+
+ String
+
+ Name of the currently running TV Channel. Changing this to a value from the List, switches the
+ channel.
+
+
+
+
+ String
+
+ Keyword(s) to search for on TV via Google Assistant
+
+
+
+ Switch
+
+ Ambilight power. Turns ambilight on or off.
+ Ambilight
+
+
+
+ Switch
+
+ Ambilight + Hue power. Turns ambilight with connected Philips Hue Lamps on or off.
+ Ambilight
+
+
+
+ Switch
+
+ Ambilight lounge power. Turns ambilight lounge on or off.
+ Ambilight
+
+
+
+ String
+
+ Current ambilight style. Changing this to a value from the List, switches the ambilight style.
+
+ Ambilight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Color
+
+ Sets the Ambilight color for all sides.
+ Ambilight
+
+
+
+ Color
+
+ Sets the Ambilight color for the left side.
+ Ambilight
+
+
+
+ Color
+
+ Sets the Ambilight color for the right side.
+ Ambilight
+
+
+
+ Color
+
+ Sets the Ambilight color for the top.
+ Ambilight
+
+
+
+ Color
+
+ Sets the Ambilight color for the bottom.
+ Ambilight
+
+
+
+ Dimmer
+
+ Brightness of the TV picture.
+ Tv Picture
+
+
+
+ Dimmer
+
+ Contrast of the TV picture.
+ Tv Picture
+
+
+
+ Dimmer
+
+ Sharpness of the TV picture.
+ Tv Picture
+
+