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.shelly.internal.discovery;
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;
19 import java.io.IOException;
20 import java.net.Inet4Address;
23 import java.util.TreeMap;
25 import javax.jmdns.ServiceInfo;
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.Shelly1HttpApi;
35 import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
36 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
37 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
38 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
39 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
40 import org.openhab.core.config.discovery.DiscoveryResult;
41 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
42 import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
43 import org.openhab.core.i18n.LocaleProvider;
44 import org.openhab.core.io.net.http.HttpClientFactory;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.ThingUID;
47 import org.osgi.service.cm.Configuration;
48 import org.osgi.service.cm.ConfigurationAdmin;
49 import org.osgi.service.component.ComponentContext;
50 import org.osgi.service.component.annotations.Activate;
51 import org.osgi.service.component.annotations.Component;
52 import org.osgi.service.component.annotations.Modified;
53 import org.osgi.service.component.annotations.Reference;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * This class identifies Shelly devices by their mDNS service information.
60 * @author Markus Michels - Initial contribution
63 @Component(service = MDNSDiscoveryParticipant.class)
64 public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
65 private final Logger logger = LoggerFactory.getLogger(ShellyDiscoveryParticipant.class);
66 private final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
67 private final ShellyTranslationProvider messages;
68 private final HttpClient httpClient;
69 private final ConfigurationAdmin configurationAdmin;
72 public ShellyDiscoveryParticipant(@Reference ConfigurationAdmin configurationAdmin,
73 @Reference HttpClientFactory httpClientFactory, @Reference LocaleProvider localeProvider,
74 @Reference ShellyTranslationProvider translationProvider, ComponentContext componentContext) {
75 logger.debug("Activating ShellyDiscovery service");
76 this.configurationAdmin = configurationAdmin;
77 this.messages = translationProvider;
78 this.httpClient = httpClientFactory.getCommonHttpClient();
79 bindingConfig.updateFromProperties(componentContext.getProperties());
83 public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
84 return SUPPORTED_THING_TYPES_UIDS;
88 public String getServiceType() {
93 * Process updates to Binding Config
95 * @param componentContext
98 protected void modified(final ComponentContext componentContext) {
99 logger.debug("Shelly Binding Configuration refreshed");
100 bindingConfig.updateFromProperties(componentContext.getProperties());
105 public DiscoveryResult createResult(final ServiceInfo service) {
106 String name = service.getName().toLowerCase(); // Shelly Duo: Name starts with" Shelly" rather than "shelly"
107 if (!name.startsWith("shelly")) {
114 String model = "unknown";
115 String deviceName = "";
116 ThingUID thingUID = null;
117 ShellyDeviceProfile profile;
118 Map<String, Object> properties = new TreeMap<>();
120 name = service.getName().toLowerCase();
121 Inet4Address[] hostAddresses = service.getInet4Addresses();
122 if ((hostAddresses != null) && (hostAddresses.length > 0)) {
123 address = substringAfter(hostAddresses[0].toString(), "/");
125 if (address.isEmpty()) {
126 logger.trace("{}: Shelly device discovered with empty IP address (service-name={})", name, service);
129 String thingType = service.getQualifiedName().contains(SERVICE_TYPE) && name.contains("-")
130 ? substringBeforeLast(name, "-")
132 logger.debug("{}: Shelly device discovered: IP-Adress={}, type={}", name, address, thingType);
134 // Get device settings
135 Configuration serviceConfig = configurationAdmin.getConfiguration("binding.shelly");
136 if (serviceConfig.getProperties() != null) {
137 bindingConfig.updateFromProperties(serviceConfig.getProperties());
140 ShellyThingConfiguration config = new ShellyThingConfiguration();
141 config.deviceIp = address;
142 config.userId = bindingConfig.defaultUserId;
143 config.password = bindingConfig.defaultPassword;
145 boolean gen2 = "2".equals(service.getPropertyString("gen"));
146 ShellyApiInterface api = null;
148 api = gen2 ? new Shelly2ApiRpc(name, config, httpClient) : new Shelly1HttpApi(name, config, httpClient);
150 profile = api.getDeviceProfile(thingType);
151 logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
152 deviceName = profile.name;
153 model = profile.deviceType;
155 properties = ShellyBaseHandler.fillDeviceProperties(profile);
156 logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
157 profile.deviceType, mode.isEmpty() ? "<standard>" : mode, deviceName);
159 // get thing type from device name
160 thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
161 } catch (ShellyApiException e) {
162 ShellyApiResult result = e.getApiResult();
163 if (result.isHttpAccessUnauthorized()) {
164 logger.info("{}: {}", name, messages.get("discovery.protected", address));
166 // create shellyunknown thing - will be changed during thing initialization with valid credentials
167 thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
169 logger.debug("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
171 } catch (IllegalArgumentException e) { // maybe some format description was buggy
172 logger.debug("{}: Discovery failed!", name, e);
179 if (thingUID != null) {
180 addProperty(properties, CONFIG_DEVICEIP, address);
181 addProperty(properties, PROPERTY_MODEL_ID, model);
182 addProperty(properties, PROPERTY_SERVICE_NAME, name);
183 addProperty(properties, PROPERTY_DEV_NAME, deviceName);
184 addProperty(properties, PROPERTY_DEV_TYPE, thingType);
185 addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
186 addProperty(properties, PROPERTY_DEV_MODE, mode);
188 logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());
189 String thingLabel = deviceName.isEmpty() ? name + " - " + address
190 : deviceName + " (" + name + "@" + address + ")";
191 return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
192 .withRepresentationProperty(PROPERTY_SERVICE_NAME).build();
194 } catch (IOException | NullPointerException e) {
195 // maybe some format description was buggy
196 logger.debug("{}: Exception on processing serviceInfo '{}'", name, service.getNiceTextString(), e);
201 private void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
202 properties.put(key, value != null ? value : "");
207 public ThingUID getThingUID(@Nullable ServiceInfo service) throws IllegalArgumentException {
208 logger.debug("ServiceInfo {}", service);
209 if (service == null) {
210 throw new IllegalArgumentException("service must not be null!");
212 String serviceName = service.getName();
213 if (serviceName == null) {
214 throw new IllegalArgumentException("serviceName must not be null!");
216 serviceName = serviceName.toLowerCase();
217 if (!serviceName.contains(VENDOR.toLowerCase())) {
218 logger.debug("Not a " + VENDOR + " device!");
221 return ShellyThingCreator.getThingUID(serviceName, "", "", false);