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.mybmw.internal.discovery;
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUPPORTED_THING_SET;
17 import java.lang.reflect.Field;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
22 import java.util.Optional;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.mybmw.internal.MyBMWConstants;
27 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
28 import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
29 import org.openhab.binding.mybmw.internal.handler.RemoteServiceHandler;
30 import org.openhab.binding.mybmw.internal.utils.Constants;
31 import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
32 import org.openhab.core.config.core.Configuration;
33 import org.openhab.core.config.discovery.AbstractDiscoveryService;
34 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
35 import org.openhab.core.config.discovery.DiscoveryService;
36 import org.openhab.core.thing.ThingUID;
37 import org.openhab.core.thing.binding.ThingHandler;
38 import org.openhab.core.thing.binding.ThingHandlerService;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * The {@link VehicleDiscovery} requests data from BMW API and is identifying the Vehicles after response
45 * @author Bernd Weymann - Initial contribution
48 public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
49 private static final Logger LOGGER = LoggerFactory.getLogger(VehicleDiscovery.class);
50 public static final String SUPPORTED_SUFFIX = "Supported";
51 public static final String ENABLE_SUFFIX = "Enable";
52 public static final String ENABLED_SUFFIX = "Enabled";
53 private static final int DISCOVERY_TIMEOUT = 10;
54 private Optional<MyBMWBridgeHandler> bridgeHandler = Optional.empty();
56 public VehicleDiscovery() {
57 super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
60 public void onResponse(List<Vehicle> vehicleList) {
61 bridgeHandler.ifPresent(bridge -> {
62 final ThingUID bridgeUID = bridge.getThing().getUID();
63 vehicleList.forEach(vehicle -> {
64 // the DriveTrain field in the delivered json is defining the Vehicle Type
65 String vehicleType = VehicleStatusUtils.vehicleType(vehicle.driveTrain, vehicle.model).toString();
66 SUPPORTED_THING_SET.forEach(entry -> {
67 if (entry.getId().equals(vehicleType)) {
68 ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId());
69 Map<String, String> properties = new HashMap<>();
71 properties.put("vehicleModel", vehicle.model);
72 properties.put("vehicleDriveTrain", vehicle.driveTrain);
73 properties.put("vehicleConstructionYear", Integer.toString(vehicle.year));
74 properties.put("vehicleBodytype", vehicle.bodyType);
76 properties.put("servicesSupported", getServices(vehicle, SUPPORTED_SUFFIX, true));
77 properties.put("servicesUnsupported", getServices(vehicle, SUPPORTED_SUFFIX, false));
78 String servicesEnabled = getServices(vehicle, ENABLED_SUFFIX, true) + Constants.SEMICOLON
79 + getServices(vehicle, ENABLE_SUFFIX, true);
80 properties.put("servicesEnabled", servicesEnabled.trim());
81 String servicesDisabled = getServices(vehicle, ENABLED_SUFFIX, false) + Constants.SEMICOLON
82 + getServices(vehicle, ENABLE_SUFFIX, false);
83 properties.put("servicesDisabled", servicesDisabled.trim());
85 // For RemoteServices we need to do it step-by-step
86 StringBuffer remoteServicesEnabled = new StringBuffer();
87 StringBuffer remoteServicesDisabled = new StringBuffer();
88 if (vehicle.capabilities.lock.isEnabled) {
89 remoteServicesEnabled.append(
90 RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
92 remoteServicesDisabled.append(
93 RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
95 if (vehicle.capabilities.unlock.isEnabled) {
96 remoteServicesEnabled.append(
97 RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
99 remoteServicesDisabled.append(
100 RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
102 if (vehicle.capabilities.lights.isEnabled) {
103 remoteServicesEnabled.append(
104 RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
106 remoteServicesDisabled.append(
107 RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
109 if (vehicle.capabilities.horn.isEnabled) {
110 remoteServicesEnabled.append(
111 RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
113 remoteServicesDisabled.append(
114 RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
116 if (vehicle.capabilities.vehicleFinder.isEnabled) {
117 remoteServicesEnabled.append(
118 RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
120 remoteServicesDisabled.append(
121 RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
123 if (vehicle.capabilities.climateNow.isEnabled) {
124 remoteServicesEnabled.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
125 + Constants.SEMICOLON);
127 remoteServicesDisabled
128 .append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
129 + Constants.SEMICOLON);
131 properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim());
132 properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim());
134 // Update Properties for already created Things
135 bridge.getThing().getThings().forEach(vehicleThing -> {
136 Configuration c = vehicleThing.getConfiguration();
137 if (c.containsKey(MyBMWConstants.VIN)) {
138 String thingVIN = c.get(MyBMWConstants.VIN).toString();
139 if (vehicle.vin.equals(thingVIN)) {
140 vehicleThing.setProperties(properties);
145 // Properties needed for functional Thing
146 properties.put(MyBMWConstants.VIN, vehicle.vin);
147 properties.put("vehicleBrand", vehicle.brand);
148 properties.put("refreshInterval",
149 Integer.toString(MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
151 String vehicleLabel = vehicle.brand + " " + vehicle.model;
152 Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
153 thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
154 .withRepresentationProperty(MyBMWConstants.VIN).withLabel(vehicleLabel)
155 .withProperties(convertedProperties).build());
163 public void setThingHandler(ThingHandler handler) {
164 if (handler instanceof MyBMWBridgeHandler) {
165 bridgeHandler = Optional.of((MyBMWBridgeHandler) handler);
166 bridgeHandler.get().setDiscoveryService(this);
171 public @Nullable ThingHandler getThingHandler() {
172 return bridgeHandler.orElse(null);
176 protected void startScan() {
177 bridgeHandler.ifPresent(MyBMWBridgeHandler::requestVehicles);
181 public void deactivate() {
185 public static String getServices(Vehicle vehicle, String suffix, boolean enabled) {
186 StringBuffer sb = new StringBuffer();
187 List<String> l = getObject(vehicle.capabilities, enabled);
188 for (String capEntry : l) {
189 // remove "is" prefix
190 String cut = capEntry.substring(2);
191 if (cut.endsWith(suffix)) {
192 if (sb.length() > 0) {
193 sb.append(Constants.SEMICOLON);
195 sb.append(cut.substring(0, cut.length() - suffix.length()));
198 return sb.toString();
202 * Get all field names from a DTO with a specific value
203 * Used to get e.g. all services which are "ACTIVATED"
206 * @param compare String which needs to map with the value
207 * @return String with all field names matching this value separated with Spaces
209 public static List<String> getObject(Object dto, Object compare) {
210 List<String> l = new ArrayList<String>();
211 for (Field field : dto.getClass().getDeclaredFields()) {
213 Object value = field.get(dto);
214 if (compare.equals(value)) {
215 l.add(field.getName());
217 } catch (IllegalArgumentException | IllegalAccessException e) {
218 LOGGER.debug("Field {} not found {}", compare, e.getMessage());