[smaenergymeter] Fix handling of broadcast frames (#11718)

* 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>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Łukasz Dywicki 2024-05-10 17:25:48 +02:00 committed by Ciprian Pascu
parent 1e63944801
commit 6e14bfd750
12 changed files with 468 additions and 120 deletions

View 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 |

View 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;

View File

@ -12,18 +12,23 @@
*/
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;
}
}

View 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
protected void stopBackgroundDiscovery() {
PacketListener packetListener = this.packetListener;
if (packetListener != null) {
packetListener.removePayloadHandler(this);
this.packetListener = null;
}
}
@Override
public void startScan() {
logger.debug("Start SMAEnergyMeter scan");
discover();
}
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;
}
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);

View File

@ -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;
@ -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());
}

View 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());
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View 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

View File

@ -32,6 +32,10 @@
</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>