diff --git a/bundles/org.openhab.binding.smaenergymeter/README.md b/bundles/org.openhab.binding.smaenergymeter/README.md index c85c0a1e33917..8f6be8db0d080 100644 --- a/bundles/org.openhab.binding.smaenergymeter/README.md +++ b/bundles/org.openhab.binding.smaenergymeter/README.md @@ -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 | diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/SMAEnergyMeterHandlerFactory.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/SMAEnergyMeterHandlerFactory.java index 77a0b0596c4c4..a5d96570487d2 100644 --- a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/SMAEnergyMeterHandlerFactory.java +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/SMAEnergyMeterHandlerFactory.java @@ -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 @@ -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); @@ -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; diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/configuration/EnergyMeterConfig.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/configuration/EnergyMeterConfig.java index 08c989e559cf9..ef09ae5c9f4c2 100644 --- a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/configuration/EnergyMeterConfig.java +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/configuration/EnergyMeterConfig.java @@ -22,6 +22,7 @@ public class EnergyMeterConfig { private String mcastGroup; private Integer port; private Integer pollingPeriod; + private String serialNumber; public String getMcastGroup() { return mcastGroup; @@ -46,4 +47,12 @@ public Integer getPollingPeriod() { public void setPollingPeriod(Integer pollingPeriod) { this.pollingPeriod = pollingPeriod; } + + public String getSerialNumber() { + return serialNumber; + } + + public void setSerialNumber(String serialNumber) { + this.serialNumber = serialNumber; + } } diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/discovery/SMAEnergyMeterDiscoveryService.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/discovery/SMAEnergyMeterDiscoveryService.java index 161a1483ce540..6c84f19f697a6 100644 --- a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/discovery/SMAEnergyMeterDiscoveryService.java +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/discovery/SMAEnergyMeterDiscoveryService.java @@ -18,9 +18,11 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; 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 +30,9 @@ 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; @@ -39,12 +43,16 @@ * @author Osman Basha - Initial contribution */ @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 PacketListener packetListener; - public SMAEnergyMeterDiscoveryService() { + @Activate + public SMAEnergyMeterDiscoveryService(@Reference PacketListenerRegistry listenerRegistry) { super(SUPPORTED_THING_TYPES_UIDS, 15, true); + this.listenerRegistry = listenerRegistry; } @Override @@ -54,35 +62,43 @@ public Set getSupportedThingTypes() { @Override protected void startBackgroundDiscovery() { + 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.error("Could not start background discovery", e); + return; + } + + packetListener.addPayloadHandler(this); } @Override - public void startScan() { - logger.debug("Start SMAEnergyMeter scan"); - discover(); + protected void stopBackgroundDiscovery() { + packetListener.removePayloadHandler(this); } - 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 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); diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/EnergyMeter.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/EnergyMeter.java index f7450c52c0884..9d4be34779b24 100644 --- a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/EnergyMeter.java +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/EnergyMeter.java @@ -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 @@ * 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 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)); - if (!"SMA".equals(sma)) { + 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 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); } @@ -129,10 +113,6 @@ public String getSerialNumber() { return serialNumber; } - public Date getLastUpdate() { - return lastUpdate; - } - public DecimalType getPowerIn() { return new DecimalType(powerIn.getValue()); } diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/SMAEnergyMeterHandler.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/SMAEnergyMeterHandler.java index 651dccea7d27b..3d2081cf90e12 100644 --- a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/SMAEnergyMeterHandler.java +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/handler/SMAEnergyMeterHandler.java @@ -15,10 +15,13 @@ import static org.openhab.binding.smaenergymeter.internal.SMAEnergyMeterBindingConstants.*; import java.io.IOException; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.Map; +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 +38,25 @@ * * @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(); + if (listener != null) { + listener.request(); + } } else { logger.warn("This binding is a read-only binding and cannot handle commands"); } @@ -61,68 +68,81 @@ public void initialize() { EnergyMeterConfig config = getConfigAs(EnergyMeterConfig.class); - int port = (config.getPort() == null) ? EnergyMeter.DEFAULT_MCAST_PORT : config.getPort(); - energyMeter = new EnergyMeter(config.getMcastGroup(), port); + int port = (config.getPort() == null) ? PacketListener.DEFAULT_MCAST_PORT : config.getPort(); + try { - energyMeter.update(); + listener = listenerRegistry.getListener(config.getMcastGroup(), port); + serialNumber = config.getSerialNumber(); + if (serialNumber == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, + "Meter serial number missing"); + return; + } - updateProperty(Thing.PROPERTY_VENDOR, "SMA"); - updateProperty(Thing.PROPERTY_SERIAL_NUMBER, energyMeter.getSerialNumber()); - logger.debug("Found a SMA Energy Meter with S/N '{}'", energyMeter.getSerialNumber()); + logger.debug("Activated handler for SMA Energy Meter with S/N '{}'", serialNumber); + + int pollingPeriod = (config.getPollingPeriod() == null) ? 30 : config.getPollingPeriod(); + listener.addPayloadHandler(this); + listener.open(pollingPeriod); + logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, 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; + if (listener != null) { + listener.removePayloadHandler(this); } - energyMeter = null; } - private synchronized void updateData() { - logger.debug("Update SMAEnergyMeter data '{}'", getThing().getUID()); + @Override + public void handle(EnergyMeter energyMeter) { + if (serialNumber == null || !serialNumber.equals(energyMeter.getSerialNumber())) { + return; + } + 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()); + + updateStatus(ThingStatus.ONLINE); + } + + @Nullable + private static String retrieveSerialNumber(Map properties) { + String property = properties.get(Thing.PROPERTY_SERIAL_NUMBER); + if (property == null) { + return null; + } 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()); + // old format in decimal form + int identifier = Integer.parseInt(property); + return Integer.toHexString(identifier); + } catch (NumberFormatException e) { + // hex form for newly discovered meters + return property; } } } 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 index 0000000000000..44e8fb3667651 --- /dev/null +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/DefaultPacketListenerRegistry.java @@ -0,0 +1,85 @@ +/** + * 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.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 + */ +@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, "sma-energy-meter-receiver")); + private final Map 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 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.scheduleAtFixedRate(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 index 0000000000000..02eb87c4a659c --- /dev/null +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListener.java @@ -0,0 +1,130 @@ +/** + * 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.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 + */ +public class PacketListener { + + private final DefaultPacketListenerRegistry registry; + private final List 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 MulticastSocket socket; + private 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() { + return socket != null && socket.isConnected(); + } + + public void open(int intervalSec) throws IOException { + if (isOpen()) { + // no need to bind socket second time + return; + } + 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); + } + + void close() throws IOException { + if (future != null) { + future.cancel(true); + } + + InetAddress address = InetAddress.getByName(multicastGroup); + socket.leaveGroup(address); + socket.close(); + } + + public void request() { + 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 handlers; + + ReceivingTask(DatagramSocket socket, String group, List 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); + socket.receive(msgPacket); + + try { + EnergyMeter meter = new EnergyMeter(); + meter.parse(bytes); + + for (PayloadHandler handler : handlers) { + handler.handle(meter); + } + } catch (IOException e) { + logger.info("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 index 0000000000000..c2617e2b033bd --- /dev/null +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListenerRegistry.java @@ -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 index 0000000000000..61baa89d885d8 --- /dev/null +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PayloadHandler.java @@ -0,0 +1,27 @@ +/** + * 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.openhab.binding.smaenergymeter.internal.handler.EnergyMeter; + +/** + * Definition of data recipient. + * + * @author Łukasz Dywicki - Initial contribution + */ +public interface PayloadHandler { + + void handle(EnergyMeter energyMeter) throws IOException; +} diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/resources/OH-INF/thing/energyMeter.xml b/bundles/org.openhab.binding.smaenergymeter/src/main/resources/OH-INF/thing/energyMeter.xml index 8bf381bee85fe..2aea72349026f 100644 --- a/bundles/org.openhab.binding.smaenergymeter/src/main/resources/OH-INF/thing/energyMeter.xml +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/resources/OH-INF/thing/energyMeter.xml @@ -32,6 +32,10 @@ + + + Identifier of meter + IP address of the multicast group