Skip to content

Commit

Permalink
bugfixes to remoteControlService
Browse files Browse the repository at this point in the history
  • Loading branch information
NickWaterton committed Dec 29, 2021
1 parent aee754a commit 2613b1a
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 108 deletions.
15 changes: 11 additions & 4 deletions bundles/org.openhab.binding.samsungtv/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Samsung TV Binding (Frame TV Version)

This binding integrates the [Samsung TV's](https://www.samsung.com).
This binding integrates the [Samsung TV's](https://www.samsung.com), and is for openHAB 3.1 and above.

## Supported Things

Expand Down Expand Up @@ -33,8 +33,11 @@ Tested TV models:
| UE55LS003 | PARTIAL | Supported channels: `volume`, `mute`, `sourceApp`, `url`, `keyCode`, `power`, `artMode` |
| UE58RU7179UXZG | PARTIAL | Supported channels: `volume`, `mute`, `power`, `keyCode` (at least) |
| UN50J5200 | PARTIAL | Status is retrieved (confirmed `power`, `media title`). Operating device seems not working. |
| UN46EH5300 | OK | All channels except `programTitle` and `channelName` are working |
| QN55LS03AAFXZC | PARTIAL | Supported channels: `volume`, `mute`, `keyCode`, `power`, `artMode`, `url`, `artImage`, `artLabel`, `artJson`, `artBrightness`,`artColorTemperature` |

This version of the binding was developed and tested on QN55LS03AAFXZC (2021) and UN46EH5300 (2012).

If you enable manual app control, this adds back the `sourceApp` channel.
If you enable the Smartthings interface, this adds back the `sourceName`, `sourceId`, `programTitle` and `channelName` channels

Expand Down Expand Up @@ -81,7 +84,9 @@ Under `advanced`, you can enter a Smartthings PAT, and Device Id. This enables m

## General info

Only channels that are linked are polled. Some channels are not polled at all. If you don't have any channels linked, you won't see any output in the log.
Only channels that are linked are polled. Some channels are not polled at all. If you don't have any channels linked, you won't see any polling output in the log.
If you want to watch the polling in the log (to make sure everything is working), you have to set the logging level to `TRACE`.

On legacy TV's, you may see an error like this:

```
Expand All @@ -90,13 +95,15 @@ On legacy TV's, you may see an error like this:

This is not an actual error, but is what is returned when a value is polled that does not yet exist, such as the URL for the TV browser, when the browser isn’t running. These messages are not new, and can be ignored.

Some channels do not work on some TV's. It depends on the age of your TV, and what kind of interface it has. Only link channels that work on your TV, polling channels that your TV doesn't have will cause errors, and other problems.
Some channels do not work on some TV's. It depends on the age of your TV, and what kind of interface it has. Only link channels that work on your TV, polling channels that your TV doesn't have may cause errors, and other problems.

If you see errors that say `no route to host` or smilar things, it means your TV is off. The binding cannot control or poll a TV that is off. It can't discover a TV that is off. Just saying.

The `getSupportedChannelNames` messages are not UPnP services, they are not actually services that are supported *by your TV* at all. They are the internal capabilities of whatever method is being used for communication (which could be direct port connection, UPnP or websocket).
They also do not reflect the actual capabilities of your TV, just what that method supports, on your TV, they may do nothing.

You should get `volume` and `mute` channels working at the minnimum. Other channels may or may not work, depending on your TV and the binding configuration.

### Separating the Samsung logging into its own file

To separate all the Samsung logging information into a separate file, please edit the file `userdata/etc/log4j2.xml` as follows:
Expand Down Expand Up @@ -126,7 +133,7 @@ Example for logging all DEBUG logs into a separate file `samsungtv.log` under th
</Loggers>
```

If you have problems with the binding, set the log level to `TRACE` (in place of `DEBUG`) and post a message to me (Nick Waterton) with a TRACE log covering 30 seconds before the issue and 30 seconds after (please don't send me a log with one line that you think is relevant in it, I can't tell much from this).
If you have problems with the binding, set the log level to `TRACE` (in place of `DEBUG`) and post a message with a TRACE log covering 30 seconds before the issue and 30 seconds after (please don't send me a log with one line that you think is relevant in it, I can't tell much from this).

### Text Files

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -86,7 +84,8 @@ public class SamsungTvHandler extends BaseThingHandler implements RegistryListen
/** Path for the information endpoint (note the final slash!) */
private static final String HTTP_ENDPOINT_V2 = "/api/v2/";

// common Samsung TV remote control ports
// common Samsung TV remote control ports (7676 is also used, but don't include it here as these
// TV's have websocket control as well)
private final List<Integer> ports = new ArrayList<>(List.of(55000, 1515, 7001, 15500));

private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
Expand All @@ -110,7 +109,7 @@ public class SamsungTvHandler extends BaseThingHandler implements RegistryListen
/* Store if art mode is supported to be able to skip switching power state to ON during initialization */
public boolean artModeSupported = false;

private @Nullable ScheduledFuture<?> pollingJob;
private Optional<ScheduledFuture<?>> pollingJob = Optional.empty();
private wolSend wolTask = new wolSend();

/** Description of the json returned for the information endpoint */
Expand Down Expand Up @@ -169,7 +168,7 @@ private class wolSend {
String channel = POWER;
Command command = OnOffType.ON;
String macAddress = "";
private @Nullable ScheduledFuture<?> wolJob;
private Optional<ScheduledFuture<?>> wolJob = Optional.empty();

public wolSend() {
}
Expand All @@ -179,36 +178,43 @@ public wolSend() {
*
* @param channel Channel to resend command on
* @param command Command to resend
* @return boolean true/false if WOL job started
*/
public boolean send(String channel, Command command) {
if (macAddress.isBlank()) {
logger.warn("{}: Cannot send WOL packet, MAC address unknown", host);
return false;
}
if ((channel.equals(POWER) || channel.equals(ART_MODE)) && OnOffType.ON.equals(command)) {
macAddress = configuration.getMacAddress();
if (macAddress.isBlank() || macAddress.length() != 17) {
logger.warn("{}: Cannot send WOL packet, MAC address invalid: {}", host, macAddress);
return false;
}
wolCount = 0;
this.channel = channel;
this.command = command;
cancel();
wolJob = scheduler.scheduleWithFixedDelay(this::wolCheckPeriodic, 0, 1000, TimeUnit.MILLISECONDS);
startWoljob();
return true;
}
return false;
}

public void setMacAddress(@Nullable String macAddress) {
if (macAddress != null) {
this.macAddress = macAddress;
}
private void startWoljob() {
int interval = 1000;
wolJob.ifPresentOrElse(job -> {
if (job.isCancelled()) {
wolJob = Optional.of(scheduler.scheduleWithFixedDelay(this::wolCheckPeriodic, 0, interval,
TimeUnit.MILLISECONDS));
} // else - scheduler is already running!
}, () -> {
wolJob = Optional.of(
scheduler.scheduleWithFixedDelay(this::wolCheckPeriodic, 0, interval, TimeUnit.MILLISECONDS));
});
}

@SuppressWarnings("null")
public synchronized void cancel() {
if (wolJob != null && !wolJob.isCancelled()) {
wolJob.ifPresent(job -> {
logger.info("{}: cancelling WOL Job", host);
wolJob.cancel(true);
}
wolJob = null;
job.cancel(true);
});
}

private void sendWOL() {
Expand All @@ -235,17 +241,17 @@ private void wolCheckPeriodic() {
sendWOL();
}
// after RemoteService up again to ensure state is properly set
SamsungTvService service = findServiceInstance(RemoteControllerService.SERVICE_NAME);
if (service != null) {
Optional<SamsungTvService> service = findServiceInstance(RemoteControllerService.SERVICE_NAME);
service.ifPresent(s -> {
logger.info("{}: RemoteControllerService found after {} attempts", host, wolCount);
// do not resend command if artMode command as TV wakes up in artMode
if (!channel.equals(ART_MODE)) {
logger.info("{}: resend command {} to channel {} in 2 seconds...", host, command, channel);
// send in 2 seconds to allow time for connection to re-establish
sendCommand((RemoteControllerService) service);
sendCommand((RemoteControllerService) s);
}
cancel();
}
});
// cancel job
if (wolCount++ > WOL_SERVICE_CHECK_COUNT) {
logger.warn("{}: Service NOT found after {} attempts: stopping WOL attempts", host, wolCount);
Expand Down Expand Up @@ -305,7 +311,6 @@ public void discoverConfiguration() {
logger.debug("{}: updated macAddress: {}", host, macAddress);
}
}
wolTask.setMacAddress(configuration.getMacAddress());

if (PROTOCOL_NONE.equals(configuration.getProtocol())) {
for (int port : ports) {
Expand Down Expand Up @@ -339,7 +344,6 @@ public void discoverConfiguration() {
if (properties.getWifiMac().length() == 17) {
putConfig(MAC_ADDRESS, properties.getWifiMac());
logger.debug("{}: updated macAddress: {}", host, properties.getWifiMac());
wolTask.setMacAddress(configuration.getMacAddress());
}
}
setModelName(properties.getModelName());
Expand Down Expand Up @@ -424,6 +428,9 @@ public synchronized boolean getArtModeSupported() {
}

public synchronized void setArtModeSupported(boolean artmode) {
if (!artModeSupported && artmode) {
logger.debug("{}: ArtMode Enabled", host);
}
artModeSupported = artmode;
}

Expand All @@ -441,37 +448,28 @@ public void initialize() {
checkAndCreateServices();
}

public void startPolling() {
try {
if (pollingJob == null || pollingJob.isCancelled() || pollingJob.isDone()) {
if (pollingJob != null && pollingJob.isDone()) {
pollingJob.get();
}
logger.debug("{}: Start refresh task, interval={}", host, configuration.getRefreshInterval());
pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, configuration.getRefreshInterval(),
TimeUnit.MILLISECONDS);
}
} catch (CancellationException | InterruptedException | ExecutionException e) {
if (logger.isTraceEnabled()) {
logger.trace("{}: Polling Job Exception: ", host, e);
} else {
logger.debug("{}: Polling Job Exception: {}", host, e.getMessage());
}
pollingJob = null;
startPolling();
}
/**
* Start polling job with initial delay of 10 seconds
*
*/
private void startPolling() {
int interval = configuration.getRefreshInterval();
pollingJob.ifPresentOrElse(job -> {
if (job.isCancelled()) {
pollingJob = Optional
.of(scheduler.scheduleWithFixedDelay(this::poll, 10000, interval, TimeUnit.MILLISECONDS));
} // else - scheduler is already running!
}, () -> {
logger.debug("{}: Start refresh task, interval={}", host, interval);
pollingJob = Optional
.of(scheduler.scheduleWithFixedDelay(this::poll, 10000, interval, TimeUnit.MILLISECONDS));
});
}

@Override
@SuppressWarnings("null")
public void dispose() {
logger.debug("{}: Disposing SamsungTvHandler", host);

if (pollingJob != null && !pollingJob.isCancelled()) {
pollingJob.cancel(true);
}
pollingJob = null;

pollingJob.ifPresent(job -> job.cancel(true));
wolTask.cancel();

upnpService.getRegistry().removeListener(this);
Expand Down Expand Up @@ -526,9 +524,9 @@ private void poll() {
.forEach(channel -> service.handleCommand(channel, RefreshType.REFRESH)));
} catch (Exception e) {
if (logger.isTraceEnabled()) {
logger.trace("{}: Polling Job threw exception: ", host, e);
logger.trace("{}: Polling Job exception: ", host, e);
} else {
logger.debug("{}: Polling Job threw exception: {}", host, e.getMessage());
logger.debug("{}: Polling Job exception: {}", host, e.getMessage());
}
}
}
Expand All @@ -538,8 +536,6 @@ public synchronized void valueReceived(String variable, State value) {

if (POWER.equals(variable)) {
setPowerState(OnOffType.ON.equals(value));
} else if (ART_MODE.equals(variable)) {
setArtModeSupported(true);
}
updateState(variable, value);
}
Expand All @@ -558,7 +554,8 @@ public void reportError(ThingStatusDetail statusDetail, @Nullable String message
* Media Renderer UPnP device. This function tries to find another UPnP
* devices related to same Samsung TV and create handler for those.
* Also attempts to create websocket services if protocol is set to websocket
* and Smartthings service if PAT (Api key) is entered
* And at least one UPNP service is discovered
* Smartthings service is also started if PAT (Api key) is entered
*/
private void checkAndCreateServices() {
logger.debug("{}: Check and create missing services", host);
Expand All @@ -570,25 +567,29 @@ private void checkAndCreateServices() {
RemoteDevice rdevice = (RemoteDevice) device;
if (host.equals(Utils.getHost(rdevice))) {
setModelName(Utils.getModelName(rdevice));
if (RemoteControllerService.SERVICE_NAME.equals(Utils.getType(rdevice))
&& configuration.isWebsocketProtocol()) {
continue;
}
isOnline = createService(Utils.getType(rdevice), Utils.getUdn(rdevice)) || isOnline;
}
}

// Websocket services and Smartthings service
if (configuration.isWebsocketProtocol()) {
isOnline = createService(RemoteControllerService.SERVICE_NAME, "") || isOnline;
if (isOnline && configuration.isWebsocketProtocol()) {
createService(RemoteControllerService.SERVICE_NAME, "");
if (!configuration.getSmartThingsApiKey().isBlank()) {
isOnline = createService(SmartThingsApiService.SERVICE_NAME, "") || isOnline;
createService(SmartThingsApiService.SERVICE_NAME, "");
}
}

if (isOnline) {
putOnline();
startPolling();
} else {
putOffline();
}
logger.debug("{}: TV is {}online", host, isOnline ? "" : "NOT ");
startPolling();
}

/**
Expand All @@ -600,16 +601,17 @@ private void checkAndCreateServices() {
*/
private synchronized boolean createService(String type, String udn) {

SamsungTvService service = findServiceInstance(type);
Optional<SamsungTvService> service = findServiceInstance(type);

if (service != null) {
if (service.isPresent()) {
logger.debug("{}: Service rediscovered, clearing caches: {}, {} ({})", host, getModelName(), type, udn);
service.clearCache();
service.get().clearCache();
return true;
}

service = createNewService(type, udn);
if (service != null) {
startService(service);
if (service.isPresent()) {
startService(service.get());
logger.debug("{}: Started service for: {}, {} ({})", host, getModelName(), type, udn);
return true;
}
Expand All @@ -624,32 +626,33 @@ private synchronized boolean createService(String type, String udn) {
* @param udn
* @return service or null
*/
private synchronized @Nullable SamsungTvService createNewService(String type, String udn) {
SamsungTvService service = null;
private synchronized Optional<SamsungTvService> createNewService(String type, String udn) {
Optional<SamsungTvService> service = Optional.empty();

switch (type) {
case MainTVServerService.SERVICE_NAME:
service = new MainTVServerService(upnpIOService, udn, host, this);
service = Optional.of(new MainTVServerService(upnpIOService, udn, host, this));
break;
case MediaRendererService.SERVICE_NAME:
service = new MediaRendererService(upnpIOService, udn, host, this);
service = Optional.of(new MediaRendererService(upnpIOService, udn, host, this));
break;
case RemoteControllerService.SERVICE_NAME:
try {
service = new RemoteControllerService(host, configuration.getPort(), !udn.isEmpty(), this);
service = Optional
.of(new RemoteControllerService(host, configuration.getPort(), !udn.isEmpty(), this));
} catch (RemoteControllerException e) {
logger.warn("Cannot create remote controller service: {}", e.getMessage());
}
break;
case SmartThingsApiService.SERVICE_NAME:
service = new SmartThingsApiService(host, this);
service = Optional.of(new SmartThingsApiService(host, this));
break;
}
return service;
}

private synchronized @Nullable SamsungTvService findServiceInstance(String serviceName) {
return services.stream().filter(a -> a.getServiceName().equals(serviceName)).findFirst().orElse(null);
private synchronized Optional<SamsungTvService> findServiceInstance(String serviceName) {
return services.stream().filter(a -> a.getServiceName().equals(serviceName)).findFirst();
}

private synchronized void startService(SamsungTvService service) {
Expand Down
Loading

0 comments on commit 2613b1a

Please sign in to comment.