]> git.basschouten.com Git - openhab-addons.git/blob
0db8a986fc30f4b784c1ce446369dc5c201ea320
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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      * @param dsmrBridgeHandler The bridge handler this discovery service is attached to
60      */
61     public DSMRMeterDiscoveryService() {
62         super();
63         this.i18nProvider = DSMRI18nProviderTracker.i18nProvider;
64         this.localeProvider = DSMRI18nProviderTracker.localeProvider;
65     }
66
67     @Override
68     public void deactivate() {
69         super.deactivate();
70     }
71
72     @Override
73     public @Nullable ThingHandler getThingHandler() {
74         return dsmrBridgeHandler;
75     }
76
77     @Override
78     public void setThingHandler(final ThingHandler handler) {
79         if (handler instanceof DSMRBridgeHandler bridgeHandler) {
80             dsmrBridgeHandler = bridgeHandler;
81         }
82     }
83
84     @Override
85     protected void startScan() {
86         logger.debug("Start discovery on existing DSMR bridge.");
87         dsmrBridgeHandler.setLenientMode(true);
88         dsmrBridgeHandler.registerDSMRMeterListener(this);
89     }
90
91     @Override
92     protected synchronized void stopScan() {
93         logger.debug("Stop discovery on existing DSMR bridge.");
94         dsmrBridgeHandler.setLenientMode(false);
95         super.stopScan();
96         dsmrBridgeHandler.unregisterDSMRMeterListener(this);
97     }
98
99     @Override
100     public void telegramReceived(final P1Telegram telegram) {
101         if (logger.isDebugEnabled()) {
102             logger.debug("Detect meters from #{} objects", telegram.getCosemObjects().size());
103         }
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()));
110     }
111
112     @Override
113     public void onError(final DSMRErrorStatus state, final String message) {
114         logger.info("Telegram could not be parsed correctly, failed with state: {}, {}", state, message);
115     }
116
117     protected void verifyUnregisteredCosemObjects(final P1Telegram telegram, final List<CosemObject> list) {
118         if (!list.isEmpty()) {
119             if (list.stream()
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();
125             } else {
126                 reportUnrecognizedCosemObjects(list);
127                 logger.info("""
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());
131             }
132         }
133         if (!telegram.getUnknownCosemObjects().isEmpty()) {
134             logger.info(
135                     """
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: {}\
138                             """,
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());
143         }
144     }
145
146     /**
147      * Called when Unrecognized cosem objects where found. This can be a bug or a new meter not yet supported.
148      *
149      * @param list Map with the unrecognized.
150      */
151     protected void reportUnrecognizedCosemObjects(final List<CosemObject> list) {
152         list.forEach(c -> logger.info("Unrecognized cosem object '{}' found in the data: {}", c.getType(), c));
153     }
154
155     /**
156      * Called when a meter equipment identifier is found that has an empty value. This
157      */
158     protected void reportUnregisteredMeters() {
159         logger.info(
160                 "An unregistered meter has been found. Probably a new meter. Retry discovery once the meter is registered with the energy provider.");
161     }
162
163     /**
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.
167      *
168      * @param things The list of configured things
169      * @param configuredMeterTypes The set of meters detected in the telegram
170      */
171     private void validateConfiguredMeters(final List<Thing> things, final Set<DSMRMeterType> configuredMeterTypes) {
172         // @formatter:off
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());
181         // @formatter:on
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());
189
190         if (!invalidConfigured.isEmpty()) {
191             reportConfigurationValidationResults(invalidConfigured, unconfiguredMeters);
192         }
193     }
194
195     /**
196      * Called when the validation finds in inconsistency between configured meters.
197      *
198      * @param invalidConfigured The list of invalid configured meters
199      * @param unconfiguredMeters The list of meters that were detected, but not configured
200      */
201     protected void reportConfigurationValidationResults(final List<DSMRMeterType> invalidConfigured,
202             final List<DSMRMeterType> unconfiguredMeters) {
203         logger.info("""
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(", ")));
208     }
209 }