]> git.basschouten.com Git - openhab-addons.git/blob
471094f21808310311245bc10de8d08d2c318d43
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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;
44
45 /**
46  * The {@link SomfyTahomaItemDiscoveryService} discovers rollershutters and
47  * action groups associated with your TahomaLink cloud account.
48  *
49  * @author Ondrej Pecta - Initial contribution
50  * @author Laurent Garnier - Include the place into the inbox label (when defined for the device)
51  */
52 @NonNullByDefault
53 public class SomfyTahomaItemDiscoveryService extends AbstractDiscoveryService
54         implements DiscoveryService, ThingHandlerService {
55
56     private static final int DISCOVERY_TIMEOUT_SEC = 10;
57     private static final int DISCOVERY_REFRESH_SEC = 3600;
58
59     private final Logger logger = LoggerFactory.getLogger(SomfyTahomaItemDiscoveryService.class);
60
61     private @Nullable SomfyTahomaBridgeHandler bridgeHandler;
62
63     private @Nullable ScheduledFuture<?> discoveryJob;
64
65     public SomfyTahomaItemDiscoveryService() {
66         super(DISCOVERY_TIMEOUT_SEC);
67         logger.debug("Creating discovery service");
68     }
69
70     @Override
71     public void activate() {
72         super.activate(null);
73     }
74
75     @Override
76     public void deactivate() {
77         super.deactivate();
78     }
79
80     @Override
81     public void setThingHandler(@NonNullByDefault({}) ThingHandler handler) {
82         if (handler instanceof SomfyTahomaBridgeHandler tahomaBridgeHandler) {
83             bridgeHandler = tahomaBridgeHandler;
84         }
85     }
86
87     @Override
88     public @Nullable ThingHandler getThingHandler() {
89         return bridgeHandler;
90     }
91
92     @Override
93     protected void startBackgroundDiscovery() {
94         logger.debug("Starting SomfyTahoma background discovery");
95
96         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
97         if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
98             discoveryJob = scheduler.scheduleWithFixedDelay(this::runDiscovery, 10, DISCOVERY_REFRESH_SEC,
99                     TimeUnit.SECONDS);
100         }
101     }
102
103     @Override
104     protected void stopBackgroundDiscovery() {
105         logger.debug("Stopping SomfyTahoma background discovery");
106         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
107         if (localDiscoveryJob != null) {
108             localDiscoveryJob.cancel(true);
109         }
110     }
111
112     @Override
113     public Set<ThingTypeUID> getSupportedThingTypes() {
114         return SUPPORTED_THING_TYPES_UIDS;
115     }
116
117     @Override
118     protected void startScan() {
119         runDiscovery();
120     }
121
122     private synchronized void runDiscovery() {
123         logger.debug("Starting scanning for things...");
124
125         SomfyTahomaBridgeHandler localBridgeHandler = bridgeHandler;
126         if (localBridgeHandler != null && ThingStatus.ONLINE == localBridgeHandler.getThing().getStatus()) {
127             SomfyTahomaSetup setup = localBridgeHandler.getSetup();
128
129             if (setup == null) {
130                 return;
131             }
132
133             for (SomfyTahomaDevice device : setup.getDevices()) {
134                 discoverDevice(device, setup);
135             }
136             for (SomfyTahomaGateway gw : setup.getGateways()) {
137                 gatewayDiscovered(gw);
138             }
139
140             // local mode does not have action groups
141             if (!localBridgeHandler.isDevModeReady()) {
142                 List<SomfyTahomaActionGroup> actions = localBridgeHandler.listActionGroups();
143
144                 for (SomfyTahomaActionGroup group : actions) {
145                     String oid = group.getOid();
146                     String label = group.getLabel();
147
148                     // actiongroups use oid as deviceURL
149                     actionGroupDiscovered(label, oid);
150                 }
151             }
152         } else {
153             logger.debug("Cannot start discovery since the bridge is not online!");
154         }
155     }
156
157     private void discoverDevice(SomfyTahomaDevice device, SomfyTahomaSetup setup) {
158         logger.debug("url: {}", device.getDeviceURL());
159         String place = getPlaceLabel(setup, device.getPlaceOID());
160         String widget = device.getDefinition().getWidgetName();
161         switch (device.getDefinition().getUiClass()) {
162             case CLASS_AWNING:
163                 // widget: PositionableHorizontalAwning
164                 // widget: DynamicAwning
165                 // widget: UpDownHorizontalAwning
166                 deviceDiscovered(device, THING_TYPE_AWNING, place);
167                 break;
168             case CLASS_CONTACT_SENSOR:
169                 // widget: ContactSensor
170                 deviceDiscovered(device, THING_TYPE_CONTACTSENSOR, place);
171                 break;
172             case CLASS_CURTAIN:
173                 deviceDiscovered(device, THING_TYPE_CURTAIN, place);
174                 break;
175             case CLASS_EXTERIOR_SCREEN:
176                 // widget: PositionableScreen
177                 deviceDiscovered(device, THING_TYPE_EXTERIORSCREEN, place);
178                 break;
179             case CLASS_EXTERIOR_VENETIAN_BLIND:
180                 // widget: PositionableExteriorVenetianBlind
181                 deviceDiscovered(device, THING_TYPE_EXTERIORVENETIANBLIND, place);
182                 break;
183             case CLASS_GARAGE_DOOR:
184                 deviceDiscovered(device, THING_TYPE_GARAGEDOOR, place);
185                 break;
186             case CLASS_LIGHT:
187                 if ("DimmerLight".equals(widget) || "DynamicLight".equals(widget)) {
188                     // widget: DimmerLight
189                     // widget: DynamicLight
190                     deviceDiscovered(device, THING_TYPE_DIMMER_LIGHT, place);
191                 } else {
192                     // widget: TimedOnOffLight
193                     // widget: StatefulOnOffLight
194                     deviceDiscovered(device, THING_TYPE_LIGHT, place);
195                 }
196                 break;
197             case CLASS_LIGHT_SENSOR:
198                 deviceDiscovered(device, THING_TYPE_LIGHTSENSOR, place);
199                 break;
200             case CLASS_OCCUPANCY_SENSOR:
201                 // widget: OccupancySensor
202                 deviceDiscovered(device, THING_TYPE_OCCUPANCYSENSOR, place);
203                 break;
204             case CLASS_ON_OFF:
205                 // widget: StatefulOnOff
206                 deviceDiscovered(device, THING_TYPE_ONOFF, place);
207                 break;
208             case CLASS_ROLLER_SHUTTER:
209                 if (isSilentRollerShutter(device)) {
210                     // widget: PositionableRollerShutterWithLowSpeedManagement
211                     deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER_SILENT, place);
212                 } else if (isUnoRollerShutter(device)) {
213                     // widget: PositionableRollerShutterUno
214                     deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER_UNO, place);
215                 } else {
216                     // widget: PositionableRollerShutter
217                     // widget: PositionableTiltedRollerShutter
218                     deviceDiscovered(device, THING_TYPE_ROLLERSHUTTER, place);
219                 }
220                 break;
221             case CLASS_SHUTTER:
222                 // widget: DynamicShutter
223                 deviceDiscovered(device, THING_TYPE_SHUTTER, place);
224                 break;
225             case CLASS_SCREEN:
226                 // widget: PositionableTiltedScreen
227                 deviceDiscovered(device, THING_TYPE_SCREEN, place);
228                 break;
229             case CLASS_SMOKE_SENSOR:
230                 // widget: SmokeSensor
231                 deviceDiscovered(device, THING_TYPE_SMOKESENSOR, place);
232                 break;
233             case CLASS_VENETIAN_BLIND:
234                 // widget: DynamicVenetianBlind
235                 if (hasCommmand(device, "setOrientation")) {
236                     deviceDiscovered(device, THING_TYPE_VENETIANBLIND, place);
237                 } else {
238                     // simple venetian blind without orientation
239                     deviceDiscovered(device, THING_TYPE_SHUTTER, place);
240                 }
241                 break;
242             case CLASS_WINDOW:
243                 // widget: PositionableTiltedWindow
244                 deviceDiscovered(device, THING_TYPE_WINDOW, place);
245                 break;
246             case CLASS_ALARM:
247                 if (device.getDeviceURL().startsWith("internal:")) {
248                     // widget: TSKAlarmController
249                     deviceDiscovered(device, THING_TYPE_INTERNAL_ALARM, place);
250                 } else if ("MyFoxAlarmController".equals(widget)) {
251                     // widget: MyFoxAlarmController
252                     deviceDiscovered(device, THING_TYPE_MYFOX_ALARM, place);
253                 } else {
254                     deviceDiscovered(device, THING_TYPE_EXTERNAL_ALARM, place);
255                 }
256                 break;
257             case CLASS_POD:
258                 if (hasState(device, CYCLIC_BUTTON_STATE)) {
259                     deviceDiscovered(device, THING_TYPE_POD, place);
260                 }
261                 break;
262             case CLASS_HEATING_SYSTEM:
263                 if ("SomfyThermostat".equals(widget)) {
264                     deviceDiscovered(device, THING_TYPE_THERMOSTAT, place);
265                 } else if ("ValveHeatingTemperatureInterface".equals(widget)) {
266                     deviceDiscovered(device, THING_TYPE_VALVE_HEATING_SYSTEM, place);
267                 } else if (isOnOffHeatingSystem(device)) {
268                     deviceDiscovered(device, THING_TYPE_ONOFF_HEATING_SYSTEM, place);
269                 } else if (isZwaveHeatingSystem(device)) {
270                     deviceDiscovered(device, THING_TYPE_ZWAVE_HEATING_SYSTEM, place);
271                 } else {
272                     logUnsupportedDevice(device);
273                 }
274                 break;
275             case CLASS_EXTERIOR_HEATING_SYSTEM:
276                 if ("DimmerExteriorHeating".equals(widget)) {
277                     // widget: DimmerExteriorHeating
278                     deviceDiscovered(device, THING_TYPE_EXTERIOR_HEATING_SYSTEM, place);
279                 } else {
280                     logUnsupportedDevice(device);
281                 }
282                 break;
283             case CLASS_HUMIDITY_SENSOR:
284                 if (hasState(device, WATER_DETECTION_STATE)) {
285                     deviceDiscovered(device, THING_TYPE_WATERSENSOR, place);
286                 } else {
287                     // widget: RelativeHumiditySensor
288                     deviceDiscovered(device, THING_TYPE_HUMIDITYSENSOR, place);
289                 }
290                 break;
291             case CLASS_DOOR_LOCK:
292                 // widget: UnlockDoorLockWithUnknownPosition
293                 deviceDiscovered(device, THING_TYPE_DOOR_LOCK, place);
294                 break;
295             case CLASS_PERGOLA:
296                 if ("BioclimaticPergola".equals(widget)) {
297                     // widget: BioclimaticPergola
298                     deviceDiscovered(device, THING_TYPE_BIOCLIMATIC_PERGOLA, place);
299                 } else {
300                     deviceDiscovered(device, THING_TYPE_PERGOLA, place);
301                 }
302                 break;
303             case CLASS_WINDOW_HANDLE:
304                 // widget: ThreeWayWindowHandle
305                 deviceDiscovered(device, THING_TYPE_WINDOW_HANDLE, place);
306                 break;
307             case CLASS_TEMPERATURE_SENSOR:
308                 // widget: TemperatureSensor
309                 deviceDiscovered(device, THING_TYPE_TEMPERATURESENSOR, place);
310                 break;
311             case CLASS_GATE:
312                 deviceDiscovered(device, THING_TYPE_GATE, place);
313                 break;
314             case CLASS_ELECTRICITY_SENSOR:
315                 if (hasEnergyConsumption(device)) {
316                     deviceDiscovered(device, THING_TYPE_ELECTRICITYSENSOR, place);
317                 } else {
318                     logUnsupportedDevice(device);
319                 }
320                 break;
321             case CLASS_WATER_HEATING_SYSTEM:
322                 // widget: DomesticHotWaterProduction
323                 if ("DomesticHotWaterProduction".equals(widget)) {
324                     deviceDiscovered(device, THING_TYPE_WATERHEATINGSYSTEM, place);
325                 } else {
326                     logUnsupportedDevice(device);
327                 }
328                 break;
329             case CLASS_DOCK:
330                 // widget: Dock
331                 deviceDiscovered(device, THING_TYPE_DOCK, place);
332                 break;
333             case CLASS_SIREN:
334                 deviceDiscovered(device, THING_TYPE_SIREN, place);
335                 break;
336             case CLASS_ADJUSTABLE_SLATS_ROLLER_SHUTTER:
337                 deviceDiscovered(device, THING_TYPE_ADJUSTABLE_SLATS_ROLLERSHUTTER, place);
338                 break;
339             case CLASS_CAMERA:
340                 if (hasMyfoxShutter(device)) {
341                     // widget: MyFoxSecurityCamera
342                     deviceDiscovered(device, THING_TYPE_MYFOX_CAMERA, place);
343                 } else {
344                     logUnsupportedDevice(device);
345                 }
346                 break;
347             case CLASS_HITACHI_HEATING_SYSTEM:
348                 if ("HitachiAirToWaterHeatingZone".equals(widget)) {
349                     // widget: HitachiAirToWaterHeatingZone
350                     deviceDiscovered(device, THING_TYPE_HITACHI_ATWHZ, place);
351                 } else if ("HitachiAirToWaterMainComponent".equals(widget)) {
352                     // widget: HitachiAirToWaterMainComponent
353                     deviceDiscovered(device, THING_TYPE_HITACHI_ATWMC, place);
354                 } else if ("HitachiDHW".equals(widget)) {
355                     // widget: HitachiDHW
356                     deviceDiscovered(device, THING_TYPE_HITACHI_DHW, place);
357                 } else {
358                     logUnsupportedDevice(device);
359                 }
360                 break;
361             case CLASS_RAIN_SENSOR:
362                 if ("RainSensor".equals(widget)) {
363                     // widget: RainSensor
364                     deviceDiscovered(device, THING_TYPE_RAINSENSOR, place);
365                 } else {
366                     logUnsupportedDevice(device);
367                 }
368                 break;
369             case CLASS_CARBON_DIOXIDE_SENSOR:
370                 // widget: CO2Sensor
371                 deviceDiscovered(device, THING_TYPE_CARBON_DIOXIDE_SENSOR, place);
372                 break;
373             case CLASS_NOISE_SENSOR:
374                 // widget: NoiseSensor
375                 deviceDiscovered(device, THING_TYPE_NOISE_SENSOR, place);
376                 break;
377             case THING_PROTOCOL_GATEWAY:
378             case THING_REMOTE_CONTROLLER:
379                 // widget: AlarmRemoteController
380             case THING_NETWORK_COMPONENT:
381             case THING_CONFIGURATION_COMPONENT:
382                 // widget: NetatmoHome
383             case THING_GENERIC:
384                 // widget: unknown
385                 break;
386
387             default:
388                 logUnsupportedDevice(device);
389         }
390     }
391
392     private @Nullable String getPlaceLabel(SomfyTahomaSetup setup, String oid) {
393         SomfyTahomaRootPlace root = setup.getRootPlace();
394         if (!oid.isEmpty() && root != null) {
395             for (SomfyTahomaSubPlace place : root.getSubPlaces()) {
396                 if (oid.equals(place.getOid())) {
397                     return place.getLabel();
398                 }
399             }
400         }
401         return null;
402     }
403
404     private boolean isStateLess(SomfyTahomaDevice device) {
405         return device.getStates().isEmpty() || (device.getStates().size() == 1 && hasState(device, STATUS_STATE));
406     }
407
408     private void logUnsupportedDevice(SomfyTahomaDevice device) {
409         if (!isStateLess(device)) {
410             logger.debug("Detected a new unsupported device: {} with widgetName: {}",
411                     device.getDefinition().getUiClass(), device.getDefinition().getWidgetName());
412             logger.debug("If you want to add the support, please create a new issue and attach the information below");
413             logger.debug("Device definition:\n{}", device.getDefinition());
414
415             StringBuilder sb = new StringBuilder().append('\n');
416             for (SomfyTahomaState state : device.getStates()) {
417                 sb.append(state.toString()).append('\n');
418             }
419             logger.debug("Current device states: {}", sb);
420         }
421     }
422
423     private boolean hasState(SomfyTahomaDevice device, String state) {
424         return device.getDefinition().getStates().stream().anyMatch(st -> state.equals(st.getQualifiedName()));
425     }
426
427     private boolean hasMyfoxShutter(SomfyTahomaDevice device) {
428         return hasState(device, MYFOX_SHUTTER_STATUS_STATE);
429     }
430
431     private boolean hasEnergyConsumption(SomfyTahomaDevice device) {
432         return hasState(device, ENERGY_CONSUMPTION_STATE);
433     }
434
435     private boolean isSilentRollerShutter(SomfyTahomaDevice device) {
436         return "PositionableRollerShutterWithLowSpeedManagement".equals(device.getDefinition().getWidgetName());
437     }
438
439     private boolean isUnoRollerShutter(SomfyTahomaDevice device) {
440         return "PositionableRollerShutterUno".equals(device.getDefinition().getWidgetName());
441     }
442
443     private boolean isOnOffHeatingSystem(SomfyTahomaDevice device) {
444         return hasCommmand(device, COMMAND_SET_HEATINGLEVEL);
445     }
446
447     private boolean isZwaveHeatingSystem(SomfyTahomaDevice device) {
448         return hasState(device, ZWAVE_SET_POINT_TYPE_STATE);
449     }
450
451     private boolean hasCommmand(SomfyTahomaDevice device, String command) {
452         return device.getDefinition().getCommands().stream().anyMatch(cmd -> command.equals(cmd.getCommandName()));
453     }
454
455     private void deviceDiscovered(SomfyTahomaDevice device, ThingTypeUID thingTypeUID, @Nullable String place) {
456         String label = device.getLabel();
457         if (place != null && !place.isBlank()) {
458             label += " (" + place + ")";
459         }
460         deviceDiscovered(label, device.getDeviceURL(), thingTypeUID, hasState(device, RSSI_LEVEL_STATE));
461     }
462
463     private void deviceDiscovered(String label, String deviceURL, ThingTypeUID thingTypeUID, boolean rssi) {
464         Map<String, Object> properties = new HashMap<>();
465         properties.put("url", deviceURL);
466         properties.put(NAME_STATE, label);
467         if (rssi) {
468             properties.put(RSSI_LEVEL_STATE, "-1");
469         }
470
471         SomfyTahomaBridgeHandler localBridgeHandler = bridgeHandler;
472         if (localBridgeHandler != null) {
473             ThingUID thingUID = new ThingUID(thingTypeUID, localBridgeHandler.getThing().getUID(),
474                     deviceURL.replaceAll("[^a-zA-Z0-9_]", ""));
475
476             logger.debug("Detected a/an {} - label: {} device URL: {}", thingTypeUID.getId(), label, deviceURL);
477             thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
478                     .withProperties(properties).withRepresentationProperty("url").withLabel(label)
479                     .withBridge(localBridgeHandler.getThing().getUID()).build());
480         }
481     }
482
483     private void actionGroupDiscovered(String label, String deviceURL) {
484         deviceDiscovered(label, deviceURL, THING_TYPE_ACTIONGROUP, false);
485     }
486
487     private void gatewayDiscovered(SomfyTahomaGateway gw) {
488         Map<String, Object> properties = new HashMap<>(1);
489         String type = gatewayTypes.getOrDefault(gw.getType(), "UNKNOWN");
490         String id = gw.getGatewayId();
491         properties.put("id", id);
492         properties.put("type", type);
493
494         SomfyTahomaBridgeHandler localBridgeHandler = bridgeHandler;
495         if (localBridgeHandler != null) {
496             ThingUID thingUID = new ThingUID(THING_TYPE_GATEWAY, localBridgeHandler.getThing().getUID(), id);
497
498             logger.debug("Detected a gateway with id: {} and type: {}", id, type);
499             thingDiscovered(
500                     DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_GATEWAY).withProperties(properties)
501                             .withRepresentationProperty("id").withLabel("Somfy Gateway (" + type + ")")
502                             .withBridge(localBridgeHandler.getThing().getUID()).build());
503         }
504     }
505 }