2 * Copyright (c) 2010-2022 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.AbstractDiscoveryService;
35 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
36 import org.openhab.core.config.discovery.DiscoveryService;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.ThingUID;
40 import org.openhab.core.thing.binding.ThingHandler;
41 import org.openhab.core.thing.binding.ThingHandlerService;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * The {@link SomfyTahomaItemDiscoveryService} discovers rollershutters and
47 * action groups associated with your TahomaLink cloud account.
49 * @author Ondrej Pecta - Initial contribution
50 * @author Laurent Garnier - Include the place into the inbox label (when defined for the device)
53 public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
54 implements DiscoveryService, ThingHandlerService {
56 private static final int DISCOVERY_TIMEOUT_SEC = 10;
57 private static final int DISCOVERY_REFRESH_SEC = 3600;
59 private final Logger logger = LoggerFactory.getLogger(SomfyTahomaItemDiscoveryService.class);
61 private @Nullable SomfyTahomaBridgeHandler bridgeHandler;
63 private @Nullable ScheduledFuture<?> discoveryJob;
65 public SomfyTahomaItemDiscoveryService() {
66 super(DISCOVERY_TIMEOUT_SEC);
67 logger.debug("Creating discovery service");
71 public void activate() {
76 public void deactivate() {
81 public void setThingHandler(@NonNullByDefault({}) ThingHandler handler) {
82 if (handler instanceof SomfyTahomaBridgeHandler) {
83 bridgeHandler = (SomfyTahomaBridgeHandler) handler;
88 public @Nullable ThingHandler getThingHandler() {
93 protected void startBackgroundDiscovery() {
94 logger.debug("Starting SomfyTahoma background discovery");
96 ScheduledFuture<?> localDiscoveryJob = discoveryJob;
97 if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
98 discoveryJob = scheduler.scheduleWithFixedDelay(this::runDiscovery, 10, DISCOVERY_REFRESH_SEC,
104 protected void stopBackgroundDiscovery() {
105 logger.debug("Stopping SomfyTahoma background discovery");
106 ScheduledFuture<?> localDiscoveryJob = discoveryJob;
107 if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
108 localDiscoveryJob.cancel(true);
113 public Set<ThingTypeUID> getSupportedThingTypes() {
114 return SUPPORTED_THING_TYPES_UIDS;
118 protected void startScan() {
122 private synchronized void runDiscovery() {
123 logger.debug("Starting scanning for things...");
125 SomfyTahomaBridgeHandler localBridgeHandler = bridgeHandler;
126 if (localBridgeHandler != null && ThingStatus.ONLINE == localBridgeHandler.getThing().getStatus()) {
127 SomfyTahomaSetup setup = localBridgeHandler.getSetup();
133 for (SomfyTahomaDevice device : setup.getDevices()) {
134 discoverDevice(device, setup);
136 for (SomfyTahomaGateway gw : setup.getGateways()) {
137 gatewayDiscovered(gw);
140 List<SomfyTahomaActionGroup> actions = localBridgeHandler.listActionGroups();
142 for (SomfyTahomaActionGroup group : actions) {
143 String oid = group.getOid();
144 String label = group.getLabel();
146 // actiongroups use oid as deviceURL
147 actionGroupDiscovered(label, oid, oid);
150 logger.debug("Cannot start discovery since the bridge is not online!");
154 private void discoverDevice(SomfyTahomaDevice device, SomfyTahomaSetup setup) {
155 logger.debug("url: {}", device.getDeviceURL());
156 String place = getPlaceLabel(setup, device.getPlaceOID());
157 switch (device.getUiClass()) {
159 // widget: PositionableHorizontalAwning
160 // widget: DynamicAwning
161 // widget: UpDownHorizontalAwning
162 deviceDiscovered(device, THING_TYPE_AWNING, place);
164 case CLASS_CONTACT_SENSOR:
165 // widget: ContactSensor
166 deviceDiscovered(device, THING_TYPE_CONTACTSENSOR, place);
169 deviceDiscovered(device, THING_TYPE_CURTAIN, place);
171 case CLASS_EXTERIOR_SCREEN:
172 // widget: PositionableScreen
173 deviceDiscovered(device, THING_TYPE_EXTERIORSCREEN, place);
175 case CLASS_EXTERIOR_VENETIAN_BLIND:
176 // widget: PositionableExteriorVenetianBlind
177 deviceDiscovered(device, THING_TYPE_EXTERIORVENETIANBLIND, place);
179 case CLASS_GARAGE_DOOR:
180 deviceDiscovered(device, THING_TYPE_GARAGEDOOR, place);
183 if ("DimmerLight".equals(device.getWidget()) || "DynamicLight".equals(device.getWidget())) {
184 // widget: DimmerLight
185 // widget: DynamicLight
186 deviceDiscovered(device, THING_TYPE_DIMMER_LIGHT, place);
188 // widget: TimedOnOffLight
189 // widget: StatefulOnOffLight
190 deviceDiscovered(device, THING_TYPE_LIGHT, place);
193 case CLASS_LIGHT_SENSOR:
194 deviceDiscovered(device, THING_TYPE_LIGHTSENSOR, place);
196 case CLASS_OCCUPANCY_SENSOR:
197 // widget: OccupancySensor
198 deviceDiscovered(device, THING_TYPE_OCCUPANCYSENSOR, place);
201 // widget: StatefulOnOff
202 deviceDiscovered(device, THING_TYPE_ONOFF, place);
204 case CLASS_ROLLER_SHUTTER:
205 if (isSilentRollerShutter(device)) {
206 // widget: PositionableRollerShutterWithLowSpeedManagement
207 deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER_SILENT, place);
208 } else if (isUnoRollerShutter(device)) {
209 // widget: PositionableRollerShutterUno
210 deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER_UNO, place);
212 // widget: PositionableRollerShutter
213 // widget: PositionableTiltedRollerShutter
214 deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER, place);
218 // widget: DynamicShutter
219 deviceDiscovered(device, THING_TYPE_SHUTTER, place);
222 // widget: PositionableTiltedScreen
223 deviceDiscovered(device, THING_TYPE_SCREEN, place);
225 case CLASS_SMOKE_SENSOR:
226 // widget: SmokeSensor
227 deviceDiscovered(device, THING_TYPE_SMOKESENSOR, place);
229 case CLASS_VENETIAN_BLIND:
230 // widget: DynamicVenetianBlind
231 if (hasCommmand(device, "setOrientation")) {
232 deviceDiscovered(device, THING_TYPE_VENETIANBLIND, place);
234 // simple venetian blind without orientation
235 deviceDiscovered(device, THING_TYPE_SHUTTER, place);
239 // widget: PositionableTiltedWindow
240 deviceDiscovered(device, THING_TYPE_WINDOW, place);
243 if (device.getDeviceURL().startsWith("internal:")) {
244 // widget: TSKAlarmController
245 deviceDiscovered(device, THING_TYPE_INTERNAL_ALARM, place);
246 } else if ("MyFoxAlarmController".equals(device.getWidget())) {
247 // widget: MyFoxAlarmController
248 deviceDiscovered(device, THING_TYPE_MYFOX_ALARM, place);
250 deviceDiscovered(device, THING_TYPE_EXTERNAL_ALARM, place);
254 if (hasState(device, CYCLIC_BUTTON_STATE)) {
255 deviceDiscovered(device, THING_TYPE_POD, place);
258 case CLASS_HEATING_SYSTEM:
259 if ("SomfyThermostat".equals(device.getWidget())) {
260 deviceDiscovered(device, THING_TYPE_THERMOSTAT, place);
261 } else if ("ValveHeatingTemperatureInterface".equals(device.getWidget())) {
262 deviceDiscovered(device, THING_TYPE_VALVE_HEATING_SYSTEM, place);
263 } else if (isOnOffHeatingSystem(device)) {
264 deviceDiscovered(device, THING_TYPE_ONOFF_HEATING_SYSTEM, place);
265 } else if (isZwaveHeatingSystem(device)) {
266 deviceDiscovered(device, THING_TYPE_ZWAVE_HEATING_SYSTEM, place);
268 logUnsupportedDevice(device);
271 case CLASS_EXTERIOR_HEATING_SYSTEM:
272 if ("DimmerExteriorHeating".equals(device.getWidget())) {
273 // widget: DimmerExteriorHeating
274 deviceDiscovered(device, THING_TYPE_EXTERIOR_HEATING_SYSTEM, place);
276 logUnsupportedDevice(device);
279 case CLASS_HUMIDITY_SENSOR:
280 if (hasState(device, WATER_DETECTION_STATE)) {
281 deviceDiscovered(device, THING_TYPE_WATERSENSOR, place);
283 // widget: RelativeHumiditySensor
284 deviceDiscovered(device, THING_TYPE_HUMIDITYSENSOR, place);
286 case CLASS_DOOR_LOCK:
287 // widget: UnlockDoorLockWithUnknownPosition
288 deviceDiscovered(device, THING_TYPE_DOOR_LOCK, place);
291 if ("BioclimaticPergola".equals(device.getWidget())) {
292 // widget: BioclimaticPergola
293 deviceDiscovered(device, THING_TYPE_BIOCLIMATIC_PERGOLA, place);
295 deviceDiscovered(device, THING_TYPE_PERGOLA, place);
298 case CLASS_WINDOW_HANDLE:
299 // widget: ThreeWayWindowHandle
300 deviceDiscovered(device, THING_TYPE_WINDOW_HANDLE, place);
302 case CLASS_TEMPERATURE_SENSOR:
303 // widget: TemperatureSensor
304 deviceDiscovered(device, THING_TYPE_TEMPERATURESENSOR, place);
307 deviceDiscovered(device, THING_TYPE_GATE, place);
309 case CLASS_ELECTRICITY_SENSOR:
310 if (hasEnergyConsumption(device)) {
311 deviceDiscovered(device, THING_TYPE_ELECTRICITYSENSOR, place);
313 logUnsupportedDevice(device);
316 case CLASS_WATER_HEATING_SYSTEM:
317 // widget: DomesticHotWaterProduction
318 if ("DomesticHotWaterProduction".equals(device.getWidget())) {
319 deviceDiscovered(device, THING_TYPE_WATERHEATINGSYSTEM, place);
321 logUnsupportedDevice(device);
326 deviceDiscovered(device, THING_TYPE_DOCK, place);
329 deviceDiscovered(device, THING_TYPE_SIREN, place);
331 case CLASS_ADJUSTABLE_SLATS_ROLLER_SHUTTER:
332 deviceDiscovered(device, THING_TYPE_ADJUSTABLE_SLATS_ROLLERSHUTTER, place);
335 if (hasMyfoxShutter(device)) {
336 // widget: MyFoxSecurityCamera
337 deviceDiscovered(device, THING_TYPE_MYFOX_CAMERA, place);
339 logUnsupportedDevice(device);
342 case CLASS_HITACHI_HEATING_SYSTEM:
343 if ("HitachiAirToWaterHeatingZone".equals(device.getWidget())) {
344 // widget: HitachiAirToWaterHeatingZone
345 deviceDiscovered(device, THING_TYPE_HITACHI_ATWHZ, place);
346 } else if ("HitachiAirToWaterMainComponent".equals(device.getWidget())) {
347 // widget: HitachiAirToWaterMainComponent
348 deviceDiscovered(device, THING_TYPE_HITACHI_ATWMC, place);
349 } else if ("HitachiDHW".equals(device.getWidget())) {
350 // widget: HitachiDHW
351 deviceDiscovered(device, THING_TYPE_HITACHI_DHW, place);
353 logUnsupportedDevice(device);
356 case CLASS_RAIN_SENSOR:
357 if ("RainSensor".equals(device.getWidget())) {
358 // widget: RainSensor
359 deviceDiscovered(device, THING_TYPE_RAINSENSOR, place);
361 logUnsupportedDevice(device);
363 case THING_PROTOCOL_GATEWAY:
364 case THING_REMOTE_CONTROLLER:
365 // widget: AlarmRemoteController
366 case THING_NETWORK_COMPONENT:
372 logUnsupportedDevice(device);
376 private @Nullable String getPlaceLabel(SomfyTahomaSetup setup, String oid) {
377 SomfyTahomaRootPlace root = setup.getRootPlace();
378 if (!oid.isEmpty() && root != null) {
379 for (SomfyTahomaSubPlace place : root.getSubPlaces()) {
380 if (oid.equals(place.getOid())) {
381 return place.getLabel();
388 private boolean isStateLess(SomfyTahomaDevice device) {
389 return device.getStates().isEmpty() || (device.getStates().size() == 1 && hasState(device, STATUS_STATE));
392 private void logUnsupportedDevice(SomfyTahomaDevice device) {
393 if (!isStateLess(device)) {
394 logger.debug("Detected a new unsupported device: {} with widgetName: {}", device.getUiClass(),
396 logger.debug("If you want to add the support, please create a new issue and attach the information below");
397 logger.debug("Device definition:\n{}", device.getDefinition());
399 StringBuilder sb = new StringBuilder().append('\n');
400 for (SomfyTahomaState state : device.getStates()) {
401 sb.append(state.toString()).append('\n');
403 logger.debug("Current device states: {}", sb);
407 private boolean hasState(SomfyTahomaDevice device, String state) {
408 return device.getDefinition().getStates().stream().anyMatch(st -> state.equals(st.getQualifiedName()));
411 private boolean hasMyfoxShutter(SomfyTahomaDevice device) {
412 return hasState(device, MYFOX_SHUTTER_STATUS_STATE);
415 private boolean hasEnergyConsumption(SomfyTahomaDevice device) {
416 return hasState(device, ENERGY_CONSUMPTION_STATE);
419 private boolean isSilentRollerShutter(SomfyTahomaDevice device) {
420 return "PositionableRollerShutterWithLowSpeedManagement".equals(device.getWidget());
423 private boolean isUnoRollerShutter(SomfyTahomaDevice device) {
424 return "PositionableRollerShutterUno".equals(device.getWidget());
427 private boolean isOnOffHeatingSystem(SomfyTahomaDevice device) {
428 return hasCommmand(device, COMMAND_SET_HEATINGLEVEL);
431 private boolean isZwaveHeatingSystem(SomfyTahomaDevice device) {
432 return hasState(device, ZWAVE_SET_POINT_TYPE_STATE);
435 private boolean hasCommmand(SomfyTahomaDevice device, String command) {
436 return device.getDefinition().getCommands().stream().anyMatch(cmd -> command.equals(cmd.getCommandName()));
439 private void deviceDiscovered(SomfyTahomaDevice device, ThingTypeUID thingTypeUID, @Nullable String place) {
440 String label = device.getLabel();
441 if (place != null && !place.isBlank()) {
442 label += " (" + place + ")";
444 deviceDiscovered(label, device.getDeviceURL(), device.getOid(), thingTypeUID,
445 hasState(device, RSSI_LEVEL_STATE));
448 private void deviceDiscovered(String label, String deviceURL, String oid, ThingTypeUID thingTypeUID, boolean rssi) {
449 Map<String, Object> properties = new HashMap<>();
450 properties.put("url", deviceURL);
451 properties.put(NAME_STATE, label);
453 properties.put(RSSI_LEVEL_STATE, "-1");
456 SomfyTahomaBridgeHandler localBridgeHandler = bridgeHandler;
457 if (localBridgeHandler != null) {
458 ThingUID thingUID = new ThingUID(thingTypeUID, localBridgeHandler.getThing().getUID(), oid);
460 logger.debug("Detected a/an {} - label: {} oid: {}", thingTypeUID.getId(), label, oid);
461 thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
462 .withProperties(properties).withRepresentationProperty("url").withLabel(label)
463 .withBridge(localBridgeHandler.getThing().getUID()).build());
467 private void actionGroupDiscovered(String label, String deviceURL, String oid) {
468 deviceDiscovered(label, deviceURL, oid, THING_TYPE_ACTIONGROUP, false);
471 private void gatewayDiscovered(SomfyTahomaGateway gw) {
472 Map<String, Object> properties = new HashMap<>(1);
473 String type = gatewayTypes.getOrDefault(gw.getType(), "UNKNOWN");
474 String id = gw.getGatewayId();
475 properties.put("id", id);
476 properties.put("type", type);
478 SomfyTahomaBridgeHandler localBridgeHandler = bridgeHandler;
479 if (localBridgeHandler != null) {
480 ThingUID thingUID = new ThingUID(THING_TYPE_GATEWAY, localBridgeHandler.getThing().getUID(), id);
482 logger.debug("Detected a gateway with id: {} and type: {}", id, type);
484 DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_GATEWAY).withProperties(properties)
485 .withRepresentationProperty("id").withLabel("Somfy Gateway (" + type + ")")
486 .withBridge(localBridgeHandler.getThing().getUID()).build());