]> git.basschouten.com Git - openhab-addons.git/blob
d795ac16a3f7a2c5294be94f50f17f4e35449a71
[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.shelly.internal.discovery;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.util.ShellyUtils.substringBeforeLast;
17 import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
18
19 import java.io.IOException;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.TreeMap;
23
24 import javax.jmdns.ServiceInfo;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.openhab.binding.shelly.internal.api.ShellyApiException;
30 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
31 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
32 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
33 import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
34 import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
35 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
36 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
37 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
38 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
39 import org.openhab.core.config.discovery.DiscoveryResult;
40 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
41 import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
42 import org.openhab.core.i18n.LocaleProvider;
43 import org.openhab.core.io.net.http.HttpClientFactory;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.thing.ThingUID;
46 import org.osgi.service.cm.Configuration;
47 import org.osgi.service.cm.ConfigurationAdmin;
48 import org.osgi.service.component.ComponentContext;
49 import org.osgi.service.component.annotations.Activate;
50 import org.osgi.service.component.annotations.Component;
51 import org.osgi.service.component.annotations.Modified;
52 import org.osgi.service.component.annotations.Reference;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  * This class identifies Shelly devices by their mDNS service information.
58  *
59  * @author Markus Michels - Initial contribution
60  */
61 @NonNullByDefault
62 @Component(service = MDNSDiscoveryParticipant.class)
63 public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
64     private final Logger logger = LoggerFactory.getLogger(ShellyDiscoveryParticipant.class);
65     private final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
66     private final ShellyTranslationProvider messages;
67     private final HttpClient httpClient;
68     private final ConfigurationAdmin configurationAdmin;
69
70     @Activate
71     public ShellyDiscoveryParticipant(@Reference ConfigurationAdmin configurationAdmin,
72             @Reference HttpClientFactory httpClientFactory, @Reference LocaleProvider localeProvider,
73             @Reference ShellyTranslationProvider translationProvider, ComponentContext componentContext) {
74         logger.debug("Activating ShellyDiscovery service");
75         this.configurationAdmin = configurationAdmin;
76         this.messages = translationProvider;
77         this.httpClient = httpClientFactory.getCommonHttpClient();
78         bindingConfig.updateFromProperties(componentContext.getProperties());
79     }
80
81     @Override
82     public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
83         return SUPPORTED_THING_TYPES_UIDS;
84     }
85
86     @Override
87     public String getServiceType() {
88         return SERVICE_TYPE;
89     }
90
91     /**
92      * Process updates to Binding Config
93      *
94      * @param componentContext
95      */
96     @Modified
97     protected void modified(final ComponentContext componentContext) {
98         logger.debug("Shelly Binding Configuration refreshed");
99         bindingConfig.updateFromProperties(componentContext.getProperties());
100     }
101
102     @Nullable
103     @Override
104     public DiscoveryResult createResult(final ServiceInfo service) {
105         String name = service.getName().toLowerCase(); // Shelly Duo: Name starts with" Shelly" rather than "shelly"
106         if (!name.startsWith("shelly")) {
107             return null;
108         }
109
110         String address = "";
111         try {
112             String mode = "";
113             String model = "unknown";
114             String deviceName = "";
115             ThingUID thingUID = null;
116             ShellyDeviceProfile profile;
117             Map<String, Object> properties = new TreeMap<>();
118
119             name = service.getName().toLowerCase();
120             String[] hostAddresses = service.getHostAddresses();
121             if ((hostAddresses != null) && (hostAddresses.length > 0)) {
122                 address = hostAddresses[0];
123             }
124             if (address.isEmpty()) {
125                 logger.trace("{}: Shelly device discovered with empty IP address (service-name={})", name, service);
126                 return null;
127             }
128             String thingType = service.getQualifiedName().contains(SERVICE_TYPE) && name.contains("-")
129                     ? substringBeforeLast(name, "-")
130                     : name;
131             logger.debug("{}: Shelly device discovered: IP-Adress={}, type={}", name, address, thingType);
132
133             // Get device settings
134             Configuration serviceConfig = configurationAdmin.getConfiguration("binding.shelly");
135             if (serviceConfig.getProperties() != null) {
136                 bindingConfig.updateFromProperties(serviceConfig.getProperties());
137             }
138
139             ShellyThingConfiguration config = new ShellyThingConfiguration();
140             config.deviceIp = address;
141             config.userId = bindingConfig.defaultUserId;
142             config.password = bindingConfig.defaultPassword;
143
144             boolean gen2 = "2".equals(service.getPropertyString("gen"));
145             try {
146                 ShellyApiInterface api = gen2 ? new Shelly2ApiRpc(name, config, httpClient)
147                         : new Shelly1HttpApi(name, config, httpClient);
148                 if (name.contains("plus1pm")) {
149                     int i = 1;
150                 }
151                 api.initialize();
152                 profile = api.getDeviceProfile(thingType);
153                 api.close();
154                 logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
155                 deviceName = profile.name;
156                 model = profile.deviceType;
157                 mode = profile.mode;
158                 properties = ShellyBaseHandler.fillDeviceProperties(profile);
159                 logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
160                         profile.deviceType, mode.isEmpty() ? "<standard>" : mode, deviceName);
161
162                 // get thing type from device name
163                 thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
164             } catch (ShellyApiException e) {
165                 ShellyApiResult result = e.getApiResult();
166                 if (result.isHttpAccessUnauthorized()) {
167                     logger.info("{}: {}", name, messages.get("discovery.protected", address));
168
169                     // create shellyunknown thing - will be changed during thing initialization with valid credentials
170                     thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
171                 } else {
172                     logger.debug("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
173                 }
174             } catch (IllegalArgumentException e) { // maybe some format description was buggy
175                 logger.debug("{}: Discovery failed!", name, e);
176             }
177
178             if (thingUID != null) {
179                 addProperty(properties, CONFIG_DEVICEIP, address);
180                 addProperty(properties, PROPERTY_MODEL_ID, model);
181                 addProperty(properties, PROPERTY_SERVICE_NAME, name);
182                 addProperty(properties, PROPERTY_DEV_NAME, deviceName);
183                 addProperty(properties, PROPERTY_DEV_TYPE, thingType);
184                 addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
185                 addProperty(properties, PROPERTY_DEV_MODE, mode);
186
187                 logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());
188                 String thingLabel = deviceName.isEmpty() ? name + " - " + address
189                         : deviceName + " (" + name + "@" + address + ")";
190                 return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
191                         .withRepresentationProperty(PROPERTY_DEV_NAME).build();
192             }
193         } catch (IOException | NullPointerException e) {
194             // maybe some format description was buggy
195             logger.debug("{}: Exception on processing serviceInfo '{}'", name, service.getNiceTextString(), e);
196         }
197         return null;
198     }
199
200     private void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
201         properties.put(key, value != null ? value : "");
202     }
203
204     @Nullable
205     @Override
206     public ThingUID getThingUID(@Nullable ServiceInfo service) throws IllegalArgumentException {
207         logger.debug("ServiceInfo {}", service);
208         if (service == null) {
209             throw new IllegalArgumentException("service must not be null!");
210         }
211         String serviceName = service.getName();
212         if (serviceName == null) {
213             throw new IllegalArgumentException("serviceName must not be null!");
214         }
215         serviceName = serviceName.toLowerCase();
216         if (!serviceName.contains(VENDOR.toLowerCase())) {
217             logger.debug("Not a " + VENDOR + " device!");
218             return null;
219         }
220         return ShellyThingCreator.getThingUID(serviceName, "", "", false);
221     }
222 }