Skip to content

Commit

Permalink
[network] Add support for Wake-on-LAN thing action (openhab#8336)
Browse files Browse the repository at this point in the history
Closes openhab#3799

Signed-off-by: Wouter Born <[email protected]>
  • Loading branch information
wborn authored and CSchlipp committed Sep 12, 2020
1 parent b641d62 commit 2b9f805
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 6 deletions.
22 changes: 19 additions & 3 deletions bundles/org.openhab.binding.network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ Please note: things discovered by the network binding will be provided with a ti

```
network:pingdevice:one_device [ hostname="192.168.0.64" ]
network:pingdevice:second_device [ hostname="192.168.0.65", retry=1, timeout=5000, refreshInterval=60000 ]
network:pingdevice:second_device [ hostname="192.168.0.65", macAddress="6f:70:65:6e:48:41", retry=1, timeout=5000, refreshInterval=60000 ]
network:servicedevice:important_server [ hostname="192.168.0.62", port=1234 ]
network:speedtest:local "SpeedTest 50Mo" @ "Internet" [refreshInterval=20, uploadSize=1000000, url="https://bouygues.testdebit.info/", fileName="50M.iso"]
```

Use the following options for a **network:pingdevice**:

- **hostname:** IP address or hostname of the device
- **hostname:** IP address or hostname of the device.
- **macAddress:** MAC address used for waking the device by the Wake-on-LAN action.
- **retry:** After how many refresh interval cycles the device will be assumed to be offline. Default: `1`.
- **timeout:** How long the ping will wait for an answer, in milliseconds. Default: `5000` (5 seconds).
- **refreshInterval:** How often the device will be checked, in milliseconds. Default: `60000` (one minute).
Expand Down Expand Up @@ -185,7 +186,7 @@ Things support the following channels:
demo.things:

```xtend
Thing network:pingdevice:devicename [ hostname="192.168.0.42" ]
Thing network:pingdevice:devicename [ hostname="192.168.0.42", macAddress="6f:70:65:6e:48:41" ]
Thing network:speedtest:local "SpeedTest 50Mo" @ "Internet" [url="https://bouygues.testdebit.info/", fileName="50M.iso"]
```

Expand Down Expand Up @@ -237,3 +238,18 @@ sitemap demo label="Main Menu"
}
}
```

## Rule Actions

A Wake-on-LAN action is supported by this binding for the `pingdevice` and `servicedevice` thing types.
In classic rules this action is accessible as shown in the example below:

```
val actions = getActions("network", "network:pingdevice:devicename")
if (actions === null) {
logInfo("actions", "Actions not found, check thing ID")
return
} else {
actions.sendWakeOnLanPacket()
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
@NonNullByDefault
public class NetworkHandlerConfiguration {
public String hostname = "";
public String macAddress = "";
public @Nullable Integer port;
public Integer retry = 1;
public Integer refreshInterval = 60000;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* 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.network.internal;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.net.NetUtil;
import org.eclipse.smarthome.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link WakeOnLanPacketSender} broadcasts a magic packet to wake a device.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WakeOnLanPacketSender {

private static final int WOL_UDP_PORT = 9;

// Wake-on-LAN magic packet constants
static final int PREFIX_BYTE_SIZE = 6;
static final int MAC_REPETITIONS = 16;
static final int MAC_BYTE_SIZE = 6;
static final int MAGIC_PACKET_BYTE_SIZE = PREFIX_BYTE_SIZE + MAC_REPETITIONS * MAC_BYTE_SIZE;
static final String[] MAC_SEPARATORS = new String[] { ":", "-" };

private final Logger logger = LoggerFactory.getLogger(WakeOnLanPacketSender.class);

private final String macAddress;
private byte @Nullable [] magicPacket;
private final Consumer<byte[]> magicPacketSender;

public WakeOnLanPacketSender(String macAddress) {
this.macAddress = macAddress;
this.magicPacketSender = this::broadcastMagicPacket;
}

/**
* Used for testing only.
*/
WakeOnLanPacketSender(String macAddress, Consumer<byte[]> magicPacketSender) {
this.macAddress = macAddress;
this.magicPacketSender = magicPacketSender;
}

public void sendPacket() {
byte[] localMagicPacket = magicPacket;
if (localMagicPacket == null) {
localMagicPacket = createMagicPacket(createMacBytes(macAddress));
magicPacket = localMagicPacket;
}

magicPacketSender.accept(localMagicPacket);
}

private byte[] createMacBytes(String macAddress) {
String hexString = macAddress;
for (String macSeparator : MAC_SEPARATORS) {
hexString = hexString.replaceAll(macSeparator, "");
}
if (hexString.length() != 2 * MAC_BYTE_SIZE) {
throw new IllegalStateException("Invalid MAC address: " + macAddress);
}
return HexUtils.hexToBytes(hexString);
}

private byte[] createMagicPacket(byte[] macBytes) {
byte[] bytes = new byte[MAGIC_PACKET_BYTE_SIZE];
Arrays.fill(bytes, 0, PREFIX_BYTE_SIZE, (byte) 0xff);
for (int i = PREFIX_BYTE_SIZE; i < MAGIC_PACKET_BYTE_SIZE; i += MAC_BYTE_SIZE) {
System.arraycopy(macBytes, 0, bytes, i, macBytes.length);
}
return bytes;
}

private void broadcastMagicPacket(byte[] magicPacket) {
try (DatagramSocket socket = new DatagramSocket()) {
broadcastAddressStream().forEach(broadcastAddress -> {
try {
DatagramPacket packet = new DatagramPacket(magicPacket, MAGIC_PACKET_BYTE_SIZE, broadcastAddress,
WOL_UDP_PORT);
socket.send(packet);
logger.debug("Wake-on-LAN packet sent (MAC address: {}, broadcast address: {})", macAddress,
broadcastAddress.getHostAddress());
} catch (IOException e) {
logger.debug("Failed to send Wake-on-LAN packet (MAC address: {}, broadcast address: {})",
macAddress, broadcastAddress.getHostAddress(), e);
}
});
logger.info("Wake-on-LAN packets sent (MAC address: {})", macAddress);
} catch (SocketException e) {
logger.error("Failed to open Wake-on-LAN datagram socket", e);
}
}

private Stream<InetAddress> broadcastAddressStream() {
return NetUtil.getAllBroadcastAddresses().stream().map(address -> {
try {
return InetAddress.getByName(address);
} catch (UnknownHostException e) {
logger.debug("Failed to get broadcast address '{}' by name", address, e);
return null;
}
}).filter(Objects::nonNull);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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.network.internal.action;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* The {@link INetworkActions} defines the interface for all thing actions supported by the binding.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface INetworkActions {

void sendWakeOnLanPacket();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* 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.network.internal.action;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.thing.binding.ThingActions;
import org.eclipse.smarthome.core.thing.binding.ThingActionsScope;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.openhab.binding.network.internal.handler.NetworkHandler;
import org.openhab.core.automation.annotation.RuleAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The class is responsible to call corresponding actions on {@link NetworkHandler}.
* <p>
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
* the test <i>actions instanceof NetworkActions</i> fails. This test can fail
* due to an issue in openHAB core v2.5.0 where the {@link NetworkActions} class
* can be loaded by a different classloader than the <i>actions</i> instance.
*
* @author Wouter Born - Initial contribution
*/
@ThingActionsScope(name = "network")
@NonNullByDefault
public class NetworkActions implements ThingActions, INetworkActions {

private final Logger logger = LoggerFactory.getLogger(NetworkActions.class);

private @Nullable NetworkHandler handler;

@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof NetworkHandler) {
this.handler = (NetworkHandler) handler;
}
}

@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}

@Override
@RuleAction(label = "Send WoL Packet", description = "Send a Wake-on-LAN packet to wake the device")
public void sendWakeOnLanPacket() {
NetworkHandler localHandler = handler;
if (localHandler != null) {
localHandler.sendWakeOnLanPacket();
} else {
logger.warn("Failed to send Wake-on-LAN packet (handler null)");
}
}

public static void sendWakeOnLanPacket(@Nullable ThingActions actions) {
invokeMethodOf(actions).sendWakeOnLanPacket();
}

private static INetworkActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(NetworkActions.class.getName())) {
if (actions instanceof INetworkActions) {
return (INetworkActions) actions;
} else {
return (INetworkActions) Proxy.newProxyInstance(INetworkActions.class.getClassLoader(),
new Class[] { INetworkActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of " + NetworkActions.class.getName());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.TimeZone;
Expand All @@ -32,10 +33,19 @@
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerService;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.UnDefType;
import org.openhab.binding.network.internal.*;
import org.openhab.binding.network.internal.NetworkBindingConfiguration;
import org.openhab.binding.network.internal.NetworkBindingConfigurationListener;
import org.openhab.binding.network.internal.NetworkBindingConstants;
import org.openhab.binding.network.internal.NetworkHandlerConfiguration;
import org.openhab.binding.network.internal.PresenceDetection;
import org.openhab.binding.network.internal.PresenceDetectionListener;
import org.openhab.binding.network.internal.PresenceDetectionValue;
import org.openhab.binding.network.internal.WakeOnLanPacketSender;
import org.openhab.binding.network.internal.action.NetworkActions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -45,12 +55,14 @@
*
* @author Marc Mettke - Initial contribution
* @author David Graeff - Rewritten
* @author Wouter Born - Add Wake-on-LAN thing action support
*/
@NonNullByDefault
public class NetworkHandler extends BaseThingHandler
implements PresenceDetectionListener, NetworkBindingConfigurationListener {
private final Logger logger = LoggerFactory.getLogger(NetworkHandler.class);
private @NonNullByDefault({}) PresenceDetection presenceDetection;
private @NonNullByDefault({}) WakeOnLanPacketSender wakeOnLanPacketSender;

private boolean isTCPServiceDevice;
private NetworkBindingConfiguration configuration;
Expand All @@ -72,8 +84,8 @@ public NetworkHandler(Thing thing, boolean isTCPServiceDevice, NetworkBindingCon
}

private void refreshValue(ChannelUID channelUID) {
// We are not yet even initialised, don't do anything
if (!presenceDetection.isAutomaticRefreshing()) {
// We are not yet even initialized, don't do anything
if (presenceDetection == null || !presenceDetection.isAutomaticRefreshing()) {
return;
}

Expand Down Expand Up @@ -187,6 +199,8 @@ void initialize(PresenceDetection presenceDetection) {
presenceDetection.setRefreshInterval(handlerConfiguration.refreshInterval.longValue());
presenceDetection.setTimeout(handlerConfiguration.timeout.intValue());

wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress);

updateStatus(ThingStatus.ONLINE);
presenceDetection.startAutomaticRefresh(scheduler);

Expand Down Expand Up @@ -222,4 +236,17 @@ public void bindingConfigurationChanged() {
// Make sure that changed binding configuration is reflected
presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
}

@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(NetworkActions.class);
}

public void sendWakeOnLanPacket() {
if (handlerConfiguration.macAddress.isEmpty()) {
throw new IllegalStateException(
"Cannot send WoL packet because the 'macAddress' is not configured for " + thing.getUID());
}
wakeOnLanPacketSender.sendPacket();
}
}
Loading

0 comments on commit 2b9f805

Please sign in to comment.