]> git.basschouten.com Git - openhab-addons.git/blob
969c9210b5dfc77183a77e6d1ae88110ac92c08c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.dsmr.internal.discovery;
14
15 import java.util.Collection;
16 import java.util.List;
17 import java.util.Map.Entry;
18 import java.util.Objects;
19 import java.util.Set;
20 import java.util.stream.Collectors;
21
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;
39
40 /**
41  * This implements the discovery service for new DSMR Meters on an active DSMR bridge.
42  *
43  * @author M. Volaart - Initial contribution
44  * @author Hilbrand Bouwkamp - Refactored code to detect meters during actual discovery phase.
45  */
46 @NonNullByDefault
47 public class DSMRMeterDiscoveryService extends DSMRDiscoveryService implements P1TelegramListener, ThingHandlerService {
48
49     private final Logger logger = LoggerFactory.getLogger(DSMRMeterDiscoveryService.class);
50
51     /**
52      * The {@link DSMRBridgeHandler} instance
53      */
54     private @NonNullByDefault({}) DSMRBridgeHandler dsmrBridgeHandler;
55
56     /**
57      * Constructs a new {@link DSMRMeterDiscoveryService} attached to the give bridge handler.
58      */
59     public DSMRMeterDiscoveryService() {
60         super();
61         this.i18nProvider = DSMRI18nProviderTracker.i18nProvider;
62         this.localeProvider = DSMRI18nProviderTracker.localeProvider;
63     }
64
65     @Override
66     public void deactivate() {
67         super.deactivate();
68     }
69
70     @Override
71     public @Nullable ThingHandler getThingHandler() {
72         return dsmrBridgeHandler;
73     }
74
75     @Override
76     public void setThingHandler(final ThingHandler handler) {
77         if (handler instanceof DSMRBridgeHandler bridgeHandler) {
78             dsmrBridgeHandler = bridgeHandler;
79         }
80     }
81
82     @Override
83     protected void startScan() {
84         logger.debug("Start discovery on existing DSMR bridge.");
85         dsmrBridgeHandler.setLenientMode(true);
86         dsmrBridgeHandler.registerDSMRMeterListener(this);
87     }
88
89     @Override
90     protected synchronized void stopScan() {
91         logger.debug("Stop discovery on existing DSMR bridge.");
92         dsmrBridgeHandler.setLenientMode(false);
93         super.stopScan();
94         dsmrBridgeHandler.unregisterDSMRMeterListener(this);
95     }
96
97     @Override
98     public void telegramReceived(final P1Telegram telegram) {
99         if (logger.isDebugEnabled()) {
100             logger.debug("Detect meters from #{} objects", telegram.getCosemObjects().size());
101         }
102         final Entry<Collection<DSMRMeterDescriptor>, List<CosemObject>> detectedMeters = meterDetector
103                 .detectMeters(telegram);
104         verifyUnregisteredCosemObjects(telegram, detectedMeters.getValue());
105         validateConfiguredMeters(dsmrBridgeHandler.getThing().getThings(),
106                 detectedMeters.getKey().stream().map(md -> md.getMeterType()).collect(Collectors.toSet()));
107         detectedMeters.getKey().forEach(m -> meterDiscovered(m, dsmrBridgeHandler.getThing().getUID()));
108     }
109
110     @Override
111     public void onError(final DSMRErrorStatus state, final String message) {
112         logger.info("Telegram could not be parsed correctly, failed with state: {}, {}", state, message);
113     }
114
115     protected void verifyUnregisteredCosemObjects(final P1Telegram telegram, final List<CosemObject> list) {
116         if (!list.isEmpty()) {
117             if (list.stream()
118                     .anyMatch(e -> e.getType() == CosemObjectType.METER_EQUIPMENT_IDENTIFIER
119                             && e.getCosemValues().entrySet().stream().anyMatch(
120                                     cv -> cv.getValue() instanceof StringType && cv.getValue().toString().isEmpty()))) {
121                 // Unregistered meter detected. log to the user.
122                 reportUnregisteredMeters();
123             } else {
124                 reportUnrecognizedCosemObjects(list);
125                 logger.info("""
126                         There are unrecognized cosem values in the data received from the meter,\
127                          which means some meters might not be detected. Please report your raw data as reference: {}\
128                         """, telegram.getRawTelegram());
129             }
130         }
131         if (!telegram.getUnknownCosemObjects().isEmpty()) {
132             logger.info(
133                     """
134                             There are unrecognized cosem values in the data received from the meter,\
135                              which means you have values that can't be read by a channel: {}. Please report them and your raw data as reference: {}\
136                             """,
137                     telegram.getUnknownCosemObjects().stream()
138                             .map(e -> String.format("obis id:%s, value:%s", e.getKey(), e.getValue()))
139                             .collect(Collectors.joining(", ")),
140                     telegram.getRawTelegram());
141         }
142     }
143
144     /**
145      * Called when Unrecognized cosem objects where found. This can be a bug or a new meter not yet supported.
146      *
147      * @param list Map with the unrecognized.
148      */
149     protected void reportUnrecognizedCosemObjects(final List<CosemObject> list) {
150         list.forEach(c -> logger.info("Unrecognized cosem object '{}' found in the data: {}", c.getType(), c));
151     }
152
153     /**
154      * Called when a meter equipment identifier is found that has an empty value. This
155      */
156     protected void reportUnregisteredMeters() {
157         logger.info(
158                 "An unregistered meter has been found. Probably a new meter. Retry discovery once the meter is registered with the energy provider.");
159     }
160
161     /**
162      * Validates if the meters configured by the user match with what is detected in the telegram. Some meters are a
163      * subset of other meters and therefore an invalid configured meter does work, but not all available data is
164      * available to the user.
165      *
166      * @param things The list of configured things
167      * @param configuredMeterTypes The set of meters detected in the telegram
168      */
169     private void validateConfiguredMeters(final List<Thing> things, final Set<DSMRMeterType> configuredMeterTypes) {
170         // @formatter:off
171         final Set<DSMRMeterType> configuredMeters = things.stream()
172                 .map(Thing::getHandler)
173                 .filter(DSMRMeterHandler.class::isInstance)
174                 .map(DSMRMeterHandler.class::cast)
175                 .map(DSMRMeterHandler::getMeterDescriptor)
176                 .filter(Objects::nonNull)
177                 .map(d -> d.getMeterType())
178                 .collect(Collectors.toSet());
179         // @formatter:on
180         // Create list of all configured meters that are not in the detected list. If not empty meters might not be
181         // correctly configured.
182         final List<DSMRMeterType> invalidConfigured = configuredMeters.stream()
183                 .filter(dm -> !configuredMeterTypes.contains(dm)).collect(Collectors.toList());
184         // Create a list of all detected meters not yet configured.
185         final List<DSMRMeterType> unconfiguredMeters = configuredMeterTypes.stream()
186                 .filter(dm -> !configuredMeters.contains(dm)).collect(Collectors.toList());
187
188         if (!invalidConfigured.isEmpty()) {
189             reportConfigurationValidationResults(invalidConfigured, unconfiguredMeters);
190         }
191     }
192
193     /**
194      * Called when the validation finds in inconsistency between configured meters.
195      *
196      * @param invalidConfigured The list of invalid configured meters
197      * @param unconfiguredMeters The list of meters that were detected, but not configured
198      */
199     protected void reportConfigurationValidationResults(final List<DSMRMeterType> invalidConfigured,
200             final List<DSMRMeterType> unconfiguredMeters) {
201         logger.info("""
202                 Possible incorrect meters configured. These are configured: {}.\
203                 But the following unconfigured meters are found in the data received from the meter: {}\
204                 """, invalidConfigured.stream().map(m -> m.name()).collect(Collectors.joining(", ")),
205                 unconfiguredMeters.stream().map(m -> m.name()).collect(Collectors.joining(", ")));
206     }
207 }