2 * Copyright (c) 2010-2021 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.netatmo.internal.discovery;
15 import static org.openhab.binding.netatmo.internal.APIUtils.*;
16 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
18 import java.util.HashMap;
19 import java.util.List;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
25 import org.openhab.binding.netatmo.internal.handler.NetatmoDataListener;
26 import org.openhab.core.config.discovery.AbstractDiscoveryService;
27 import org.openhab.core.config.discovery.DiscoveryResult;
28 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
29 import org.openhab.core.i18n.LocaleProvider;
30 import org.openhab.core.i18n.TranslationProvider;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingTypeUID;
33 import org.openhab.core.thing.ThingUID;
34 import org.osgi.framework.Bundle;
35 import org.osgi.framework.FrameworkUtil;
37 import io.swagger.client.model.NAHealthyHomeCoach;
38 import io.swagger.client.model.NAMain;
39 import io.swagger.client.model.NAPlug;
40 import io.swagger.client.model.NAStationModule;
41 import io.swagger.client.model.NAWelcomeCamera;
42 import io.swagger.client.model.NAWelcomeHome;
45 * The {@link NetatmoModuleDiscoveryService} searches for available Netatmo
46 * devices and modules connected to the API console
48 * @author Gaƫl L'hopital - Initial contribution
49 * @author Ing. Peter Weiss - Welcome camera implementation
53 public class NetatmoModuleDiscoveryService extends AbstractDiscoveryService implements NetatmoDataListener {
54 private static final int SEARCH_TIME = 5;
55 private final NetatmoBridgeHandler netatmoBridgeHandler;
57 public NetatmoModuleDiscoveryService(NetatmoBridgeHandler netatmoBridgeHandler, LocaleProvider localeProvider,
58 TranslationProvider translationProvider) {
59 super(SUPPORTED_DEVICE_THING_TYPES_UIDS, SEARCH_TIME);
60 this.netatmoBridgeHandler = netatmoBridgeHandler;
61 this.localeProvider = localeProvider;
62 this.i18nProvider = translationProvider;
66 public void activate(@Nullable Map<String, Object> configProperties) {
67 super.activate(configProperties);
68 netatmoBridgeHandler.registerDataListener(this);
72 public void deactivate() {
73 netatmoBridgeHandler.unregisterDataListener(this);
78 public void startScan() {
79 if (netatmoBridgeHandler.configuration.readStation) {
80 netatmoBridgeHandler.getStationsDataBody(null).ifPresent(dataBody -> {
81 nonNullList(dataBody.getDevices()).forEach(station -> {
82 discoverWeatherStation(station);
86 if (netatmoBridgeHandler.configuration.readHealthyHomeCoach) {
87 netatmoBridgeHandler.getHomecoachDataBody(null).ifPresent(dataBody -> {
88 nonNullList(dataBody.getDevices()).forEach(homecoach -> {
89 discoverHomeCoach(homecoach);
93 if (netatmoBridgeHandler.configuration.readThermostat) {
94 netatmoBridgeHandler.getThermostatsDataBody(null).ifPresent(dataBody -> {
95 nonNullList(dataBody.getDevices()).forEach(plug -> {
96 discoverThermostat(plug);
100 if (netatmoBridgeHandler.configuration.readWelcome || netatmoBridgeHandler.configuration.readPresence) {
101 netatmoBridgeHandler.getWelcomeDataBody(null).ifPresent(dataBody -> {
102 nonNullList(dataBody.getHomes()).forEach(home -> {
103 discoverWelcomeHome(home);
110 protected synchronized void stopScan() {
112 removeOlderResults(getTimestampOfLastScan(), netatmoBridgeHandler.getThing().getUID());
116 public void onDataRefreshed(Object data) {
117 if (!isBackgroundDiscoveryEnabled()) {
120 if (data instanceof NAMain) {
121 discoverWeatherStation((NAMain) data);
122 } else if (data instanceof NAPlug) {
123 discoverThermostat((NAPlug) data);
124 } else if (data instanceof NAHealthyHomeCoach) {
125 discoverHomeCoach((NAHealthyHomeCoach) data);
126 } else if (data instanceof NAWelcomeHome) {
127 discoverWelcomeHome((NAWelcomeHome) data);
131 private void discoverThermostat(NAPlug plug) {
132 onDeviceAddedInternal(plug.getId(), null, plug.getType(), plug.getStationName(), plug.getFirmware());
133 nonNullList(plug.getModules()).forEach(thermostat -> {
134 onDeviceAddedInternal(thermostat.getId(), plug.getId(), thermostat.getType(), thermostat.getModuleName(),
135 thermostat.getFirmware());
139 private void discoverHomeCoach(NAHealthyHomeCoach homecoach) {
140 onDeviceAddedInternal(homecoach.getId(), null, homecoach.getType(), homecoach.getName(),
141 homecoach.getFirmware());
144 private void discoverWeatherStation(NAMain station) {
145 final boolean isFavorite = station.isFavorite() != null && station.isFavorite();
146 final String weatherStationName = createWeatherStationName(station, isFavorite);
148 onDeviceAddedInternal(station.getId(), null, station.getType(), weatherStationName, station.getFirmware());
149 nonNullList(station.getModules()).forEach(module -> {
150 onDeviceAddedInternal(module.getId(), station.getId(), module.getType(),
151 createWeatherModuleName(station, module, isFavorite), module.getFirmware());
155 private void discoverWelcomeHome(NAWelcomeHome home) {
156 // I observed that Thermostat homes are also reported here by Netatmo API
157 // So I ignore homes that have an empty list of cameras
158 List<NAWelcomeCamera> cameras = nonNullList(home.getCameras());
159 if (!cameras.isEmpty()) {
160 onDeviceAddedInternal(home.getId(), null, WELCOME_HOME_THING_TYPE.getId(), home.getName(), null);
162 cameras.forEach(camera -> {
163 onDeviceAddedInternal(camera.getId(), home.getId(), camera.getType(), camera.getName(), null);
166 // Discover Known Persons
167 nonNullStream(home.getPersons()).filter(person -> person.getPseudo() != null).forEach(person -> {
168 onDeviceAddedInternal(person.getId(), home.getId(), WELCOME_PERSON_THING_TYPE.getId(),
169 person.getPseudo(), null);
174 private void onDeviceAddedInternal(String id, @Nullable String parentId, String type, String name,
175 @Nullable Integer firmwareVersion) {
176 ThingUID thingUID = findThingUID(type, id);
177 Map<String, Object> properties = new HashMap<>();
179 properties.put(EQUIPMENT_ID, id);
180 if (parentId != null) {
181 properties.put(PARENT_ID, parentId);
183 if (firmwareVersion != null) {
184 properties.put(Thing.PROPERTY_VENDOR, VENDOR);
185 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
186 properties.put(Thing.PROPERTY_MODEL_ID, type);
187 properties.put(Thing.PROPERTY_SERIAL_NUMBER, id);
189 addDiscoveredThing(thingUID, properties, name);
192 private void addDiscoveredThing(ThingUID thingUID, Map<String, Object> properties, String displayLabel) {
193 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
194 .withBridge(netatmoBridgeHandler.getThing().getUID()).withLabel(displayLabel)
195 .withRepresentationProperty(EQUIPMENT_ID).build();
197 thingDiscovered(discoveryResult);
200 private ThingUID findThingUID(String thingType, String thingId) throws IllegalArgumentException {
201 for (ThingTypeUID supportedThingTypeUID : getSupportedThingTypes()) {
202 String uid = supportedThingTypeUID.getId();
204 if (uid.equalsIgnoreCase(thingType)) {
205 return new ThingUID(supportedThingTypeUID, netatmoBridgeHandler.getThing().getUID(),
206 thingId.replaceAll("[^a-zA-Z0-9_]", ""));
210 throw new IllegalArgumentException("Unsupported device type discovered : " + thingType);
213 private String createWeatherStationName(NAMain station, boolean isFavorite) {
214 StringBuilder nameBuilder = new StringBuilder();
215 nameBuilder.append(localizeType(station.getType()));
216 if (station.getStationName() != null) {
217 nameBuilder.append(' ');
218 nameBuilder.append(station.getStationName());
221 nameBuilder.append(" (favorite)");
223 return nameBuilder.toString();
226 private String createWeatherModuleName(NAMain station, NAStationModule module, boolean isFavorite) {
227 StringBuilder nameBuilder = new StringBuilder();
228 if (module.getModuleName() != null) {
229 nameBuilder.append(module.getModuleName());
231 nameBuilder.append(localizeType(module.getType()));
233 if (station.getStationName() != null) {
234 nameBuilder.append(' ');
235 nameBuilder.append(station.getStationName());
238 nameBuilder.append(" (favorite)");
240 return nameBuilder.toString();
243 private String localizeType(String typeName) {
244 Bundle bundle = FrameworkUtil.getBundle(this.getClass());
246 String localizedType = i18nProvider.getText(bundle, "thing-type.netatmo." + typeName + ".label", typeName,
247 localeProvider.getLocale());
248 if (localizedType != null) {
249 return localizedType;