Skip to content

Commit

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

Signed-off-by: Wouter Born <github@maindrain.net>
  • Loading branch information
wborn committed Aug 24, 2020
1 parent 85f3744 commit 35d28da
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 8 deletions.
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,127 @@
/**
* 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 Consumer<byte[]> magicPacketSender;

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

public 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 HeosActions</i> fails. This test can fail
* due to an issue in openHAB core v2.5.0 where the {@link HeosActions} 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);

protected @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 @@ -51,6 +61,7 @@ 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 +83,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 @@ -146,11 +157,7 @@ public void finalDetectionResult(PresenceDetectionValue value) {

@Override
public void dispose() {
PresenceDetection detection = presenceDetection;
if (detection != null) {
detection.stopAutomaticRefresh();
}
presenceDetection = null;
presenceDetection.stopAutomaticRefresh();
}

/**
Expand Down Expand Up @@ -187,6 +194,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 +231,13 @@ 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() {
wakeOnLanPacketSender.sendPacket();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
<description>Hostname or IP of the device</description>
</parameter>

<parameter name="macAddress" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>MAC Address</label>
<description>MAC address used for waking the device by the Wake-on-LAN action</description>
</parameter>

<parameter name="refreshInterval" type="integer">
<label>Refresh Interval</label>
<description>States how long to wait after a device state update before the next refresh shall occur (in ms)</description>
Expand Down Expand Up @@ -85,6 +90,11 @@
<default>80</default>
</parameter>

<parameter name="macAddress" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>MAC Address</label>
<description>MAC address used for waking the device by the Wake-on-LAN action</description>
</parameter>

<parameter name="retry" type="integer">
<label>Retry</label>
<description>Defines how many times a connection attempt shall occur, before the device is stated as offline</description>
Expand Down
Loading

0 comments on commit 35d28da

Please sign in to comment.