2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.network.internal;
15 import java.io.IOException;
16 import java.net.DatagramPacket;
17 import java.net.DatagramSocket;
18 import java.net.InetAddress;
19 import java.net.InetSocketAddress;
20 import java.net.SocketAddress;
21 import java.net.SocketException;
22 import java.net.UnknownHostException;
23 import java.util.Arrays;
24 import java.util.Objects;
25 import java.util.function.Consumer;
26 import java.util.stream.Stream;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.core.net.NetUtil;
31 import org.openhab.core.util.HexUtils;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * The {@link WakeOnLanPacketSender} broadcasts a magic packet to wake a device.
38 * @author Wouter Born - Initial contribution
41 public class WakeOnLanPacketSender {
43 private static final int WOL_UDP_PORT = 9;
45 // Wake-on-LAN magic packet constants
46 static final int PREFIX_BYTE_SIZE = 6;
47 private static final int MAC_REPETITIONS = 16;
48 static final int MAC_BYTE_SIZE = 6;
49 static final int MAGIC_PACKET_BYTE_SIZE = PREFIX_BYTE_SIZE + MAC_REPETITIONS * MAC_BYTE_SIZE;
50 private static final String[] MAC_SEPARATORS = new String[] { ":", "-" };
52 private final Logger logger = LoggerFactory.getLogger(WakeOnLanPacketSender.class);
54 private final String macAddress;
56 private final @Nullable String hostname;
57 private final @Nullable Integer port;
59 private final Consumer<byte[]> magicPacketMacSender;
60 private final Consumer<byte[]> magicPacketIpSender;
62 public WakeOnLanPacketSender(String macAddress, @Nullable String hostname, @Nullable Integer port) {
63 logger.debug("initialized WOL Packet Sender (mac: {}, hostname: {}, port: {}", macAddress, hostname, port);
64 this.macAddress = macAddress;
65 this.hostname = hostname;
67 this.magicPacketMacSender = this::sendMagicPacketViaMac;
68 this.magicPacketIpSender = this::sendMagicPacketViaIp;
72 * Used for testing only.
74 public WakeOnLanPacketSender(String macAddress) {
75 logger.debug("initialized WOL Packet Sender (mac: {}", macAddress);
76 this.macAddress = macAddress;
79 this.magicPacketMacSender = this::sendMagicPacketViaMac;
80 this.magicPacketIpSender = this::sendMagicPacketViaIp;
84 * Used for testing only.
86 WakeOnLanPacketSender(String macAddress, Consumer<byte[]> magicPacketSender) {
87 this.macAddress = macAddress;
90 this.magicPacketMacSender = magicPacketSender;
91 this.magicPacketIpSender = this::sendMagicPacketViaIp;
94 public void sendWakeOnLanPacketViaMac() {
95 byte[] magicPacket = createMagicPacket();
96 this.magicPacketMacSender.accept(magicPacket);
99 public void sendWakeOnLanPacketViaIp() {
100 byte[] magicPacket = createMagicPacket();
101 this.magicPacketIpSender.accept(magicPacket);
104 private byte[] createMagicPacket() {
105 byte[] macBytes = createMacBytes(this.macAddress);
106 byte[] magicPacket = new byte[MAGIC_PACKET_BYTE_SIZE];
107 Arrays.fill(magicPacket, 0, PREFIX_BYTE_SIZE, (byte) 0xff);
108 for (int i = PREFIX_BYTE_SIZE; i < MAGIC_PACKET_BYTE_SIZE; i += MAC_BYTE_SIZE) {
109 System.arraycopy(macBytes, 0, magicPacket, i, macBytes.length);
114 private byte[] createMacBytes(String macAddress) {
115 String hexString = macAddress;
116 for (String macSeparator : MAC_SEPARATORS) {
117 hexString = hexString.replaceAll(macSeparator, "");
119 if (hexString.length() != 2 * MAC_BYTE_SIZE) {
120 throw new IllegalStateException("Invalid MAC address: " + macAddress);
122 return HexUtils.hexToBytes(hexString);
125 private void sendMagicPacketViaMac(byte[] magicPacket) {
126 try (DatagramSocket socket = new DatagramSocket()) {
127 logger.debug("Sending Wake-on-LAN Packet via Broadcast");
128 broadcastMagicPacket(magicPacket, socket);
129 } catch (SocketException e) {
130 logger.error("Failed to open Wake-on-LAN datagram socket", e);
134 private void sendMagicPacketViaIp(byte[] magicPacket) {
135 try (DatagramSocket socket = new DatagramSocket()) {
136 if (hostname != null && !hostname.isBlank()) {
137 logger.debug("Sending Wake-on-LAN Packet via IP Address");
138 SocketAddress socketAddress = new InetSocketAddress(hostname,
139 Objects.requireNonNullElse(port, WOL_UDP_PORT));
140 sendMagicPacketToIp(magicPacket, socket, socketAddress);
142 throw new IllegalStateException("Hostname is not set!");
144 } catch (SocketException e) {
145 logger.error("Failed to open Wake-on-LAN datagram socket", e);
149 private void broadcastMagicPacket(byte[] magicPacket, DatagramSocket socket) {
150 broadcastAddressStream().forEach(broadcastAddress -> {
152 DatagramPacket packet = new DatagramPacket(magicPacket, MAGIC_PACKET_BYTE_SIZE, broadcastAddress,
155 logger.debug("Wake-on-LAN packet sent (MAC address: {}, broadcast address: {})", this.macAddress,
156 broadcastAddress.getHostAddress());
157 } catch (IOException e) {
158 logger.error("Failed to send Wake-on-LAN packet (MAC address: {}, broadcast address: {})",
159 this.macAddress, broadcastAddress.getHostAddress(), e);
162 logger.info("Wake-on-LAN packets sent (MAC address: {})", this.macAddress);
165 private void sendMagicPacketToIp(byte[] magicPacket, DatagramSocket socket, SocketAddress ip) {
166 DatagramPacket packet = new DatagramPacket(magicPacket, MAGIC_PACKET_BYTE_SIZE, ip);
169 } catch (IOException e) {
170 logger.error("Failed to send Wake-on-LAN packet (IP address: {})", ip, e);
172 logger.info("Wake-on-LAN packets sent (IP address: {})", ip);
175 private Stream<InetAddress> broadcastAddressStream() {
176 return NetUtil.getAllBroadcastAddresses().stream().map(address -> {
178 return InetAddress.getByName(address);
179 } catch (UnknownHostException e) {
180 logger.error("Failed to get broadcast address '{}' by name", address, e);
183 }).filter(Objects::nonNull);