Skip to content

Commit

Permalink
[smaenergymeter] Fix handling of broadcast frames (openhab#11718)
Browse files Browse the repository at this point in the history
* Fix handling of broadcast frames for SMA meter openhab#11497.

Added support for multiple meters in single multicast group openhab#3429.

Signed-off-by: Łukasz Dywicki <luke@code-house.org>
Co-authored-by: Leo Siepel <leosiepel@gmail.com>
  • Loading branch information
2 people authored and PRosenb committed May 22, 2024
1 parent 6d37039 commit 9cbbd46
Show file tree
Hide file tree
Showing 12 changed files with 468 additions and 120 deletions.
9 changes: 9 additions & 0 deletions bundles/org.openhab.binding.smaenergymeter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
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
Expand All @@ -31,6 +34,13 @@
@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);
Expand All @@ -41,7 +51,7 @@ protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (thingTypeUID.equals(THING_TYPE_ENERGY_METER)) {
return new SMAEnergyMeterHandler(thing);
return new SMAEnergyMeterHandler(thing, packetListenerRegistry);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,51 @@
*/
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;
}

public void setMcastGroup(String 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@
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.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;

Expand All @@ -38,13 +44,18 @@
*
* @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
Expand All @@ -54,35 +65,49 @@ public Set<ThingTypeUID> getSupportedThingTypes() {

@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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@
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;

Expand All @@ -27,15 +23,14 @@
* 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;
Expand All @@ -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);
Expand All @@ -81,23 +70,20 @@ public EnergyMeter(String multicastGroup, int port) {
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);
Expand All @@ -118,8 +104,6 @@ public void update() throws IOException {
energyInL3.updateValue(bytes);
powerOutL3.updateValue(bytes);
energyOutL3.updateValue(bytes);

lastUpdate = new Date(System.currentTimeMillis());
} catch (Exception e) {
throw new IOException(e);
}
Expand All @@ -129,10 +113,6 @@ public String getSerialNumber() {
return serialNumber;
}

public Date getLastUpdate() {
return lastUpdate;
}

public DecimalType getPowerIn() {
return new DecimalType(powerIn.getValue());
}
Expand Down
Loading

0 comments on commit 9cbbd46

Please sign in to comment.