/** * 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.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; import com.google.common.base.Preconditions; /** * {@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 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 * * @throws IllegalArgumentException if thing argument is null */ public BaseThingHandler(Thing thing) { Preconditions.checkArgument(thing != null, "The argument 'thing' must not be null."); 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> configurationParmeter : configurationParameters.entrySet()) { configuration.put(configurationParmeter.getKey(), configurationParmeter.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 Thing getThing() { return this.thing; } @Override public void handleUpdate(ChannelUID channelUID, State newState) { // can be overridden by subclasses } @Override public void initialize() { // can be overridden by subclasses // standard behavior is to set the thing to ONLINE, // assuming no further initialization is necessary. 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 update state, 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(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. * </p> * * 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(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"); } } }