]> git.basschouten.com Git - openhab-addons.git/blob
a36a3d0eb59ba41e302e0b90b0c317fbd56bdc2a
[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.somfytahoma.internal.discovery;
14
15 import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
16
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
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;
43
44 /**
45  * The {@link SomfyTahomaItemDiscoveryService} discovers rollershutters and
46  * action groups associated with your TahomaLink cloud account.
47  *
48  * @author Ondrej Pecta - Initial contribution
49  * @author Laurent Garnier - Include the place into the inbox label (when defined for the device)
50  */
51 @Component(scope = ServiceScope.PROTOTYPE, service = SomfyTahomaItemDiscoveryService.class)
52 @NonNullByDefault
53 public class SomfyTahomaItemDiscoveryService extends AbstractThingHandlerDiscoveryService<SomfyTahomaBridgeHandler> {
54
55     private static final int DISCOVERY_TIMEOUT_SEC = 10;
56     private static final int DISCOVERY_REFRESH_SEC = 3600;
57
58     private final Logger logger = LoggerFactory.getLogger(SomfyTahomaItemDiscoveryService.class);
59
60     private @Nullable ScheduledFuture<?> discoveryJob;
61
62     public SomfyTahomaItemDiscoveryService() {
63         super(SomfyTahomaBridgeHandler.class, DISCOVERY_TIMEOUT_SEC);
64         logger.debug("Creating discovery service");
65     }
66
67     @Override
68     protected void startBackgroundDiscovery() {
69         logger.debug("Starting SomfyTahoma background discovery");
70
71         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
72         if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
73             discoveryJob = scheduler.scheduleWithFixedDelay(this::runDiscovery, 10, DISCOVERY_REFRESH_SEC,
74                     TimeUnit.SECONDS);
75         }
76     }
77
78     @Override
79     protected void stopBackgroundDiscovery() {
80         logger.debug("Stopping SomfyTahoma background discovery");
81         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
82         if (localDiscoveryJob != null) {
83             localDiscoveryJob.cancel(true);
84         }
85     }
86
87     @Override
88     public Set<ThingTypeUID> getSupportedThingTypes() {
89         return SUPPORTED_THING_TYPES_UIDS;
90     }
91
92     @Override
93     protected void startScan() {
94         runDiscovery();
95     }
96
97     private synchronized void runDiscovery() {
98         logger.debug("Starting scanning for things...");
99
100         if (ThingStatus.ONLINE == thingHandler.getThing().getStatus()) {
101             SomfyTahomaSetup setup = thingHandler.getSetup();
102
103             if (setup == null) {
104                 return;
105             }
106
107             for (SomfyTahomaDevice device : setup.getDevices()) {
108                 discoverDevice(device, setup);
109             }
110             for (SomfyTahomaGateway gw : setup.getGateways()) {
111                 gatewayDiscovered(gw);
112             }
113
114             // local mode does not have action groups
115             if (!thingHandler.isDevModeReady()) {
116                 List<SomfyTahomaActionGroup> actions = thingHandler.listActionGroups();
117
118                 for (SomfyTahomaActionGroup group : actions) {
119                     String oid = group.getOid();
120                     String label = group.getLabel();
121
122                     // actiongroups use oid as deviceURL
123                     actionGroupDiscovered(label, oid);
124                 }
125             }
126         } else {
127             logger.debug("Cannot start discovery since the bridge is not online!");
128         }
129     }
130
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()) {
136             case CLASS_AWNING:
137                 // widget: PositionableHorizontalAwning
138                 // widget: DynamicAwning
139                 // widget: UpDownHorizontalAwning
140                 deviceDiscovered(device, THING_TYPE_AWNING, place);
141                 break;
142             case CLASS_CONTACT_SENSOR:
143                 // widget: ContactSensor
144                 deviceDiscovered(device, THING_TYPE_CONTACTSENSOR, place);
145                 break;
146             case CLASS_CURTAIN:
147                 deviceDiscovered(device, THING_TYPE_CURTAIN, place);
148                 break;
149             case CLASS_EXTERIOR_SCREEN:
150                 // widget: PositionableScreen
151                 deviceDiscovered(device, THING_TYPE_EXTERIORSCREEN, place);
152                 break;
153             case CLASS_EXTERIOR_VENETIAN_BLIND:
154                 // widget: PositionableExteriorVenetianBlind
155                 deviceDiscovered(device, THING_TYPE_EXTERIORVENETIANBLIND, place);
156                 break;
157             case CLASS_GARAGE_DOOR:
158                 deviceDiscovered(device, THING_TYPE_GARAGEDOOR, place);
159                 break;
160             case CLASS_LIGHT:
161                 if ("DimmerLight".equals(widget) || "DynamicLight".equals(widget)) {
162                     // widget: DimmerLight
163                     // widget: DynamicLight
164                     deviceDiscovered(device, THING_TYPE_DIMMER_LIGHT, place);
165                 } else {
166                     // widget: TimedOnOffLight
167                     // widget: StatefulOnOffLight
168                     deviceDiscovered(device, THING_TYPE_LIGHT, place);
169                 }
170                 break;
171             case CLASS_LIGHT_SENSOR:
172                 deviceDiscovered(device, THING_TYPE_LIGHTSENSOR, place);
173                 break;
174             case CLASS_OCCUPANCY_SENSOR:
175                 // widget: OccupancySensor
176                 deviceDiscovered(device, THING_TYPE_OCCUPANCYSENSOR, place);
177                 break;
178             case CLASS_ON_OFF:
179                 // widget: StatefulOnOff
180                 deviceDiscovered(device, THING_TYPE_ONOFF, place);
181                 break;
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);
189                 } else {
190                     // widget: PositionableRollerShutter
191                     // widget: PositionableTiltedRollerShutter
192                     deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER, place);
193                 }
194                 break;
195             case CLASS_SHUTTER:
196                 // widget: DynamicShutter
197                 deviceDiscovered(device, THING_TYPE_SHUTTER, place);
198                 break;
199             case CLASS_SCREEN:
200                 // widget: PositionableTiltedScreen
201                 deviceDiscovered(device, THING_TYPE_SCREEN, place);
202                 break;
203             case CLASS_SMOKE_SENSOR:
204                 // widget: SmokeSensor
205                 deviceDiscovered(device, THING_TYPE_SMOKESENSOR, place);
206                 break;
207             case CLASS_VENETIAN_BLIND:
208                 // widget: DynamicVenetianBlind
209                 if (hasCommmand(device, "setOrientation")) {
210                     deviceDiscovered(device, THING_TYPE_VENETIANBLIND, place);
211                 } else {
212                     // simple venetian blind without orientation
213                     deviceDiscovered(device, THING_TYPE_SHUTTER, place);
214                 }
215                 break;
216             case CLASS_WINDOW:
217                 // widget: PositionableTiltedWindow
218                 deviceDiscovered(device, THING_TYPE_WINDOW, place);
219                 break;
220             case CLASS_ALARM:
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);
227                 } else {
228                     deviceDiscovered(device, THING_TYPE_EXTERNAL_ALARM, place);
229                 }
230                 break;
231             case CLASS_POD:
232                 if (hasState(device, CYCLIC_BUTTON_STATE)) {
233                     deviceDiscovered(device, THING_TYPE_POD, place);
234                 }
235                 break;
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);
245                 } else {
246                     logUnsupportedDevice(device);
247                 }
248                 break;
249             case CLASS_EXTERIOR_HEATING_SYSTEM:
250                 if ("DimmerExteriorHeating".equals(widget)) {
251                     // widget: DimmerExteriorHeating
252                     deviceDiscovered(device, THING_TYPE_EXTERIOR_HEATING_SYSTEM, place);
253                 } else {
254                     logUnsupportedDevice(device);
255                 }
256                 break;
257             case CLASS_HUMIDITY_SENSOR:
258                 if (hasState(device, WATER_DETECTION_STATE)) {
259                     deviceDiscovered(device, THING_TYPE_WATERSENSOR, place);
260                 } else {
261                     // widget: RelativeHumiditySensor
262                     deviceDiscovered(device, THING_TYPE_HUMIDITYSENSOR, place);
263                 }
264                 break;
265             case CLASS_DOOR_LOCK:
266                 // widget: UnlockDoorLockWithUnknownPosition
267                 deviceDiscovered(device, THING_TYPE_DOOR_LOCK, place);
268                 break;
269             case CLASS_PERGOLA:
270                 if ("BioclimaticPergola".equals(widget)) {
271                     // widget: BioclimaticPergola
272                     deviceDiscovered(device, THING_TYPE_BIOCLIMATIC_PERGOLA, place);
273                 } else {
274                     deviceDiscovered(device, THING_TYPE_PERGOLA, place);
275                 }
276                 break;
277             case CLASS_WINDOW_HANDLE:
278                 // widget: ThreeWayWindowHandle
279                 deviceDiscovered(device, THING_TYPE_WINDOW_HANDLE, place);
280                 break;
281             case CLASS_TEMPERATURE_SENSOR:
282                 // widget: TemperatureSensor
283                 deviceDiscovered(device, THING_TYPE_TEMPERATURESENSOR, place);
284                 break;
285             case CLASS_GATE:
286                 deviceDiscovered(device, THING_TYPE_GATE, place);
287                 break;
288             case CLASS_ELECTRICITY_SENSOR:
289                 if (hasEnergyConsumption(device)) {
290                     deviceDiscovered(device, THING_TYPE_ELECTRICITYSENSOR, place);
291                 } else {
292                     logUnsupportedDevice(device);
293                 }
294                 break;
295             case CLASS_WATER_HEATING_SYSTEM:
296                 // widget: DomesticHotWaterProduction
297                 if ("DomesticHotWaterProduction".equals(widget)) {
298                     deviceDiscovered(device, THING_TYPE_WATERHEATINGSYSTEM, place);
299                 } else {
300                     logUnsupportedDevice(device);
301                 }
302                 break;
303             case CLASS_DOCK:
304                 // widget: Dock
305                 deviceDiscovered(device, THING_TYPE_DOCK, place);
306                 break;
307             case CLASS_SIREN:
308                 deviceDiscovered(device, THING_TYPE_SIREN, place);
309                 break;
310             case CLASS_ADJUSTABLE_SLATS_ROLLER_SHUTTER:
311                 deviceDiscovered(device, THING_TYPE_ADJUSTABLE_SLATS_ROLLERSHUTTER, place);
312                 break;
313             case CLASS_CAMERA:
314                 if (hasMyfoxShutter(device)) {
315                     // widget: MyFoxSecurityCamera
316                     deviceDiscovered(device, THING_TYPE_MYFOX_CAMERA, place);
317                 } else {
318                     logUnsupportedDevice(device);
319                 }
320                 break;
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);
331                 } else {
332                     logUnsupportedDevice(device);
333                 }
334                 break;
335             case CLASS_RAIN_SENSOR:
336                 if ("RainSensor".equals(widget)) {
337                     // widget: RainSensor
338                     deviceDiscovered(device, THING_TYPE_RAINSENSOR, place);
339                 } else {
340                     logUnsupportedDevice(device);
341                 }
342                 break;
343             case CLASS_CARBON_DIOXIDE_SENSOR:
344                 // widget: CO2Sensor
345                 deviceDiscovered(device, THING_TYPE_CARBON_DIOXIDE_SENSOR, place);
346                 break;
347             case CLASS_NOISE_SENSOR:
348                 // widget: NoiseSensor
349                 deviceDiscovered(device, THING_TYPE_NOISE_SENSOR, place);
350                 break;
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
357             case THING_GENERIC:
358                 // widget: unknown
359                 break;
360
361             default:
362                 logUnsupportedDevice(device);
363         }
364     }
365
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();
372                 }
373             }
374         }
375         return null;
376     }
377
378     private boolean isStateLess(SomfyTahomaDevice device) {
379         return device.getStates().isEmpty() || (device.getStates().size() == 1 && hasState(device, STATUS_STATE));
380     }
381
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());
388
389             StringBuilder sb = new StringBuilder().append('\n');
390             for (SomfyTahomaState state : device.getStates()) {
391                 sb.append(state.toString()).append('\n');
392             }
393             logger.debug("Current device states: {}", sb);
394         }
395     }
396
397     private boolean hasState(SomfyTahomaDevice device, String state) {
398         return device.getDefinition().getStates().stream().anyMatch(st -> state.equals(st.getQualifiedName()));
399     }
400
401     private boolean hasMyfoxShutter(SomfyTahomaDevice device) {
402         return hasState(device, MYFOX_SHUTTER_STATUS_STATE);
403     }
404
405     private boolean hasEnergyConsumption(SomfyTahomaDevice device) {
406         return hasState(device, ENERGY_CONSUMPTION_STATE);
407     }
408
409     private boolean isSilentRollerShutter(SomfyTahomaDevice device) {
410         return "PositionableRollerShutterWithLowSpeedManagement".equals(device.getDefinition().getWidgetName());
411     }
412
413     private boolean isUnoRollerShutter(SomfyTahomaDevice device) {
414         return "PositionableRollerShutterUno".equals(device.getDefinition().getWidgetName());
415     }
416
417     private boolean isOnOffHeatingSystem(SomfyTahomaDevice device) {
418         return hasCommmand(device, COMMAND_SET_HEATINGLEVEL);
419     }
420
421     private boolean isZwaveHeatingSystem(SomfyTahomaDevice device) {
422         return hasState(device, ZWAVE_SET_POINT_TYPE_STATE);
423     }
424
425     private boolean hasCommmand(SomfyTahomaDevice device, String command) {
426         return device.getDefinition().getCommands().stream().anyMatch(cmd -> command.equals(cmd.getCommandName()));
427     }
428
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 + ")";
433         }
434         deviceDiscovered(label, device.getDeviceURL(), thingTypeUID, hasState(device, RSSI_LEVEL_STATE));
435     }
436
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);
441         if (rssi) {
442             properties.put(RSSI_LEVEL_STATE, "-1");
443         }
444
445         ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(),
446                 deviceURL.replaceAll("[^a-zA-Z0-9_]", ""));
447
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())
451                 .build());
452     }
453
454     private void actionGroupDiscovered(String label, String deviceURL) {
455         deviceDiscovered(label, deviceURL, THING_TYPE_ACTIONGROUP, false);
456     }
457
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);
464
465         ThingUID thingUID = new ThingUID(THING_TYPE_GATEWAY, thingHandler.getThing().getUID(), id);
466
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());
471     }
472 }