diff --git a/bundles/org.openhab.binding.netatmo/README.md b/bundles/org.openhab.binding.netatmo/README.md index cf9214f33edb1..6735364cd371e 100644 --- a/bundles/org.openhab.binding.netatmo/README.md +++ b/bundles/org.openhab.binding.netatmo/README.md @@ -123,6 +123,12 @@ NB: Allowed ports for webhooks are 80, 88, 443 and 9443. ### Configure Things +The easiest way to retrieve the IDs for all the devices and modules is to use the console command `openhab:netatmo showIds`. +It shows the hierarchy of all the devices and modules including their IDs. +This can help to define all your things in a configuration file. + +**Another way to get the IDs is to use the developer documentation on the netatmo site:** + The IDs for the modules can be extracted from the developer documentation on the netatmo site. First login with your user. Then some examples of the documentation contain the **real results** of your weather station. diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java index 58e9855104afe..82fc791f6771d 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java @@ -207,6 +207,11 @@ public URI getConfigDescription() { : ModuleType.UNKNOWN.equals(getBridge()) ? "configurable" : "device"))); } + public int getDepth() { + ModuleType parent = bridgeType; + return parent == null ? 1 : 1 + parent.getDepth(); + } + public static ModuleType from(ThingTypeUID thingTypeUID) { return ModuleType.AS_SET.stream().filter(mt -> mt.thingTypeUID.equals(thingTypeUID)).findFirst() .orElseThrow(() -> new IllegalArgumentException()); diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/console/NetatmoCommandExtension.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/console/NetatmoCommandExtension.java new file mode 100644 index 0000000000000..b8b28bef761a2 --- /dev/null +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/console/NetatmoCommandExtension.java @@ -0,0 +1,97 @@ +/** + * 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.netatmo.internal.console; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.netatmo.internal.NetatmoBindingConstants; +import org.openhab.binding.netatmo.internal.api.data.ModuleType; +import org.openhab.binding.netatmo.internal.api.dto.NAModule; +import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link NetatmoCommandExtension} is responsible for handling console commands + * + * @author Laurent Garnier - Initial contribution + */ + +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class NetatmoCommandExtension extends AbstractConsoleCommandExtension { + + private static final String SHOW_IDS = "showIds"; + + private final ThingRegistry thingRegistry; + private @Nullable Console console; + + @Activate + public NetatmoCommandExtension(final @Reference ThingRegistry thingRegistry) { + super(NetatmoBindingConstants.BINDING_ID, "Interact with the Netatmo binding."); + this.thingRegistry = thingRegistry; + } + + @Override + public void execute(String[] args, Console console) { + if (args.length == 1 && SHOW_IDS.equals(args[0])) { + this.console = console; + for (Thing thing : thingRegistry.getAll()) { + ThingHandler thingHandler = thing.getHandler(); + if (thingHandler instanceof ApiBridgeHandler) { + console.println("Account bridge: " + thing.getLabel()); + ((ApiBridgeHandler) thingHandler).identifyAllModulesAndApplyAction(this::printThing); + } + } + } else { + printUsage(console); + } + } + + private Optional printThing(NAModule module, ThingUID bridgeUID) { + Console localConsole = this.console; + Optional moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID); + if (localConsole != null && moduleUID.isPresent()) { + String indent = ""; + for (int i = 2; i <= module.getType().getDepth(); i++) { + indent += " "; + } + localConsole.println(String.format("%s- ID \"%s\" for \"%s\" (thing type %s)", indent, module.getId(), + module.getName() != null ? module.getName() : "...", module.getType().thingTypeUID)); + } + return moduleUID; + } + + private Optional findThingUID(ModuleType moduleType, String thingId, ThingUID bridgeUID) { + return moduleType.apiName.isBlank() ? Optional.empty() + : Optional.ofNullable( + new ThingUID(moduleType.thingTypeUID, bridgeUID, thingId.replaceAll("[^a-zA-Z0-9_]", ""))); + } + + @Override + public List getUsages() { + return Arrays.asList(buildCommandUsage(SHOW_IDS, "list all devices and modules ids")); + } +} diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/discovery/NetatmoDiscoveryService.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/discovery/NetatmoDiscoveryService.java index 7b567ba698db2..3e6f6933efa8f 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/discovery/NetatmoDiscoveryService.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/discovery/NetatmoDiscoveryService.java @@ -12,24 +12,12 @@ */ package org.openhab.binding.netatmo.internal.discovery; -import static java.util.Comparator.*; - -import java.util.HashMap; -import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.netatmo.internal.api.AircareApi; -import org.openhab.binding.netatmo.internal.api.HomeApi; -import org.openhab.binding.netatmo.internal.api.ListBodyResponse; -import org.openhab.binding.netatmo.internal.api.NetatmoException; -import org.openhab.binding.netatmo.internal.api.WeatherApi; import org.openhab.binding.netatmo.internal.api.data.ModuleType; -import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea; -import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule; -import org.openhab.binding.netatmo.internal.api.dto.NAMain; import org.openhab.binding.netatmo.internal.api.dto.NAModule; import org.openhab.binding.netatmo.internal.config.NAThingConfiguration; import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler; @@ -65,77 +53,20 @@ public NetatmoDiscoveryService() { public void startScan() { ApiBridgeHandler localHandler = handler; if (localHandler != null) { - ThingUID accountUID = localHandler.getThing().getUID(); - try { - AircareApi airCareApi = localHandler.getRestManager(AircareApi.class); - if (airCareApi != null) { // Search Healthy Home Coaches - ListBodyResponse body = airCareApi.getHomeCoachData(null).getBody(); - if (body != null) { - body.getElements().stream().forEach(homeCoach -> createThing(homeCoach, accountUID)); - } - } - WeatherApi weatherApi = localHandler.getRestManager(WeatherApi.class); - if (weatherApi != null) { // Search owned or favorite stations - weatherApi.getFavoriteAndGuestStationsData().stream().forEach(station -> { - if (!station.isReadOnly() || localHandler.getReadFriends()) { - createThing(station, accountUID).ifPresent(stationUID -> station.getModules().values() - .stream().forEach(module -> createThing(module, stationUID))); - } - }); - } - HomeApi homeApi = localHandler.getRestManager(HomeApi.class); - if (homeApi != null) { // Search those depending from a home that has modules + not only weather modules - homeApi.getHomesData(null, null).stream() - .filter(h -> !(h.getFeatures().isEmpty() - || h.getFeatures().contains(FeatureArea.WEATHER) && h.getFeatures().size() == 1)) - .forEach(home -> { - createThing(home, accountUID).ifPresent(homeUID -> { - home.getKnownPersons().forEach(person -> createThing(person, homeUID)); - - Map bridgesUids = new HashMap<>(); - - home.getRooms().values().stream().forEach(room -> { - room.getModuleIds().stream().map(id -> home.getModules().get(id)) - .map(m -> m != null ? m.getType().feature : FeatureArea.NONE) - .filter(f -> FeatureArea.ENERGY.equals(f)).findAny().ifPresent(f -> { - createThing(room, homeUID).ifPresent( - roomUID -> bridgesUids.put(room.getId(), roomUID)); - }); - }); - - // Creating modules that have no bridge first, avoiding weather station itself - home.getModules().values().stream() - .filter(module -> module.getType().feature != FeatureArea.WEATHER) - .sorted(comparing(HomeDataModule::getBridge, nullsFirst(naturalOrder()))) - .forEach(module -> { - String bridgeId = module.getBridge(); - if (bridgeId == null) { - createThing(module, homeUID).ifPresent( - moduleUID -> bridgesUids.put(module.getId(), moduleUID)); - } else { - createThing(module, bridgesUids.getOrDefault(bridgeId, homeUID)); - } - }); - }); - }); - } - } catch (NetatmoException e) { - logger.warn("Error during discovery process : {}", e.getMessage()); - } + localHandler.identifyAllModulesAndApplyAction(this::createThing); } } - private @Nullable ThingUID findThingUID(ModuleType thingType, String thingId, ThingUID bridgeUID) { - ThingTypeUID thingTypeUID = thingType.thingTypeUID; + private Optional findThingUID(ModuleType moduleType, String thingId, ThingUID bridgeUID) { + ThingTypeUID thingTypeUID = moduleType.thingTypeUID; return getSupportedThingTypes().stream().filter(supported -> supported.equals(thingTypeUID)).findFirst() - .map(supported -> new ThingUID(supported, bridgeUID, thingId.replaceAll("[^a-zA-Z0-9_]", ""))) - .orElse(null); + .map(supported -> new ThingUID(supported, bridgeUID, thingId.replaceAll("[^a-zA-Z0-9_]", ""))); } private Optional createThing(NAModule module, ThingUID bridgeUID) { - ThingUID moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID); - if (moduleUID != null) { - DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(moduleUID) + Optional moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID); + if (moduleUID.isPresent()) { + DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(moduleUID.get()) .withProperty(NAThingConfiguration.ID, module.getId()) .withRepresentationProperty(NAThingConfiguration.ID) .withLabel(module.getName() != null ? module.getName() : module.getId()).withBridge(bridgeUID); @@ -143,7 +74,7 @@ private Optional createThing(NAModule module, ThingUID bridgeUID) { } else { logger.info("Module '{}' is not handled by this version of the binding - it is ignored.", module.getName()); } - return Optional.ofNullable(moduleUID); + return moduleUID; } @Override diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java index bdbccab760c3e..d23d5aa7f25e5 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.netatmo.internal.handler; +import static java.util.Comparator.*; import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*; import java.io.ByteArrayInputStream; @@ -32,6 +33,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; import javax.ws.rs.core.UriBuilder; @@ -45,13 +47,21 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus.Code; +import org.openhab.binding.netatmo.internal.api.AircareApi; import org.openhab.binding.netatmo.internal.api.ApiError; import org.openhab.binding.netatmo.internal.api.AuthenticationApi; +import org.openhab.binding.netatmo.internal.api.HomeApi; +import org.openhab.binding.netatmo.internal.api.ListBodyResponse; import org.openhab.binding.netatmo.internal.api.NetatmoException; import org.openhab.binding.netatmo.internal.api.RestManager; import org.openhab.binding.netatmo.internal.api.SecurityApi; +import org.openhab.binding.netatmo.internal.api.WeatherApi; +import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea; import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope; import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.ServiceError; +import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule; +import org.openhab.binding.netatmo.internal.api.dto.NAMain; +import org.openhab.binding.netatmo.internal.api.dto.NAModule; import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration; import org.openhab.binding.netatmo.internal.config.BindingConfiguration; import org.openhab.binding.netatmo.internal.config.ConfigurationLevel; @@ -66,6 +76,7 @@ import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; @@ -286,6 +297,66 @@ public synchronized T executeUri(URI uri, HttpMethod method, Class clazz, } } + public void identifyAllModulesAndApplyAction(BiFunction> action) { + ThingUID accountUID = getThing().getUID(); + try { + AircareApi airCareApi = getRestManager(AircareApi.class); + if (airCareApi != null) { // Search Healthy Home Coaches + ListBodyResponse body = airCareApi.getHomeCoachData(null).getBody(); + if (body != null) { + body.getElements().stream().forEach(homeCoach -> action.apply(homeCoach, accountUID)); + } + } + WeatherApi weatherApi = getRestManager(WeatherApi.class); + if (weatherApi != null) { // Search owned or favorite stations + weatherApi.getFavoriteAndGuestStationsData().stream().forEach(station -> { + if (!station.isReadOnly() || getReadFriends()) { + action.apply(station, accountUID).ifPresent(stationUID -> station.getModules().values().stream() + .forEach(module -> action.apply(module, stationUID))); + } + }); + } + HomeApi homeApi = getRestManager(HomeApi.class); + if (homeApi != null) { // Search those depending from a home that has modules + not only weather modules + homeApi.getHomesData(null, null).stream() + .filter(h -> !(h.getFeatures().isEmpty() + || h.getFeatures().contains(FeatureArea.WEATHER) && h.getFeatures().size() == 1)) + .forEach(home -> { + action.apply(home, accountUID).ifPresent(homeUID -> { + home.getKnownPersons().forEach(person -> action.apply(person, homeUID)); + + Map bridgesUids = new HashMap<>(); + + home.getRooms().values().stream().forEach(room -> { + room.getModuleIds().stream().map(id -> home.getModules().get(id)) + .map(m -> m != null ? m.getType().feature : FeatureArea.NONE) + .filter(f -> FeatureArea.ENERGY.equals(f)).findAny().ifPresent(f -> { + action.apply(room, homeUID) + .ifPresent(roomUID -> bridgesUids.put(room.getId(), roomUID)); + }); + }); + + // Creating modules that have no bridge first, avoiding weather station itself + home.getModules().values().stream() + .filter(module -> module.getType().feature != FeatureArea.WEATHER) + .sorted(comparing(HomeDataModule::getBridge, nullsFirst(naturalOrder()))) + .forEach(module -> { + String bridgeId = module.getBridge(); + if (bridgeId == null) { + action.apply(module, homeUID).ifPresent( + moduleUID -> bridgesUids.put(module.getId(), moduleUID)); + } else { + action.apply(module, bridgesUids.getOrDefault(bridgeId, homeUID)); + } + }); + }); + }); + } + } catch (NetatmoException e) { + logger.warn("Error while identifying all modules : {}", e.getMessage()); + } + } + public boolean getReadFriends() { return bindingConf.readFriends; }