/** * 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.firmware; import static org.eclipse.smarthome.core.thing.firmware.FirmwareStatusInfo.createUnknownInfo; import static org.eclipse.smarthome.core.thing.firmware.FirmwareStatusInfo.createUpToDateInfo; import static org.eclipse.smarthome.core.thing.firmware.FirmwareStatusInfo.createUpdateAvailableInfo; import static org.eclipse.smarthome.core.thing.firmware.FirmwareStatusInfo.createUpdateExecutableInfo; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.smarthome.config.core.validation.ConfigDescriptionValidator; import org.eclipse.smarthome.config.core.validation.ConfigValidationException; import org.eclipse.smarthome.core.common.SafeMethodCaller; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.eclipse.smarthome.core.events.Event; import org.eclipse.smarthome.core.events.EventFilter; import org.eclipse.smarthome.core.events.EventPublisher; import org.eclipse.smarthome.core.events.EventSubscriber; import org.eclipse.smarthome.core.i18n.I18nProvider; import org.eclipse.smarthome.core.i18n.LocaleProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.firmware.Firmware; import org.eclipse.smarthome.core.thing.binding.firmware.FirmwareUID; import org.eclipse.smarthome.core.thing.binding.firmware.FirmwareUpdateBackgroundTransferHandler; import org.eclipse.smarthome.core.thing.binding.firmware.FirmwareUpdateHandler; import org.eclipse.smarthome.core.thing.binding.firmware.ProgressCallback; import org.eclipse.smarthome.core.thing.events.ThingStatusInfoChangedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; /** * The firmware update service is registered as an OSGi service and is responsible for tracking all available * {@link FirmwareUpdateHandler}s. It provides access to the current {@link FirmwareStatusInfo} of a thing and is the * central instance to start a firmware update. * * @author Thomas Höfer - Initial contribution */ public final class FirmwareUpdateService implements EventSubscriber { private static final String THREAD_POOL_NAME = FirmwareUpdateService.class.getSimpleName(); private static final Set<String> SUPPORTED_TIME_UNITS = ImmutableSet.of(TimeUnit.SECONDS.name(), TimeUnit.MINUTES.name(), TimeUnit.HOURS.name(), TimeUnit.DAYS.name()); private static final String PERIOD_CONFIG_KEY = "period"; private static final String DELAY_CONFIG_KEY = "delay"; private static final String TIME_UNIT_CONFIG_KEY = "timeUnit"; private static final String CONFIG_DESC_URI_KEY = "system:firmware-status-info-job"; private final Logger logger = LoggerFactory.getLogger(FirmwareUpdateService.class); private int firmwareStatusInfoJobPeriod = 3600; private int firmwareStatusInfoJobDelay = 3600; private TimeUnit firmwareStatusInfoJobTimeUnit = TimeUnit.SECONDS; private ScheduledFuture<?> firmwareStatusInfoJob; private int timeout = 30 * 60 * 1000; private final Set<String> subscribedEventTypes = ImmutableSet.of(ThingStatusInfoChangedEvent.TYPE); private final Map<ThingUID, FirmwareStatusInfo> firmwareStatusInfoMap = new ConcurrentHashMap<>(); private final Map<ThingUID, ProgressCallbackImpl> progressCallbackMap = new ConcurrentHashMap<>(); private final List<FirmwareUpdateHandler> firmwareUpdateHandlers = new CopyOnWriteArrayList<>(); private FirmwareRegistry firmwareRegistry; private EventPublisher eventPublisher; private I18nProvider i18nProvider; private LocaleProvider localeProvider; private Runnable firmwareStatusRunnable = new Runnable() { @Override public void run() { logger.debug("Running firmware status check."); for (FirmwareUpdateHandler firmwareUpdateHandler : firmwareUpdateHandlers) { try { logger.debug("Executing firmware status check for thing with UID {}.", firmwareUpdateHandler.getThing().getUID()); Firmware latestFirmware = firmwareRegistry .getLatestFirmware(firmwareUpdateHandler.getThing().getThingTypeUID()); FirmwareStatusInfo newFirmwareStatusInfo = getFirmwareStatusInfo(firmwareUpdateHandler, latestFirmware); processFirmwareStatusInfo(firmwareUpdateHandler, newFirmwareStatusInfo, latestFirmware); } catch (Exception e) { logger.debug("Exception occurred during firmware status check.", e); } } } }; protected void activate(Map<String, Object> config) { modified(config); } protected synchronized void modified(Map<String, Object> config) { logger.debug("Modifying the configuration of the firmware update service."); if (!isValid(config)) { return; } cancelFirmwareUpdateStatusInfoJob(); firmwareStatusInfoJobPeriod = config.containsKey(PERIOD_CONFIG_KEY) ? (Integer) config.get(PERIOD_CONFIG_KEY) : firmwareStatusInfoJobPeriod; firmwareStatusInfoJobDelay = config.containsKey(DELAY_CONFIG_KEY) ? (Integer) config.get(DELAY_CONFIG_KEY) : firmwareStatusInfoJobDelay; firmwareStatusInfoJobTimeUnit = config.containsKey(TIME_UNIT_CONFIG_KEY) ? TimeUnit.valueOf((String) config.get(TIME_UNIT_CONFIG_KEY)) : firmwareStatusInfoJobTimeUnit; if (!firmwareUpdateHandlers.isEmpty()) { createFirmwareUpdateStatusInfoJob(); } } protected void deactivate() { cancelFirmwareUpdateStatusInfoJob(); firmwareStatusInfoMap.clear(); progressCallbackMap.clear(); } /** * Returns the {@link FirmwareStatusInfo} for the thing having the given thing UID. * * @param thingUID the UID of the thing (must not be null) * * @return the firmware status info (is null if there is no {@link FirmwareUpdateHandler} for the thing * available) * * @throws NullPointerException if the given thing UID is null */ public FirmwareStatusInfo getFirmwareStatusInfo(ThingUID thingUID) { Preconditions.checkNotNull(thingUID, "Thing UID must not be null."); FirmwareUpdateHandler firmwareUpdateHandler = getFirmwareUpdateHandler(thingUID); if (firmwareUpdateHandler == null) { logger.debug("No firmware update handler available for thing with UID {}.", thingUID); return null; } Firmware latestFirmware = firmwareRegistry .getLatestFirmware(firmwareUpdateHandler.getThing().getThingTypeUID()); FirmwareStatusInfo firmwareStatusInfo = getFirmwareStatusInfo(firmwareUpdateHandler, latestFirmware); processFirmwareStatusInfo(firmwareUpdateHandler, firmwareStatusInfo, latestFirmware); return firmwareStatusInfo; } /** * Updates the firmware of the thing having the given thing UID by invoking the operation * {@link FirmwareUpdateHandler#updateFirmware(Firmware, ProgressCallback)} of the thing´s firmware update handler. * <p> * This operation is a non-blocking operation by spawning a new thread around the invocation of the firmware update * handler. The time out of the thread is 30 minutes. * </p> * * * @param thingUID the thing UID (must not be null) * @param firmwareUID the UID of the firmware to be updated (must not be null) * @param locale the locale to be used to internationalize error messages (if null then the locale provided by the * {@link LocaleProvider} is used) * * @throws NullPointerException if given thing UID or firmware UID is null * @throws IllegalStateException if * <ul> * <li>there is no firmware update handler for the thing</li> * <li>the firmware update handler is not able to execute the firmware update</li> * </ul> * @throws IllegalArgumentException if * <ul> * <li>the firmware cannot be found</li> * <li>the firmware is not suitable for the thing</li> * <li>the firmware requires another prerequisite firmware version</li> * </ul> */ public void updateFirmware(final ThingUID thingUID, final FirmwareUID firmwareUID, final Locale locale) { Preconditions.checkNotNull(thingUID, "Thing UID must not be null."); Preconditions.checkNotNull(firmwareUID, "Firmware UID must not be null."); final FirmwareUpdateHandler firmwareUpdateHandler = getFirmwareUpdateHandler(thingUID); if (firmwareUpdateHandler == null) { throw new IllegalArgumentException( String.format("There is no firmware update handler for thing with UID %s.", thingUID)); } final Firmware firmware = getFirmware(firmwareUID); validateFirmwareUpdateConditions(firmware, firmwareUpdateHandler); final Locale loc = locale != null ? locale : localeProvider.getLocale(); final ProgressCallbackImpl progressCallback = new ProgressCallbackImpl(firmwareUpdateHandler, eventPublisher, i18nProvider, thingUID, firmwareUID, loc); progressCallbackMap.put(thingUID, progressCallback); logger.debug("Starting firmware update for thing with UID {} and firmware with UID {}", thingUID, firmwareUID); getPool().submit(new Runnable() { @Override public void run() { try { SafeMethodCaller.call(new SafeMethodCaller.ActionWithException<Void>() { @Override public Void call() { firmwareUpdateHandler.updateFirmware(firmware, progressCallback); return null; } }, timeout); } catch (ExecutionException e) { logger.error(String.format( "Unexpected exception occurred for firmware update of thing with UID %s and firmware with UID %s.", thingUID, firmwareUID), e.getCause()); progressCallback.failedInternal("unexpected-handler-error"); } catch (TimeoutException e) { logger.error(String.format( "Timeout occurred for firmware update of thing with UID %s and firmware with UID %s.", thingUID, firmwareUID), e); progressCallback.failedInternal("timeout-error"); } } }); } /** * Cancels the firmware update of the thing having the given thing UID by invoking the operation * {@link FirmwareUpdateHandler#cancel()} of the thing´s firmware update handler. * * @param thingUID the thing UID (must not be null) */ public void cancelFirmwareUpdate(final ThingUID thingUID) { Preconditions.checkNotNull(thingUID, "Thing UID must not be null."); final FirmwareUpdateHandler firmwareUpdateHandler = getFirmwareUpdateHandler(thingUID); if (firmwareUpdateHandler == null) { throw new IllegalArgumentException( String.format("There is no firmware update handler for thing with UID %s.", thingUID)); } final ProgressCallbackImpl progressCallback = getProgressCallback(thingUID); getPool().submit(new Runnable() { @Override public void run() { try { SafeMethodCaller.call(new SafeMethodCaller.ActionWithException<Void>() { @Override public Void call() { logger.debug("Canceling firmware update for thing with UID {}.", thingUID); firmwareUpdateHandler.cancel(); return null; } }); } catch (ExecutionException e) { logger.error(String.format( "Unexpected exception occurred while canceling firmware update of thing with UID %s.", thingUID), e.getCause()); progressCallback.failedInternal("unexpected-handler-error-during-cancel"); } catch (TimeoutException e) { logger.error(String.format("Timeout occurred while canceling firmware update of thing with UID %s.", thingUID), e); progressCallback.failedInternal("timeout-error-during-cancel"); } } }); } @Override public Set<String> getSubscribedEventTypes() { return subscribedEventTypes; } @Override public EventFilter getEventFilter() { return null; } @Override public void receive(Event event) { if (event instanceof ThingStatusInfoChangedEvent) { ThingStatusInfoChangedEvent changedEvent = (ThingStatusInfoChangedEvent) event; if (changedEvent.getStatusInfo().getStatus() != ThingStatus.ONLINE) { return; } ThingUID thingUID = changedEvent.getThingUID(); FirmwareUpdateHandler firmwareUpdateHandler = getFirmwareUpdateHandler(thingUID); if (firmwareUpdateHandler != null && !firmwareStatusInfoMap.containsKey(thingUID)) { initializeFirmwareStatus(firmwareUpdateHandler); } } } private ProgressCallbackImpl getProgressCallback(ThingUID thingUID) { if (!progressCallbackMap.containsKey(thingUID)) { throw new IllegalStateException( String.format("No ProgressCallback available for thing with UID %s.", thingUID)); } return progressCallbackMap.get(thingUID); } private FirmwareStatusInfo getFirmwareStatusInfo(FirmwareUpdateHandler firmwareUpdateHandler, Firmware latestFirmware) { String thingFirmwareVersion = getThingFirmwareVersion(firmwareUpdateHandler); if (latestFirmware == null || thingFirmwareVersion == null) { return createUnknownInfo(); } if (latestFirmware.isSuccessorVersion(thingFirmwareVersion)) { if (firmwareUpdateHandler.isUpdateExecutable()) { return createUpdateExecutableInfo(latestFirmware.getUID()); } return createUpdateAvailableInfo(); } return createUpToDateInfo(); } private synchronized void processFirmwareStatusInfo(FirmwareUpdateHandler firmwareUpdateHandler, FirmwareStatusInfo newFirmwareStatusInfo, Firmware latestFirmware) { ThingUID thingUID = firmwareUpdateHandler.getThing().getUID(); FirmwareStatusInfo previousFirmwareStatusInfo = firmwareStatusInfoMap.put(thingUID, newFirmwareStatusInfo); if (previousFirmwareStatusInfo == null || !previousFirmwareStatusInfo.equals(newFirmwareStatusInfo)) { eventPublisher.post(FirmwareEventFactory.createFirmwareStatusInfoEvent(newFirmwareStatusInfo, thingUID)); if (newFirmwareStatusInfo.getFirmwareStatus() == FirmwareStatus.UPDATE_AVAILABLE && firmwareUpdateHandler instanceof FirmwareUpdateBackgroundTransferHandler && !firmwareUpdateHandler.isUpdateExecutable()) { transferLatestFirmware((FirmwareUpdateBackgroundTransferHandler) firmwareUpdateHandler, latestFirmware, previousFirmwareStatusInfo); } } } private void transferLatestFirmware(final FirmwareUpdateBackgroundTransferHandler fubtHandler, final Firmware latestFirmware, final FirmwareStatusInfo previousFirmwareStatusInfo) { getPool().submit(new Runnable() { @Override public void run() { try { fubtHandler.transferFirmware(latestFirmware); } catch (Exception e) { logger.error("Exception occurred during background firmware transfer.", e); synchronized (this) { // restore previous firmware status info in order that transfer can be re-triggered firmwareStatusInfoMap.put(fubtHandler.getThing().getUID(), previousFirmwareStatusInfo); } } } }); } private void validateFirmwareUpdateConditions(Firmware firmware, FirmwareUpdateHandler firmwareUpdateHandler) { if (!firmwareUpdateHandler.isUpdateExecutable()) { throw new IllegalStateException(String.format("The firmware update of thing with UID %s is not executable.", firmwareUpdateHandler.getThing().getUID())); } validateFirmwareSuitability(firmware, firmwareUpdateHandler); } private void validateFirmwareSuitability(Firmware firmware, FirmwareUpdateHandler firmwareUpdateHandler) { Thing thing = firmwareUpdateHandler.getThing(); if (!firmware.getUID().getThingTypeUID().equals(thing.getThingTypeUID())) { throw new IllegalArgumentException(String.format( "Firmware with UID %s is not suitable for thing with UID %s.", firmware.getUID(), thing.getUID())); } String firmwareVersion = getThingFirmwareVersion(firmwareUpdateHandler); if (firmware.getPrerequisiteVersion() != null && !firmware.isPrerequisiteVersion(firmwareVersion)) { throw new IllegalArgumentException(String.format( "Firmware with UID %s requires at least firmware version %s to get installed. But the current firmware version of the thing with UID %s is %s.", firmware.getUID(), firmware.getPrerequisiteVersion(), thing.getUID(), firmwareVersion)); } } private Firmware getFirmware(FirmwareUID firmwareUID) { Firmware firmware = firmwareRegistry.getFirmware(firmwareUID); if (firmware == null) { throw new IllegalArgumentException(String.format("Firmware with UID %s was not found.", firmwareUID)); } return firmware; } private FirmwareUpdateHandler getFirmwareUpdateHandler(ThingUID thingUID) { for (FirmwareUpdateHandler firmwareUpdateHandler : firmwareUpdateHandlers) { if (thingUID.equals(firmwareUpdateHandler.getThing().getUID())) { return firmwareUpdateHandler; } } return null; } private String getThingFirmwareVersion(FirmwareUpdateHandler firmwareUpdateHandler) { return firmwareUpdateHandler.getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION); } private void createFirmwareUpdateStatusInfoJob() { if (firmwareStatusInfoJob == null || firmwareStatusInfoJob.isCancelled()) { logger.debug("Creating firmware status info job. [delay:{}, period:{}, time unit: {}]", firmwareStatusInfoJobDelay, firmwareStatusInfoJobPeriod, firmwareStatusInfoJobTimeUnit); firmwareStatusInfoJob = getPool().scheduleAtFixedRate(firmwareStatusRunnable, firmwareStatusInfoJobDelay, firmwareStatusInfoJobPeriod, firmwareStatusInfoJobTimeUnit); } } private void cancelFirmwareUpdateStatusInfoJob() { if (firmwareStatusInfoJob != null && !firmwareStatusInfoJob.isCancelled()) { logger.debug("Cancelling firmware status info job."); firmwareStatusInfoJob.cancel(true); firmwareStatusInfoJob = null; } } private boolean isValid(Map<String, Object> config) { // the config description validator does not support option value validation at the moment; so we will validate // the time unit here if (!SUPPORTED_TIME_UNITS.contains(config.get(TIME_UNIT_CONFIG_KEY))) { logger.debug("Given time unit {} is not supported. Will keep current configuration.", config.get(TIME_UNIT_CONFIG_KEY)); return false; } try { ConfigDescriptionValidator.validate(config, new URI(CONFIG_DESC_URI_KEY)); } catch (URISyntaxException | ConfigValidationException e) { logger.debug("Validation of new configuration values failed. Will keep current configuration.", e); return false; } return true; } private void initializeFirmwareStatus(final FirmwareUpdateHandler firmwareUpdateHandler) { getPool().submit(new Runnable() { @Override public void run() { ThingUID thingUID = firmwareUpdateHandler.getThing().getUID(); FirmwareStatusInfo info = getFirmwareStatusInfo(thingUID); logger.debug("Firmware status {} for thing {} initialized.", info.getFirmwareStatus(), thingUID); firmwareStatusInfoMap.put(thingUID, info); } }); } private static ScheduledExecutorService getPool() { return ThreadPoolManager.getScheduledPool(THREAD_POOL_NAME); } protected synchronized void addFirmwareUpdateHandler(FirmwareUpdateHandler firmwareUpdateHandler) { if (firmwareUpdateHandlers.isEmpty()) { createFirmwareUpdateStatusInfoJob(); } firmwareUpdateHandlers.add(firmwareUpdateHandler); } protected synchronized void removeFirmwareUpdateHandler(FirmwareUpdateHandler firmwareUpdateHandler) { firmwareStatusInfoMap.remove(firmwareUpdateHandler.getThing().getUID()); firmwareUpdateHandlers.remove(firmwareUpdateHandler); if (firmwareUpdateHandlers.isEmpty()) { cancelFirmwareUpdateStatusInfoJob(); } progressCallbackMap.remove(firmwareUpdateHandler.getThing().getUID()); } protected void setFirmwareRegistry(FirmwareRegistry firmwareRegistry) { this.firmwareRegistry = firmwareRegistry; } protected void unsetFirmwareRegistry(FirmwareRegistry firmwareRegistry) { this.firmwareRegistry = null; } protected void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } protected void unsetEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = null; } protected void setI18nProvider(I18nProvider i18nProvider) { this.i18nProvider = i18nProvider; } protected void unsetI18nProvider(I18nProvider i18nProvider) { this.i18nProvider = null; } protected void setLocaleProvider(final LocaleProvider localeProvider) { this.localeProvider = localeProvider; } protected void unsetLocaleProvider(final LocaleProvider localeProvider) { this.localeProvider = null; } }