All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.joblo2213.sma.speedwire.Speedwire Maven / Gradle / Ivy

Go to download

SMA-Speedwire is an api for communicating with SMA products over Ethernet using the speedwire interface. It allows listening for exchanged data between your devices and logging meter readings.

There is a newer version: 1.0.2
Show newest version
package io.github.joblo2213.sma.speedwire;

import io.github.joblo2213.sma.speedwire.protocol.telegrams.DiscoveryResponse;
import io.github.joblo2213.sma.speedwire.protocol.telegrams.Telegram;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 

* Main class for interacting with speedwire devices. *


* All devices that use the speedwire protocol send data periodically via * UDP * Multicast, by default to group {@code 239.12.255.254} * on port {@code 9522}. *


* To join the multicast group and listen for incoming data call the {@code start()} method.
* The thread will read all incoming data and parse the telegrams.
* Register callbacks that receive the parsed telegrams from the thread by using {@code onData()}.
* Callbacks that handle occurring errors or timeouts can also be registered but aren't mandatory.
*


* Example: *

{@code
 * Speedwire sw = new Speedwire();
 * sw.onData(telegram -> {
 *     if (telegram instanceof EnergyMeterTelegram) {
 *         EnergyMeterTelegram em = (EnergyMeterTelegram) telegram;
 *
 *         //log current ingress power of energy meter
 *         System.out.println(em.getData(EnergyMeterChannels.TOTAL_P_IN));
 *     }
 * });
 * //print stacktrace for all occurring exceptions
 * sw.onError(e -> e.printStackTrace());
 * sw.start;
 * }
*

* To find all devices that support the speedwire protocol in your local network * send a discovery request using {@code sendDiscoveryRequest()}.
* Each device will answer with a {@link DiscoveryResponse} that you can listen for using the {@code onData()} callback. *

*/ public class Speedwire extends Thread { private static final String DEFAULT_GROUP = "239.12.255.254"; private static final int DEFAULT_PORT = 9522; private static final int TIMEOUT = 5000; private final InetAddress hostAddress; private final InetAddress multicastGroup; private final int port; private final CopyOnWriteArraySet callbacks = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet errorHandlers = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet timeoutHandlers = new CopyOnWriteArraySet<>(); private MulticastSocket socket; /** * Construct a new thread for exchanging data with speedwire devices in your local network.
* In most cases the default constructor ({@link #Speedwire()}) will better suit your needs. * * @param hostAddress the ip address of the host (the device you are running this program on) * in your local network * @param multicastGroup the ip address of the multicast group to which all speedwire data is send, * by default this is {@code 239.12.255.254} * @param port the iana registered udp port over which all speedwire data is send and received. * Default port is {@code 9522} * @throws IOException if one of the given addresses isn't a valid inet address or the {@code multicastGroup} * is not a multicast address. */ public Speedwire(String hostAddress, String multicastGroup, int port) throws IOException { this.hostAddress = InetAddress.getByName(hostAddress); this.multicastGroup = InetAddress.getByName(multicastGroup); this.port = port; if (!this.multicastGroup.isMulticastAddress()) throw new IOException("multicastGroup is not a multicast address"); } /** * Construct a new thread for exchanging data with speedwire devices in your local network.
* Automatically determines your host address and uses the default port ({@code 9522}) and the * default multicast group ({@code 239.12.255.254}). * * @throws IOException if your host address couldn't be determined. * Then use {@link #Speedwire(String, String, int)} instead. */ public Speedwire() throws IOException { this(getLocalAddress().getHostAddress(), DEFAULT_GROUP, DEFAULT_PORT); } /** * Try to determine your host address by opening a socket as {@link InetAddress#getLocalHost()} often returns the * wrong address if a device has multiple ethernet adapters. * * @return your host address * @throws IOException if the socket could not be opened and therefore the host address could not be determined */ private static InetAddress getLocalAddress() throws IOException { if (System.getProperty("os.name").contains("mac")) { try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress("google.com", 80)); return socket.getLocalAddress(); } catch (IOException e) { throw new IOException("Could not automatically determine your host address. Please specify it.", e); } } else { try (final DatagramSocket socket = new DatagramSocket()) { socket.connect(InetAddress.getByName("8.8.8.8"), 10002); return socket.getLocalAddress(); } catch (IOException e) { throw new IOException("Could not automatically determine your host address. Please specify it.", e); } } } /** *

* Register a callback that is run when a new telegram is received.
* Multiple callbacks may be registered. * Callbacks are allowed to be registered while the speedwire thread is running, although it is advised to register * them before calling {@code start()}.
* The callback will be run on the speedwire thread so avoid blocking or very slow operations. *


* Note:
* Telegrams send from your host address will be redirected back by the multicast group and are therefore * filtered out. They will not trigger this callback. *

* * @param callback callback that listens for all incoming telegrams */ public void onData(SpeedwireCallback callback) { callbacks.add(callback); } /** * Register a callback that is run whenever an error occurs while reading or parsing incoming data
* Multiple error handlers may be registered, registering new error handlers while the thread is running is possible. * * @param errorHandler errorHandler that listens for all occurring exceptions while receiving or parsing incoming data */ public void onError(SpeedwireErrorHandler errorHandler) { errorHandlers.add(errorHandler); } /** * Register a callback that is run when no data is received for over 5 seconds.
* As most known speedwire devices send data every second this indicates a network error. * * @param timeoutHandler runnable that is run on timeout */ public void onTimeout(Runnable timeoutHandler) { timeoutHandlers.add(timeoutHandler); } /** * Send any packet of bytes to the multicast group.
* Please make sure the send data does not violate the speedwire protocol as this isn't checked. * * @param packet an array of bytes that will be send to all devices that joined the multicast group. */ public void send(byte[] packet) { try { DatagramPacket pkt = new DatagramPacket(packet, packet.length, multicastGroup, port); socket.send(pkt); } catch (IOException e) { errorHandlers.forEach(h -> h.onError(e)); } } /** *

* Sends a discovery request to all speedwire devices in the multicast group.
* Every speedwire device will answer this request with a {@link DiscoveryResponse} for which you can listen * using {@code onData()}.
* This allows detecting all unknown devices and determining their ip addresses by using {@link Telegram#getOrigin()}. *


* More information on speedwire device discovery can be found in * * SpeedwireDD-TI-en-10.pdf * . *

*/ public void sendDiscoveryRequest() { byte[] discoveryRequest = new byte[]{ (byte) 0x53, (byte) 0x4d, (byte) 0x41, (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x02, (byte) 0xa0, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; send(discoveryRequest); } /** * Start the speedwire thread.
* This will cause your device to open a socket, join the multicast group and listen for incoming data.
* If for some reason, like network issues, the socket can't be opened the io exception will be logged. */ @Override public synchronized void start() { try { socket = new MulticastSocket(port); socket.setInterface(getLocalAddress()); socket.setReuseAddress(true); socket.joinGroup(multicastGroup); socket.setSoTimeout(TIMEOUT); super.start(); } catch (IOException e) { System.err.println("Could not start Speedwire: " + e.getMessage()); e.printStackTrace(); if (socket != null) socket.close(); } } /** * Don't touch this! */ @Override public void run() { if (socket == null) { throw new RuntimeException("Multicast socket isn't started"); } while (!interrupted()) { try { //receive and decode incoming packets DatagramPacket packet = new DatagramPacket(new byte[8192], 8192); socket.receive(packet); final Telegram telegram = Telegram.from(packet); //Ignore own packets as multicast will also redirect them back to the sender if (telegram.getOrigin().equals(hostAddress)) continue; callbacks.forEach(h -> h.onDataReceived(telegram)); } catch (SocketTimeoutException e) { timeoutHandlers.forEach(Runnable::run); } catch (Exception e) { errorHandlers.forEach(h -> h.onError(e)); } } socket.close(); } /** * Call this method to gracefully shut down the speedwire thread to stop listening for incoming data * and close the socket.
* This method does not await the termination of the thread. */ public void shutdown() { interrupt(); } /** * Returns the host address used for joining the multicast group (your local ip address) */ public InetAddress getHostAddress() { return hostAddress; } /** * Returns the ip address of the multicast group (by default {@code 239.12.255.254}) */ public InetAddress getMulticastGroup() { return multicastGroup; } /** * Returns the udp port used for communication (by default {@code 9522}) */ public int getPort() { return port; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy