This repository has been archived by the owner on May 7, 2020. It is now read-only.
/
BaseThingHandler.java
602 lines (549 loc) · 22.7 KB
/
BaseThingHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.core.thing.binding;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.config.core.validation.ConfigDescriptionValidator;
import org.eclipse.smarthome.config.core.validation.ConfigValidationException;
import org.eclipse.smarthome.core.common.ThreadPoolManager;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.Channel;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingRegistry;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.ThingStatusInfo;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.thing.binding.builder.ThingBuilder;
import org.eclipse.smarthome.core.thing.binding.builder.ThingStatusInfoBuilder;
import org.eclipse.smarthome.core.thing.link.ItemChannelLinkRegistry;
import org.eclipse.smarthome.core.thing.type.ThingType;
import org.eclipse.smarthome.core.thing.type.TypeResolver;
import org.eclipse.smarthome.core.thing.util.ThingHandlerHelper;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link BaseThingHandler} provides a base implementation for the {@link ThingHandler} interface.
* <p>
* The default behavior for {@link Thing} updates is to {@link #dispose()} this handler first, exchange the
* {@link Thing} and {@link #initialize()} it again. Override the method {@link #thingUpdated(Thing)} to change the
* default behavior.
* <p>
* It is recommended to extend this abstract base class, because it covers a lot of common logic.
* <p>
*
* @author Dennis Nobel - Initial contribution
* @author Michael Grammling - Added dynamic configuration update
* @author Thomas Höfer - Added thing properties and config description validation
* @author Stefan Bußweiler - Added new thing status handling, refactorings thing/bridge life cycle
* @author Kai Kreuzer - Refactored isLinked method to not use deprecated functions anymore
*/
public abstract class BaseThingHandler implements ThingHandler {
private static final String THING_HANDLER_THREADPOOL_NAME = "thingHandler";
private final Logger logger = LoggerFactory.getLogger(BaseThingHandler.class);
protected final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(THING_HANDLER_THREADPOOL_NAME);
protected ThingRegistry thingRegistry;
protected ItemChannelLinkRegistry linkRegistry;
protected BundleContext bundleContext;
protected @NonNull Thing thing;
@SuppressWarnings("rawtypes")
private ServiceTracker thingRegistryServiceTracker;
@SuppressWarnings("rawtypes")
private ServiceTracker linkRegistryServiceTracker;
private ThingHandlerCallback callback;
/**
* Creates a new instance of this class for the {@link Thing}.
*
* @param thing the thing that should be handled, not null
*/
public BaseThingHandler(@NonNull Thing thing) {
this.thing = thing;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public void setBundleContext(final BundleContext bundleContext) {
this.bundleContext = bundleContext;
thingRegistryServiceTracker = new ServiceTracker(this.bundleContext, ThingRegistry.class.getName(), null) {
@Override
public Object addingService(final ServiceReference reference) {
thingRegistry = (ThingRegistry) bundleContext.getService(reference);
return thingRegistry;
}
@Override
public void removedService(final ServiceReference reference, final Object service) {
synchronized (BaseThingHandler.this) {
thingRegistry = null;
}
}
};
thingRegistryServiceTracker.open();
linkRegistryServiceTracker = new ServiceTracker(this.bundleContext, ItemChannelLinkRegistry.class.getName(),
null) {
@Override
public Object addingService(final ServiceReference reference) {
linkRegistry = (ItemChannelLinkRegistry) bundleContext.getService(reference);
return linkRegistry;
}
@Override
public void removedService(final ServiceReference reference, final Object service) {
synchronized (BaseThingHandler.this) {
linkRegistry = null;
}
}
};
linkRegistryServiceTracker.open();
}
public void unsetBundleContext(final BundleContext bundleContext) {
linkRegistryServiceTracker.close();
thingRegistryServiceTracker.close();
this.bundleContext = null;
}
@Override
public void handleRemoval() {
// can be overridden by subclasses
updateStatus(ThingStatus.REMOVED);
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
validateConfigurationParameters(configurationParameters);
// can be overridden by subclasses
Configuration configuration = editConfiguration();
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
}
if (isInitialized()) {
// persist new configuration and reinitialize handler
dispose();
updateConfiguration(configuration);
initialize();
} else {
// persist new configuration and notify Thing Manager
updateConfiguration(configuration);
callback.configurationUpdated(getThing());
}
}
@Override
public void dispose() {
// can be overridden by subclasses
}
@Override
public @NonNull Thing getThing() {
return this.thing;
}
@Override
@Deprecated
public void handleUpdate(ChannelUID channelUID, State newState) {
// can be overridden by subclasses
}
@Override
@Deprecated
public void initialize() {
// should be overridden by subclasses!
updateStatus(ThingStatus.ONLINE);
logger.warn(
"BaseThingHandler.initialize() will be removed soon, ThingStatus can be set manually via updateStatus(ThingStatus.ONLINE)");
}
@Override
public void thingUpdated(Thing thing) {
dispose();
this.thing = thing;
initialize();
}
@Override
public void setCallback(ThingHandlerCallback thingHandlerCallback) {
synchronized (this) {
this.callback = thingHandlerCallback;
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
// can be overridden by subclasses
// standard behavior is to refresh the linked channel,
// so the newly linked items will receive a state update.
handleCommand(channelUID, RefreshType.REFRESH);
}
@Override
public void channelUnlinked(ChannelUID channelUID) {
// can be overridden by subclasses
}
/**
* Validates the given configuration parameters against the configuration description.
*
* @param configurationParameters the configuration parameters to be validated
*
* @throws ConfigValidationException if one or more of the given configuration parameters do not match
* their declarations in the configuration description
*/
protected void validateConfigurationParameters(Map<String, Object> configurationParameters) {
ThingType thingType = TypeResolver.resolve(getThing().getThingTypeUID());
if (thingType != null && thingType.getConfigDescriptionURI() != null) {
ConfigDescriptionValidator.validate(configurationParameters, thingType.getConfigDescriptionURI());
}
}
/**
* Returns the configuration of the thing.
*
* @return configuration of the thing
*/
protected Configuration getConfig() {
return getThing().getConfiguration();
}
/**
* Returns the configuration of the thing and transforms it to the given
* class.
*
* @param configurationClass
* configuration class
* @return configuration of thing in form of the given class
*/
protected <T> T getConfigAs(Class<T> configurationClass) {
return getConfig().as(configurationClass);
}
/**
*
* Updates the state of the thing.
*
* @param channelUID
* unique id of the channel, which was updated
* @param state
* new state
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void updateState(ChannelUID channelUID, State state) {
synchronized (this) {
if (this.callback != null) {
this.callback.stateUpdated(channelUID, state);
} else {
throw new IllegalStateException("Could not update state, because callback is missing");
}
}
}
/**
*
* Updates the state of the thing. Will use the thing UID to infer the
* unique channel UID.
*
* @param channel
* ID id of the channel, which was updated
* @param state
* new state
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void updateState(String channelID, State state) {
ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelID);
updateState(channelUID, state);
}
/**
* Emits an event for the given channel.
*
* @param channelUID UID of the channel over which the event will be emitted
* @param event Event to emit
*/
protected void triggerChannel(ChannelUID channelUID, String event) {
synchronized (this) {
if (this.callback != null) {
this.callback.channelTriggered(this.getThing(), channelUID, event);
} else {
throw new IllegalStateException("Could not trigger channel, because callback is missing");
}
}
}
/**
* Emits an event for the given channel. Will use the thing UID to infer the
* unique channel UID.
*
* @param channelUID UID of the channel over which the event will be emitted
* @param event Event to emit
*/
protected void triggerChannel(String channelUID, String event) {
triggerChannel(new ChannelUID(this.getThing().getUID(), channelUID), event);
}
/**
* Emits an event for the given channel. Will use the thing UID to infer the
* unique channel UID.
*
* @param channelUID UID of the channel over which the event will be emitted
*/
protected void triggerChannel(String channelUID) {
triggerChannel(new ChannelUID(this.getThing().getUID(), channelUID), "");
}
/**
* Emits an event for the given channel. Will use the thing UID to infer the
* unique channel UID.
*
* @param channelUID UID of the channel over which the event will be emitted
*/
protected void triggerChannel(ChannelUID channelUID) {
triggerChannel(channelUID, "");
}
/**
* Sends a command for a channel of the thing.
*
* @param channelID
* id of the channel, which sends the command
* @param command
* command
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void postCommand(String channelID, Command command) {
ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelID);
postCommand(channelUID, command);
}
/**
* Sends a command for a channel of the thing.
*
* @param channelUID
* unique id of the channel, which sends the command
* @param command
* command
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void postCommand(ChannelUID channelUID, Command command) {
synchronized (this) {
if (this.callback != null) {
this.callback.postCommand(channelUID, command);
} else {
throw new IllegalStateException("Could not update state, because callback is missing");
}
}
}
/**
* Updates the status of the thing.
*
* @param status the status
* @param statusDetail the detail of the status
* @param description the description of the status
*
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
synchronized (this) {
if (this.callback != null) {
ThingStatusInfoBuilder statusBuilder = ThingStatusInfoBuilder.create(status, statusDetail);
ThingStatusInfo statusInfo = statusBuilder.withDescription(description).build();
this.callback.statusUpdated(this.thing, statusInfo);
} else {
throw new IllegalStateException("Could not update status, because callback is missing");
}
}
}
/**
* Updates the status of the thing.
*
* @param status the status
* @param statusDetail the detail of the status
*
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
updateStatus(status, statusDetail, null);
}
/**
* Updates the status of the thing. The detail of the status will be 'NONE'.
*
* @param status the status
*
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void updateStatus(ThingStatus status) {
updateStatus(status, ThingStatusDetail.NONE, null);
}
/**
* Creates a thing builder, which allows to modify the thing. The method
* {@link BaseThingHandler#updateThing(Thing)} must be called to persist the changes.
*
* @return {@link ThingBuilder} which builds an exact copy of the thing (not null)
*/
protected ThingBuilder editThing() {
return ThingBuilder.create(this.thing.getThingTypeUID(), this.thing.getUID())
.withBridge(this.thing.getBridgeUID()).withChannels(this.thing.getChannels())
.withConfiguration(this.thing.getConfiguration()).withLabel(this.thing.getLabel())
.withLocation(this.thing.getLocation()).withProperties(this.thing.getProperties());
}
/**
* Informs the framework, that a thing was updated. This method must be called after the configuration or channels
* was changed.
*
* @param thing
* thing, that was updated and should be persisted
*
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void updateThing(@NonNull Thing thing) {
synchronized (this) {
if (this.callback != null) {
this.thing = thing;
this.callback.thingUpdated(thing);
} else {
throw new IllegalStateException("Could not update thing, because callback is missing");
}
}
}
/**
* Returns a copy of the configuration, that can be modified. The method
* {@link BaseThingHandler#updateConfiguration(Configuration)} must be called to persist the configuration.
*
* @return copy of the thing configuration (not null)
*/
protected Configuration editConfiguration() {
Map<String, Object> properties = this.thing.getConfiguration().getProperties();
return new Configuration(new HashMap<>(properties));
}
/**
* Updates the configuration of the thing and informs the framework about it.
*
* @param configuration
* configuration, that was updated and should be persisted
*
* @throws IllegalStateException
* if handler is not initialized correctly, because no callback is present
*/
protected void updateConfiguration(Configuration configuration) {
Map<String, Object> old = this.thing.getConfiguration().getProperties();
try {
this.thing.getConfiguration().setProperties(configuration.getProperties());
synchronized (this) {
if (this.callback != null) {
this.callback.thingUpdated(thing);
} else {
throw new IllegalStateException("Could not update configuration, because callback is missing");
}
}
} catch (RuntimeException e) {
logger.warn(
"Error while applying configuration changes: '{}: {}' - reverting configuration changes on thing '{}'.",
e.getClass().getSimpleName(), e.getMessage(), this.thing.getUID().getAsString());
this.thing.getConfiguration().setProperties(old);
throw e;
}
}
/**
* Returns a copy of the properties map, that can be modified. The method {@link
* BaseThingHandler#updateProperties(Map<String, String> properties)} must then be called to change the
* properties values for the thing that is handled by this thing handler instance.
*
* @return copy of the thing properties (not null)
*/
protected Map<String, String> editProperties() {
Map<String, String> properties = this.thing.getProperties();
return new HashMap<>(properties);
}
/**
* Updates multiple properties for the thing that is handled by this thing handler instance. Each value is only
* set for the given property name if there has not been set any value yet or if the value has been changed. If the
* value of the property to be set is null then the property is removed.
*
* @param properties
* properties map, that was updated
*/
protected void updateProperties(Map<String, String> properties) {
for (Entry<String, String> property : properties.entrySet()) {
String propertyName = property.getKey();
String propertyValue = property.getValue();
String existingPropertyValue = thing.getProperties().get(propertyName);
if (existingPropertyValue == null || !existingPropertyValue.equals(propertyValue)) {
this.thing.setProperty(propertyName, propertyValue);
}
}
}
/**
* <p>
* Updates the given property value for the thing that is handled by this thing handler instance. The value is only
* set for the given property name if there has not been set any value yet or if the value has been changed. If the
* value of the property to be set is null then the property is removed.
*
* If multiple properties should be changed at the same time, the {@link BaseThingHandler#editProperties()} method
* should be used.
*
* @param name the name of the property to be set
* @param value the value of the property
*/
protected void updateProperty(@NonNull String name, String value) {
String existingPropertyValue = thing.getProperties().get(name);
if (existingPropertyValue == null || !existingPropertyValue.equals(value)) {
thing.setProperty(name, value);
}
}
/**
* Returns the bridge of the thing.
*
* @return returns the bridge of the thing or null if the thing has no
* bridge
*/
protected Bridge getBridge() {
ThingUID bridgeUID = thing.getBridgeUID();
synchronized (this) {
if (bridgeUID != null && thingRegistry != null) {
return (Bridge) thingRegistry.get(bridgeUID);
} else {
return null;
}
}
}
/**
* Returns whether at least on item is linked for the given channel ID.
*
* @param channelId
* channel ID (must not be null)
* @return true if at least one item is linked, false otherwise
* @throws IllegalArgumentException
* if no channel with the given ID exists
*/
protected boolean isLinked(String channelId) {
Channel channel = thing.getChannel(channelId);
if (channel != null) {
return linkRegistry != null ? !linkRegistry.getLinks(channel.getUID()).isEmpty() : false;
} else {
throw new IllegalArgumentException("Channel with ID '" + channelId + "' does not exists.");
}
}
/**
* Returns whether the handler has already been initialized.
*
* @return true if handler is initialized, false otherwise
*/
protected boolean isInitialized() {
return ThingHandlerHelper.isHandlerInitialized(this);
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
} else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
protected void changeThingType(ThingTypeUID thingTypeUID, Configuration configuration) {
if (this.callback != null) {
this.callback.migrateThingType(getThing(), thingTypeUID, configuration);
} else {
throw new IllegalStateException("Could not change thing type because callback is missing");
}
}
}