Skip to content

Commit

Permalink
[lgwebos] Wake-on-Lan Integration (#7103)
Browse files Browse the repository at this point in the history
* Adding WOL Implementation to PowerControl channel with ability to determine MAC (best effort via arp) and send WOL natively.
Removed Search, as second screen service does actually show up in regular scans now.
* Addressing review comments in WakeOnLanUitility and config.xml.
* Add mac and windows commands to discover MAC.
* Break once MAC is found in result.
* Using org.eclipse.smarthome.io.net.exec.ExecUtil instead of implementing Commandline Execution in the binding itself.
* Detecting which linux tool to detect MAC exists, arp or arping.
* Fixed typo in debug message.
* Addressing review comments on formatting.
* MacAddress parameter added to README demo item and Power Control Handler updated.
* Handle power on off commands in all possible LGWebOSTVSocket states.
* Reformatting debug message.
* Fix whitespace.
* Moved If statement cases into switch statement.
* Adding comments.
* Applying code review recommendations. Reducing power channel updates. CONNECTING and REGISTERING still count as OFF states.
* Inlining Socket's isConnected method. The method name was also misleading.
Signed-off-by: Sebastian Prehn <sebastian.prehn@gmx.de>

Co-authored-by: cpmeister <mistercpp2000@gmail.com>
  • Loading branch information
sprehn and cpmeister committed Apr 18, 2020
1 parent 5cca782 commit f8a943c
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 131 deletions.
27 changes: 9 additions & 18 deletions bundles/org.openhab.binding.lgwebos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,29 @@ If automatic discovery is not possible you may still manually configure a device

## Thing Configuration

WebOS TV has two configuration parameters.
WebOS TV has three configuration parameters.

Parameters:

| Name | Description |
|---------|------------------------------------------------------------------------------|
| host | Hostname or IP address of TV |
| key | Key exchanged with TV after pairing (enter it after you paired the device) |
| Name | Description |
|------------|-----------------------------------------------------------------------------------------------------|
| host | Hostname or IP address of TV |
| key | Key exchanged with TV after pairing (enter it after you paired the device) |
| macAddress | The MAC address of your TV to turn on via Wake On Lan (WOL). The binding will attempt to detect it. |

### Configuration in .things file

Set host and key parameter as in the following example:

```
Thing lgwebos:WebOSTV:tv1 [host="192.168.2.119", key="6ef1dff6c7c936c8dc5056fc85ea3aef"]
Thing lgwebos:WebOSTV:tv1 [host="192.168.2.119", key="6ef1dff6c7c936c8dc5056fc85ea3aef", macAddress="3c:cd:93:c2:20:e0"]
```

## Channels

| Channel Type ID | Item Type | Description | Read/Write |
|-----------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|
| power | Switch | Current power setting. TV can only be powered off, not on. | RW |
| power | Switch | Current power setting. TV can only be powered off, not on, via the TV's API. Turning on is implemented via Wake On Lan, for which the MAC address must be set in the thing configuration. | RW |
| mute | Switch | Current mute setting. | RW |
| volume | Dimmer | Current volume setting. Setting and reporting absolute percent values only works when using internal speakers. When connected to an external amp, the volume should be controlled using increase and decrease commands. | RW |
| channel | String | Current channel. Use only the channel number as command to update the channel. | RW |
Expand Down Expand Up @@ -99,7 +100,7 @@ A sample HABPanel remote control widget can be found [in this github repository.
demo.things:

```
Thing lgwebos:WebOSTV:3aab9eea-953b-4272-bdbd-f0cd0ecf4a46 [host="192.168.2.119", key="6ef1dff6c7c936c8dc5056fc85ea3aef"]
Thing lgwebos:WebOSTV:3aab9eea-953b-4272-bdbd-f0cd0ecf4a46 [host="192.168.2.119", key="6ef1dff6c7c936c8dc5056fc85ea3aef", macAddress="3c:cd:93:c2:20:e0"]
```

demo.items:
Expand All @@ -116,8 +117,6 @@ Switch LG_TV0_Stop "Stop" { autoupdate="false", channel="lgwe
String LG_TV0_Application "Application [%s]" { channel="lgwebos:WebOSTV:3aab9eea-953b-4272-bdbd-f0cd0ecf4a46:appLauncher"}
Player LG_TV0_Player { channel="lgwebos:WebOSTV:3aab9eea-953b-4272-bdbd-f0cd0ecf4a46:mediaPlayer"}
// this assumes you also have the wake on lan binding configured and your TV's IP address is on this network - You would need to update your broadcast and mac address accordingly
Switch LG_TV0_WOL { wol="192.168.2.255#3c:cd:93:c2:20:e0" }
```

demo.sitemap:
Expand All @@ -142,14 +141,6 @@ sitemap demo label="Main Menu"
demo.rules:

```
// this assumes you also have the wake on lan binding configured.
rule "Power on TV via Wake on LAN"
when
Item LG_TV0_Power received command ON
then
LG_TV0_WOL.sendCommand(ON)
end
// for relative volume changes
rule "VolumeUpDown"
when Item LG_TV0_VolDummy received command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVMouseSocket.ButtonType;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.State;
import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription;
import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
Expand Down Expand Up @@ -247,7 +248,7 @@ private Optional<LGWebOSTVSocket> getConnectedSocket() {
LGWebOSHandler lgWebOSHandler = getLGWebOSHandler();
final LGWebOSTVSocket socket = lgWebOSHandler.getSocket();

if (!socket.isConnected()) {
if (socket.getState() != State.REGISTERED) {
logger.warn("Device with ThingID {} is currently not connected.", lgWebOSHandler.getThing().getUID());
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class LGWebOSBindingConstants {
*/
public static final String CONFIG_HOST = "host";
public static final String CONFIG_KEY = "key";
public static final String CONFIG_MAC_ADDRESS = "macAddress";

/*
* Property names must match property names in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
*/
package org.openhab.binding.lgwebos.internal;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler;
import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.State;
import org.openhab.binding.lgwebos.internal.handler.core.CommandConfirmation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -29,31 +33,72 @@
*/
@NonNullByDefault
public class PowerControlPower extends BaseChannelHandler<CommandConfirmation> {
private static final int WOL_PACKET_RETRY_COUNT = 10;
private static final int WOL_PACKET_RETRY_DELAY_MILLIS = 100;

private final Logger logger = LoggerFactory.getLogger(PowerControlPower.class);
private final ConfigProvider configProvider;
private final ScheduledExecutorService scheduler;

public PowerControlPower(ConfigProvider configProvider, ScheduledExecutorService scheduler) {
this.configProvider = configProvider;
this.scheduler = scheduler;
}

@Override
public void onReceiveCommand(String channelId, LGWebOSHandler handler, Command command) {
final State state = handler.getSocket().getState();
if (RefreshType.REFRESH == command) {
handler.postUpdate(channelId, handler.getSocket().isConnected() ? OnOffType.ON : OnOffType.OFF);
return;
}
if (OnOffType.ON == command) {
logger.debug(
"Received ON - Turning TV on via API is not supported by LG WebOS TVs. You may succeed using wake on lan binding, please consult lgwebos binding documentation.");
}
if (!handler.getSocket().isConnected()) {
/*
* Unable to send anything to a not connected device.
* onDeviceReady nor onDeviceRemoved will be called and item state would be permanently inconsistent.
* Therefore setting state to OFF
*/
handler.postUpdate(channelId, OnOffType.OFF);
handler.postUpdate(channelId, state == State.REGISTERED ? OnOffType.ON : OnOffType.OFF);
} else if (OnOffType.ON == command) {
switch (state) {
case CONNECTING:
case REGISTERING:
logger.debug("Received ON - TV is currently connecting.");
handler.postUpdate(channelId, OnOffType.OFF);
break;
case REGISTERED:
logger.debug("Received ON - TV is already on.");
break;
case DISCONNECTING: // WOL will not stop the shutdown process, but we must not update the state to ON
case DISCONNECTED:
String macAddress = configProvider.getMacAddress();
if (macAddress.isEmpty()) {
logger.debug("Received ON - Turning TV on via API is not supported by LG WebOS TVs. "
+ "You may succeed using wake on lan (WOL). "
+ "Please set the macAddress config value in Thing configuration to enable this.");
handler.postUpdate(channelId, OnOffType.OFF);
} else {
for (int i = 0; i < WOL_PACKET_RETRY_COUNT; i++) {
scheduler.schedule(() -> {
try {
WakeOnLanUtility.sendWOLPacket(macAddress);
} catch (IllegalArgumentException e) {
logger.debug("Failed to send WOL packet: {}", e.getMessage());
}
}, i * WOL_PACKET_RETRY_DELAY_MILLIS, TimeUnit.MILLISECONDS);
}
}
break;
}
} else if (OnOffType.OFF == command) {
handler.getSocket().powerOff(getDefaultResponseListener());
switch (state) {
case CONNECTING:
case REGISTERING:
// in both states no message will sent to TV, thus the operation won't have an effect
logger.debug("Received OFF - TV is currently connecting.");
break;
case REGISTERED:
handler.getSocket().powerOff(getDefaultResponseListener());
break;
case DISCONNECTING:
case DISCONNECTED:
logger.debug("Received OFF - TV is already off.");
break;
}
} else {
logger.info("Only accept OnOffType, RefreshType. Type was {}.", command.getClass());
}

}

@Override
Expand All @@ -65,4 +110,9 @@ public void onDeviceReady(String channelId, LGWebOSHandler handler) {
public void onDeviceRemoved(String channelId, LGWebOSHandler handler) {
handler.postUpdate(channelId, OnOffType.OFF);
}

public interface ConfigProvider {
String getMacAddress();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.lgwebos.internal;

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.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.io.net.exec.ExecUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Class with utility functions to support Wake On Lan (WOL)
*
* @author Arjan Mels - Initial contribution
* @author Sebastian Prehn - Modification to getMACAddress
*
*/
@NonNullByDefault
public class WakeOnLanUtility {

private static final Logger LOGGER = LoggerFactory.getLogger(WakeOnLanUtility.class);
private static final Pattern MAC_REGEX = Pattern.compile("(([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2})");
private static final int CMD_TIMEOUT_MS = 1000;

private static final String COMMAND;
static {
String os = System.getProperty("os.name").toLowerCase();
LOGGER.debug("os: {}", os);
if ((os.indexOf("win") >= 0)) {
COMMAND = "arp -a %s";
} else if ((os.indexOf("mac") >= 0)) {
COMMAND = "arp %s";
} else { // linux
if (checkIfLinuxCommandExists("arp")) {
COMMAND = "arp %s";
} else if (checkIfLinuxCommandExists("arping")) { // typically OH provided docker image
COMMAND = "arping -r -c 1 -C 1 %s";
} else {
COMMAND = "";
}
}
}

/**
* Get MAC address for host
*
* @param hostName Host Name (or IP address) of host to retrieve MAC address for
* @return MAC address
*/
public static @Nullable String getMACAddress(String hostName) {
if (COMMAND.isEmpty()) {
LOGGER.debug("MAC address detection not possible. No command to identify MAC found.");
return null;
}

String cmd = String.format(COMMAND, hostName);
String response = ExecUtil.executeCommandLineAndWaitResponse(cmd, CMD_TIMEOUT_MS);
Matcher matcher = MAC_REGEX.matcher(response);
String macAddress = null;

while (matcher.find()) {
String group = matcher.group();

if (group.length() == 17) {
macAddress = group;
break;
}
}

if (macAddress != null) {
LOGGER.debug("MAC address of host {} is {}", hostName, macAddress);
} else {
LOGGER.debug("Problem executing command {} to retrieve MAC address for {}: {}", cmd, hostName, response);
}
return macAddress;
}

/**
* Send single WOL (Wake On Lan) package on all interfaces
*
* @macAddress MAC address to send WOL package to
*/
public static void sendWOLPacket(String macAddress) {
byte[] bytes = getWOLPackage(macAddress);

try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isLoopback()) {
continue; // Do not want to use the loopback interface.
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress broadcast = interfaceAddress.getBroadcast();
if (broadcast == null) {
continue;
}

DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcast, 9);
try (DatagramSocket socket = new DatagramSocket()) {
socket.send(packet);
LOGGER.trace("Sent WOL packet to {} {}", broadcast, macAddress);
} catch (IOException e) {
LOGGER.warn("Problem sending WOL packet to {} {}", broadcast, macAddress);
}
}
}

} catch (IOException e) {
LOGGER.warn("Problem with interface while sending WOL packet to {}", macAddress);
}
}

/**
* Create WOL UDP package: 6 bytes 0xff and then 16 times the 6 byte mac address repeated
*
* @param macStr String representation of the MAC address (either with : or -)
* @return byte array with the WOL package
* @throws IllegalArgumentException
*/
private static byte[] getWOLPackage(String macStr) throws IllegalArgumentException {
byte[] macBytes = new byte[6];
String[] hex = macStr.split("(\\:|\\-)");
if (hex.length != 6) {
throw new IllegalArgumentException("Invalid MAC address.");
}
try {
for (int i = 0; i < 6; i++) {
macBytes[i] = (byte) Integer.parseInt(hex[i], 16);
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid hex digit in MAC address.");
}

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

return bytes;
}

private static boolean checkIfLinuxCommandExists(String cmd) {
try {
return 0 == Runtime.getRuntime().exec(String.format("which %s", cmd)).waitFor();
} catch (InterruptedException | IOException e) {
LOGGER.debug("Error trying to check if command {} exists: {}", cmd, e.getMessage());
}
return false;
}

}

0 comments on commit f8a943c

Please sign in to comment.