* Fix handling of broadcast frames for SMA meter #11497.
Added support for multiple meters in single multicast group #3429.
Signed-off-by: Łukasz Dywicki <luke@code-house.org>
Co-authored-by: Leo Siepel <leosiepel@gmail.com>
Usually no manual configuration is required, as the multicast IP address and the port remain on their factory set values.
Optionally, a refresh interval (in seconds) can be defined.
+| Parameter | Name | Description | Required | Default |
+|------------------|-----------------|---------------------------------------|----------|-----------------|
+| `serialNumber` | Serial number | Serial number of a meter. | yes | |
+| `mcastGroup` | Multicast Group | Multicast group used by meter. | yes | 239.12.255.254 |
+| `port` | Port | Port number used by meter. | no | 9522 |
+| `pollingPeriod` | Polling Period | Polling period used to readout meter. | no | 30 |
+
+The polling period parameter is used to trigger readout of meter. In case if two consecutive readout attempts fail thing will report offline status.
+
## Channels
| Channel | Description |
import static org.openhab.binding.smaenergymeter.internal.SMAEnergyMeterBindingConstants.*;
import org.openhab.binding.smaenergymeter.internal.handler.SMAEnergyMeterHandler;
+import org.openhab.binding.smaenergymeter.internal.packet.PacketListenerRegistry;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
/**
* The {@link SMAEnergyMeterHandlerFactory} is responsible for creating things and thing
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.smaenergymeter")
public class SMAEnergyMeterHandlerFactory extends BaseThingHandlerFactory {
+ private final PacketListenerRegistry packetListenerRegistry;
+
+ @Activate
+ public SMAEnergyMeterHandlerFactory(@Reference PacketListenerRegistry packetListenerRegistry) {
+ this.packetListenerRegistry = packetListenerRegistry;
+ }
+
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_ENERGY_METER)) {
- return new SMAEnergyMeterHandler(thing);
+ return new SMAEnergyMeterHandler(thing, packetListenerRegistry);
}
return null;
*/
package org.openhab.binding.smaenergymeter.internal.configuration;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
/**
* The {@link EnergyMeterConfig} class holds the configuration properties of the binding.
*
* @author Osman Basha - Initial contribution
*/
+@NonNullByDefault
public class EnergyMeterConfig {
- private String mcastGroup;
- private Integer port;
- private Integer pollingPeriod;
+ private @Nullable String mcastGroup;
+ private int port = 9522;
+ private int pollingPeriod = 30;
+ private @Nullable String serialNumber;
- public String getMcastGroup() {
+ public @Nullable String getMcastGroup() {
return mcastGroup;
}
this.mcastGroup = mcastGroup;
}
- public Integer getPort() {
+ public int getPort() {
return port;
}
- public void setPort(Integer port) {
+ public void setPort(int port) {
this.port = port;
}
- public Integer getPollingPeriod() {
+ public int getPollingPeriod() {
return pollingPeriod;
}
- public void setPollingPeriod(Integer pollingPeriod) {
+ public void setPollingPeriod(int pollingPeriod) {
this.pollingPeriod = pollingPeriod;
}
+
+ public @Nullable String getSerialNumber() {
+ return serialNumber;
+ }
+
+ public void setSerialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ }
}
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smaenergymeter.internal.handler.EnergyMeter;
+import org.openhab.binding.smaenergymeter.internal.packet.PacketListener;
+import org.openhab.binding.smaenergymeter.internal.packet.PacketListenerRegistry;
+import org.openhab.binding.smaenergymeter.internal.packet.PayloadHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
*
* @author Osman Basha - Initial contribution
*/
+@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.smaenergymeter")
-public class SMAEnergyMeterDiscoveryService extends AbstractDiscoveryService {
+public class SMAEnergyMeterDiscoveryService extends AbstractDiscoveryService implements PayloadHandler {
private final Logger logger = LoggerFactory.getLogger(SMAEnergyMeterDiscoveryService.class);
+ private final PacketListenerRegistry listenerRegistry;
+ private @Nullable PacketListener packetListener;
- public SMAEnergyMeterDiscoveryService() {
+ @Activate
+ public SMAEnergyMeterDiscoveryService(@Reference PacketListenerRegistry listenerRegistry) {
super(SUPPORTED_THING_TYPES_UIDS, 15, true);
+ this.listenerRegistry = listenerRegistry;
}
@Override
@Override
protected void startBackgroundDiscovery() {
+ PacketListener packetListener = this.packetListener;
+ if (packetListener != null) {
+ return;
+ }
+
logger.debug("Start SMAEnergyMeter background discovery");
- scheduler.schedule(this::discover, 0, TimeUnit.SECONDS);
+ try {
+ packetListener = listenerRegistry.getListener(PacketListener.DEFAULT_MCAST_GRP,
+ PacketListener.DEFAULT_MCAST_PORT);
+ packetListener.open(30);
+ } catch (IOException e) {
+ logger.warn("Could not start background discovery", e);
+ return;
+ }
+
+ packetListener.addPayloadHandler(this);
+ this.packetListener = packetListener;
}
@Override
- public void startScan() {
- logger.debug("Start SMAEnergyMeter scan");
- discover();
+ protected void stopBackgroundDiscovery() {
+ PacketListener packetListener = this.packetListener;
+ if (packetListener != null) {
+ packetListener.removePayloadHandler(this);
+ this.packetListener = null;
+ }
}
- private synchronized void discover() {
- logger.debug("Try to discover a SMA Energy Meter device");
-
- EnergyMeter energyMeter = new EnergyMeter(EnergyMeter.DEFAULT_MCAST_GRP, EnergyMeter.DEFAULT_MCAST_PORT);
- try {
- energyMeter.update();
- } catch (IOException e) {
- logger.debug("No SMA Energy Meter found.");
- logger.debug("Diagnostic: ", e);
- return;
- }
+ @Override
+ public void startScan() {
+ }
- logger.debug("Adding a new SMA Engergy Meter with S/N '{}' to inbox", energyMeter.getSerialNumber());
+ @Override
+ public void handle(EnergyMeter energyMeter) throws IOException {
+ String identifier = energyMeter.getSerialNumber();
+ logger.debug("Adding a new SMA Energy Meter with S/N '{}' to inbox", identifier);
Map<String, Object> properties = new HashMap<>();
properties.put(Thing.PROPERTY_VENDOR, "SMA");
- properties.put(Thing.PROPERTY_SERIAL_NUMBER, energyMeter.getSerialNumber());
- ThingUID uid = new ThingUID(THING_TYPE_ENERGY_METER, energyMeter.getSerialNumber());
+ properties.put(Thing.PROPERTY_SERIAL_NUMBER, identifier);
+ ThingUID uid = new ThingUID(THING_TYPE_ENERGY_METER, identifier);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
- .withLabel("SMA Energy Meter").build();
+ .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).withLabel("SMA Energy Meter #" + identifier)
+ .build();
thingDiscovered(result);
logger.debug("Thing discovered '{}'", result);
package org.openhab.binding.smaenergymeter.internal.handler;
import java.io.IOException;
-import java.net.DatagramPacket;
-import java.net.InetAddress;
-import java.net.MulticastSocket;
import java.nio.ByteBuffer;
import java.util.Arrays;
-import java.util.Date;
import org.openhab.core.library.types.DecimalType;
* and extracting the data fields out of the received telegrams.
*
* @author Osman Basha - Initial contribution
+ * @author Łukasz Dywicki - Extracted multicast group handling to
+ * {@link org.openhab.binding.smaenergymeter.internal.packet.PacketListener}.
*/
public class EnergyMeter {
- private String multicastGroup;
- private int port;
+ private static final byte[] E_METER_PROTOCOL_ID = new byte[] { 0x60, 0x69 };
private String serialNumber;
- private Date lastUpdate;
-
private final FieldDTO powerIn;
private final FieldDTO energyIn;
private final FieldDTO powerOut;
private final FieldDTO powerOutL3;
private final FieldDTO energyOutL3;
- public static final String DEFAULT_MCAST_GRP = "239.12.255.254";
- public static final int DEFAULT_MCAST_PORT = 9522;
-
- public EnergyMeter(String multicastGroup, int port) {
- this.multicastGroup = multicastGroup;
- this.port = port;
-
+ public EnergyMeter() {
powerIn = new FieldDTO(0x20, 4, 10);
energyIn = new FieldDTO(0x28, 8, 3600000);
powerOut = new FieldDTO(0x34, 4, 10);
energyOutL3 = new FieldDTO(0x1E4, 8, 3600000); // +8
}
- public void update() throws IOException {
- byte[] bytes = new byte[608];
- try (MulticastSocket socket = new MulticastSocket(port)) {
- socket.setSoTimeout(5000);
- InetAddress address = InetAddress.getByName(multicastGroup);
- socket.joinGroup(address);
-
- DatagramPacket msgPacket = new DatagramPacket(bytes, bytes.length);
- socket.receive(msgPacket);
-
- String sma = new String(Arrays.copyOfRange(bytes, 0x00, 0x03));
+ public void parse(byte[] bytes) throws IOException {
+ try {
+ String sma = new String(Arrays.copyOfRange(bytes, 0, 3));
if (!"SMA".equals(sma)) {
throw new IOException("Not a SMA telegram." + sma);
}
+ byte[] protocolId = Arrays.copyOfRange(bytes, 16, 18);
+ if (!Arrays.equals(protocolId, E_METER_PROTOCOL_ID)) {
+ throw new IllegalArgumentException(
+ "Received frame with wrong protocol ID " + Arrays.toString(protocolId));
+ }
ByteBuffer buffer = ByteBuffer.wrap(Arrays.copyOfRange(bytes, 0x14, 0x18));
- serialNumber = String.valueOf(buffer.getInt());
+ serialNumber = Integer.toHexString(buffer.getInt());
powerIn.updateValue(bytes);
energyIn.updateValue(bytes);
energyInL3.updateValue(bytes);
powerOutL3.updateValue(bytes);
energyOutL3.updateValue(bytes);
-
- lastUpdate = new Date(System.currentTimeMillis());
} catch (Exception e) {
throw new IOException(e);
}
return serialNumber;
}
- public Date getLastUpdate() {
- return lastUpdate;
- }
-
public DecimalType getPowerIn() {
return new DecimalType(powerIn.getValue());
}
import static org.openhab.binding.smaenergymeter.internal.SMAEnergyMeterBindingConstants.*;
import java.io.IOException;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smaenergymeter.internal.configuration.EnergyMeterConfig;
+import org.openhab.binding.smaenergymeter.internal.packet.PacketListener;
+import org.openhab.binding.smaenergymeter.internal.packet.PacketListenerRegistry;
+import org.openhab.binding.smaenergymeter.internal.packet.PayloadHandler;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
*
* @author Osman Basha - Initial contribution
*/
-public class SMAEnergyMeterHandler extends BaseThingHandler {
+public class SMAEnergyMeterHandler extends BaseThingHandler implements PayloadHandler {
private final Logger logger = LoggerFactory.getLogger(SMAEnergyMeterHandler.class);
- private EnergyMeter energyMeter;
- private ScheduledFuture<?> pollingJob;
+ private final PacketListenerRegistry listenerRegistry;
+ private @Nullable PacketListener listener;
+ private @Nullable String serialNumber;
- public SMAEnergyMeterHandler(Thing thing) {
+ public SMAEnergyMeterHandler(Thing thing, PacketListenerRegistry listenerRegistry) {
super(thing);
+ this.listenerRegistry = listenerRegistry;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
logger.debug("Refreshing {}", channelUID);
- updateData();
+ PacketListener listener = this.listener;
+ if (listener != null) {
+ listener.request();
+ }
} else {
logger.warn("This binding is a read-only binding and cannot handle commands");
}
EnergyMeterConfig config = getConfigAs(EnergyMeterConfig.class);
- int port = (config.getPort() == null) ? EnergyMeter.DEFAULT_MCAST_PORT : config.getPort();
- energyMeter = new EnergyMeter(config.getMcastGroup(), port);
try {
- energyMeter.update();
+ serialNumber = config.getSerialNumber();
+ if (serialNumber == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ "Meter serial number missing");
+ return;
+ }
+ String mcastGroup = config.getMcastGroup();
+ if (mcastGroup == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "mcast group is missing");
+ return;
+ }
+ PacketListener listener = listenerRegistry.getListener(mcastGroup, config.getPort());
+ updateStatus(ThingStatus.UNKNOWN);
+ logger.debug("Activated handler for SMA Energy Meter with S/N '{}'", serialNumber);
- updateProperty(Thing.PROPERTY_VENDOR, "SMA");
- updateProperty(Thing.PROPERTY_SERIAL_NUMBER, energyMeter.getSerialNumber());
- logger.debug("Found a SMA Energy Meter with S/N '{}'", energyMeter.getSerialNumber());
+ listener.addPayloadHandler(this);
+
+ listener.open(config.getPollingPeriod());
+ this.listener = listener;
+ logger.debug("Polling job scheduled to run every {} sec. for '{}'", config.getPollingPeriod(),
+ getThing().getUID());
+ // we do not set online status here, it will be set only when data is received
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
- return;
}
-
- int pollingPeriod = (config.getPollingPeriod() == null) ? 30 : config.getPollingPeriod();
- pollingJob = scheduler.scheduleWithFixedDelay(this::updateData, 0, pollingPeriod, TimeUnit.SECONDS);
- logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, getThing().getUID());
-
- updateStatus(ThingStatus.ONLINE);
}
@Override
public void dispose() {
logger.debug("Disposing SMAEnergyMeter handler '{}'", getThing().getUID());
-
- if (pollingJob != null) {
- pollingJob.cancel(true);
- pollingJob = null;
+ PacketListener listener = this.listener;
+ if (listener != null) {
+ listener.removePayloadHandler(this);
+ this.listener = null;
}
- energyMeter = null;
}
- private synchronized void updateData() {
- logger.debug("Update SMAEnergyMeter data '{}'", getThing().getUID());
-
- try {
- energyMeter.update();
-
- updateState(CHANNEL_POWER_IN, energyMeter.getPowerIn());
- updateState(CHANNEL_POWER_OUT, energyMeter.getPowerOut());
- updateState(CHANNEL_ENERGY_IN, energyMeter.getEnergyIn());
- updateState(CHANNEL_ENERGY_OUT, energyMeter.getEnergyOut());
-
- updateState(CHANNEL_POWER_IN_L1, energyMeter.getPowerInL1());
- updateState(CHANNEL_POWER_OUT_L1, energyMeter.getPowerOutL1());
- updateState(CHANNEL_ENERGY_IN_L1, energyMeter.getEnergyInL1());
- updateState(CHANNEL_ENERGY_OUT_L1, energyMeter.getEnergyOutL1());
-
- updateState(CHANNEL_POWER_IN_L2, energyMeter.getPowerInL2());
- updateState(CHANNEL_POWER_OUT_L2, energyMeter.getPowerOutL2());
- updateState(CHANNEL_ENERGY_IN_L2, energyMeter.getEnergyInL2());
- updateState(CHANNEL_ENERGY_OUT_L2, energyMeter.getEnergyOutL2());
-
- updateState(CHANNEL_POWER_IN_L3, energyMeter.getPowerInL3());
- updateState(CHANNEL_POWER_OUT_L3, energyMeter.getPowerOutL3());
- updateState(CHANNEL_ENERGY_IN_L3, energyMeter.getEnergyInL3());
- updateState(CHANNEL_ENERGY_OUT_L3, energyMeter.getEnergyOutL3());
-
- if (getThing().getStatus().equals(ThingStatus.OFFLINE)) {
- updateStatus(ThingStatus.ONLINE);
- }
- } catch (IOException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ @Override
+ public void handle(EnergyMeter energyMeter) {
+ String serialNumber = this.serialNumber;
+ if (serialNumber == null || !serialNumber.equals(energyMeter.getSerialNumber())) {
+ return;
}
+ updateStatus(ThingStatus.ONLINE);
+
+ logger.debug("Update SMAEnergyMeter {} data '{}'", serialNumber, getThing().getUID());
+
+ updateState(CHANNEL_POWER_IN, energyMeter.getPowerIn());
+ updateState(CHANNEL_POWER_OUT, energyMeter.getPowerOut());
+ updateState(CHANNEL_ENERGY_IN, energyMeter.getEnergyIn());
+ updateState(CHANNEL_ENERGY_OUT, energyMeter.getEnergyOut());
+
+ updateState(CHANNEL_POWER_IN_L1, energyMeter.getPowerInL1());
+ updateState(CHANNEL_POWER_OUT_L1, energyMeter.getPowerOutL1());
+ updateState(CHANNEL_ENERGY_IN_L1, energyMeter.getEnergyInL1());
+ updateState(CHANNEL_ENERGY_OUT_L1, energyMeter.getEnergyOutL1());
+
+ updateState(CHANNEL_POWER_IN_L2, energyMeter.getPowerInL2());
+ updateState(CHANNEL_POWER_OUT_L2, energyMeter.getPowerOutL2());
+ updateState(CHANNEL_ENERGY_IN_L2, energyMeter.getEnergyInL2());
+ updateState(CHANNEL_ENERGY_OUT_L2, energyMeter.getEnergyOutL2());
+
+ updateState(CHANNEL_POWER_IN_L3, energyMeter.getPowerInL3());
+ updateState(CHANNEL_POWER_OUT_L3, energyMeter.getPowerOutL3());
+ updateState(CHANNEL_ENERGY_IN_L3, energyMeter.getEnergyInL3());
+ updateState(CHANNEL_ENERGY_OUT_L3, energyMeter.getEnergyOutL3());
}
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 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.smaenergymeter.internal.packet;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.smaenergymeter.internal.SMAEnergyMeterBindingConstants;
+import org.openhab.binding.smaenergymeter.internal.packet.PacketListener.ReceivingTask;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of packet listener registry which manage multicast sockets.
+ *
+ * @author Łukasz Dywicki - Initial contribution
+ */
+
+@NonNullByDefault
+@Component
+public class DefaultPacketListenerRegistry implements PacketListenerRegistry {
+
+ private final Logger logger = LoggerFactory.getLogger(DefaultPacketListenerRegistry.class);
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
+ (runnable) -> new Thread(runnable,
+ "OH-binding-" + SMAEnergyMeterBindingConstants.BINDING_ID + "-listener"));
+ private final Map<String, PacketListener> listeners = new ConcurrentHashMap<>();
+
+ @Override
+ public PacketListener getListener(String group, int port) throws IOException {
+ String identifier = group + ":" + port;
+ PacketListener listener = listeners.get(identifier);
+ if (listener == null) {
+ listener = new PacketListener(this, group, port);
+ listeners.put(identifier, listener);
+ }
+ return listener;
+ }
+
+ @Deactivate
+ protected void shutdown() throws IOException {
+ for (Entry<String, PacketListener> entry : listeners.entrySet()) {
+ try {
+ entry.getValue().close();
+ } catch (IOException e) {
+ logger.warn("Multicast socket {} failed to terminate", entry.getKey(), e);
+ }
+ }
+ scheduler.shutdownNow();
+ }
+
+ public ScheduledFuture<?> addTask(Runnable runnable, int intervalSec) {
+ return scheduler.scheduleWithFixedDelay(runnable, 0, intervalSec, TimeUnit.SECONDS);
+ }
+
+ public void execute(ReceivingTask receivingTask) {
+ scheduler.execute(receivingTask);
+ }
+
+ public void close(String group, int port) {
+ String listenerId = group + ":" + port;
+ PacketListener listener = listeners.remove(listenerId);
+ if (listener != null) {
+ try {
+ listener.close();
+ } catch (IOException e) {
+ logger.warn("Multicast socket {} failed to terminate", listenerId, e);
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 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.smaenergymeter.internal.packet;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ScheduledFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.smaenergymeter.internal.handler.EnergyMeter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PacketListener} class is responsible for communication with the SMA devices.
+ * It handles udp/multicast traffic and broadcast received data to subsequent payload handlers.
+ *
+ * @author Łukasz Dywicki - Initial contribution
+ */
+
+@NonNullByDefault
+public class PacketListener {
+
+ private final DefaultPacketListenerRegistry registry;
+ private final List<PayloadHandler> handlers = new CopyOnWriteArrayList<>();
+
+ private String multicastGroup;
+ private int port;
+
+ public static final String DEFAULT_MCAST_GRP = "239.12.255.254";
+ public static final int DEFAULT_MCAST_PORT = 9522;
+
+ private @Nullable MulticastSocket socket;
+ private @Nullable ScheduledFuture<?> future;
+
+ public PacketListener(DefaultPacketListenerRegistry registry, String multicastGroup, int port) {
+ this.registry = registry;
+ this.multicastGroup = multicastGroup;
+ this.port = port;
+ }
+
+ public void addPayloadHandler(PayloadHandler handler) {
+ handlers.add(handler);
+ }
+
+ public void removePayloadHandler(PayloadHandler handler) {
+ handlers.remove(handler);
+
+ if (handlers.isEmpty()) {
+ registry.close(multicastGroup, port);
+ }
+ }
+
+ public boolean isOpen() {
+ MulticastSocket socket = this.socket;
+ return socket != null && socket.isConnected();
+ }
+
+ public void open(int intervalSec) throws IOException {
+ if (isOpen()) {
+ // no need to bind socket second time
+ return;
+ }
+ MulticastSocket socket = new MulticastSocket(port);
+ socket.setSoTimeout(5000);
+ InetAddress address = InetAddress.getByName(multicastGroup);
+ socket.joinGroup(address);
+
+ future = registry.addTask(new ReceivingTask(socket, multicastGroup + ":" + port, handlers), intervalSec);
+ this.socket = socket;
+ }
+
+ void close() throws IOException {
+ ScheduledFuture<?> future = this.future;
+ if (future != null) {
+ future.cancel(true);
+ this.future = null;
+ }
+
+ InetAddress address = InetAddress.getByName(multicastGroup);
+ MulticastSocket socket = this.socket;
+ if (socket != null) {
+ socket.leaveGroup(address);
+ socket.close();
+ this.socket = null;
+ }
+ }
+
+ public void request() {
+ MulticastSocket socket = this.socket;
+ if (socket != null) {
+ registry.execute(new ReceivingTask(socket, multicastGroup + ":" + port, handlers));
+ }
+ }
+
+ static class ReceivingTask implements Runnable {
+ private final Logger logger = LoggerFactory.getLogger(ReceivingTask.class);
+ private final DatagramSocket socket;
+ private final String group;
+ private final List<PayloadHandler> handlers;
+
+ ReceivingTask(DatagramSocket socket, String group, List<PayloadHandler> handlers) {
+ this.socket = socket;
+ this.group = group;
+ this.handlers = handlers;
+ }
+
+ public void run() {
+ try {
+ byte[] bytes = new byte[608];
+ DatagramPacket msgPacket = new DatagramPacket(bytes, bytes.length);
+ DatagramSocket socket = this.socket;
+ socket.receive(msgPacket);
+
+ try {
+ EnergyMeter meter = new EnergyMeter();
+ meter.parse(bytes);
+
+ for (PayloadHandler handler : handlers) {
+ handler.handle(meter);
+ }
+ } catch (IOException e) {
+ logger.debug("Unexpected payload received for group {}", group, e);
+ }
+ } catch (IOException e) {
+ logger.warn("Failed to receive data for multicast group {}", group, e);
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 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.smaenergymeter.internal.packet;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Definition of packet listener registry - a central place to track all registered sockets and
+ * multicast groups.
+ *
+ * @author Łukasz Dywicki - Initial contribution
+ */
+@NonNullByDefault
+public interface PacketListenerRegistry {
+
+ PacketListener getListener(String group, int port) throws IOException;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 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.smaenergymeter.internal.packet;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.smaenergymeter.internal.handler.EnergyMeter;
+
+/**
+ * Definition of data recipient.
+ *
+ * @author Łukasz Dywicki - Initial contribution
+ */
+@NonNullByDefault
+public interface PayloadHandler {
+
+ void handle(EnergyMeter energyMeter) throws IOException;
+}
thing-type.config.smaenergymeter.energymeter.pollingPeriod.description = Polling period for refreshing the data in s
thing-type.config.smaenergymeter.energymeter.port.label = Port
thing-type.config.smaenergymeter.energymeter.port.description = Port of the multicast group
+thing-type.config.smaenergymeter.energymeter.serialNumber.label = Serial number
+thing-type.config.smaenergymeter.energymeter.serialNumber.description = Identifier of meter
# channel types
</properties>
<config-description>
+ <parameter name="serialNumber" type="text" required="true">
+ <label>Serial number</label>
+ <description>Identifier of meter </description>
+ </parameter>
<parameter name="mcastGroup" type="text" required="true">
<label>Multicast Group</label>
<description>IP address of the multicast group</description>