]> git.basschouten.com Git - openhab-addons.git/commitdiff
[smaenergymeter] Fix handling of broadcast frames (#11718)
authorŁukasz Dywicki <luke@code-house.org>
Fri, 10 May 2024 15:25:48 +0000 (17:25 +0200)
committerGitHub <noreply@github.com>
Fri, 10 May 2024 15:25:48 +0000 (17:25 +0200)
* 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>
12 files changed:
bundles/org.openhab.binding.smaenergymeter/README.md
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/SMAEnergyMeterHandlerFactory.java
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/configuration/EnergyMeterConfig.java
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/discovery/SMAEnergyMeterDiscoveryService.java
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/EnergyMeter.java
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/SMAEnergyMeterHandler.java
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/DefaultPacketListenerRegistry.java [new file with mode: 0644]
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListener.java [new file with mode: 0644]
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListenerRegistry.java [new file with mode: 0644]
bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PayloadHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.smaenergymeter/src/main/resources/OH-INF/i18n/smaenergymeter.properties
bundles/org.openhab.binding.smaenergymeter/src/main/resources/OH-INF/thing/energyMeter.xml

index c85c0a1e3391793bab351f899ddf781167ef6f48..8f6be8db0d0809e4f393bb13de5907ed252fab3e 100644 (file)
@@ -20,6 +20,15 @@ No binding configuration required.
 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            |
index 77a0b0596c4c41e387a34b188fda218818bb5f2d..a5d96570487d22b907e1f329975ab2233e4d9497 100644 (file)
@@ -15,12 +15,15 @@ package org.openhab.binding.smaenergymeter.internal;
 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
@@ -31,6 +34,13 @@ import org.osgi.service.component.annotations.Component;
 @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);
@@ -41,7 +51,7 @@ public class SMAEnergyMeterHandlerFactory extends BaseThingHandlerFactory {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (thingTypeUID.equals(THING_TYPE_ENERGY_METER)) {
-            return new SMAEnergyMeterHandler(thing);
+            return new SMAEnergyMeterHandler(thing, packetListenerRegistry);
         }
 
         return null;
index 08c989e559cf9f5504ef215c95c60140b75f85de..b29963c86fd0c5174290ba8fba4d3abdd4dc7a9a 100644 (file)
  */
 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;
     }
 
@@ -31,19 +36,27 @@ public class EnergyMeterConfig {
         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;
+    }
 }
index 161a1483ce5400ed0fc57effa1555464c5e2d203..e959f0466461b3eace697d102d70f5c81936dc80 100644 (file)
@@ -18,9 +18,13 @@ import java.io.IOException;
 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;
@@ -28,7 +32,9 @@ import org.openhab.core.config.discovery.DiscoveryService;
 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;
 
@@ -38,13 +44,18 @@ 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
@@ -54,35 +65,49 @@ public class SMAEnergyMeterDiscoveryService extends AbstractDiscoveryService {
 
     @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);
index f7450c52c0884bb2ff003be30ea0680cecf48fb6..91dea24a86c369765c2bac62203b0a8552e89275 100644 (file)
 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;
 
@@ -27,15 +23,14 @@ 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;
@@ -53,13 +48,7 @@ public class EnergyMeter {
     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);
@@ -81,23 +70,20 @@ public class EnergyMeter {
         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);
@@ -118,8 +104,6 @@ public class EnergyMeter {
             energyInL3.updateValue(bytes);
             powerOutL3.updateValue(bytes);
             energyOutL3.updateValue(bytes);
-
-            lastUpdate = new Date(System.currentTimeMillis());
         } catch (Exception e) {
             throw new IOException(e);
         }
@@ -129,10 +113,6 @@ public class EnergyMeter {
         return serialNumber;
     }
 
-    public Date getLastUpdate() {
-        return lastUpdate;
-    }
-
     public DecimalType getPowerIn() {
         return new DecimalType(powerIn.getValue());
     }
index 651dccea7d27b888803a737a2e193b9171cdd1e8..6308a4bf16a95be092caa917206236ee1911bf78 100644 (file)
@@ -15,10 +15,12 @@ package org.openhab.binding.smaenergymeter.internal.handler;
 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;
@@ -35,21 +37,26 @@ import org.slf4j.LoggerFactory;
  *
  * @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");
         }
@@ -61,68 +68,72 @@ public class SMAEnergyMeterHandler extends BaseThingHandler {
 
         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());
     }
 }
diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/DefaultPacketListenerRegistry.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/DefaultPacketListenerRegistry.java
new file mode 100644 (file)
index 0000000..a479d6a
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * 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);
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListener.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListener.java
new file mode 100644 (file)
index 0000000..976c617
--- /dev/null
@@ -0,0 +1,146 @@
+/**
+ * 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);
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListenerRegistry.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListenerRegistry.java
new file mode 100644 (file)
index 0000000..c2617e2
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * 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;
+}
diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PayloadHandler.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PayloadHandler.java
new file mode 100644 (file)
index 0000000..f6cce92
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * 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;
+}
index 12f9ef6c93c35a77e24c2e0f9fc5c22e1d6e6111..ef86d58a37e233d89cf3845bc7120dd3417bb7b3 100644 (file)
@@ -15,6 +15,8 @@ thing-type.config.smaenergymeter.energymeter.pollingPeriod.label = Polling Perio
 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
 
index 8bf381bee85fefbfab6d505ad5bde13e6861bc3f..2aea72349026f3168e730dba52005b8291a96b96 100644 (file)
                </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>