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