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 e4c0110bcbc25..3d734c5f121a9 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 3066a3e70a098..d368fb7a9923d 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 8aa9c4010b21d..68a521f9c5ac7 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,52 @@ 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(); + } 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); + @Override + public void startScan() { + logger.debug("Start SMAEnergyMeter scan"); + startBackgroundDiscovery(); try { - energyMeter.update(); - } catch (IOException e) { - logger.debug("No SMA Energy Meter found."); - logger.debug("Diagnostic: ", e); - return; + Thread.sleep(60_000); + } catch (InterruptedException e) { + logger.debug("Discovery task terminated", e); + } finally { + stopBackgroundDiscovery(); } + } - 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 73d04e1261a74..1153f18a64a6d 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,14 +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; @@ -53,13 +49,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 +71,21 @@ public EnergyMeter(String multicastGroup, int port) { energyOutL3 = new FieldDTO(0x1E4, 8, 3600000); // +8 } - public void update() throws IOException { - byte[] bytes = new byte[608]; - try (MulticastSocket socket = new MulticastSocket(port)) { - socket.setSoTimeout(5000); - InetAddress address = InetAddress.getByName(multicastGroup); - socket.joinGroup(address); - - DatagramPacket msgPacket = new DatagramPacket(bytes, bytes.length); - socket.receive(msgPacket); - - String sma = new String(Arrays.copyOfRange(bytes, 0x00, 0x03)); + public void parse(byte[] bytes) throws IOException { + try { + String sma = new String(Arrays.copyOfRange(bytes, 0, 3)); if (!sma.equals("SMA")) { throw new IOException("Not a SMA telegram." + sma); } + byte[] protocolId = Arrays.copyOfRange(bytes, 16, 18); + if (!Arrays.equals(protocolId, E_METER_PROTOCOL_ID)) { + throw new IllegalArgumentException( + "Received frame with wrong protocol ID " + Arrays.toString(protocolId)); + } + byte[] idf = Arrays.copyOfRange(bytes, 0x14, 0x18); ByteBuffer buffer = ByteBuffer.wrap(Arrays.copyOfRange(bytes, 0x14, 0x18)); - serialNumber = String.valueOf(buffer.getInt()); + serialNumber = Long.toString(buffer.getInt() & 0xFFFFFFFFL); powerIn.updateValue(bytes); energyIn.updateValue(bytes); @@ -118,8 +106,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 +115,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 cc68064db2f2b..c4acda553ce73 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,17 @@ import static org.openhab.binding.smaenergymeter.internal.SMAEnergyMeterBindingConstants.*; import java.io.IOException; +import java.util.Map; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +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.config.core.Configuration; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -35,21 +42,27 @@ * * @author Osman Basha - Initial contribution */ -public class SMAEnergyMeterHandler extends BaseThingHandler { +public class SMAEnergyMeterHandler extends BaseThingHandler implements PayloadHandler, Runnable { private final Logger logger = LoggerFactory.getLogger(SMAEnergyMeterHandler.class); - private EnergyMeter energyMeter; - private ScheduledFuture pollingJob; - - public SMAEnergyMeterHandler(Thing thing) { + private final PacketListenerRegistry listenerRegistry; + private final AtomicBoolean refresh = new AtomicBoolean(false); + private @Nullable PacketListener listener; + private @Nullable String serialNumber; + private long updateInterval; + private long lastUpdate; + private ScheduledFuture timeoutFuture; + + 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(); + refresh.set(true); } else { logger.warn("This binding is a read-only binding and cannot handle commands"); } @@ -61,68 +74,121 @@ 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(); + PacketListener thingListener = listenerRegistry.getListener(config.getMcastGroup(), port); + serialNumber = config.getSerialNumber(); + if (serialNumber == null) { + if (!thing.getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER) + || (serialNumber = retrieveSerialNumber(thing.getProperties())) == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, + "Meter serial number missing"); + return; + } + + // copy serial number from thing properties into config + Map newConfig = editConfiguration().getProperties(); + newConfig.put("serialNumber", serialNumber); + Thing thing = editThing().withConfiguration(new Configuration(newConfig)).build(); + updateThing(thing); + updateStatus(ThingStatus.OFFLINE); + return; + } else { + if (!thing.getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER)) { + Map props = editProperties(); + props.put(Thing.PROPERTY_VENDOR, "SMA"); + props.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber); + updateProperties(props); + } + } - 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); + + this.updateInterval = TimeUnit.SECONDS + .toMillis(config.getPollingPeriod() == null ? 30 : config.getPollingPeriod()); + timeoutFuture = scheduler.scheduleWithFixedDelay(this, updateInterval * 2, updateInterval, + TimeUnit.MILLISECONDS); + this.listener = thingListener; + thingListener.addPayloadHandler(this); + thingListener.open(); + logger.debug("Listening to meter updates and publishing its measurements every {}ms for '{}'", + updateInterval, 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); + } + if (timeoutFuture != null) { + timeoutFuture.cancel(true); } - 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; + } - 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); + long currentUpdate = System.currentTimeMillis(); + if (updateInterval > currentUpdate - lastUpdate) { + logger.trace("Update is to early {}, waiting to {}", currentUpdate - lastUpdate, updateInterval); + if (!refresh.get()) { + return; } - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); } + lastUpdate = currentUpdate; + + logger.debug("Update SMAEnergyMeter {} data '{}'", serialNumber, getThing().getUID()); + updateStatus(ThingStatus.ONLINE); + + 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()); + + refresh.set(false); + } + + @Override + public void run() { + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis - lastUpdate > updateInterval * 2) { + long missedWindow = TimeUnit.MILLISECONDS.toSeconds((updateInterval * 2)); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE, + "No update received within " + missedWindow + " seconds"); + } + } + + @Nullable + private static String retrieveSerialNumber(Map properties) { + String property = properties.get(Thing.PROPERTY_SERIAL_NUMBER); + if (property == null) { + return null; + } + return property.trim(); } } 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..e4ae7a29800d1 --- /dev/null +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/DefaultPacketListenerRegistry.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2022 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 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 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); + } + } + } + + 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..8507b523c565a --- /dev/null +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListener.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2010-2022 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.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.openhab.binding.smaenergymeter.internal.handler.EnergyMeter; +import org.openhab.core.util.HexUtils; +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 Set handlers = new CopyOnWriteArraySet<>(); + private final AtomicBoolean stop = new AtomicBoolean(); + + private Thread receiverThread; + 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; + + 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() 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); + + this.receiverThread = new Thread(new ReceivingTask(socket, multicastGroup + ":" + port, handlers, stop), + "smaenergymeter-receiver-" + multicastGroup + ":" + port); + this.receiverThread.setDaemon(true); + this.receiverThread.start(); + } + + void close() throws IOException { + stop.set(true); + InetAddress address = InetAddress.getByName(multicastGroup); + socket.leaveGroup(address); + socket.close(); + } + + static class ReceivingTask implements Runnable { + private final Logger logger = LoggerFactory.getLogger(ReceivingTask.class); + private final DatagramSocket socket; + private final String group; + private final Set handlers; + private final AtomicBoolean stop; + + ReceivingTask(DatagramSocket socket, String group, Set handlers, AtomicBoolean stop) { + this.socket = socket; + this.group = group; + this.handlers = handlers; + this.stop = stop; + } + + public void run() { + try { + byte[] bytes = new byte[608]; + while (!stop.get()) { + DatagramPacket msgPacket = new DatagramPacket(bytes, bytes.length); + socket.receive(msgPacket); + + try { + if (logger.isTraceEnabled()) { + logger.trace("Received frame {}", HexUtils.bytesToHex(bytes)); + } + + EnergyMeter meter = new EnergyMeter(); + meter.parse(bytes); + + for (PayloadHandler handler : handlers) { + handler.handle(meter); + } + } catch (IOException e) { + logger.debug("Unexpected payload received for group {}", group, e); + } + } + } catch (IOException e) { + logger.warn("Failed to receive data for multicast group {}", group, e); + } + } + } +} diff --git a/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListenerRegistry.java b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListenerRegistry.java new file mode 100644 index 0000000000000..0df026ffd1da5 --- /dev/null +++ b/bundles/org.openhab.binding.smaenergymeter/src/main/java/org/openhab/binding/smaenergymeter/internal/packet/PacketListenerRegistry.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2022 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; + +/** + * Definition of packet listener registry - a central place to track all registered sockets and + * multicast groups. + * + * @author Łukasz Dywicki - Initial contribution + */ +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..ff7ce0c5b5d9f --- /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-2022 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..15e6addc22137 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