]> git.basschouten.com Git - openhab-addons.git/blob
c001aff7833b7535fcd242650988fa9506e45879
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.network.internal;
14
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;
27
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;
34
35 /**
36  * The {@link WakeOnLanPacketSender} broadcasts a magic packet to wake a device.
37  *
38  * @author Wouter Born - Initial contribution
39  */
40 @NonNullByDefault
41 public class WakeOnLanPacketSender {
42
43     private static final int WOL_UDP_PORT = 9;
44
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[] { ":", "-" };
51
52     private final Logger logger = LoggerFactory.getLogger(WakeOnLanPacketSender.class);
53
54     private final String macAddress;
55
56     private final @Nullable String hostname;
57     private final @Nullable Integer port;
58
59     private final Consumer<byte[]> magicPacketMacSender;
60     private final Consumer<byte[]> magicPacketIpSender;
61
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;
66         this.port = port;
67         this.magicPacketMacSender = this::sendMagicPacketViaMac;
68         this.magicPacketIpSender = this::sendMagicPacketViaIp;
69     }
70
71     /**
72      * Used for testing only.
73      */
74     public WakeOnLanPacketSender(String macAddress) {
75         logger.debug("initialized WOL Packet Sender (mac: {}", macAddress);
76         this.macAddress = macAddress;
77         this.hostname = null;
78         this.port = null;
79         this.magicPacketMacSender = this::sendMagicPacketViaMac;
80         this.magicPacketIpSender = this::sendMagicPacketViaIp;
81     }
82
83     /**
84      * Used for testing only.
85      */
86     WakeOnLanPacketSender(String macAddress, Consumer<byte[]> magicPacketSender) {
87         this.macAddress = macAddress;
88         this.hostname = null;
89         this.port = null;
90         this.magicPacketMacSender = magicPacketSender;
91         this.magicPacketIpSender = this::sendMagicPacketViaIp;
92     }
93
94     public void sendWakeOnLanPacketViaMac() {
95         byte[] magicPacket = createMagicPacket();
96         this.magicPacketMacSender.accept(magicPacket);
97     }
98
99     public void sendWakeOnLanPacketViaIp() {
100         byte[] magicPacket = createMagicPacket();
101         this.magicPacketIpSender.accept(magicPacket);
102     }
103
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);
110         }
111         return magicPacket;
112     }
113
114     private byte[] createMacBytes(String macAddress) {
115         String hexString = macAddress;
116         for (String macSeparator : MAC_SEPARATORS) {
117             hexString = hexString.replaceAll(macSeparator, "");
118         }
119         if (hexString.length() != 2 * MAC_BYTE_SIZE) {
120             throw new IllegalStateException("Invalid MAC address: " + macAddress);
121         }
122         return HexUtils.hexToBytes(hexString);
123     }
124
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);
131         }
132     }
133
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);
141             } else {
142                 throw new IllegalStateException("Hostname is not set!");
143             }
144         } catch (SocketException e) {
145             logger.error("Failed to open Wake-on-LAN datagram socket", e);
146         }
147     }
148
149     private void broadcastMagicPacket(byte[] magicPacket, DatagramSocket socket) {
150         broadcastAddressStream().forEach(broadcastAddress -> {
151             try {
152                 DatagramPacket packet = new DatagramPacket(magicPacket, MAGIC_PACKET_BYTE_SIZE, broadcastAddress,
153                         WOL_UDP_PORT);
154                 socket.send(packet);
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);
160             }
161         });
162         logger.info("Wake-on-LAN packets sent (MAC address: {})", this.macAddress);
163     }
164
165     private void sendMagicPacketToIp(byte[] magicPacket, DatagramSocket socket, SocketAddress ip) {
166         DatagramPacket packet = new DatagramPacket(magicPacket, MAGIC_PACKET_BYTE_SIZE, ip);
167         try {
168             socket.send(packet);
169         } catch (IOException e) {
170             logger.error("Failed to send Wake-on-LAN packet (IP address: {})", ip, e);
171         }
172         logger.info("Wake-on-LAN packets sent (IP address: {})", ip);
173     }
174
175     private Stream<InetAddress> broadcastAddressStream() {
176         return NetUtil.getAllBroadcastAddresses().stream().map(address -> {
177             try {
178                 return InetAddress.getByName(address);
179             } catch (UnknownHostException e) {
180                 logger.error("Failed to get broadcast address '{}' by name", address, e);
181                 return null;
182             }
183         }).filter(Objects::nonNull);
184     }
185 }