mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[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>
This commit is contained in:
parent
fc68df7a6e
commit
3c0eb946f1
@ -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 |
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user