2 * Copyright (c) 2010-2024 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.somfytahoma.internal.discovery;
15 import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
17 import java.util.HashMap;
18 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.somfytahoma.internal.handler.SomfyTahomaBridgeHandler;
27 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaActionGroup;
28 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaDevice;
29 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaGateway;
30 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaRootPlace;
31 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaSetup;
32 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaState;
33 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaSubPlace;
34 import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
35 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.thing.ThingUID;
39 import org.osgi.service.component.annotations.Component;
40 import org.osgi.service.component.annotations.ServiceScope;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link SomfyTahomaItemDiscoveryService} discovers rollershutters and
46 * action groups associated with your TahomaLink cloud account.
48 * @author Ondrej Pecta - Initial contribution
49 * @author Laurent Garnier - Include the place into the inbox label (when defined for the device)
51 @Component(scope = ServiceScope.PROTOTYPE, service = SomfyTahomaItemDiscoveryService.class)
53 public class SomfyTahomaItemDiscoveryService extends AbstractThingHandlerDiscoveryService<SomfyTahomaBridgeHandler> {
55 private static final int DISCOVERY_TIMEOUT_SEC = 10;
56 private static final int DISCOVERY_REFRESH_SEC = 3600;
58 private final Logger logger = LoggerFactory.getLogger(SomfyTahomaItemDiscoveryService.class);
60 private @Nullable ScheduledFuture<?> discoveryJob;
62 public SomfyTahomaItemDiscoveryService() {
63 super(SomfyTahomaBridgeHandler.class, DISCOVERY_TIMEOUT_SEC);
64 logger.debug("Creating discovery service");
68 protected void startBackgroundDiscovery() {
69 logger.debug("Starting SomfyTahoma background discovery");
71 ScheduledFuture<?> localDiscoveryJob = discoveryJob;
72 if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
73 discoveryJob = scheduler.scheduleWithFixedDelay(this::runDiscovery, 10, DISCOVERY_REFRESH_SEC,
79 protected void stopBackgroundDiscovery() {
80 logger.debug("Stopping SomfyTahoma background discovery");
81 ScheduledFuture<?> localDiscoveryJob = discoveryJob;
82 if (localDiscoveryJob != null) {
83 localDiscoveryJob.cancel(true);
88 public Set<ThingTypeUID> getSupportedThingTypes() {
89 return SUPPORTED_THING_TYPES_UIDS;
93 protected void startScan() {
97 private synchronized void runDiscovery() {
98 logger.debug("Starting scanning for things...");
100 if (ThingStatus.ONLINE == thingHandler.getThing().getStatus()) {
101 SomfyTahomaSetup setup = thingHandler.getSetup();
107 for (SomfyTahomaDevice device : setup.getDevices()) {
108 discoverDevice(device, setup);
110 for (SomfyTahomaGateway gw : setup.getGateways()) {
111 gatewayDiscovered(gw);
114 // local mode does not have action groups
115 if (!thingHandler.isDevModeReady()) {
116 List<SomfyTahomaActionGroup> actions = thingHandler.listActionGroups();
118 for (SomfyTahomaActionGroup group : actions) {
119 String oid = group.getOid();
120 String label = group.getLabel();
122 // actiongroups use oid as deviceURL
123 actionGroupDiscovered(label, oid);
127 logger.debug("Cannot start discovery since the bridge is not online!");
131 private void discoverDevice(SomfyTahomaDevice device, SomfyTahomaSetup setup) {
132 logger.debug("url: {}", device.getDeviceURL());
133 String place = getPlaceLabel(setup, device.getPlaceOID());
134 String widget = device.getDefinition().getWidgetName();
135 switch (device.getDefinition().getUiClass()) {
137 // widget: PositionableHorizontalAwning
138 // widget: DynamicAwning
139 // widget: UpDownHorizontalAwning
140 deviceDiscovered(device, THING_TYPE_AWNING, place);
142 case CLASS_CONTACT_SENSOR:
143 // widget: ContactSensor
144 deviceDiscovered(device, THING_TYPE_CONTACTSENSOR, place);
147 deviceDiscovered(device, THING_TYPE_CURTAIN, place);
149 case CLASS_EXTERIOR_SCREEN:
150 // widget: PositionableScreen
151 deviceDiscovered(device, THING_TYPE_EXTERIORSCREEN, place);
153 case CLASS_EXTERIOR_VENETIAN_BLIND:
154 // widget: PositionableExteriorVenetianBlind
155 deviceDiscovered(device, THING_TYPE_EXTERIORVENETIANBLIND, place);
157 case CLASS_GARAGE_DOOR:
158 deviceDiscovered(device, THING_TYPE_GARAGEDOOR, place);
161 if ("DimmerLight".equals(widget) || "DynamicLight".equals(widget)) {
162 // widget: DimmerLight
163 // widget: DynamicLight
164 deviceDiscovered(device, THING_TYPE_DIMMER_LIGHT, place);
166 // widget: TimedOnOffLight
167 // widget: StatefulOnOffLight
168 deviceDiscovered(device, THING_TYPE_LIGHT, place);
171 case CLASS_LIGHT_SENSOR:
172 deviceDiscovered(device, THING_TYPE_LIGHTSENSOR, place);
174 case CLASS_OCCUPANCY_SENSOR:
175 // widget: OccupancySensor
176 deviceDiscovered(device, THING_TYPE_OCCUPANCYSENSOR, place);
179 // widget: StatefulOnOff
180 deviceDiscovered(device, THING_TYPE_ONOFF, place);
182 case CLASS_ROLLER_SHUTTER:
183 if (isSilentRollerShutter(device)) {
184 // widget: PositionableRollerShutterWithLowSpeedManagement
185 deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER_SILENT, place);
186 } else if (isUnoRollerShutter(device)) {
187 // widget: PositionableRollerShutterUno
188 deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER_UNO, place);
190 // widget: PositionableRollerShutter
191 // widget: PositionableTiltedRollerShutter
192 deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER, place);
196 // widget: DynamicShutter
197 deviceDiscovered(device, THING_TYPE_SHUTTER, place);
200 // widget: PositionableTiltedScreen
201 deviceDiscovered(device, THING_TYPE_SCREEN, place);
203 case CLASS_SMOKE_SENSOR:
204 // widget: SmokeSensor
205 deviceDiscovered(device, THING_TYPE_SMOKESENSOR, place);
207 case CLASS_VENETIAN_BLIND:
208 // widget: DynamicVenetianBlind
209 if (hasCommmand(device, "setOrientation")) {
210 deviceDiscovered(device, THING_TYPE_VENETIANBLIND, place);
212 // simple venetian blind without orientation
213 deviceDiscovered(device, THING_TYPE_SHUTTER, place);
217 // widget: PositionableTiltedWindow
218 deviceDiscovered(device, THING_TYPE_WINDOW, place);
221 if (device.getDeviceURL().startsWith("internal:")) {
222 // widget: TSKAlarmController
223 deviceDiscovered(device, THING_TYPE_INTERNAL_ALARM, place);
224 } else if ("MyFoxAlarmController".equals(widget)) {
225 // widget: MyFoxAlarmController
226 deviceDiscovered(device, THING_TYPE_MYFOX_ALARM, place);
228 deviceDiscovered(device, THING_TYPE_EXTERNAL_ALARM, place);
232 if (hasState(device, CYCLIC_BUTTON_STATE)) {
233 deviceDiscovered(device, THING_TYPE_POD, place);
236 case CLASS_HEATING_SYSTEM:
237 if ("SomfyThermostat".equals(widget)) {
238 deviceDiscovered(device, THING_TYPE_THERMOSTAT, place);
239 } else if ("ValveHeatingTemperatureInterface".equals(widget)) {
240 deviceDiscovered(device, THING_TYPE_VALVE_HEATING_SYSTEM, place);
241 } else if (isOnOffHeatingSystem(device)) {
242 deviceDiscovered(device, THING_TYPE_ONOFF_HEATING_SYSTEM, place);
243 } else if (isZwaveHeatingSystem(device)) {
244 deviceDiscovered(device, THING_TYPE_ZWAVE_HEATING_SYSTEM, place);
246 logUnsupportedDevice(device);
249 case CLASS_EXTERIOR_HEATING_SYSTEM:
250 if ("DimmerExteriorHeating".equals(widget)) {
251 // widget: DimmerExteriorHeating
252 deviceDiscovered(device, THING_TYPE_EXTERIOR_HEATING_SYSTEM, place);
254 logUnsupportedDevice(device);
257 case CLASS_HUMIDITY_SENSOR:
258 if (hasState(device, WATER_DETECTION_STATE)) {
259 deviceDiscovered(device, THING_TYPE_WATERSENSOR, place);
261 // widget: RelativeHumiditySensor
262 deviceDiscovered(device, THING_TYPE_HUMIDITYSENSOR, place);
265 case CLASS_DOOR_LOCK:
266 // widget: UnlockDoorLockWithUnknownPosition
267 deviceDiscovered(device, THING_TYPE_DOOR_LOCK, place);
270 if ("BioclimaticPergola".equals(widget)) {
271 // widget: BioclimaticPergola
272 deviceDiscovered(device, THING_TYPE_BIOCLIMATIC_PERGOLA, place);
274 deviceDiscovered(device, THING_TYPE_PERGOLA, place);
277 case CLASS_WINDOW_HANDLE:
278 // widget: ThreeWayWindowHandle
279 deviceDiscovered(device, THING_TYPE_WINDOW_HANDLE, place);
281 case CLASS_TEMPERATURE_SENSOR:
282 // widget: TemperatureSensor
283 deviceDiscovered(device, THING_TYPE_TEMPERATURESENSOR, place);
286 deviceDiscovered(device, THING_TYPE_GATE, place);
288 case CLASS_ELECTRICITY_SENSOR:
289 if (hasEnergyConsumption(device)) {
290 deviceDiscovered(device, THING_TYPE_ELECTRICITYSENSOR, place);
292 logUnsupportedDevice(device);
295 case CLASS_WATER_HEATING_SYSTEM:
296 // widget: DomesticHotWaterProduction
297 if ("DomesticHotWaterProduction".equals(widget)) {
298 deviceDiscovered(device, THING_TYPE_WATERHEATINGSYSTEM, place);
300 logUnsupportedDevice(device);
305 deviceDiscovered(device, THING_TYPE_DOCK, place);
308 deviceDiscovered(device, THING_TYPE_SIREN, place);
310 case CLASS_ADJUSTABLE_SLATS_ROLLER_SHUTTER:
311 deviceDiscovered(device, THING_TYPE_ADJUSTABLE_SLATS_ROLLERSHUTTER, place);
314 if (hasMyfoxShutter(device)) {
315 // widget: MyFoxSecurityCamera
316 deviceDiscovered(device, THING_TYPE_MYFOX_CAMERA, place);
318 logUnsupportedDevice(device);
321 case CLASS_HITACHI_HEATING_SYSTEM:
322 if ("HitachiAirToWaterHeatingZone".equals(widget)) {
323 // widget: HitachiAirToWaterHeatingZone
324 deviceDiscovered(device, THING_TYPE_HITACHI_ATWHZ, place);
325 } else if ("HitachiAirToWaterMainComponent".equals(widget)) {
326 // widget: HitachiAirToWaterMainComponent
327 deviceDiscovered(device, THING_TYPE_HITACHI_ATWMC, place);
328 } else if ("HitachiDHW".equals(widget)) {
329 // widget: HitachiDHW
330 deviceDiscovered(device, THING_TYPE_HITACHI_DHW, place);
332 logUnsupportedDevice(device);
335 case CLASS_RAIN_SENSOR:
336 if ("RainSensor".equals(widget)) {
337 // widget: RainSensor
338 deviceDiscovered(device, THING_TYPE_RAINSENSOR, place);
340 logUnsupportedDevice(device);
343 case CLASS_CARBON_DIOXIDE_SENSOR:
345 deviceDiscovered(device, THING_TYPE_CARBON_DIOXIDE_SENSOR, place);
347 case CLASS_NOISE_SENSOR:
348 // widget: NoiseSensor
349 deviceDiscovered(device, THING_TYPE_NOISE_SENSOR, place);
351 case THING_PROTOCOL_GATEWAY:
352 case THING_REMOTE_CONTROLLER:
353 // widget: AlarmRemoteController
354 case THING_NETWORK_COMPONENT:
355 case THING_CONFIGURATION_COMPONENT:
356 // widget: NetatmoHome
362 logUnsupportedDevice(device);
366 private @Nullable String getPlaceLabel(SomfyTahomaSetup setup, String oid) {
367 SomfyTahomaRootPlace root = setup.getRootPlace();
368 if (!oid.isEmpty() && root != null) {
369 for (SomfyTahomaSubPlace place : root.getSubPlaces()) {
370 if (oid.equals(place.getOid())) {
371 return place.getLabel();
378 private boolean isStateLess(SomfyTahomaDevice device) {
379 return device.getStates().isEmpty() || (device.getStates().size() == 1 && hasState(device, STATUS_STATE));
382 private void logUnsupportedDevice(SomfyTahomaDevice device) {
383 if (!isStateLess(device)) {
384 logger.debug("Detected a new unsupported device: {} with widgetName: {}",
385 device.getDefinition().getUiClass(), device.getDefinition().getWidgetName());
386 logger.debug("If you want to add the support, please create a new issue and attach the information below");
387 logger.debug("Device definition:\n{}", device.getDefinition());
389 StringBuilder sb = new StringBuilder().append('\n');
390 for (SomfyTahomaState state : device.getStates()) {
391 sb.append(state.toString()).append('\n');
393 logger.debug("Current device states: {}", sb);
397 private boolean hasState(SomfyTahomaDevice device, String state) {
398 return device.getDefinition().getStates().stream().anyMatch(st -> state.equals(st.getQualifiedName()));
401 private boolean hasMyfoxShutter(SomfyTahomaDevice device) {
402 return hasState(device, MYFOX_SHUTTER_STATUS_STATE);
405 private boolean hasEnergyConsumption(SomfyTahomaDevice device) {
406 return hasState(device, ENERGY_CONSUMPTION_STATE);
409 private boolean isSilentRollerShutter(SomfyTahomaDevice device) {
410 return "PositionableRollerShutterWithLowSpeedManagement".equals(device.getDefinition().getWidgetName());
413 private boolean isUnoRollerShutter(SomfyTahomaDevice device) {
414 return "PositionableRollerShutterUno".equals(device.getDefinition().getWidgetName());
417 private boolean isOnOffHeatingSystem(SomfyTahomaDevice device) {
418 return hasCommmand(device, COMMAND_SET_HEATINGLEVEL);
421 private boolean isZwaveHeatingSystem(SomfyTahomaDevice device) {
422 return hasState(device, ZWAVE_SET_POINT_TYPE_STATE);
425 private boolean hasCommmand(SomfyTahomaDevice device, String command) {
426 return device.getDefinition().getCommands().stream().anyMatch(cmd -> command.equals(cmd.getCommandName()));
429 private void deviceDiscovered(SomfyTahomaDevice device, ThingTypeUID thingTypeUID, @Nullable String place) {
430 String label = device.getLabel();
431 if (place != null && !place.isBlank()) {
432 label += " (" + place + ")";
434 deviceDiscovered(label, device.getDeviceURL(), thingTypeUID, hasState(device, RSSI_LEVEL_STATE));
437 private void deviceDiscovered(String label, String deviceURL, ThingTypeUID thingTypeUID, boolean rssi) {
438 Map<String, Object> properties = new HashMap<>();
439 properties.put("url", deviceURL);
440 properties.put(NAME_STATE, label);
442 properties.put(RSSI_LEVEL_STATE, "-1");
445 ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(),
446 deviceURL.replaceAll("[^a-zA-Z0-9_]", ""));
448 logger.debug("Detected a/an {} - label: {} device URL: {}", thingTypeUID.getId(), label, deviceURL);
449 thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID).withProperties(properties)
450 .withRepresentationProperty("url").withLabel(label).withBridge(thingHandler.getThing().getUID())
454 private void actionGroupDiscovered(String label, String deviceURL) {
455 deviceDiscovered(label, deviceURL, THING_TYPE_ACTIONGROUP, false);
458 private void gatewayDiscovered(SomfyTahomaGateway gw) {
459 Map<String, Object> properties = new HashMap<>(1);
460 String type = gatewayTypes.getOrDefault(gw.getType(), "UNKNOWN");
461 String id = gw.getGatewayId();
462 properties.put("id", id);
463 properties.put("type", type);
465 ThingUID thingUID = new ThingUID(THING_TYPE_GATEWAY, thingHandler.getThing().getUID(), id);
467 logger.debug("Detected a gateway with id: {} and type: {}", id, type);
468 thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_GATEWAY)
469 .withProperties(properties).withRepresentationProperty("id").withLabel("Somfy Gateway (" + type + ")")
470 .withBridge(thingHandler.getThing().getUID()).build());