2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.dsmr.internal.discovery;
15 import java.util.Collection;
16 import java.util.List;
17 import java.util.Map.Entry;
18 import java.util.Objects;
20 import java.util.stream.Collectors;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.dsmr.internal.device.connector.DSMRErrorStatus;
25 import org.openhab.binding.dsmr.internal.device.cosem.CosemObject;
26 import org.openhab.binding.dsmr.internal.device.cosem.CosemObjectType;
27 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram;
28 import org.openhab.binding.dsmr.internal.device.p1telegram.P1TelegramListener;
29 import org.openhab.binding.dsmr.internal.handler.DSMRBridgeHandler;
30 import org.openhab.binding.dsmr.internal.handler.DSMRMeterHandler;
31 import org.openhab.binding.dsmr.internal.meter.DSMRMeterDescriptor;
32 import org.openhab.binding.dsmr.internal.meter.DSMRMeterType;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.binding.ThingHandler;
36 import org.openhab.core.thing.binding.ThingHandlerService;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * This implements the discovery service for new DSMR Meters on an active DSMR bridge.
43 * @author M. Volaart - Initial contribution
44 * @author Hilbrand Bouwkamp - Refactored code to detect meters during actual discovery phase.
47 public class DSMRMeterDiscoveryService extends DSMRDiscoveryService implements P1TelegramListener, ThingHandlerService {
49 private final Logger logger = LoggerFactory.getLogger(DSMRMeterDiscoveryService.class);
52 * The {@link DSMRBridgeHandler} instance
54 private @NonNullByDefault({}) DSMRBridgeHandler dsmrBridgeHandler;
57 * Constructs a new {@link DSMRMeterDiscoveryService} attached to the give bridge handler.
59 * @param dsmrBridgeHandler The bridge handler this discovery service is attached to
61 public DSMRMeterDiscoveryService() {
63 this.i18nProvider = DSMRI18nProviderTracker.i18nProvider;
64 this.localeProvider = DSMRI18nProviderTracker.localeProvider;
68 public void deactivate() {
73 public @Nullable ThingHandler getThingHandler() {
74 return dsmrBridgeHandler;
78 public void setThingHandler(final ThingHandler handler) {
79 if (handler instanceof DSMRBridgeHandler bridgeHandler) {
80 dsmrBridgeHandler = bridgeHandler;
85 protected void startScan() {
86 logger.debug("Start discovery on existing DSMR bridge.");
87 dsmrBridgeHandler.setLenientMode(true);
88 dsmrBridgeHandler.registerDSMRMeterListener(this);
92 protected synchronized void stopScan() {
93 logger.debug("Stop discovery on existing DSMR bridge.");
94 dsmrBridgeHandler.setLenientMode(false);
96 dsmrBridgeHandler.unregisterDSMRMeterListener(this);
100 public void telegramReceived(final P1Telegram telegram) {
101 if (logger.isDebugEnabled()) {
102 logger.debug("Detect meters from #{} objects", telegram.getCosemObjects().size());
104 final Entry<Collection<DSMRMeterDescriptor>, List<CosemObject>> detectedMeters = meterDetector
105 .detectMeters(telegram);
106 verifyUnregisteredCosemObjects(telegram, detectedMeters.getValue());
107 validateConfiguredMeters(dsmrBridgeHandler.getThing().getThings(),
108 detectedMeters.getKey().stream().map(md -> md.getMeterType()).collect(Collectors.toSet()));
109 detectedMeters.getKey().forEach(m -> meterDiscovered(m, dsmrBridgeHandler.getThing().getUID()));
113 public void onError(final DSMRErrorStatus state, final String message) {
114 logger.info("Telegram could not be parsed correctly, failed with state: {}, {}", state, message);
117 protected void verifyUnregisteredCosemObjects(final P1Telegram telegram, final List<CosemObject> list) {
118 if (!list.isEmpty()) {
120 .anyMatch(e -> e.getType() == CosemObjectType.METER_EQUIPMENT_IDENTIFIER
121 && e.getCosemValues().entrySet().stream().anyMatch(
122 cv -> cv.getValue() instanceof StringType && cv.getValue().toString().isEmpty()))) {
123 // Unregistered meter detected. log to the user.
124 reportUnregisteredMeters();
126 reportUnrecognizedCosemObjects(list);
128 There are unrecognized cosem values in the data received from the meter,\
129 which means some meters might not be detected. Please report your raw data as reference: {}\
130 """, telegram.getRawTelegram());
133 if (!telegram.getUnknownCosemObjects().isEmpty()) {
136 There are unrecognized cosem values in the data received from the meter,\
137 which means you have values that can't be read by a channel: {}. Please report them and your raw data as reference: {}\
139 telegram.getUnknownCosemObjects().stream()
140 .map(e -> String.format("obis id:%s, value:%s", e.getKey(), e.getValue()))
141 .collect(Collectors.joining(", ")),
142 telegram.getRawTelegram());
147 * Called when Unrecognized cosem objects where found. This can be a bug or a new meter not yet supported.
149 * @param list Map with the unrecognized.
151 protected void reportUnrecognizedCosemObjects(final List<CosemObject> list) {
152 list.forEach(c -> logger.info("Unrecognized cosem object '{}' found in the data: {}", c.getType(), c));
156 * Called when a meter equipment identifier is found that has an empty value. This
158 protected void reportUnregisteredMeters() {
160 "An unregistered meter has been found. Probably a new meter. Retry discovery once the meter is registered with the energy provider.");
164 * Validates if the meters configured by the user match with what is detected in the telegram. Some meters are a
165 * subset of other meters and therefore an invalid configured meter does work, but not all available data is
166 * available to the user.
168 * @param things The list of configured things
169 * @param configuredMeterTypes The set of meters detected in the telegram
171 private void validateConfiguredMeters(final List<Thing> things, final Set<DSMRMeterType> configuredMeterTypes) {
173 final Set<DSMRMeterType> configuredMeters = things.stream()
174 .map(Thing::getHandler)
175 .filter(DSMRMeterHandler.class::isInstance)
176 .map(DSMRMeterHandler.class::cast)
177 .map(DSMRMeterHandler::getMeterDescriptor)
178 .filter(Objects::nonNull)
179 .map(d -> d.getMeterType())
180 .collect(Collectors.toSet());
182 // Create list of all configured meters that are not in the detected list. If not empty meters might not be
183 // correctly configured.
184 final List<DSMRMeterType> invalidConfigured = configuredMeters.stream()
185 .filter(dm -> !configuredMeterTypes.contains(dm)).collect(Collectors.toList());
186 // Create a list of all detected meters not yet configured.
187 final List<DSMRMeterType> unconfiguredMeters = configuredMeterTypes.stream()
188 .filter(dm -> !configuredMeters.contains(dm)).collect(Collectors.toList());
190 if (!invalidConfigured.isEmpty()) {
191 reportConfigurationValidationResults(invalidConfigured, unconfiguredMeters);
196 * Called when the validation finds in inconsistency between configured meters.
198 * @param invalidConfigured The list of invalid configured meters
199 * @param unconfiguredMeters The list of meters that were detected, but not configured
201 protected void reportConfigurationValidationResults(final List<DSMRMeterType> invalidConfigured,
202 final List<DSMRMeterType> unconfiguredMeters) {
204 Possible incorrect meters configured. These are configured: {}.\
205 But the following unconfigured meters are found in the data received from the meter: {}\
206 """, invalidConfigured.stream().map(m -> m.name()).collect(Collectors.joining(", ")),
207 unconfiguredMeters.stream().map(m -> m.name()).collect(Collectors.joining(", ")));