]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mielecloud] Initial contribution of the Miele Cloud binding (#9146)
authorBjörn Lange <bjoern.lange@udo.edu>
Tue, 25 May 2021 20:06:49 +0000 (22:06 +0200)
committerGitHub <noreply@github.com>
Tue, 25 May 2021 20:06:49 +0000 (22:06 +0200)
Also-by: Bert Plonus <bert.plonus@miele.com>
Also-by: Martin Lepsy <martin.lepsy@miele.com>
Also-by: Benjamin Bolte <benjamin.bolte@itemis.de>
Signed-off-by: Björn Lange <bjoern.lange@itemis.de>
230 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.mielecloud/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/README.md [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/doc/account-overview-empty.png [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/doc/account-overview-with-bridge.png [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/doc/miele-login.png [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/doc/pair-account.png [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/doc/pairing-success.png [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefreshListener.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefresher.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresher.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/MieleCloudConfigService.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGenerator.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeCreationFailedException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeReconfigurationFailedException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/NoOngoingAuthorizationException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/OngoingAuthorizationException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractRedirectionServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractShowPageServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingInformationExtractor.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/AbstractMieleThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoffeeSystemThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoolingDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishWarmerDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishwasherDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DryerDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/HobDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/HoodDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/OvenDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/RoboticVacuumCleanerDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/WashingDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/WineStorageDeviceThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/ActionsChannelState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/ChannelTypeUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/DeviceChannelState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/TransitionChannelState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/util/EmailValidator.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/util/LocaleValidator.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ActionStateFetcher.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ConnectionError.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ConnectionStatusListener.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebservice.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebserviceFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceCache.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateDispatcher.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateListener.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/HttpUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebservice.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebserviceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebserviceFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/UnavailableMieleWebservice.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/ActionsState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/CoolingDeviceTemperatureState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/DeviceState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/PowerStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/ProgramStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/TransitionState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/WineStorageDeviceTemperatureState.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Actions.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Device.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceCollection.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceIdentLabel.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceType.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DryingStep.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ErrorMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Ident.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Light.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/MieleSyntaxException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/PlateStep.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProcessAction.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramId.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramPhase.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramType.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/RemoteEnable.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/SpinningSpeed.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/State.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StateType.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Status.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Temperature.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Type.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/VentilationStep.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/XkmIdentLabel.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/AuthorizationFailedException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceDisconnectSseException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceInitializationException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceTransientException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/TooManyRequestsException.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/CombiningLanguageProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/JvmLanguageProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/LanguageProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/OpenHabLanguageProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/request/RequestFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/request/RequestFactoryImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/AuthorizationFailedRetryStrategy.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/NTimesRetryStrategy.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategy.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategyCombiner.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/BackoffStrategy.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/ExponentialBackoffWithJitter.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/ServerSentEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseConnection.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseListener.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseRequestFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseStreamParser.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/config/configDescription.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/i18n/mielecloud.properties [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/bridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/channelTypes.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/coffeeSystem.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dishWarmerDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dishwasherDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dryerDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/freezer.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/fridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/fridgeFreezer.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/hobDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/hoodDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/ovenDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/roboticVacuumCleanerDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/washerDryer.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/washingMachine.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/wineStorageDevice.xml [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/css/main.css [new file with mode: 0755]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/css/rtl.css [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/OpenHAB_logo.svg [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/favicon.ico [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/miele.png [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/js/main.js [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/js/main.js.map [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/failure.html [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/index.html [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/pairing.html [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/success.html [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingTestConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresherTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImplTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGeneratorTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/discovery/ThingInformationExtractorTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/LocaleValidatorTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/MockUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/ReflectionUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/ResourceUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/ActionStateFetcherTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebserviceTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DeviceCacheTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateDispatcherTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/HttpUtilTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/RequestFactoryImplTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/ActionsStateTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/CoolingDeviceTemperatureStateTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/DeviceStateTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/TransitionStateTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/WineStorageDeviceTemperatureStateTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ActionsTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceCollectionTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceIdentLabelTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ErrorMessageTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/LightTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StateTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StatusTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/TypeTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/exception/TooManyRequestsExceptionTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/language/CombiningLanguageProviderTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/language/OpenHabLanguageProviderTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/AuthorizationFailedRetryStrategyTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/NTimesRetryStrategyTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategyCombinerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/ExponentialBackoffWithJitterTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseConnectionTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseStreamParserTest.java [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollection.json [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithFloatingPointTargetTemperature.json [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithLargeProgramID.json [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithSpinningSpeedObject.json [new file with mode: 0644]
bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/invalidDeviceCollection.json [new file with mode: 0644]
bundles/pom.xml
itests/org.openhab.binding.mielecloud.tests/NOTICE [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/itest.bndrun [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/pom.xml [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/ConfigFlowTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServletTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServletTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServletTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServletTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServletTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServletTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingDiscoveryTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/AbstractMieleThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoffeeDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoolingDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishWarmerDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishwasherDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DryerDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/HobDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/HoodDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleBridgeHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleHandlerFactoryTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/OvenDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/RoboticVacuumCleanerDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/WashingDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/WineStorageDeviceThingHandlerTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/AbstractConfigFlowTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/MieleCloudBindingIntegrationTestConstants.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/OpenHabOsgiTest.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/ReflectionUtil.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/Website.java [new file with mode: 0644]
itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/WebsiteCrawler.java [new file with mode: 0644]
itests/pom.xml

index 65619f4bc71c9878125d8de5e09abed7b20c7a78..bed0f44786a315974a60acd2aaafb7ca1b4177ab 100644 (file)
 /bundles/org.openhab.binding.meteoblue/ @9037568
 /bundles/org.openhab.binding.meteostick/ @cdjackson
 /bundles/org.openhab.binding.miele/ @kgoderis
+/bundles/org.openhab.binding.mielecloud/ @BjoernLange
 /bundles/org.openhab.binding.mihome/ @pboos
 /bundles/org.openhab.binding.miio/ @marcelrv
 /bundles/org.openhab.binding.milight/ @davidgraeff
index a3396a3a3ddf660d6b247b68871632c193c69d07..b183a255f0598f5fbc68215e727c04bf1b34fab8 100644 (file)
       <artifactId>org.openhab.binding.miele</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.mielecloud</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.mihome</artifactId>
diff --git a/bundles/org.openhab.binding.mielecloud/NOTICE b/bundles/org.openhab.binding.mielecloud/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.mielecloud/README.md b/bundles/org.openhab.binding.mielecloud/README.md
new file mode 100644 (file)
index 0000000..9da358d
--- /dev/null
@@ -0,0 +1,623 @@
+# Miele Cloud Binding
+
+This binding integrates [Miele@home](https://www.miele.de/brand/smarthome-42801.htm) appliances via a cloud connection.
+A Miele cloud account and a set of developer credentials is required to use the binding.
+The latter can be requested from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
+
+## Supported Things
+
+Most Miele appliances that directly connect to the cloud via a Wi-Fi module are supported.
+Appliances connecting to the XGW3000 gateway via ZigBee are also supported when registered with the cloud account.
+However they might be better supported by the [gateway-based Miele binding](https://www.openhab.org/addons/bindings/miele/).
+Depending on the age of your appliance the functionality of the binding might be limited.
+Appliances from recent generations will support all functionality.
+
+The following types of appliances are supported:
+
+| Appliance type                   | Thing type               |
+| -------------------------------- | ------------------------ |
+| Coffee Machine                   | `coffee_system`          |
+| Dishwasher                       | `dishwasher`             |
+| Dish Warmer                      | `dish_warmer`            |
+| Freezer                          | `freezer`                |
+| Fridge                           | `fridge`                 |
+| Fridge-Freezer Combination       | `fridge_freezer`         |
+| Hob                              | `hob`                    |
+| Hood                             | `hood`                   |
+| Microwave Oven                   | `oven`                   |
+| Oven                             | `oven`                   |
+| Robotic Vacuum Cleaner           | `robotic_vacuum_cleaner` |
+| Tumble Dryer                     | `dryer`                  |
+| Washer Dryer                     | `washer_dryer`           |
+| Washing Machine                  | `washing_machine`        |
+| Wine Cabinet                     | `wine_storage`           |
+| Wine Cabinet Freezer Combination | `wine_storage`           |
+
+## Discovery
+
+Please take the following steps prior to using the binding. Create a Miele cloud account in the Miele@mobile app for [Android](https://play.google.com/store/apps/details?id=de.miele.infocontrol&hl=en_US) or [iOS](https://apps.apple.com/de/app/miele-mobile/id930406907?l=en) (if not already done).
+Afterwards, pair your appliances.
+Once your appliances are set up, register at the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
+You will receive a pair of client ID and client secret which will be used to pair your Miele cloud account to the Miele cloud openHAB binding.
+Keep these credentials to yourself and treat them like a password!
+It may take some time until the registration e-mail arrives.
+
+There is no auto discovery for the Miele cloud account.
+The account is paired using OAuth2 with your Miele login and the developer credentials obtained from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
+To pair the account go to the binding's configuration UI at `https://<your openHAB address>/mielecloud`.
+For a standard openHABian Pi installation the address is [https://openhabianpi:8443/mielecloud](https://openhabianpi:8443/mielecloud).
+Note that your browser will file a warning that the certificate is self-signed.
+This is fine and you can safely continue.
+It is also possible to use an unsecured connection for pairing but it is strongly recommended to use a secured connection because your credentials will otherwise be transferred without encryption over the local network.
+For more information on this topic, see [Securing access to openHAB](https://www.openhab.org/docs/installation/security.html#encrypted-communication).
+For a detailed walk through the account configuration, see [Account Configuration Example](#account-configuration-example).
+
+Once a Miele account is paired, all supported appliances are automatically discovered as individual things and placed in the inbox.
+They can then be paired with your favorite management UI.
+As an alternative, the binding configuration UI provides a things-file template per paired account that can be used to pair the appliances.
+
+## Thing Configuration
+
+A Miele cloud account needs to be configured to get access to your appliances.
+After that appliances can be configured.
+
+### Account Configuration
+
+The Miele cloud account must be paired via the binding configuration UI before a bridge that relies on it can be configured in openHAB.
+For details on the configuration UI see [Discovery](#discovery) and [Account Configuration Example](#account-configuration-example).
+The account serves as a bridge for the things representing your appliances.
+On success the configuration assistant will directly configure the account without requiring further actions.
+As an alternative, it provides a things-file template.
+
+The account has the following parameters:
+
+| Name        | Type      | Description                                                                                                                                                                     |
+| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| email       | required  | E-mail address identifying this account. This exists only to distinguish accounts. If the address is changed after authorization then the account needs to be authorized again. |
+| locale      | optional  | The locale to use for full text channels of things from this account. Possible values are `en`, `de`, `da`, `es`, `fr`, `it`, `nl`, `nb`. Default is `en`.                      |
+
+
+### Appliance Configuration
+
+The binding configuration UI will show a things-file template containing things for all supported appliances from the paired account.
+This can be used as a starting point for a custom things-file.
+
+All Miele cloud appliance things have the following parameters:
+
+| Name             | Type      | Description                                                                                                                              |
+| ---------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| deviceIdentifier | required  | Technical device identifier uniquely identifying the Miele appliance. Use the discovery result or the things-file template to obtain it. |
+
+
+## Channels
+
+The following table lists all available channels.
+See the following chapters for detailed information about which appliance supports which channels.
+Depending on the exact appliance configuration not all channels might be supported, e.g. a hob with four plates will only fill the channels for plates 1-4.
+Channel ID and channel type ID match unless noted.
+
+| Channel Type ID               | Item Type            | Description                                                                                                                               | Read only |
+| ----------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------- |
+| remote_control_can_be_started | Switch | Indicates if this device can be started remotely. | Yes |
+| remote_control_can_be_stopped | Switch | Indicates if this device can be stopped remotely. | Yes |
+| remote_control_can_be_paused | Switch | Indicates if this device can be paused remotely. | Yes |
+| remote_control_can_be_switched_on | Switch | Indicates if the device can be switched on remotely. | Yes |
+| remote_control_can_be_switched_off | Switch | Indicates if the device can be switched off remotely. | Yes |
+| remote_control_can_set_program_active | Switch | Indicates if the active program of the device can be set remotely. | Yes |
+| spinning_speed | String | The spinning speed of the active program. | Yes |
+| spinning_speed_raw | Number | The raw spinning speed of the active program. | Yes |
+| program_active | String | The active program of the device. | Yes |
+| program_active_raw | Number | The raw active program of the device. | Yes |
+| dish_warmer_program_active | String | The active program of the device. | No |
+| vacuum_cleaner_program_active | String | The active program of the device. | No |
+| program_phase | String | The phase of the active program. | Yes |
+| program_phase_raw | Number | The raw phase of the active program. | Yes |
+| operation_state | String | The operation state of the device. | Yes |
+| operation_state_raw | Number | The raw operation state of the device. | Yes |
+| program_start | Switch | Starts the currently selected program. | No |
+| program_stop | Switch | Stops the currently selected program. | No |
+| program_start_stop | String | Starts or stops the currently selected program. | No |
+| program_start_stop_pause | String | Starts, stops or pauses the currently selected program. | No |
+| power_state_on_off | String | Switches the device On or Off. | No |
+| finish_state | Switch | Indicates whether the most recent program finished. | Yes |
+| delayed_start_time | Number | The delayed start time of the selected program. | Yes |
+| program_remaining_time | Number | The remaining time of the active program. | Yes |
+| program_elapsed_time | Number | The elapsed time of the active program. | Yes |
+| program_progress | Number | The progress of the active program. | Yes |
+| drying_target | String | The target drying step of the laundry. | Yes |
+| drying_target_raw | Number | The raw target drying step of the laundry. | Yes |
+| pre_heat_finished | Switch | Indicates whether the pre-heating finished. | Yes |
+| temperature_target | Number | The target temperature of the device. | Yes |
+| temperature_current | Number | The currently measured temperature of the device. | Yes |
+| ventilation_power | String | The current ventilation power of the hood. | Yes |
+| ventilation_power_raw | Number | The current raw ventilation power of the hood. | Yes |
+| error_state | Switch | Indication flag which signals an error state for the device. | Yes |
+| info_state | Switch | Indication flag which signals an information of the device. | Yes |
+| fridge_super_cool | Switch | Start the super cooling mode of the fridge. | No |
+| freezer_super_freeze | Switch | Start the super freezing mode of the freezer. | No |
+| super_cool_can_be_controlled | Switch | Indicates if super cooling can be toggled. | Yes |
+| super_freeze_can_be_controlled | Switch | Indicates if super freezing can be toggled | Yes |
+| fridge_temperature_target | Number | The target temperature of the fridge. | Yes |
+| fridge_temperature_current | Number | The currently measured temperature of the fridge. | Yes |
+| freezer_temperature_target | Number | The target temperature of the freezer. | Yes |
+| freezer_temperature_current | Number | The currently measured temperature of the freezer. | Yes |
+| top_temperature_target | Number | The target temperature of the top area. | Yes |
+| top_temperature_current | Number | The currently measured temperature of the top area. | Yes |
+| middle_temperature_target | Number | The target temperature of the middle area. | Yes |
+| middle_temperature_current | Number | The currently measured temperature of the middle area. | Yes |
+| bottom_temperature_target | Number | The target temperature of the bottom area. | Yes |
+| bottom_temperature_current | Number | The currently measured temperature of the bottom area. | Yes |
+| light_switch | Switch | Indicates if the light of the device is enabled. | No |
+| light_can_be_controlled | Switch | Indicates if the light of the device can be controlled. | Yes |
+| plate_power_step | String | The power level of the heating plate. | Yes |
+| plate_power_step_raw | Number | The raw power level of the heating plate. | Yes |
+| door_state | Switch | Indicates if the door of the device is open. | Yes |
+| door_alarm | Switch | Indicates if the door alarm of the device is active. | Yes |
+| battery_level | Number | The battery level of the robotic vacuum cleaner. | Yes |
+
+### Coffee System
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- finish_state
+- power_state_on_off
+- program_remaining_time
+- program_elapsed_time
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+
+### Dish Warmer
+
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- dish_warmer_program_active
+- program_active_raw
+- operation_state
+- operation_state_raw
+- power_state_on_off
+- finish_state
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- temperature_target
+- temperature_current
+- error_state
+- info_state
+- door_state
+
+### Dishwasher
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- error_state
+- info_state
+- door_state
+
+### Tumble Dryer
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- drying_target
+- drying_target_raw
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Freezer
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- freezer_super_freeze
+- super_freeze_can_be_controlled
+- freezer_temperature_target
+- freezer_temperature_current
+- door_state
+- door_alarm
+
+### Fridge
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- fridge_super_cool
+- super_cool_can_be_controlled
+- fridge_temperature_target
+- fridge_temperature_current
+- door_state
+- door_alarm
+
+### Fridge Freezer
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- fridge_super_cool
+- freezer_super_freeze
+- super_cool_can_be_controlled
+- super_freeze_can_be_controlled
+- fridge_temperature_target
+- fridge_temperature_current
+- freezer_temperature_target
+- freezer_temperature_current
+- door_state
+- door_alarm
+
+### Hob
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- plate_1_power_step to plate_6_power_step with channel type ID plate_power_step
+- plate_1_power_step_raw to plate_6_power_step_raw with channel type ID plate_power_step_raw
+
+### Hood
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- power_state_on_off
+- ventilation_power
+- ventilation_power_raw
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+
+### Oven
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- pre_heat_finished
+- temperature_target
+- temperature_current
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Robotic Vacuum Cleaner
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_paused
+- remote_control_can_set_program_active
+- vacuum_cleaner_program_active
+- program_active_raw
+- operation_state
+- operation_state_raw
+- finish_state
+- program_start_stop_pause
+- power_state_on_off
+- error_state
+- info_state
+- battery_level
+
+### Washer Dryer
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- spinning_speed
+- spinning_speed_raw
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- drying_target
+- drying_target_raw
+- error_state
+- info_state
+- temperature_target
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Washing Machine
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- spinning_speed
+- spinning_speed_raw
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- error_state
+- info_state
+- temperature_target
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Wine Storage
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- operation_state
+- operation_state_raw
+- power_state_on_off
+- error_state
+- info_state
+- temperature_target
+- temperature_current
+- top_temperature_target
+- top_temperature_current
+- middle_temperature_target
+- middle_temperature_current
+- bottom_temperature_target
+- bottom_temperature_current
+
+### Note on plate_power_step channels
+
+Hob things have an additional property `plateCount` that indicates the number of plates present on the appliance.
+Only the channels `plate_1_power_step` to `plate_x_power_step` will be populated by the binding where `x` is the value of the `plateCount` property.
+
+The plate numbers do not represent the physical layout of the plates on the appliance, but always start with the `plate_1_power_step` channel.
+This means that a hob with two plates will have `plate_1_power_step` and `plate_2_power_step` populated and all other `plate_x_power_step` channels empty.
+
+The `plate_x_power_step` channels show the current power step of the according plate.
+**Please note that some hobs may use dynamic numbering for plates.**
+Hobs that use dynamic numbering will use the first power step channel that is currently at a power step of zero when the plate is turned on.
+Additionally, when a plate is turned off all other plates with higher numbers will decrease their number by one.
+For example if plate 1, 2 and 3 are active and plate 1 is turned off then plate 2 will become plate 1, plate 3 will become plate 2 and plate 3 will have a power step of zero.
+This behavior is a fixed part of the affected appliances and cannot be changed.
+
+### Note on door_state channel
+
+The `door_state` channel might not always provide a value matching the actual state.
+For example, a washing machine will not provide a valid `door_state` when the appliance is turned off.
+A valid door state can be expected when the appliance is in one of the following raw operation states, compare the `operation_state_raw` channel:
+
+- `3`: Program selected
+- `4`: Program selected, waiting to start
+- `5`: Running
+- `6`: Paused
+
+## Properties
+
+The following chapters list the properties offered by appliances.
+
+### Common Properties
+
+| Property Name | Description                                                                   |
+| ------------- | ----------------------------------------------------------------------------- |
+| serialNumber  | Serial number of the appliance, only present for physical appliances          |
+| modelId       | Model ID of the appliance                                                     |
+| vendor        | Always "Miele"                                                                |
+
+### Account
+
+| Property Name | Description                                                                   |
+| ------------- | ----------------------------------------------------------------------------- |
+| connection    | Type of connection used by the account, always "INTERNET"                     |
+| accessToken   | The currently used OAuth 2 access token for accessing the Miele 3rd Party API |
+
+### Hob
+
+| Property Name | Description                                                                   |
+| ------------- | ----------------------------------------------------------------------------- |
+| plateCount    | Number of plates offered by the appliance                                     |
+
+## Full Example
+
+### demo.things:
+
+```
+Bridge mielecloud:account:home [ email="me@openhab.org", locale="en" ] {
+    Thing coffee_system 000703261234 "Coffee machine CVA7440" [ deviceIdentifier="000703261234" ]
+    Thing hob 000160102345 "Cooktop KM7677" [ deviceIdentifier="000160102345" ]
+}
+```
+
+### demo.items:
+
+```
+// Coffee system
+Switch coffee_system_remote_control_can_be_started      { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_started" }
+Switch coffee_system_remote_control_can_be_stopped      { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_stopped" }
+Switch coffee_system_remote_control_can_be_switched_on  { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_on" }
+Switch coffee_system_remote_control_can_be_switched_off { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_off" }
+String coffee_system_program_active                     { channel="mielecloud:coffee_system:home:000703261234:program_active" }
+String coffee_system_program_phase                      { channel="mielecloud:coffee_system:home:000703261234:program_phase" }
+String coffee_system_power_state_on_off                 { channel="mielecloud:coffee_system:home:000703261234:power_state_on_off" }
+String coffee_system_operation_state                    { channel="mielecloud:coffee_system:home:000703261234:operation_state" }
+Switch coffee_system_finish_state                       { channel="mielecloud:coffee_system:home:000703261234:finish_state" }
+Number coffee_system_program_remaining_time             { channel="mielecloud:coffee_system:home:000703261234:program_remaining_time" }
+Switch coffee_system_error_state                        { channel="mielecloud:coffee_system:home:000703261234:error_state" }
+Switch coffee_system_info_state                         { channel="mielecloud:coffee_system:home:000703261234:info_state" }
+Switch coffee_system_light_switch                       { channel="mielecloud:coffee_system:home:000703261234:light_switch" }
+Switch coffee_system_light_can_be_controlled            { channel="mielecloud:coffee_system:home:000703261234:light_can_be_controlled" }
+
+// Hob
+Switch hob_remote_control_can_be_started { channel="mielecloud:hob:home:000160102345:remote_control_can_be_started" }
+Switch hob_remote_control_can_be_stopped { channel="mielecloud:hob:home:000160102345:remote_control_can_be_stopped" }
+String hob_operation_state               { channel="mielecloud:hob:home:000160102345:operation_state" }
+Switch hob_error_state                   { channel="mielecloud:hob:home:000160102345:error_state" }
+Switch hob_info_state                    { channel="mielecloud:hob:home:000160102345:info_state" }
+Switch hob_plate_1_is_present            { channel="mielecloud:hob:home:000160102345:plate_1_is_present" }
+String hob_plate_1_power_step            { channel="mielecloud:hob:home:000160102345:plate_1_power_step" }
+Switch hob_plate_2_is_present            { channel="mielecloud:hob:home:000160102345:plate_2_is_present" }
+String hob_plate_2_power_step            { channel="mielecloud:hob:home:000160102345:plate_2_power_step" }
+Switch hob_plate_3_is_present            { channel="mielecloud:hob:home:000160102345:plate_3_is_present" }
+String hob_plate_3_power_step            { channel="mielecloud:hob:home:000160102345:plate_3_power_step" }
+Switch hob_plate_4_is_present            { channel="mielecloud:hob:home:000160102345:plate_4_is_present" }
+String hob_plate_4_power_step            { channel="mielecloud:hob:home:000160102345:plate_4_power_step" }
+Switch hob_plate_5_is_present            { channel="mielecloud:hob:home:000160102345:plate_5_is_present" }
+String hob_plate_5_power_step            { channel="mielecloud:hob:home:000160102345:plate_5_power_step" }
+Switch hob_plate_6_is_present            { channel="mielecloud:hob:home:000160102345:plate_6_is_present" }
+String hob_plate_6_power_step            { channel="mielecloud:hob:home:000160102345:plate_6_power_step" }
+```
+
+### demo.sitemap:
+
+```
+sitemap demo label="Kitchen"
+{
+    Frame {
+        // Coffee system
+        Text    item=coffee_system_program_active
+        Text    item=coffee_system_program_phase
+        Text    item=coffee_system_power_state_on_off
+        Text    item=coffee_system_operation_state
+        Switch  item=coffee_system_finish_state
+        Default item=coffee_system_program_remaining_time
+        Switch  item=coffee_system_error_state
+        Switch  item=coffee_system_info_state
+        Switch  item=coffee_system_light_switch
+
+        // Hob
+        Text   item=hob_operation_state
+        Switch item=hob_error_state
+        Switch item=hob_info_state
+        Text   item=hob_plate_1_power_step
+        Text   item=hob_plate_2_power_step
+        Text   item=hob_plate_3_power_step
+        Text   item=hob_plate_4_power_step
+        Text   item=hob_plate_5_power_step
+        Text   item=hob_plate_6_power_step
+    }
+}
+```
+
+## Account Configuration Example
+
+The configuration UI is accessible at `https://<your openHAB address>/mielecloud`.
+See [Discovery](#discovery) for a detailed description of how to open the configuration UI in a browser.
+
+When first opening the configuration UI no account will be paired.
+
+![Empty Account Overview](doc/account-overview-empty.png)
+
+We strongly recommend to use a secure connection for pairing, details on this topic can also be found in the [Discovery](#discovery) section.
+Click `Pair Account` to start the pairing process.
+If not already done, go to the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx), register there and wait for the confirmation e-mail.
+Obtain your client ID and client secret according to the instructions presented there.
+Once you obtained your client ID and client secret continue pairing by filling in your client ID, client secret, bridge ID and an e-mail address that you wish to use for identifying the account.
+You may choose any bridge ID you like as long as you only use letters, numbers, underscores and dashes.
+The e-mail address does not need to match the e-mail address used for your Miele Cloud Account.
+If you need to change the e-mail address later then you will need to authorize the account again.
+
+![Pair Account](doc/pair-account.png)
+
+A click on `Pair Account` will take you to the Miele cloud service login form where you need to log in with the same account as you used for the Miele@mobile app.
+
+![Miele Login Form](doc/miele-login.png)
+
+When this is the first time you pair an account, you will need to allow openHAB to access your account.
+
+When everything worked, you are presented with a page stating that pairing was successful.
+Select the locale which should be used to display localized texts in openHAB channels.
+From here, you have two options:
+Either let the binding automatically configure a bridge instance or copy the presented things-file template to a things-file and return to the overview page.
+
+![Pairing Successful](doc/pairing-success.png)
+
+Once the bridge instance is `ONLINE`, you can either pair things for all appliances via your favorite management UI or use a things-file.
+The account overview provides a things-file template that is shown when you expand the account.
+This can serve as a starting point for your own things-file.
+
+![Account Overview With Bridge](doc/account-overview-with-bridge.png)
+
+## Rule Ideas
+
+Here are some ideas on what could be done with this binding. You have more ideas or even an example? Great! Feel free to contribute!
+
+- Notify yourself of a finished dishwasher, tumble dryer, washer dryer or washing machine, e.g. by changing the lighting
+- Control the supercooler / superfreezer of your freezer, fridge or fridge-freezer combination with a voice assistant
+- Notify yourself when the oven has finished pre-heating
+
+## Acknowledgements
+
+The development of this binding was initiated and sponsored by Miele & Cie. KG.
+
diff --git a/bundles/org.openhab.binding.mielecloud/doc/account-overview-empty.png b/bundles/org.openhab.binding.mielecloud/doc/account-overview-empty.png
new file mode 100644 (file)
index 0000000..7b733af
Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/account-overview-empty.png differ
diff --git a/bundles/org.openhab.binding.mielecloud/doc/account-overview-with-bridge.png b/bundles/org.openhab.binding.mielecloud/doc/account-overview-with-bridge.png
new file mode 100644 (file)
index 0000000..2a9361b
Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/account-overview-with-bridge.png differ
diff --git a/bundles/org.openhab.binding.mielecloud/doc/miele-login.png b/bundles/org.openhab.binding.mielecloud/doc/miele-login.png
new file mode 100644 (file)
index 0000000..f6a14db
Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/miele-login.png differ
diff --git a/bundles/org.openhab.binding.mielecloud/doc/pair-account.png b/bundles/org.openhab.binding.mielecloud/doc/pair-account.png
new file mode 100644 (file)
index 0000000..5493221
Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/pair-account.png differ
diff --git a/bundles/org.openhab.binding.mielecloud/doc/pairing-success.png b/bundles/org.openhab.binding.mielecloud/doc/pairing-success.png
new file mode 100644 (file)
index 0000000..f10bf62
Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/pairing-success.png differ
diff --git a/bundles/org.openhab.binding.mielecloud/pom.xml b/bundles/org.openhab.binding.mielecloud/pom.xml
new file mode 100644 (file)
index 0000000..2e6d1f6
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.mielecloud</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Miele Cloud Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/feature/feature.xml b/bundles/org.openhab.binding.mielecloud/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..4024caa
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+       Copyright (c) 2010-2020 Contributors to the openHAB project
+
+       See the NOTICE file(s) distributed with this work for additional
+       information.
+
+       This program and the accompanying materials are made available under the
+       terms of the Eclipse Public License 2.0 which is available at
+       http://www.eclipse.org/legal/epl-2.0
+
+       SPDX-License-Identifier: EPL-2.0
+
+-->
+<features name="org.openhab.binding.mielecloud-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-mielecloud" description="Miele Cloud Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mielecloud/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingConstants.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingConstants.java
new file mode 100644 (file)
index 0000000..59b1426
--- /dev/null
@@ -0,0 +1,238 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link MieleCloudBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Added locale config parameter, added i18n key collection
+ * @author Benjamin Bolte - Add pre-heat finished and plate step channels, door state and door alarm channels, info
+ *         state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel, dish warmer thing
+ */
+@NonNullByDefault
+public final class MieleCloudBindingConstants {
+
+    private MieleCloudBindingConstants() {
+    }
+
+    /**
+     * ID of the binding.
+     */
+    public static final String BINDING_ID = "mielecloud";
+
+    /**
+     * Thing type ID of Miele cloud bridges / accounts.
+     */
+    public static final String BRIDGE_TYPE_ID = "account";
+
+    /**
+     * The {@link ThingTypeUID} of Miele cloud bridges / accounts.
+     */
+    public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, BRIDGE_TYPE_ID);
+
+    /**
+     * The {@link ThingTypeUID} of Miele washing machines.
+     */
+    public static final ThingTypeUID THING_TYPE_WASHING_MACHINE = new ThingTypeUID(BINDING_ID, "washing_machine");
+
+    /**
+     * The {@link ThingTypeUID} of Miele washer-dryers.
+     */
+    public static final ThingTypeUID THING_TYPE_WASHER_DRYER = new ThingTypeUID(BINDING_ID, "washer_dryer");
+
+    /**
+     * The {@link ThingTypeUID} of Miele coffee machines.
+     */
+    public static final ThingTypeUID THING_TYPE_COFFEE_SYSTEM = new ThingTypeUID(BINDING_ID, "coffee_system");
+
+    /**
+     * The {@link ThingTypeUID} of Miele fridge-freezers.
+     */
+    public static final ThingTypeUID THING_TYPE_FRIDGE_FREEZER = new ThingTypeUID(BINDING_ID, "fridge_freezer");
+
+    /**
+     * The {@link ThingTypeUID} of Miele fridges.
+     */
+    public static final ThingTypeUID THING_TYPE_FRIDGE = new ThingTypeUID(BINDING_ID, "fridge");
+
+    /**
+     * The {@link ThingTypeUID} of Miele freezers.
+     */
+    public static final ThingTypeUID THING_TYPE_FREEZER = new ThingTypeUID(BINDING_ID, "freezer");
+
+    /**
+     * The {@link ThingTypeUID} of Miele ovens.
+     */
+    public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, "oven");
+
+    /**
+     * The {@link ThingTypeUID} of Miele hobs.
+     */
+    public static final ThingTypeUID THING_TYPE_HOB = new ThingTypeUID(BINDING_ID, "hob");
+
+    /**
+     * The {@link ThingTypeUID} of Miele wine storages.
+     */
+    public static final ThingTypeUID THING_TYPE_WINE_STORAGE = new ThingTypeUID(BINDING_ID, "wine_storage");
+
+    /**
+     * The {@link ThingTypeUID} of Miele dishwashers.
+     */
+    public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, "dishwasher");
+
+    /**
+     * The {@link ThingTypeUID} of Miele dryers.
+     */
+    public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, "dryer");
+
+    /**
+     * The {@link ThingTypeUID} of Miele hoods.
+     */
+    public static final ThingTypeUID THING_TYPE_HOOD = new ThingTypeUID(BINDING_ID, "hood");
+
+    /**
+     * The {@link ThingTypeUID} of Miele dish warmers.
+     */
+    public static final ThingTypeUID THING_TYPE_DISH_WARMER = new ThingTypeUID(BINDING_ID, "dish_warmer");
+
+    /**
+     * The {@link ThingTypeUID} of Miele robotic vacuum cleaners.
+     */
+    public static final ThingTypeUID THING_TYPE_ROBOTIC_VACUUM_CLEANER = new ThingTypeUID(BINDING_ID,
+            "robotic_vacuum_cleaner");
+
+    /**
+     * Name of the property storing the OAuth2 access token.
+     */
+    public static final String PROPERTY_ACCESS_TOKEN = "accessToken";
+
+    /**
+     * Name of the configuration parameter for the e-mail address.
+     */
+    public static final String CONFIG_PARAM_EMAIL = "email";
+
+    /**
+     * Name of the configuration parameter for the device identifier uniquely identifying a Miele device.
+     */
+    public static final String CONFIG_PARAM_DEVICE_IDENTIFIER = "deviceIdentifier";
+
+    /**
+     * Name of the configuration parameter for the locale. The locale is stored as a 2-letter language code.
+     */
+    public static final String CONFIG_PARAM_LOCALE = "locale";
+
+    /**
+     * Name of the property storing the number of plates for hobs.
+     */
+    public static final String PROPERTY_PLATE_COUNT = "plateCount";
+
+    /**
+     * Constants for all channels.
+     */
+    public static final class Channels {
+        private Channels() {
+        }
+
+        public static final String REMOTE_CONTROL_CAN_BE_STARTED = "remote_control_can_be_started";
+        public static final String REMOTE_CONTROL_CAN_BE_STOPPED = "remote_control_can_be_stopped";
+        public static final String REMOTE_CONTROL_CAN_BE_PAUSED = "remote_control_can_be_paused";
+        public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_ON = "remote_control_can_be_switched_on";
+        public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_OFF = "remote_control_can_be_switched_off";
+        public static final String REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE = "remote_control_can_set_program_active";
+        public static final String SPINNING_SPEED = "spinning_speed";
+        public static final String SPINNING_SPEED_RAW = "spinning_speed_raw";
+        public static final String PROGRAM_ACTIVE = "program_active";
+        public static final String PROGRAM_ACTIVE_RAW = "program_active_raw";
+        public static final String DISH_WARMER_PROGRAM_ACTIVE = "dish_warmer_program_active";
+        public static final String VACUUM_CLEANER_PROGRAM_ACTIVE = "vacuum_cleaner_program_active";
+        public static final String PROGRAM_PHASE = "program_phase";
+        public static final String PROGRAM_PHASE_RAW = "program_phase_raw";
+        public static final String OPERATION_STATE = "operation_state";
+        public static final String OPERATION_STATE_RAW = "operation_state_raw";
+        public static final String PROGRAM_START_STOP = "program_start_stop";
+        public static final String PROGRAM_START_STOP_PAUSE = "program_start_stop_pause";
+        public static final String POWER_ON_OFF = "power_state_on_off";
+        public static final String FINISH_STATE = "finish_state";
+        public static final String DELAYED_START_TIME = "delayed_start_time";
+        public static final String PROGRAM_REMAINING_TIME = "program_remaining_time";
+        public static final String PROGRAM_ELAPSED_TIME = "program_elapsed_time";
+        public static final String PROGRAM_PROGRESS = "program_progress";
+        public static final String DRYING_TARGET = "drying_target";
+        public static final String DRYING_TARGET_RAW = "drying_target_raw";
+        public static final String PRE_HEAT_FINISHED = "pre_heat_finished";
+        public static final String TEMPERATURE_TARGET = "temperature_target";
+        public static final String TEMPERATURE_CURRENT = "temperature_current";
+        public static final String TEMPERATURE_CORE_TARGET = "temperature_core_target";
+        public static final String TEMPERATURE_CORE_CURRENT = "temperature_core_current";
+        public static final String VENTILATION_POWER = "ventilation_power";
+        public static final String VENTILATION_POWER_RAW = "ventilation_power_raw";
+        public static final String ERROR_STATE = "error_state";
+        public static final String INFO_STATE = "info_state";
+        public static final String FRIDGE_SUPER_COOL = "fridge_super_cool";
+        public static final String FREEZER_SUPER_FREEZE = "freezer_super_freeze";
+        public static final String SUPER_COOL_CAN_BE_CONTROLLED = "super_cool_can_be_controlled";
+        public static final String SUPER_FREEZE_CAN_BE_CONTROLLED = "super_freeze_can_be_controlled";
+        public static final String FRIDGE_TEMPERATURE_TARGET = "fridge_temperature_target";
+        public static final String FRIDGE_TEMPERATURE_CURRENT = "fridge_temperature_current";
+        public static final String FREEZER_TEMPERATURE_TARGET = "freezer_temperature_target";
+        public static final String FREEZER_TEMPERATURE_CURRENT = "freezer_temperature_current";
+        public static final String TOP_TEMPERATURE_TARGET = "top_temperature_target";
+        public static final String TOP_TEMPERATURE_CURRENT = "top_temperature_current";
+        public static final String MIDDLE_TEMPERATURE_TARGET = "middle_temperature_target";
+        public static final String MIDDLE_TEMPERATURE_CURRENT = "middle_temperature_current";
+        public static final String BOTTOM_TEMPERATURE_TARGET = "bottom_temperature_target";
+        public static final String BOTTOM_TEMPERATURE_CURRENT = "bottom_temperature_current";
+        public static final String LIGHT_SWITCH = "light_switch";
+        public static final String LIGHT_CAN_BE_CONTROLLED = "light_can_be_controlled";
+        public static final String PLATE_1_POWER_STEP = "plate_1_power_step";
+        public static final String PLATE_1_POWER_STEP_RAW = "plate_1_power_step_raw";
+        public static final String PLATE_2_POWER_STEP = "plate_2_power_step";
+        public static final String PLATE_2_POWER_STEP_RAW = "plate_2_power_step_raw";
+        public static final String PLATE_3_POWER_STEP = "plate_3_power_step";
+        public static final String PLATE_3_POWER_STEP_RAW = "plate_3_power_step_raw";
+        public static final String PLATE_4_POWER_STEP = "plate_4_power_step";
+        public static final String PLATE_4_POWER_STEP_RAW = "plate_4_power_step_raw";
+        public static final String PLATE_5_POWER_STEP = "plate_5_power_step";
+        public static final String PLATE_5_POWER_STEP_RAW = "plate_5_power_step_raw";
+        public static final String PLATE_6_POWER_STEP = "plate_6_power_step";
+        public static final String PLATE_6_POWER_STEP_RAW = "plate_6_power_step_raw";
+        public static final String DOOR_STATE = "door_state";
+        public static final String DOOR_ALARM = "door_alarm";
+        public static final String BATTERY_LEVEL = "battery_level";
+    }
+
+    /**
+     * Constants for i18n keys.
+     */
+    public static final class I18NKeys {
+        private I18NKeys() {
+        }
+
+        public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED = "@text/mielecloud.bridge.status.access.token.not.configured";
+        public static final String BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED = "@text/mielecloud.bridge.status.account.not.authorized";
+        public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED = "@text/mielecloud.bridge.status.access.token.refresh.failed";
+        public static final String BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL = "@text/mielecloud.bridge.status.invalid.email";
+        public static final String BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR = "@text/mielecloud.bridge.status.transient.http.error";
+
+        public static final String THING_STATUS_DESCRIPTION_WEBSERVICE_MISSING = "@text/mielecloud.thing.status.webservice.missing";
+        public static final String THING_STATUS_DESCRIPTION_REMOVED = "@text/mielecloud.thing.status.removed";
+        public static final String THING_STATUS_DESCRIPTION_RATELIMIT = "@text/mielecloud.thing.status.ratelimit";
+        public static final String THING_STATUS_DESCRIPTION_DISCONNECTED = "@text/mielecloud.thing.status.disconnected";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthException.java
new file mode 100644 (file)
index 0000000..e607db6
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Indicates an error in the OAuth2 authorization process.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class OAuthException extends RuntimeException {
+    private static final long serialVersionUID = -1863609233382694104L;
+
+    public OAuthException(final String message) {
+        super(message);
+    }
+
+    public OAuthException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefreshListener.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefreshListener.java
new file mode 100644 (file)
index 0000000..94b723b
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Listener that is invoked when an OAuth 2 access token was refreshed.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface OAuthTokenRefreshListener {
+    /**
+     * Invoked when a new access token becomes available.
+     *
+     * @param accessToken The new access token.
+     */
+    public void onNewAccessToken(String accessToken);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefresher.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefresher.java
new file mode 100644 (file)
index 0000000..c5cbd3f
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * An {@link OAuthTokenRefresher} offers convenient access to OAuth 2 authentication related functionality,
+ * especially refreshing the access token.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Allow removing tokens from the storage
+ */
+@NonNullByDefault
+public interface OAuthTokenRefresher {
+    /**
+     * Sets the listener that is called when the access token was refreshed.
+     *
+     * @param listener The listener to register.
+     * @param serviceHandle The service handle identifying the internal OAuth configuration.
+     * @throws OAuthException if the listener needs to be registered at an underlying service which is not available
+     *             because the account has not yet been authorized
+     */
+    public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle);
+
+    /**
+     * Unsets a listener.
+     *
+     * @param serviceHandle The service handle identifying the internal OAuth configuration.
+     */
+    public void unsetRefreshListener(String serviceHandle);
+
+    /**
+     * Refreshes the access and refresh tokens for the given service handle. If an {@link OAuthTokenRefreshListener} is
+     * registered for the service handle then it is notified after the refresh has completed.
+     *
+     * This call will succeed if the access token is still valid or a valid refresh token exists, which can be used to
+     * refresh the expired access token. If refreshing fails, an {@link OAuthException} is thrown.
+     *
+     * @param serviceHandle The service handle identifying the internal OAuth configuration.
+     * @throws OAuthException if the token cannot be obtained or refreshed
+     */
+    public void refreshToken(String serviceHandle);
+
+    /**
+     * Gets the currently stored access token from persistent storage.
+     *
+     * @param serviceHandle The service handle identifying the internal OAuth configuration.
+     * @return The currently stored access token or an empty {@link Optional} if there is no stored token.
+     */
+    public Optional<String> getAccessTokenFromStorage(String serviceHandle);
+
+    /**
+     * Removes the tokens from persistent storage.
+     *
+     * Note: Calling this method will force the user to run through the pairing process again in order to obtain a
+     * working bridge.
+     *
+     * @param serviceHandle The service handle identifying the internal OAuth configuration.
+     */
+    public void removeTokensFromStorage(String serviceHandle);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresher.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresher.java
new file mode 100644 (file)
index 0000000..a947da3
--- /dev/null
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles refreshing of OAuth2 tokens managed by the openHAB runtime.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@Component
+@NonNullByDefault
+public final class OpenHabOAuthTokenRefresher implements OAuthTokenRefresher {
+    private final Logger logger = LoggerFactory.getLogger(OpenHabOAuthTokenRefresher.class);
+
+    private final OAuthFactory oauthFactory;
+    private Map<String, @Nullable AccessTokenRefreshListener> listenerByServiceHandle = new HashMap<>();
+
+    @Activate
+    public OpenHabOAuthTokenRefresher(@Reference OAuthFactory oauthFactory) {
+        this.oauthFactory = oauthFactory;
+    }
+
+    @Override
+    public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle) {
+        final AccessTokenRefreshListener refreshListener = tokenResponse -> {
+            final String accessToken = tokenResponse.getAccessToken();
+            if (accessToken == null) {
+                // Fail without exception to ensure that the OAuthClientService notifies all listeners.
+                logger.warn("Ignoring access token response without access token.");
+            } else {
+                listener.onNewAccessToken(accessToken);
+            }
+        };
+
+        OAuthClientService clientService = getOAuthClientService(serviceHandle);
+        clientService.addAccessTokenRefreshListener(refreshListener);
+        listenerByServiceHandle.put(serviceHandle, refreshListener);
+    }
+
+    @Override
+    public void unsetRefreshListener(String serviceHandle) {
+        final AccessTokenRefreshListener refreshListener = listenerByServiceHandle.get(serviceHandle);
+        if (refreshListener != null) {
+            try {
+                OAuthClientService clientService = getOAuthClientService(serviceHandle);
+                clientService.removeAccessTokenRefreshListener(refreshListener);
+            } catch (OAuthException e) {
+                logger.warn("Failed to remove refresh listener: OAuth client service is unavailable. Cause: {}",
+                        e.getMessage());
+            }
+        }
+        listenerByServiceHandle.remove(serviceHandle);
+    }
+
+    @Override
+    public void refreshToken(String serviceHandle) {
+        if (listenerByServiceHandle.get(serviceHandle) == null) {
+            logger.warn("Token refreshing was requested but there is no token refresh listener registered!");
+            return;
+        }
+
+        OAuthClientService clientService = getOAuthClientService(serviceHandle);
+        refreshAccessToken(clientService);
+    }
+
+    private OAuthClientService getOAuthClientService(String serviceHandle) {
+        final OAuthClientService clientService = oauthFactory.getOAuthClientService(serviceHandle);
+        if (clientService == null) {
+            throw new OAuthException("OAuth client service is not available.");
+        }
+        return clientService;
+    }
+
+    private void refreshAccessToken(OAuthClientService clientService) {
+        try {
+            final AccessTokenResponse accessTokenResponse = clientService.refreshToken();
+            final String accessToken = accessTokenResponse.getAccessToken();
+            if (accessToken == null) {
+                throw new OAuthException("Access token is not available.");
+            }
+        } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+            throw new OAuthException("An error occured during token refresh: " + e.getMessage(), e);
+        } catch (IOException e) {
+            throw new OAuthException("A network error occured during token refresh: " + e.getMessage(), e);
+        } catch (OAuthResponseException e) {
+            throw new OAuthException("Miele cloud service returned an illegal response: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public Optional<String> getAccessTokenFromStorage(String serviceHandle) {
+        try {
+            AccessTokenResponse tokenResponse = getOAuthClientService(serviceHandle).getAccessTokenResponse();
+            if (tokenResponse == null) {
+                return Optional.empty();
+            } else {
+                return Optional.of(tokenResponse.getAccessToken());
+            }
+        } catch (OAuthException | org.openhab.core.auth.client.oauth2.OAuthException | IOException
+                | OAuthResponseException e) {
+            logger.debug("Cannot obtain access token from persistent storage.", e);
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public void removeTokensFromStorage(String serviceHandle) {
+        oauthFactory.deleteServiceAndAccessToken(serviceHandle);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/MieleCloudConfigService.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/MieleCloudConfigService.java
new file mode 100644 (file)
index 0000000..a2b15cc
--- /dev/null
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.config.servlet.AccountOverviewServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.FailureServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.PairAccountServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ResourceLoader;
+import org.openhab.binding.mielecloud.internal.config.servlet.ResultServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.SuccessServlet;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.JvmLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.OpenHabLanguageProvider;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.thing.ThingRegistry;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles the lifecycle of the Miele Cloud binding's configuration UI.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@Component(service = MieleCloudConfigService.class, immediate = true, configurationPid = "binding.mielecloud.configService")
+@NonNullByDefault
+public final class MieleCloudConfigService {
+    private static final String ROOT_ALIAS = "/mielecloud";
+    private static final String PAIR_ALIAS = ROOT_ALIAS + "/pair";
+    private static final String FORWARD_TO_LOGIN_ALIAS = ROOT_ALIAS + "/forwardToLogin";
+    private static final String RESULT_ALIAS = ROOT_ALIAS + "/result";
+    private static final String SUCCESS_ALIAS = ROOT_ALIAS + "/success";
+    private static final String CREATE_BRIDGE_THING_ALIAS = ROOT_ALIAS + "/createBridgeThing";
+    private static final String FAILURE_ALIAS = ROOT_ALIAS + "/failure";
+    private static final String CSS_ALIAS = ROOT_ALIAS + "/assets/css";
+    private static final String JS_ALIAS = ROOT_ALIAS + "/assets/js";
+    private static final String IMG_ALIAS = ROOT_ALIAS + "/assets/img";
+
+    private static final String WEBSITE_RESOURCE_BASE_PATH = "org/openhab/binding/mielecloud/internal/config";
+    private static final String WEBSITE_CSS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/css";
+    private static final String WEBSITE_JS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/js";
+    private static final String WEBSITE_IMG_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/img";
+
+    private final Logger logger = LoggerFactory.getLogger(MieleCloudConfigService.class);
+
+    private HttpService httpService;
+    private OAuthFactory oauthFactory;
+    private Inbox inbox;
+    private ThingRegistry thingRegistry;
+    private LocaleProvider localeProvider;
+
+    /**
+     * For integration test purposes only.
+     */
+    @Nullable
+    private AccountOverviewServlet accountOverviewServlet;
+
+    /**
+     * For integration test purposes only.
+     */
+    @Nullable
+    private ForwardToLoginServlet forwardToLoginServlet;
+
+    /**
+     * For integration test purposes only.
+     */
+    @Nullable
+    private ResultServlet resultServlet;
+
+    /**
+     * For integration test purposes only.
+     */
+    @Nullable
+    private SuccessServlet successServlet;
+
+    /**
+     * For integration test purposes only.
+     */
+    @Nullable
+    private CreateBridgeServlet createBridgeServlet;
+
+    @Activate
+    public MieleCloudConfigService(@Reference HttpService httpService, @Reference OAuthFactory oauthFactory,
+            @Reference Inbox inbox, @Reference ThingRegistry thingRegistry, @Reference LocaleProvider localeProvider) {
+        this.httpService = httpService;
+        this.oauthFactory = oauthFactory;
+        this.inbox = inbox;
+        this.thingRegistry = thingRegistry;
+        this.localeProvider = localeProvider;
+    }
+
+    @Nullable
+    public AccountOverviewServlet getAccountOverviewServlet() {
+        return accountOverviewServlet;
+    }
+
+    @Nullable
+    public ForwardToLoginServlet getForwardToLoginServlet() {
+        return forwardToLoginServlet;
+    }
+
+    @Nullable
+    public ResultServlet getResultServlet() {
+        return resultServlet;
+    }
+
+    @Nullable
+    public SuccessServlet getSuccessServlet() {
+        return successServlet;
+    }
+
+    @Nullable
+    public CreateBridgeServlet getCreateBridgeServlet() {
+        return createBridgeServlet;
+    }
+
+    @Activate
+    protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
+        registerWebsite(componentContext.getBundleContext());
+    }
+
+    private void registerWebsite(BundleContext bundleContext) {
+        ResourceLoader resourceLoader = new ResourceLoader(WEBSITE_RESOURCE_BASE_PATH, bundleContext);
+        OAuthAuthorizationHandler authorizationHandler = new OAuthAuthorizationHandlerImpl(oauthFactory,
+                ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON));
+
+        try {
+            HttpContext httpContext = httpService.createDefaultHttpContext();
+            httpService.registerServlet(ROOT_ALIAS,
+                    accountOverviewServlet = new AccountOverviewServlet(resourceLoader, thingRegistry, inbox),
+                    new Hashtable<>(), httpContext);
+            httpService.registerServlet(PAIR_ALIAS, new PairAccountServlet(resourceLoader), new Hashtable<>(),
+                    httpContext);
+            httpService.registerServlet(FORWARD_TO_LOGIN_ALIAS,
+                    forwardToLoginServlet = new ForwardToLoginServlet(authorizationHandler), new Hashtable<>(),
+                    httpContext);
+            httpService.registerServlet(RESULT_ALIAS, resultServlet = new ResultServlet(authorizationHandler),
+                    new Hashtable<>(), httpContext);
+            httpService.registerServlet(SUCCESS_ALIAS,
+                    successServlet = new SuccessServlet(resourceLoader, createLanguageProvider()), new Hashtable<>(),
+                    httpContext);
+            httpService.registerServlet(CREATE_BRIDGE_THING_ALIAS,
+                    createBridgeServlet = new CreateBridgeServlet(inbox, thingRegistry), new Hashtable<>(),
+                    httpContext);
+            httpService.registerServlet(FAILURE_ALIAS, new FailureServlet(resourceLoader), new Hashtable<>(),
+                    httpContext);
+            httpService.registerResources(CSS_ALIAS, WEBSITE_CSS_RESOURCE_PATH, httpContext);
+            httpService.registerResources(JS_ALIAS, WEBSITE_JS_RESOURCE_PATH, httpContext);
+            httpService.registerResources(IMG_ALIAS, WEBSITE_IMG_RESOURCE_PATH, httpContext);
+            logger.debug("Registered Miele Cloud binding website at /mielecloud");
+        } catch (NamespaceException | ServletException e) {
+            logger.warn(
+                    "Failed to register Miele Cloud binding website. Miele Cloud binding website will not be available.",
+                    e);
+            unregisterWebsite();
+        }
+    }
+
+    private LanguageProvider createLanguageProvider() {
+        return new CombiningLanguageProvider(new OpenHabLanguageProvider(localeProvider), new JvmLanguageProvider());
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        unregisterWebsite();
+    }
+
+    private void unregisterWebsite() {
+        unregisterWebResource(ROOT_ALIAS);
+        unregisterWebResource(PAIR_ALIAS);
+        unregisterWebResource(FORWARD_TO_LOGIN_ALIAS);
+        unregisterWebResource(RESULT_ALIAS);
+        unregisterWebResource(SUCCESS_ALIAS);
+        unregisterWebResource(CREATE_BRIDGE_THING_ALIAS);
+        unregisterWebResource(CSS_ALIAS);
+        unregisterWebResource(JS_ALIAS);
+        unregisterWebResource(IMG_ALIAS);
+        forwardToLoginServlet = null;
+        resultServlet = null;
+        createBridgeServlet = null;
+        logger.debug("Unregistered Miele Cloud binding website at /mielecloud");
+    }
+
+    private void unregisterWebResource(String alias) {
+        try {
+            httpService.unregister(alias);
+        } catch (IllegalArgumentException e) {
+            logger.warn("Failed to unregister Miele Cloud binding website alias {}", alias, e);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandler.java
new file mode 100644 (file)
index 0000000..9acf503
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * Handles OAuth 2 authorization processes.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface OAuthAuthorizationHandler {
+    /**
+     * Begins the authorization process after the user provided client ID, client secret and a bridge ID.
+     *
+     * @param clientId Client ID.
+     * @param clientSecret Client secret.
+     * @param bridgeUid The UID of the bridge to authorize.
+     * @param email E-mail address identifying the account to authorize.
+     * @throws OngoingAuthorizationException if there already is an ongoing authorization.
+     */
+    void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid, String email);
+
+    /**
+     * Creates the authorization URL for the ongoing authorization.
+     *
+     * @param redirectUri The URI to which the user is redirected after a successful login. This should point to our own
+     *            service.
+     * @return The authorization URL to which the user is redirected for the log in.
+     * @throws NoOngoingAuthorizationException if there is no ongoing authorization.
+     * @throws OAuthException if the authorization URL cannot be determined. In this case the ongoing authorization is
+     *             cancelled.
+     */
+    String getAuthorizationUrl(String redirectUri);
+
+    /**
+     * Gets the UID of the bridge that is currently being authorized.
+     */
+    ThingUID getBridgeUid();
+
+    /**
+     * Gets the e-mail address associated with the account that is currently being authorized.
+     */
+    String getEmail();
+
+    /**
+     * Completes the authorization by extracting the authorization code from the given redirection URL, fetching the
+     * access token response and persisting it. After this method succeeded the access token can be read from the
+     * persistent storage.
+     *
+     * @param redirectUrlWithParameters The URL the remote service redirected the user to. This is the URL our servlet
+     *            was called with.
+     * @throws NoOngoingAuthorizationException if there is no ongoing authorization.
+     * @throws OAuthException if the authorization failed. In this case the ongoing authorization is cancelled.
+     */
+    void completeAuthorization(String redirectUrlWithParameters);
+
+    /**
+     * Gets the access token from persistent storage.
+     *
+     * @param email E-mail address for which the access token is requested.
+     * @return The access token.
+     * @throws OAuthException if the access token cannot be obtained.
+     */
+    String getAccessToken(String email);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImpl.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImpl.java
new file mode 100644 (file)
index 0000000..86ef742
--- /dev/null
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.webservice.DefaultMieleWebservice;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * {@link OAuthAuthorizationHandler} implementation handling the OAuth 2 authorization via openHAB services.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class OAuthAuthorizationHandlerImpl implements OAuthAuthorizationHandler {
+    private static final String TOKEN_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/token";
+    private static final String AUTHORIZATION_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/login";
+
+    private static final long AUTHORIZATION_TIMEOUT_IN_MINUTES = 5;
+
+    private final OAuthFactory oauthFactory;
+    private final ScheduledExecutorService scheduler;
+
+    @Nullable
+    private OAuthClientService oauthClientService;
+    @Nullable
+    private ThingUID bridgeUid;
+    @Nullable
+    private String email;
+    @Nullable
+    private String redirectUri;
+    @Nullable
+    private ScheduledFuture<?> timer;
+    @Nullable
+    private LocalDateTime timerExpiryTimestamp;
+
+    /**
+     * Creates a new {@link OAuthAuthorizationHandlerImpl}.
+     *
+     * @param oauthFactory Factory for accessing the {@link OAuthClientService}.
+     * @param scheduler System-wide scheduler.
+     */
+    public OAuthAuthorizationHandlerImpl(OAuthFactory oauthFactory, ScheduledExecutorService scheduler) {
+        this.oauthFactory = oauthFactory;
+        this.scheduler = scheduler;
+    }
+
+    @Override
+    public synchronized void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid,
+            String email) {
+        if (this.oauthClientService != null) {
+            throw new OngoingAuthorizationException("There is already an ongoing authorization!", timerExpiryTimestamp);
+        }
+
+        this.oauthClientService = oauthFactory.createOAuthClientService(email, TOKEN_URL, AUTHORIZATION_URL, clientId,
+                clientSecret, null, false);
+        this.bridgeUid = bridgeUid;
+        this.email = email;
+        redirectUri = null;
+        timer = null;
+        timerExpiryTimestamp = null;
+    }
+
+    @Override
+    public synchronized String getAuthorizationUrl(String redirectUri) {
+        final OAuthClientService oauthClientService = this.oauthClientService;
+        if (oauthClientService == null) {
+            throw new NoOngoingAuthorizationException("There is no ongoing authorization!");
+        }
+
+        this.redirectUri = redirectUri;
+        try {
+            timer = scheduler.schedule(this::cancelAuthorization, AUTHORIZATION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
+            timerExpiryTimestamp = LocalDateTime.now().plusMinutes(AUTHORIZATION_TIMEOUT_IN_MINUTES);
+            return oauthClientService.getAuthorizationUrl(redirectUri, null, null);
+        } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+            abortTimer();
+            cancelAuthorization();
+            throw new OAuthException("Failed to determine authorization URL: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public ThingUID getBridgeUid() {
+        final ThingUID bridgeUid = this.bridgeUid;
+        if (bridgeUid == null) {
+            throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
+        }
+        return bridgeUid;
+    }
+
+    @Override
+    public String getEmail() {
+        final String email = this.email;
+        if (email == null) {
+            throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
+        }
+        return email;
+    }
+
+    @Override
+    public synchronized void completeAuthorization(String redirectUrlWithParameters) {
+        abortTimer();
+
+        final OAuthClientService oauthClientService = this.oauthClientService;
+        if (oauthClientService == null) {
+            throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
+        }
+
+        try {
+            String authorizationCode = oauthClientService.extractAuthCodeFromAuthResponse(redirectUrlWithParameters);
+
+            // Although this method is called "get" it actually fetches and stores the token response as a side effect.
+            oauthClientService.getAccessTokenResponseByAuthorizationCode(authorizationCode, redirectUri);
+        } catch (IOException e) {
+            throw new OAuthException("Network error while retrieving token response: " + e.getMessage(), e);
+        } catch (OAuthResponseException e) {
+            throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e);
+        } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+            throw new OAuthException("Error while processing Miele service response: " + e.getMessage(), e);
+        } finally {
+            this.oauthClientService = null;
+            this.bridgeUid = null;
+            this.email = null;
+            this.redirectUri = null;
+        }
+    }
+
+    /**
+     * Aborts the timer.
+     *
+     * Note: All calls to this method must be {@code synchronized} to ensure thread-safety. Also note that
+     * {@link #cancelAuthorization()} is {@code synchronized} so the execution of this method and
+     * {@link #cancelAuthorization()} cannot overlap. Therefore, this method is an atomic operation from the timer's
+     * perspective.
+     */
+    private void abortTimer() {
+        final ScheduledFuture<?> timer = this.timer;
+        if (timer == null) {
+            return;
+        }
+
+        if (!timer.isDone()) {
+            timer.cancel(false);
+        }
+        this.timer = null;
+        timerExpiryTimestamp = null;
+    }
+
+    private synchronized void cancelAuthorization() {
+        oauthClientService = null;
+        bridgeUid = null;
+        email = null;
+        redirectUri = null;
+        final ScheduledFuture<?> timer = this.timer;
+        if (timer != null) {
+            timer.cancel(false);
+            this.timer = null;
+            timerExpiryTimestamp = null;
+        }
+    }
+
+    @Override
+    public String getAccessToken(String email) {
+        OAuthClientService clientService = oauthFactory.getOAuthClientService(email);
+        if (clientService == null) {
+            throw new OAuthException("There is no access token registered for '" + email + "'");
+        }
+
+        try {
+            AccessTokenResponse response = clientService.getAccessTokenResponse();
+            if (response == null) {
+                throw new OAuthException(
+                        "There is no access token in the persistent storage or it already expired and could not be refreshed");
+            } else {
+                return response.getAccessToken();
+            }
+        } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+            throw new OAuthException("Failed to read access token from persistent storage: " + e.getMessage(), e);
+        } catch (IOException e) {
+            throw new OAuthException(
+                    "Network error during token refresh or error while reading from persistent storage: "
+                            + e.getMessage(),
+                    e);
+        } catch (OAuthResponseException e) {
+            throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGenerator.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGenerator.java
new file mode 100644 (file)
index 0000000..613466c
--- /dev/null
@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Generator for templates which can be copy-pasted into .things files by the user.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ThingsTemplateGenerator {
+    /**
+     * Creates a template for the bridge.
+     *
+     * @param bridgeId Id of the bridge (last part of the thing UID).
+     * @param locale Locale for accessing the Miele cloud service.
+     * @return The template.
+     */
+    public String createBridgeConfigurationTemplate(String bridgeId, String email, String locale) {
+        var builder = new StringBuilder();
+        builder.append("Bridge ");
+        builder.append(MieleCloudBindingConstants.THING_TYPE_BRIDGE.getAsString());
+        builder.append(":");
+        builder.append(bridgeId);
+        builder.append(" [ email=\"");
+        builder.append(email);
+        builder.append("\", locale=\"");
+        builder.append(locale);
+        builder.append("\" ]");
+        return builder.toString();
+    }
+
+    /**
+     * Creates a complete template containing the bridge and all paired devices.
+     *
+     * @param bridge The bridge which is used to pair the things.
+     * @param pairedThings The paired things.
+     * @param discoveryResults The discovery results which can be paired.
+     * @return The template.
+     */
+    public String createBridgeAndThingConfigurationTemplate(Bridge bridge, List<Thing> pairedThings,
+            List<DiscoveryResult> discoveryResults) {
+        StringBuilder result = new StringBuilder();
+        result.append(createBridgeConfigurationTemplate(bridge.getUID().getId(),
+                bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString(),
+                getLocale(bridge)));
+        result.append(" {\n");
+
+        for (Thing thing : pairedThings) {
+            result.append("    ").append(createThingConfigurationTemplate(thing)).append("\n");
+        }
+
+        for (DiscoveryResult discoveryResult : discoveryResults) {
+            result.append("    ").append(createThingConfigurationTemplate(discoveryResult)).append("\n");
+        }
+
+        result.append("}");
+        return result.toString();
+    }
+
+    private String getLocale(Bridge bridge) {
+        var locale = bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
+        if (locale instanceof String) {
+            return (String) locale;
+        } else {
+            return "en";
+        }
+    }
+
+    private String createThingConfigurationTemplate(Thing thing) {
+        StringBuilder result = new StringBuilder();
+        result.append("Thing ").append(thing.getThingTypeUID().getId()).append(" ").append(thing.getUID().getId())
+                .append(" ");
+
+        final String label = thing.getLabel();
+        if (label != null) {
+            result.append("\"").append(label).append("\" ");
+        }
+
+        result.append("[ ");
+        result.append("deviceIdentifier=\"");
+        result.append(
+                thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER).toString());
+        result.append("\"");
+        result.append(" ]");
+        return result.toString();
+    }
+
+    private String createThingConfigurationTemplate(DiscoveryResult discoveryResult) {
+        return "Thing " + discoveryResult.getThingTypeUID().getId() + " " + discoveryResult.getThingUID().getId()
+                + " \"" + discoveryResult.getLabel() + "\" [ deviceIdentifier=\""
+                + getProperty(discoveryResult, MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER) + "\" ]";
+    }
+
+    private String getProperty(DiscoveryResult discoveryResult, String propertyName) {
+        var value = discoveryResult.getProperties().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER);
+        if (value == null) {
+            return "";
+        } else {
+            return value.toString();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeCreationFailedException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeCreationFailedException.java
new file mode 100644 (file)
index 0000000..54c2263
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when a bridge cannot be created in the configuration flow.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class BridgeCreationFailedException extends RuntimeException {
+    private static final long serialVersionUID = -6150154333256723312L;
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeReconfigurationFailedException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeReconfigurationFailedException.java
new file mode 100644 (file)
index 0000000..97f66aa
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when reconfiguring an existing bridge fails.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class BridgeReconfigurationFailedException extends RuntimeException {
+    private static final long serialVersionUID = -6341258448724364940L;
+
+    public BridgeReconfigurationFailedException(String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/NoOngoingAuthorizationException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/NoOngoingAuthorizationException.java
new file mode 100644 (file)
index 0000000..2ba3768
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when no authorization is ongoing.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class NoOngoingAuthorizationException extends RuntimeException {
+    private static final long serialVersionUID = 3074275827393542416L;
+
+    public NoOngoingAuthorizationException(String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/OngoingAuthorizationException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/OngoingAuthorizationException.java
new file mode 100644 (file)
index 0000000..e232ee5
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Exception thrown when there already is an ongoing authorization process.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class OngoingAuthorizationException extends RuntimeException {
+    private static final long serialVersionUID = -6742384930140134244L;
+
+    @Nullable
+    private final LocalDateTime ongoingAuthorizationExpiryTimestamp;
+
+    /**
+     * Creates a new {@link OngoingAuthorizationException}.
+     *
+     * @param message Exception message.
+     * @param ongoingAuthorizationExpiryTimestamp Timestamp when the ongoing authorization will expire.
+     */
+    public OngoingAuthorizationException(String message, @Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) {
+        super(message);
+        this.ongoingAuthorizationExpiryTimestamp = ongoingAuthorizationExpiryTimestamp;
+    }
+
+    /**
+     * Gets the timestamp representing when the ongoing authorization will expire.
+     */
+    @Nullable
+    public LocalDateTime getOngoingAuthorizationExpiryTimestamp() {
+        return ongoingAuthorizationExpiryTimestamp;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractRedirectionServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractRedirectionServlet.java
new file mode 100644 (file)
index 0000000..d8a4090
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for servlets that have no visible frontend and just serve the purpose of redirecting the user to another
+ * website.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractRedirectionServlet extends HttpServlet {
+    private static final long serialVersionUID = 4280026301732437523L;
+
+    private final Logger logger = LoggerFactory.getLogger(AbstractRedirectionServlet.class);
+
+    @Override
+    protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
+            throws ServletException, IOException {
+        if (response == null) {
+            logger.warn("Ignoring received request without response.");
+            return;
+        }
+        if (request == null) {
+            logger.warn("Ignoring illegal request.");
+            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+
+        response.sendRedirect(getRedirectionDestination(request));
+    }
+
+    /**
+     * Gets the redirection destination. This can be a relative or absolute path or a link to another website.
+     *
+     * @param request The original request sent by the browser.
+     * @return The redirection destination.
+     */
+    protected abstract String getRedirectionDestination(HttpServletRequest request);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractShowPageServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractShowPageServlet.java
new file mode 100644 (file)
index 0000000..8faa7bb
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for servlets that show a visible frontend in the browser.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractShowPageServlet extends HttpServlet {
+    private static final long serialVersionUID = 3820684716753275768L;
+
+    private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
+
+    private final Logger logger = LoggerFactory.getLogger(AbstractShowPageServlet.class);
+
+    private final ResourceLoader resourceLoader;
+
+    protected ResourceLoader getResourceLoader() {
+        return resourceLoader;
+    }
+
+    /**
+     * Creates a new {@link AbstractShowPageServlet}.
+     *
+     * @param resourceLoader Loader for resource files.
+     */
+    public AbstractShowPageServlet(ResourceLoader resourceLoader) {
+        this.resourceLoader = resourceLoader;
+    }
+
+    @Override
+    protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
+            throws ServletException, IOException {
+        if (response == null) {
+            logger.warn("Ignoring received request without response.");
+            return;
+        }
+        if (request == null) {
+            logger.warn("Ignoring illegal request.");
+            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+
+        try {
+            String html = handleGetRequest(request, response);
+            response.setContentType(CONTENT_TYPE);
+            response.getWriter().write(html);
+            response.getWriter().close();
+        } catch (MieleHttpException e) {
+            response.sendError(e.getHttpErrorCode());
+        } catch (IOException e) {
+            logger.warn("Failed to load resources.", e);
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Handles a GET request.
+     *
+     * @param request The request.
+     * @param response The response.
+     * @return A rendered HTML body to be displayed in the browser. The body will be framed by the binding's frontend
+     *         layout.
+     * @throws MieleHttpException if an error occurs that should be handled by sending a default error response.
+     * @throws IOException if an error occurs while loading resources.
+     */
+    protected abstract String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+            throws MieleHttpException, IOException;
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServlet.java
new file mode 100644 (file)
index 0000000..8944e0d
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+
+/**
+ * Servlet showing the account overview page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class AccountOverviewServlet extends AbstractShowPageServlet {
+    private static final long serialVersionUID = -4551210904923220429L;
+    private static final String ACCOUNTS_SKELETON = "index.html";
+
+    private static final String BRIDGES_TITLE_PLACEHOLDER = "<!-- BRIDGES TITLE -->";
+    private static final String BRIDGES_PLACEHOLDER = "<!-- BRIDGES -->";
+    private static final String NO_SSL_WARNING_PLACEHOLDER = "<!-- NO SSL WARNING -->";
+
+    private final ThingRegistry thingRegistry;
+    private final Inbox inbox;
+    private final ThingsTemplateGenerator templateGenerator;
+
+    /**
+     * Creates a new {@link AccountOverviewServlet}.
+     *
+     * @param resourceLoader Loader to use for resources.
+     * @param thingRegistry openHAB thing registry.
+     * @param inbox openHAB inbox for discovery results.
+     */
+    public AccountOverviewServlet(ResourceLoader resourceLoader, ThingRegistry thingRegistry, Inbox inbox) {
+        super(resourceLoader);
+        this.thingRegistry = thingRegistry;
+        this.inbox = inbox;
+        this.templateGenerator = new ThingsTemplateGenerator();
+    }
+
+    @Override
+    protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+            throws MieleHttpException, IOException {
+        String skeleton = getResourceLoader().loadResourceAsString(ACCOUNTS_SKELETON);
+        skeleton = renderBridges(skeleton);
+        skeleton = renderSslWarning(request, skeleton);
+        return skeleton;
+    }
+
+    private String renderBridges(String skeleton) {
+        List<Thing> bridges = thingRegistry.stream().filter(this::isMieleCloudBridge).collect(Collectors.toList());
+        if (bridges.isEmpty()) {
+            return renderNoBridges(skeleton);
+        } else {
+            return renderBridgesIntoSkeleton(skeleton, bridges);
+        }
+    }
+
+    private String renderNoBridges(String skeleton) {
+        return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "There is no account paired at the moment.")
+                .replace(BRIDGES_PLACEHOLDER, "");
+    }
+
+    private String renderBridgesIntoSkeleton(String skeleton, List<Thing> bridges) {
+        StringBuilder builder = new StringBuilder();
+
+        int index = 0;
+        Iterator<Thing> bridgeIterator = bridges.iterator();
+        while (bridgeIterator.hasNext()) {
+            builder.append(renderBridge(bridgeIterator.next(), index));
+            index++;
+        }
+
+        return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "The following bridges are paired")
+                .replace(BRIDGES_PLACEHOLDER, builder.toString());
+    }
+
+    private String renderBridge(Thing bridge, int index) {
+        StringBuilder builder = new StringBuilder();
+        builder.append("                    <li>\n");
+
+        String thingUid = bridge.getUID().getAsString();
+        String thingId = bridge.getUID().getId();
+        builder.append("                        ");
+        builder.append(thingUid.substring(0, thingUid.length() - thingId.length()));
+        builder.append(" ");
+        builder.append(thingId);
+        builder.append(" ");
+        builder.append(bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString());
+        builder.append("\n");
+
+        builder.append("                        <span class=\"status ");
+        final ThingStatus status = bridge.getStatus();
+        if (status == ThingStatus.ONLINE) {
+            builder.append("online");
+        } else {
+            builder.append("offline");
+        }
+        builder.append("\">");
+        builder.append(status.toString());
+        builder.append("</span>\n");
+
+        builder.append("                        <input class=\"trigger\" id=\"mielecloud-account-");
+        builder.append(thingId);
+        builder.append("\" type=\"checkbox\" name=\"things-file\" />\n");
+
+        builder.append("                        <label for=\"mielecloud-account-");
+        builder.append(thingId);
+        builder.append("\">&lt; &gt;</label>\n");
+
+        builder.append("                        <div class=\"things\">\n");
+        builder.append(
+                "                            <span class=\"legend\">You can use this things-file template to pair all available devices:</span>\n");
+        builder.append("                            <div class=\"code-container\">\n");
+        builder.append(
+                "                                <a href=\"#\" onclick=\"copyCodeToClipboard(event, this);\" class=\"btn btn-outline-info btn-sm copy\">Copy</a>\n");
+        builder.append("                                <textarea readonly>");
+        builder.append(generateConfigurationTemplate((Bridge) bridge));
+        builder.append("</textarea>\n");
+        builder.append("                            </div>\n");
+        builder.append("                        </div>\n");
+        builder.append("                    </li>");
+
+        return builder.toString();
+    }
+
+    private String generateConfigurationTemplate(Bridge bridge) {
+        List<Thing> pairedThings = thingRegistry.stream().filter(thing -> isConnectedVia(thing, bridge))
+                .collect(Collectors.toList());
+        List<DiscoveryResult> discoveryResults = inbox.stream()
+                .filter(discoveryResult -> willConnectVia(discoveryResult, bridge)).collect(Collectors.toList());
+
+        return templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings, discoveryResults);
+    }
+
+    private boolean isConnectedVia(Thing thing, Bridge bridge) {
+        return bridge.getUID().equals(thing.getBridgeUID());
+    }
+
+    private boolean willConnectVia(DiscoveryResult discoveryResult, Bridge bridge) {
+        return bridge.getUID().equals(discoveryResult.getBridgeUID());
+    }
+
+    private boolean isMieleCloudBridge(Thing thing) {
+        return MieleCloudBindingConstants.THING_TYPE_BRIDGE.equals(thing.getThingTypeUID());
+    }
+
+    private String renderSslWarning(HttpServletRequest request, String skeleton) {
+        if (!request.isSecure()) {
+            return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "<div class=\"alert alert-danger\" role=\"alert\">\n"
+                    + "                    Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange.\n"
+                    + "                    See <a href=\"https://www.openhab.org/docs/installation/security.html\">Securing access to openHAB</a> for details.\n"
+                    + "                </div>");
+        } else {
+            return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java
new file mode 100644 (file)
index 0000000..3b667ce
--- /dev/null
@@ -0,0 +1,217 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.config.exception.BridgeCreationFailedException;
+import org.openhab.binding.mielecloud.internal.config.exception.BridgeReconfigurationFailedException;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet that automatically creates a bridge and then redirects the browser to the account overview page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class CreateBridgeServlet extends AbstractRedirectionServlet {
+    private static final String MIELE_CLOUD_BRIDGE_NAME = "Cloud Connector";
+    private static final String MIELE_CLOUD_BRIDGE_LABEL = "Miele@home Account";
+
+    private static final String LOCALE_PARAMETER_NAME = "locale";
+    public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
+    public static final String EMAIL_PARAMETER_NAME = "email";
+
+    private static final long serialVersionUID = -2912042079128722887L;
+
+    private static final String DEFAULT_LOCALE = "en";
+
+    private static final long ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS = 5000;
+    private static final long DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS = 5000;
+    private static final long CHECK_INTERVAL_IN_MILLISECONDS = 100;
+
+    private final Logger logger = LoggerFactory.getLogger(CreateBridgeServlet.class);
+
+    private final Inbox inbox;
+    private final ThingRegistry thingRegistry;
+
+    /**
+     * Creates a new {@link CreateBridgeServlet}.
+     *
+     * @param inbox openHAB inbox for discovery results.
+     * @param thingRegistry openHAB thing registry.
+     */
+    public CreateBridgeServlet(Inbox inbox, ThingRegistry thingRegistry) {
+        this.inbox = inbox;
+        this.thingRegistry = thingRegistry;
+    }
+
+    @Override
+    protected String getRedirectionDestination(HttpServletRequest request) {
+        String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
+        if (bridgeUidString == null || bridgeUidString.isEmpty()) {
+            logger.warn("Cannot create bridge: Bridge UID is missing.");
+            return "/mielecloud/failure?" + FailureServlet.MISSING_BRIDGE_UID_PARAMETER_NAME + "=true";
+        }
+
+        String email = request.getParameter(EMAIL_PARAMETER_NAME);
+        if (email == null || email.isEmpty()) {
+            logger.warn("Cannot create bridge: E-mail address is missing.");
+            return "/mielecloud/failure?" + FailureServlet.MISSING_EMAIL_PARAMETER_NAME + "=true";
+        }
+
+        ThingUID bridgeUid = null;
+        try {
+            bridgeUid = new ThingUID(bridgeUidString);
+        } catch (IllegalArgumentException e) {
+            logger.warn("Cannot create bridge: Bridge UID '{}' is malformed.", bridgeUid);
+            return "/mielecloud/failure?" + FailureServlet.MALFORMED_BRIDGE_UID_PARAMETER_NAME + "=true";
+        }
+
+        if (!EmailValidator.isValid(email)) {
+            logger.warn("Cannot create bridge: E-mail address '{}' is malformed.", email);
+            return "/mielecloud/failure?" + FailureServlet.MALFORMED_EMAIL_PARAMETER_NAME + "=true";
+        }
+
+        String locale = getValidLocale(request.getParameter(LOCALE_PARAMETER_NAME));
+
+        logger.debug("Auto configuring Miele account using locale '{}' (requested locale was '{}')", locale,
+                request.getParameter(LOCALE_PARAMETER_NAME));
+        try {
+            Thing bridge = pairOrReconfigureBridge(locale, bridgeUid, email);
+            waitForBridgeToComeOnline(bridge);
+            return "/mielecloud";
+        } catch (BridgeReconfigurationFailedException e) {
+            logger.warn("{}", e.getMessage());
+            return "/mielecloud/success?" + SuccessServlet.BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME + "=true&"
+                    + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+                    + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+        } catch (BridgeCreationFailedException e) {
+            logger.warn("Thing creation failed because there was no binding available that supports the thing.");
+            return "/mielecloud/success?" + SuccessServlet.BRIDGE_CREATION_FAILED_PARAMETER_NAME + "=true&"
+                    + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+                    + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+        }
+    }
+
+    private Thing pairOrReconfigureBridge(String locale, ThingUID bridgeUid, String email) {
+        DiscoveryResult result = DiscoveryResultBuilder.create(bridgeUid)
+                .withRepresentationProperty(Thing.PROPERTY_MODEL_ID).withLabel(MIELE_CLOUD_BRIDGE_LABEL)
+                .withProperty(Thing.PROPERTY_MODEL_ID, MIELE_CLOUD_BRIDGE_NAME)
+                .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE, locale)
+                .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, email).build();
+        if (inbox.add(result)) {
+            return pairBridge(bridgeUid);
+        } else {
+            return reconfigureBridge(bridgeUid, locale, email);
+        }
+    }
+
+    private Thing pairBridge(ThingUID thingUid) {
+        Thing thing = inbox.approve(thingUid, MIELE_CLOUD_BRIDGE_LABEL, null);
+        if (thing == null) {
+            throw new BridgeCreationFailedException();
+        }
+
+        logger.debug("Successfully created bridge {}", thingUid);
+        return thing;
+    }
+
+    private Thing reconfigureBridge(ThingUID thingUid, String locale, String email) {
+        logger.debug("Thing already exists. Modifying configuration.");
+        Thing thing = thingRegistry.get(thingUid);
+        if (thing == null) {
+            throw new BridgeReconfigurationFailedException(
+                    "Cannot modify non existing bridge: Could neither add bridge via inbox nor find existing bridge.");
+        }
+
+        ThingHandler handler = thing.getHandler();
+        if (handler == null) {
+            throw new BridgeReconfigurationFailedException("Bridge exists but has no handler.");
+        }
+        if (!(handler instanceof MieleBridgeHandler)) {
+            throw new BridgeReconfigurationFailedException("Bridge handler is of wrong type, expected '"
+                    + MieleBridgeHandler.class.getSimpleName() + "' but got '" + handler.getClass().getName() + "'.");
+        }
+
+        MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) handler;
+        bridgeHandler.disposeWebservice();
+        bridgeHandler.initializeWebservice();
+
+        return thing;
+    }
+
+    private String getValidLocale(@Nullable String localeParameterValue) {
+        if (localeParameterValue == null || localeParameterValue.isEmpty()
+                || !LocaleValidator.isValidLanguage(localeParameterValue)) {
+            return DEFAULT_LOCALE;
+        } else {
+            return localeParameterValue;
+        }
+    }
+
+    private void waitForBridgeToComeOnline(Thing bridge) {
+        try {
+            waitForConditionWithTimeout(() -> bridge.getStatus() == ThingStatus.ONLINE,
+                    ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS);
+            waitForConditionWithTimeout(new DiscoveryResultCountDoesNotChangeCondition(),
+                    DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    private void waitForConditionWithTimeout(BooleanSupplier condition, long timeoutInMilliseconds)
+            throws InterruptedException {
+        long remainingWaitTime = timeoutInMilliseconds;
+        while (!condition.getAsBoolean() && remainingWaitTime > 0) {
+            TimeUnit.MILLISECONDS.sleep(CHECK_INTERVAL_IN_MILLISECONDS);
+            remainingWaitTime -= CHECK_INTERVAL_IN_MILLISECONDS;
+        }
+    }
+
+    private class DiscoveryResultCountDoesNotChangeCondition implements BooleanSupplier {
+        private long previousDiscoveryResultCount = 0;
+
+        @Override
+        public boolean getAsBoolean() {
+            var discoveryResultCount = countOwnDiscoveryResults();
+            var discoveryResultCountUnchanged = previousDiscoveryResultCount == discoveryResultCount;
+            previousDiscoveryResultCount = discoveryResultCount;
+            return discoveryResultCountUnchanged;
+        }
+
+        private long countOwnDiscoveryResults() {
+            return inbox.stream().map(DiscoveryResult::getBindingId)
+                    .filter(MieleCloudBindingConstants.BINDING_ID::equals).count();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java
new file mode 100644 (file)
index 0000000..a24802b
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Servlet showing a failure page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class FailureServlet extends AbstractShowPageServlet {
+    private static final long serialVersionUID = -5195984256535664942L;
+
+    public static final String OAUTH2_ERROR_PARAMETER_NAME = "oauth2Error";
+    public static final String ILLEGAL_RESPONSE_PARAMETER_NAME = "illegalResponse";
+    public static final String NO_ONGOING_AUTHORIZATION_PARAMETER_NAME = "noOngoingAuthorization";
+    public static final String FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME = "failedToCompleteAuthorization";
+    public static final String MISSING_BRIDGE_UID_PARAMETER_NAME = "missingBridgeUid";
+    public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
+    public static final String MALFORMED_BRIDGE_UID_PARAMETER_NAME = "malformedBridgeUid";
+    public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
+    public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
+
+    public static final String OAUTH2_ERROR_ACCESS_DENIED = "access_denied";
+    public static final String OAUTH2_ERROR_INVALID_REQUEST = "invalid_request";
+    public static final String OAUTH2_ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client";
+    public static final String OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
+    public static final String OAUTH2_ERROR_INVALID_SCOPE = "invalid_scope";
+    public static final String OAUTH2_ERROR_SERVER_ERROR = "server_error";
+    public static final String OAUTH2_ERROR_TEMPORARY_UNAVAILABLE = "temporarily_unavailable";
+
+    private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "<!-- ERROR MESSAGE TEXT -->";
+
+    /**
+     * Creates a new {@link FailureServlet}.
+     *
+     * @param resourceLoader Loader to use for resources.
+     */
+    public FailureServlet(ResourceLoader resourceLoader) {
+        super(resourceLoader);
+    }
+
+    @Override
+    protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+            throws MieleHttpException, IOException {
+        return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+                getErrorMessage(request));
+    }
+
+    private String getErrorMessage(HttpServletRequest request) {
+        String oauth2Error = request.getParameter(OAUTH2_ERROR_PARAMETER_NAME);
+        if (oauth2Error != null) {
+            return getOAuth2ErrorMessage(oauth2Error);
+        } else if (ServletUtil.isParameterEnabled(request, ILLEGAL_RESPONSE_PARAMETER_NAME)) {
+            return "Miele cloud service returned an illegal response.";
+        } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_PARAMETER_NAME)) {
+            return "There is no ongoing authorization. Please start an authorization first.";
+        } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME)) {
+            return "Completing the final authorization request failed. Please try the config flow again.";
+        } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_UID_PARAMETER_NAME)) {
+            return "Missing bridge UID.";
+        } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
+            return "Missing e-mail address.";
+        } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_UID_PARAMETER_NAME)) {
+            return "Malformed bridge UID.";
+        } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
+            return "Malformed e-mail address.";
+        } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
+            return "Missing request URL. Please try the config flow again.";
+        } else {
+            return "Unknown error.";
+        }
+    }
+
+    private String getOAuth2ErrorMessage(String oauth2Error) {
+        return "OAuth2 authentication with Miele cloud service failed: " + getOAuth2ErrorDetailMessage(oauth2Error);
+    }
+
+    private String getOAuth2ErrorDetailMessage(String oauth2Error) {
+        switch (oauth2Error) {
+            case OAUTH2_ERROR_ACCESS_DENIED:
+                return "Access denied.";
+            case OAUTH2_ERROR_INVALID_REQUEST:
+                return "Malformed request.";
+            case OAUTH2_ERROR_UNAUTHORIZED_CLIENT:
+                return "Account not authorized to request authorization code.";
+            case OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE:
+                return "Obtaining an authorization code is not supported.";
+            case OAUTH2_ERROR_INVALID_SCOPE:
+                return "Invalid scope.";
+            case OAUTH2_ERROR_SERVER_ERROR:
+                return "Unexpected server error.";
+            case OAUTH2_ERROR_TEMPORARY_UNAVAILABLE:
+                return "Authorization server temporarily unavailable.";
+            default:
+                return "Unknown error code \"" + oauth2Error + "\".";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java
new file mode 100644 (file)
index 0000000..e817463
--- /dev/null
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet gathers and processes required information to perform an authorization with the Miele cloud service
+ * and create a bridge afterwards. Required parameters are the client ID, client secret, an ID for the bridge and an
+ * e-mail address. If the given parameters are valid, the browser is redirected to the Miele service login. Otherwise,
+ * the browser is redirected to the previous page with an according error message.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ForwardToLoginServlet extends AbstractRedirectionServlet {
+    private static final long serialVersionUID = -9094642228439994183L;
+
+    public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
+    public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
+    public static final String BRIDGE_ID_PARAMETER_NAME = "bridgeId";
+    public static final String EMAIL_PARAMETER_NAME = "email";
+
+    private final Logger logger = LoggerFactory.getLogger(ForwardToLoginServlet.class);
+
+    private final OAuthAuthorizationHandler authorizationHandler;
+
+    /**
+     * Creates a new {@link ForwardToLoginServlet}.
+     *
+     * @param authorizationHandler Handler implementing the OAuth authorization process.
+     */
+    public ForwardToLoginServlet(OAuthAuthorizationHandler authorizationHandler) {
+        this.authorizationHandler = authorizationHandler;
+    }
+
+    @Override
+    protected String getRedirectionDestination(HttpServletRequest request) {
+        String clientId = request.getParameter(CLIENT_ID_PARAMETER_NAME);
+        String clientSecret = request.getParameter(CLIENT_SECRET_PARAMETER_NAME);
+        String bridgeId = request.getParameter(BRIDGE_ID_PARAMETER_NAME);
+        String email = request.getParameter(EMAIL_PARAMETER_NAME);
+
+        if (clientId == null || clientId.isEmpty()) {
+            logger.warn("Request is missing client ID.");
+            return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_ID_PARAMETER_NAME);
+        }
+        if (clientSecret == null || clientSecret.isEmpty()) {
+            logger.warn("Request is missing client secret.");
+            return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_SECRET_PARAMETER_NAME);
+        }
+        if (bridgeId == null || bridgeId.isEmpty()) {
+            logger.warn("Request is missing bridge ID.");
+            return getErrorRedirectionUrl(PairAccountServlet.MISSING_BRIDGE_ID_PARAMETER_NAME);
+        }
+        if (email == null || email.isEmpty()) {
+            logger.warn("Request is missing e-mail address.");
+            return getErrorRedirectionUrl(PairAccountServlet.MISSING_EMAIL_PARAMETER_NAME);
+        }
+
+        ThingUID bridgeUid = null;
+        try {
+            bridgeUid = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, bridgeId);
+        } catch (IllegalArgumentException e) {
+            logger.warn("Passed bridge ID '{}' is invalid.", bridgeId);
+            return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_BRIDGE_ID_PARAMETER_NAME);
+        }
+
+        if (!EmailValidator.isValid(email)) {
+            logger.warn("Passed e-mail address '{}' is invalid.", email);
+            return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_EMAIL_PARAMETER_NAME);
+        }
+
+        try {
+            authorizationHandler.beginAuthorization(clientId, clientSecret, bridgeUid, email);
+        } catch (OngoingAuthorizationException e) {
+            logger.warn("Cannot begin new authorization process while another one is still running.");
+            return getErrorRedirectUrlWithExpiryTime(e.getOngoingAuthorizationExpiryTimestamp());
+        }
+
+        StringBuffer requestUrl = request.getRequestURL();
+        if (requestUrl == null) {
+            return getErrorRedirectionUrl(PairAccountServlet.MISSING_REQUEST_URL_PARAMETER_NAME);
+        }
+
+        try {
+            return authorizationHandler.getAuthorizationUrl(deriveRedirectUri(requestUrl.toString()));
+        } catch (NoOngoingAuthorizationException e) {
+            logger.warn(
+                    "Failed to create authorization URL: There was no ongoing authorization although we just started one.");
+            return getErrorRedirectionUrl(PairAccountServlet.NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME);
+        } catch (OAuthException e) {
+            logger.warn("Failed to create authorization URL.", e);
+            return getErrorRedirectionUrl(PairAccountServlet.FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME);
+        }
+    }
+
+    private String getErrorRedirectUrlWithExpiryTime(@Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) {
+        if (ongoingAuthorizationExpiryTimestamp == null) {
+            return getErrorRedirectionUrl(
+                    PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
+                    PairAccountServlet.ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME);
+        }
+
+        long minutesUntilExpiry = ChronoUnit.MINUTES.between(LocalDateTime.now(), ongoingAuthorizationExpiryTimestamp)
+                + 1;
+        return getErrorRedirectionUrl(
+                PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
+                Long.toString(minutesUntilExpiry));
+    }
+
+    private String getErrorRedirectionUrl(String errorCode) {
+        return getErrorRedirectionUrl(errorCode, "true");
+    }
+
+    private String getErrorRedirectionUrl(String errorCode, String parameterValue) {
+        return "/mielecloud/pair?" + errorCode + "=" + parameterValue;
+    }
+
+    private String deriveRedirectUri(String requestUrl) {
+        return requestUrl + "/../result";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java
new file mode 100644 (file)
index 0000000..c5eff7b
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception wrapping a HTTP error code for further processing.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class MieleHttpException extends Exception {
+    private static final long serialVersionUID = 1825214275413952809L;
+
+    private final int httpErrorCode;
+
+    public MieleHttpException(int httpErrorCode) {
+        this.httpErrorCode = httpErrorCode;
+    }
+
+    public int getHttpErrorCode() {
+        return httpErrorCode;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java
new file mode 100644 (file)
index 0000000..79d872a
--- /dev/null
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Servlet showing the pair account page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class PairAccountServlet extends AbstractShowPageServlet {
+    private static final long serialVersionUID = 6565378471951635420L;
+
+    public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
+    public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
+
+    public static final String MISSING_CLIENT_ID_PARAMETER_NAME = "missingClientId";
+    public static final String MISSING_CLIENT_SECRET_PARAMETER_NAME = "missingClientSecret";
+    public static final String MISSING_BRIDGE_ID_PARAMETER_NAME = "missingBridgeId";
+    public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
+    public static final String MALFORMED_BRIDGE_ID_PARAMETER_NAME = "malformedBridgeId";
+    public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
+    public static final String FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME = "failedToDeriveRedirectUrl";
+    public static final String ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME = "ongoingAuthorizationInStep1ExpiresInMinutes";
+    public static final String ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME = "unknown";
+    public static final String NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME = "noOngoingAuthorizationInStep2";
+    public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
+
+    private static final String PAIR_ACCOUNT_SKELETON = "pairing.html";
+
+    private static final String CLIENT_ID_PLACEHOLDER = "<!-- CLIENT ID -->";
+    private static final String CLIENT_SECRET_PLACEHOLDER = "<!-- CLIENT SECRET -->";
+    private static final String ERROR_MESSAGE_PLACEHOLDER = "<!-- ERROR MESSAGE -->";
+
+    /**
+     * Creates a new {@link PairAccountServlet}.
+     *
+     * @param resourceLoader Loader for resources.
+     */
+    public PairAccountServlet(ResourceLoader resourceLoader) {
+        super(resourceLoader);
+    }
+
+    @Override
+    protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+            throws MieleHttpException, IOException {
+        String skeleton = getResourceLoader().loadResourceAsString(PAIR_ACCOUNT_SKELETON);
+        skeleton = renderClientIdAndClientSecret(request, skeleton);
+        skeleton = renderErrorMessage(request, skeleton);
+        return skeleton;
+    }
+
+    private String renderClientIdAndClientSecret(HttpServletRequest request, String skeleton) {
+        String prefilledClientId = Optional.ofNullable(request.getParameter(CLIENT_ID_PARAMETER_NAME)).orElse("");
+        String prefilledClientSecret = Optional.ofNullable(request.getParameter(CLIENT_SECRET_PARAMETER_NAME))
+                .orElse("");
+        return skeleton.replace(CLIENT_ID_PLACEHOLDER, prefilledClientId).replace(CLIENT_SECRET_PLACEHOLDER,
+                prefilledClientSecret);
+    }
+
+    private String renderErrorMessage(HttpServletRequest request, String skeleton) {
+        if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_ID_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Missing client ID.</div>");
+        } else if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_SECRET_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+
+                    "<div class=\"alert alert-danger\" role=\"alert\">Missing client secret.</div>");
+        } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_ID_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Missing bridge ID.</div>");
+        } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Missing e-mail address.</div>");
+        } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_ID_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Malformed bridge ID. A bridge ID may only contain letters, numbers, '-' and '_'!</div>");
+        } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Malformed e-mail address.</div>");
+        } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Failed to derive redirect URL.</div>");
+        } else if (ServletUtil.isParameterPresent(request,
+                ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME)) {
+            String minutesUntilExpiry = request
+                    .getParameter(ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME);
+            if (ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME.equals(minutesUntilExpiry)) {
+                return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                        "<div class=\"alert alert-danger\" role=\"alert\">There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again later.</div>");
+            } else {
+                return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                        "<div class=\"alert alert-danger\" role=\"alert\">There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in "
+                                + minutesUntilExpiry + " minutes.</div>");
+            }
+        } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?</div>");
+        } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Missing request URL. Please try again.</div>");
+        } else {
+            return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, "");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java
new file mode 100644 (file)
index 0000000..d93a3c9
--- /dev/null
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.osgi.framework.BundleContext;
+
+/**
+ * Provides access to resource files for servlets.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ResourceLoader {
+    private static final String BEGINNING_OF_INPUT = "\\A";
+
+    private final String basePath;
+    private final BundleContext bundleContext;
+
+    /**
+     * Creates a new {@link ResourceLoader}.
+     *
+     * @param basePath The base path to use for loading. A trailing {@code "/"} is removed.
+     * @param bundleContext {@link BundleContext} to load from.
+     */
+    public ResourceLoader(String basePath, BundleContext bundleContext) {
+        this.basePath = removeTrailingSlashes(basePath);
+        this.bundleContext = bundleContext;
+    }
+
+    private String removeTrailingSlashes(String value) {
+        String ret = value;
+        while (ret.endsWith("/")) {
+            ret = ret.substring(0, ret.length() - 1);
+        }
+        return ret;
+    }
+
+    /**
+     * Opens a resource relative to the base path.
+     *
+     * @param filename The filename of the resource to load.
+     * @return A stream reading from the resource file.
+     * @throws FileNotFoundException If the requested resource file cannot be found.
+     * @throws IOException If an error occurs while opening a stream to the resource.
+     */
+    public InputStream openResource(String filename) throws IOException {
+        URL url = bundleContext.getBundle().getEntry(basePath + "/" + filename);
+        if (url == null) {
+            throw new FileNotFoundException("Cannot find '" + filename + "' relative to '" + basePath + "'");
+        }
+
+        return url.openStream();
+    }
+
+    /**
+     * Loads the contents of a resource file as UTF-8 encoded {@link String}.
+     *
+     * @param filename The filename of the resource to load.
+     * @return The contents of the file.
+     * @throws FileNotFoundException If the requested resource file cannot be found.
+     * @throws IOException If an error occurs while opening a stream to the resource or reading from it.
+     */
+    public String loadResourceAsString(String filename) throws IOException {
+        try (Scanner scanner = new Scanner(openResource(filename), StandardCharsets.UTF_8.name())) {
+            return scanner.useDelimiter(BEGINNING_OF_INPUT).next();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java
new file mode 100644 (file)
index 0000000..5a5db09
--- /dev/null
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet processing the response by the Miele service after a login. This servlet is called as a result of a
+ * completed login to the Miele service and assumes that the OAuth 2 parameters are passed. Depending on the parameters
+ * and whether the token response can be fetched either the browser is redirected to the success or the failure page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ResultServlet extends AbstractRedirectionServlet {
+    private static final long serialVersionUID = 2157912755568949550L;
+
+    public static final String CODE_PARAMETER_NAME = "code";
+    public static final String STATE_PARAMETER_NAME = "state";
+    public static final String ERROR_PARAMETER_NAME = "error";
+
+    private final Logger logger = LoggerFactory.getLogger(ResultServlet.class);
+
+    private final OAuthAuthorizationHandler authorizationHandler;
+
+    /**
+     * Creates a new {@link ResultServlet}.
+     *
+     * @param authorizationHandler Handler implementing the OAuth authorization.
+     */
+    public ResultServlet(OAuthAuthorizationHandler authorizationHandler) {
+        this.authorizationHandler = authorizationHandler;
+    }
+
+    @Override
+    protected String getRedirectionDestination(HttpServletRequest request) {
+        String error = request.getParameter(ERROR_PARAMETER_NAME);
+        if (error != null) {
+            logger.warn("Received error response: {}", error);
+            return "/mielecloud/failure?" + FailureServlet.OAUTH2_ERROR_PARAMETER_NAME + "=" + error;
+        }
+
+        String code = request.getParameter(CODE_PARAMETER_NAME);
+        if (code == null) {
+            logger.warn("Code is null");
+            return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
+        }
+        String state = request.getParameter(STATE_PARAMETER_NAME);
+        if (state == null) {
+            logger.warn("State is null");
+            return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
+        }
+
+        try {
+            ThingUID bridgeId = authorizationHandler.getBridgeUid();
+            String email = authorizationHandler.getEmail();
+
+            StringBuffer requestUrl = request.getRequestURL();
+            if (requestUrl == null) {
+                return "/mielecloud/failure?" + FailureServlet.MISSING_REQUEST_URL_PARAMETER_NAME + "=true";
+            }
+
+            try {
+                authorizationHandler.completeAuthorization(requestUrl.toString() + "?" + request.getQueryString());
+            } catch (OAuthException e) {
+                logger.warn("Failed to complete authorization.", e);
+                return "/mielecloud/failure?" + FailureServlet.FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME
+                        + "=true";
+            }
+
+            return "/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeId.getAsString()
+                    + "&" + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+        } catch (NoOngoingAuthorizationException e) {
+            logger.warn("Failed to complete authorization: There is no ongoing authorization or it timed out");
+            return "/mielecloud/failure?" + FailureServlet.NO_ONGOING_AUTHORIZATION_PARAMETER_NAME + "=true";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java
new file mode 100644 (file)
index 0000000..4441aca
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility class for common servlet tasks.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ServletUtil {
+    private ServletUtil() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Gets the value of a request parameter or returns a default if the parameter is not present.
+     */
+    public static String getParameterValueOrDefault(HttpServletRequest request, String parameterName,
+            String defaultValue) {
+        String parameterValue = request.getParameter(parameterName);
+        if (parameterValue == null) {
+            return defaultValue;
+        } else {
+            return parameterValue;
+        }
+    }
+
+    /**
+     * Checks whether a request parameter is enabled.
+     */
+    public static boolean isParameterEnabled(HttpServletRequest request, String parameterName) {
+        return "true".equalsIgnoreCase(getParameterValueOrDefault(request, parameterName, "false"));
+    }
+
+    /**
+     * Checks whether a parameter is present in a request.
+     */
+    public static boolean isParameterPresent(HttpServletRequest request, String parameterName) {
+        String parameterValue = request.getParameter(parameterName);
+        return parameterValue != null && !parameterValue.trim().isEmpty();
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java
new file mode 100644 (file)
index 0000000..d240f21
--- /dev/null
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet showing the success page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class SuccessServlet extends AbstractShowPageServlet {
+    private static final long serialVersionUID = 7013060161686096950L;
+
+    public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
+    public static final String EMAIL_PARAMETER_NAME = "email";
+
+    public static final String BRIDGE_CREATION_FAILED_PARAMETER_NAME = "bridgeCreationFailed";
+    public static final String BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME = "bridgeReconfigurationFailed";
+
+    private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "<!-- ERROR MESSAGE TEXT -->";
+    private static final String BRIDGE_UID_PLACEHOLDER = "<!-- BRIDGE UID -->";
+    private static final String EMAIL_PLACEHOLDER = "<!-- EMAIL -->";
+    private static final String THINGS_TEMPLATE_CODE_PLACEHOLDER = "<!-- THINGS TEMPLATE CODE -->";
+
+    private static final String LOCALE_OPTIONS_PLACEHOLDER = "<!-- LOCALE OPTIONS -->";
+
+    private static final String DEFAULT_LANGUAGE = "en";
+    private static final Set<String> SUPPORTED_LANGUAGES = Set.of("da", "nl", "en", "fr", "de", "it", "nb", "es");
+
+    private final Logger logger = LoggerFactory.getLogger(SuccessServlet.class);
+
+    private final LanguageProvider languageProvider;
+    private final ThingsTemplateGenerator templateGenerator;
+
+    /**
+     * Creates a new {@link SuccessServlet}.
+     *
+     * @param resourceLoader Loader for resources.
+     * @param languageProvider Provider for the language to use as default selection.
+     */
+    public SuccessServlet(ResourceLoader resourceLoader, LanguageProvider languageProvider) {
+        super(resourceLoader);
+        this.languageProvider = languageProvider;
+        this.templateGenerator = new ThingsTemplateGenerator();
+    }
+
+    @Override
+    protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+            throws MieleHttpException, IOException {
+        String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
+        if (bridgeUidString == null || bridgeUidString.isEmpty()) {
+            logger.warn("Success page is missing bridge UID.");
+            return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+                    "Missing bridge UID.");
+        }
+
+        String email = request.getParameter(EMAIL_PARAMETER_NAME);
+        if (email == null || email.isEmpty()) {
+            logger.warn("Success page is missing e-mail address.");
+            return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+                    "Missing e-mail address.");
+        }
+
+        ThingUID bridgeUid = null;
+        try {
+            bridgeUid = new ThingUID(bridgeUidString);
+        } catch (IllegalArgumentException e) {
+            logger.warn("Success page received malformed bridge UID '{}'.", bridgeUidString);
+            return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+                    "Malformed bridge UID.");
+        }
+
+        if (!EmailValidator.isValid(email)) {
+            logger.warn("Success page received malformed e-mail address '{}'.", email);
+            return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+                    "Malformed e-mail address.");
+        }
+
+        String skeleton = getResourceLoader().loadResourceAsString("success.html");
+        skeleton = renderErrorMessage(request, skeleton);
+        skeleton = renderBridgeUid(skeleton, bridgeUid);
+        skeleton = renderEmail(skeleton, email);
+        skeleton = renderLocaleSelection(skeleton);
+        skeleton = renderBridgeConfigurationTemplate(skeleton, bridgeUid, email);
+        return skeleton;
+    }
+
+    private String renderErrorMessage(HttpServletRequest request, String skeleton) {
+        if (ServletUtil.isParameterEnabled(request, BRIDGE_CREATION_FAILED_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again.</div>");
+        } else if (ServletUtil.isParameterEnabled(request, BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME)) {
+            return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+                    "<div class=\"alert alert-danger\" role=\"alert\">Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again.</div>");
+        } else {
+            return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, "");
+        }
+    }
+
+    private String renderBridgeUid(String skeleton, ThingUID bridgeUid) {
+        return skeleton.replace(BRIDGE_UID_PLACEHOLDER, bridgeUid.getAsString());
+    }
+
+    private String renderEmail(String skeleton, String email) {
+        return skeleton.replace(EMAIL_PLACEHOLDER, email);
+    }
+
+    private String renderLocaleSelection(String skeleton) {
+        String preSelectedLanguage = languageProvider.getLanguage().filter(SUPPORTED_LANGUAGES::contains)
+                .orElse(DEFAULT_LANGUAGE);
+
+        return skeleton.replace(LOCALE_OPTIONS_PLACEHOLDER,
+                SUPPORTED_LANGUAGES.stream().map(Language::fromCode).filter(Optional::isPresent).map(Optional::get)
+                        .sorted()
+                        .map(language -> createOptionTag(language, preSelectedLanguage.equals(language.getCode())))
+                        .collect(Collectors.joining("\n")));
+    }
+
+    private String createOptionTag(Language language, boolean selected) {
+        String firstPart = "                                    <option value=\"" + language.getCode() + "\"";
+        String secondPart = ">" + language.format() + "</option>";
+        if (selected) {
+            return firstPart + " selected=\"selected\"" + secondPart;
+        } else {
+            return firstPart + secondPart;
+        }
+    }
+
+    private String renderBridgeConfigurationTemplate(String skeleton, ThingUID bridgeUid, String email) {
+        String bridgeTemplate = templateGenerator.createBridgeConfigurationTemplate(bridgeUid.getId(), email,
+                languageProvider.getLanguage().orElse("en"));
+        return skeleton.replace(THINGS_TEMPLATE_CODE_PLACEHOLDER, bridgeTemplate);
+    }
+
+    /**
+     * A language representation for user display.
+     *
+     * @author Björn Lange - Initial contribution
+     */
+    private static final class Language implements Comparable<Language> {
+        private final String code;
+        private final String name;
+
+        private Language(String code, String name) {
+            this.code = code;
+            this.name = name;
+        }
+
+        /**
+         * Gets the 2-letter language code for accessing the Miele Cloud service.
+         */
+        public String getCode() {
+            return code;
+        }
+
+        /**
+         * Formats the language for displaying.
+         */
+        public String format() {
+            return name + " - " + code;
+        }
+
+        @Override
+        public int compareTo(Language other) {
+            return name.toUpperCase().compareTo(other.name.toUpperCase());
+        }
+
+        /**
+         * Constructs a {@link Language} from a 2-letter language code.
+         *
+         * @param code 2-letter language code.
+         * @return An {@link Optional} wrapping the {@link Language} or an empty {@link Optional} if there is no
+         *         representation for the given language code.
+         */
+        public static Optional<Language> fromCode(String code) {
+            Locale locale = new Locale(code);
+            String name = locale.getDisplayLanguage(locale);
+            if (name.isEmpty()) {
+                return Optional.empty();
+            } else {
+                return Optional.of(new Language(code, name));
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingDiscoveryService.java
new file mode 100644 (file)
index 0000000..08c8187
--- /dev/null
@@ -0,0 +1,214 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.*;
+import static org.openhab.binding.mielecloud.internal.handler.MieleHandlerFactory.SUPPORTED_THING_TYPES;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Discovery service for things linked to a Miele cloud account.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Do not directly listen to webservice events
+ */
+@NonNullByDefault
+public class ThingDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
+    private static final int BACKGROUND_DISCOVERY_TIMEOUT_IN_SECONDS = 5;
+
+    @Nullable
+    private MieleBridgeHandler bridgeHandler;
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private boolean discoveringDevices = false;
+
+    /**
+     * Creates a new {@link ThingDiscoveryService}.
+     */
+    public ThingDiscoveryService() {
+        super(SUPPORTED_THING_TYPES, BACKGROUND_DISCOVERY_TIMEOUT_IN_SECONDS);
+    }
+
+    @Nullable
+    private ThingUID getBridgeUid() {
+        var bridgeHandler = this.bridgeHandler;
+        if (bridgeHandler == null) {
+            return null;
+        } else {
+            return bridgeHandler.getThing().getUID();
+        }
+    }
+
+    @Override
+    protected void startScan() {
+    }
+
+    @Override
+    public void activate() {
+        startBackgroundDiscovery();
+    }
+
+    @Override
+    public void deactivate() {
+        stopBackgroundDiscovery();
+        removeOlderResults(System.currentTimeMillis(), getBridgeUid());
+    }
+
+    /**
+     * Invoked when a device state update is received from the Miele cloud.
+     */
+    public void onDeviceStateUpdated(DeviceState deviceState) {
+        if (!discoveringDevices) {
+            return;
+        }
+
+        Optional<ThingTypeUID> thingTypeUid = getThingTypeUID(deviceState);
+        if (thingTypeUid.isPresent()) {
+            createDiscoveryResult(deviceState, thingTypeUid.get());
+        } else {
+            logger.debug("Unsupported Miele device type: {}", deviceState.getType().orElse("<Empty>"));
+        }
+    }
+
+    private void createDiscoveryResult(DeviceState deviceState, ThingTypeUID thingTypeUid) {
+        MieleBridgeHandler bridgeHandler = this.bridgeHandler;
+        if (bridgeHandler == null) {
+            return;
+        }
+
+        ThingUID thingUid = new ThingUID(thingTypeUid, bridgeHandler.getThing().getUID(),
+                deviceState.getDeviceIdentifier());
+
+        DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUid)
+                .withBridge(bridgeHandler.getThing().getUID()).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
+                .withLabel(getLabel(deviceState));
+
+        ThingInformationExtractor.extractProperties(thingTypeUid, deviceState).entrySet()
+                .forEach(entry -> discoveryResultBuilder.withProperty(entry.getKey(), entry.getValue()));
+
+        DiscoveryResult result = discoveryResultBuilder.build();
+
+        thingDiscovered(result);
+    }
+
+    private Optional<ThingTypeUID> getThingTypeUID(DeviceState deviceState) {
+        switch (deviceState.getRawType()) {
+            case COFFEE_SYSTEM:
+                return Optional.of(THING_TYPE_COFFEE_SYSTEM);
+            case TUMBLE_DRYER:
+                return Optional.of(THING_TYPE_DRYER);
+            case WASHING_MACHINE:
+                return Optional.of(THING_TYPE_WASHING_MACHINE);
+            case WASHER_DRYER:
+                return Optional.of(THING_TYPE_WASHER_DRYER);
+            case FREEZER:
+                return Optional.of(THING_TYPE_FREEZER);
+            case FRIDGE:
+                return Optional.of(THING_TYPE_FRIDGE);
+            case FRIDGE_FREEZER_COMBINATION:
+                return Optional.of(THING_TYPE_FRIDGE_FREEZER);
+            case HOB_INDUCTION:
+            case HOB_HIGHLIGHT:
+                return Optional.of(THING_TYPE_HOB);
+            case DISHWASHER:
+                return Optional.of(THING_TYPE_DISHWASHER);
+            case OVEN:
+            case OVEN_MICROWAVE:
+            case STEAM_OVEN:
+            case STEAM_OVEN_COMBINATION:
+            case STEAM_OVEN_MICROWAVE_COMBINATION:
+            case DIALOGOVEN:
+                return Optional.of(THING_TYPE_OVEN);
+            case WINE_CABINET:
+            case WINE_STORAGE_CONDITIONING_UNIT:
+            case WINE_CONDITIONING_UNIT:
+            case WINE_CABINET_FREEZER_COMBINATION:
+                return Optional.of(THING_TYPE_WINE_STORAGE);
+            case HOOD:
+                return Optional.of(THING_TYPE_HOOD);
+            case DISH_WARMER:
+                return Optional.of(THING_TYPE_DISH_WARMER);
+            case VACUUM_CLEANER:
+                return Optional.of(THING_TYPE_ROBOTIC_VACUUM_CLEANER);
+
+            default:
+                if (deviceState.getRawType() != DeviceType.UNKNOWN) {
+                    logger.warn("Found no matching thing type for device type {}", deviceState.getRawType());
+                }
+                return Optional.empty();
+        }
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+        logger.debug("Starting background discovery");
+
+        removeOlderResults(System.currentTimeMillis(), getBridgeUid());
+        discoveringDevices = true;
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+        logger.debug("Stopping background discovery");
+        discoveringDevices = false;
+    }
+
+    /**
+     * Invoked when a device is removed from the Miele cloud.
+     */
+    public void onDeviceRemoved(String deviceIdentifier) {
+        removeOlderResults(System.currentTimeMillis(), getBridgeUid());
+    }
+
+    private String getLabel(DeviceState deviceState) {
+        Optional<String> deviceName = deviceState.getDeviceName();
+        if (deviceName.isPresent()) {
+            return deviceName.get();
+        }
+
+        return ThingInformationExtractor.getDeviceAndTechType(deviceState).orElse("Miele Device");
+    }
+
+    @Override
+    public void setThingHandler(ThingHandler handler) {
+        if (handler instanceof MieleBridgeHandler) {
+            var bridgeHandler = (MieleBridgeHandler) handler;
+            bridgeHandler.setDiscoveryService(this);
+            this.bridgeHandler = bridgeHandler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingInformationExtractor.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingInformationExtractor.java
new file mode 100644 (file)
index 0000000..52d93b0
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Helper class extracting information related to things from {@link DeviceState}s received from the Miele cloud.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ThingInformationExtractor {
+    private ThingInformationExtractor() {
+        throw new IllegalStateException(getClass().getName() + " cannot be instantiated");
+    }
+
+    /**
+     * Extracts thing properties from a {@link DeviceState}.
+     *
+     * The returned properties always contain {@link Thing#PROPERTY_SERIAL_NUMBER} and {@link Thing#PROPERTY_MODEL_ID}.
+     * More might be present depending on the type of device.
+     *
+     * @param thingTypeUid {@link ThingTypeUID} of the thing to extract properties for.
+     * @param deviceState {@link DeviceState} received from the Miele cloud.
+     * @return A {@link Map} holding the properties as key-value pairs.
+     */
+    public static Map<String, String> extractProperties(ThingTypeUID thingTypeUid, DeviceState deviceState) {
+        var propertyMap = new HashMap<String, String>();
+        propertyMap.put(Thing.PROPERTY_SERIAL_NUMBER, getSerialNumber(deviceState));
+        propertyMap.put(Thing.PROPERTY_MODEL_ID, getModelId(deviceState));
+        propertyMap.put(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceState.getDeviceIdentifier());
+
+        if (MieleCloudBindingConstants.THING_TYPE_HOB.equals(thingTypeUid)) {
+            deviceState.getPlateStepCount().ifPresent(plateCount -> propertyMap
+                    .put(MieleCloudBindingConstants.PROPERTY_PLATE_COUNT, plateCount.toString()));
+        }
+
+        return propertyMap;
+    }
+
+    private static String getSerialNumber(DeviceState deviceState) {
+        return deviceState.getFabNumber().orElse(deviceState.getDeviceIdentifier());
+    }
+
+    private static String getModelId(DeviceState deviceState) {
+        return getDeviceAndTechType(deviceState).orElse("Unknown");
+    }
+
+    /**
+     * Formats device type and tech type from the given {@link DeviceState} for the purpose of displaying then to the
+     * user.
+     *
+     * If either of device or tech type is missing then it will be omitted. If both are missing then an empty
+     * {@link Optional} will be returned.
+     *
+     * @param deviceState {@link DeviceState} obtained from the Miele cloud.
+     * @return An {@link Optional} holding the formatted value or an empty {@link Optional} if neither device type nor
+     *         tech type were present.
+     */
+    static Optional<String> getDeviceAndTechType(DeviceState deviceState) {
+        Optional<String> deviceType = deviceState.getType();
+        Optional<String> techType = deviceState.getTechType();
+        if (deviceType.isPresent() && techType.isPresent()) {
+            return Optional.of(deviceType.get() + " " + techType.get());
+        }
+        if (!deviceType.isPresent() && techType.isPresent()) {
+            return techType;
+        }
+        if (deviceType.isPresent() && !techType.isPresent()) {
+            return deviceType;
+        }
+        return Optional.empty();
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/AbstractMieleThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/AbstractMieleThingHandler.java
new file mode 100644 (file)
index 0000000..b1b94a0
--- /dev/null
@@ -0,0 +1,293 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction.*;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
+import org.openhab.binding.mielecloud.internal.discovery.ThingInformationExtractor;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.binding.mielecloud.internal.webservice.ActionStateFetcher;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.TransitionState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base class for all Miele thing handlers.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ */
+@NonNullByDefault
+public abstract class AbstractMieleThingHandler extends BaseThingHandler {
+    protected final ActionStateFetcher actionFetcher;
+    protected DeviceState latestDeviceState = new DeviceState(getDeviceId(), null);
+    protected TransitionState latestTransitionState = new TransitionState(null, latestDeviceState);
+    protected ActionsState latestActionsState = new ActionsState(getDeviceId(), null);
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /**
+     * Creates a new {@link AbstractMieleThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public AbstractMieleThingHandler(Thing thing) {
+        super(thing);
+        this.actionFetcher = new ActionStateFetcher(this::getWebservice, scheduler);
+    }
+
+    private Optional<MieleBridgeHandler> getMieleBridgeHandler() {
+        Bridge bridge = getBridge();
+        if (bridge == null) {
+            return Optional.empty();
+        }
+
+        BridgeHandler handler = bridge.getHandler();
+        if (handler == null || !(handler instanceof MieleBridgeHandler)) {
+            return Optional.empty();
+        }
+
+        return Optional.of((MieleBridgeHandler) handler);
+    }
+
+    protected MieleWebservice getWebservice() {
+        return getMieleBridgeHandler().map(MieleBridgeHandler::getWebservice)
+                .orElse(UnavailableMieleWebservice.INSTANCE);
+    }
+
+    @Override
+    public void initialize() {
+        getWebservice().dispatchDeviceState(getDeviceId());
+
+        // If no device state update was received so far, set the device to OFFLINE.
+        if (getThing().getStatus() == ThingStatus.INITIALIZING) {
+            updateStatus(ThingStatus.OFFLINE);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (RefreshType.REFRESH.equals(command)) {
+            updateDeviceState(new DeviceChannelState(latestDeviceState));
+            updateTransitionState(new TransitionChannelState(latestTransitionState));
+            updateActionState(new ActionsChannelState(latestActionsState));
+        }
+
+        switch (channelUID.getId()) {
+            case PROGRAM_START_STOP:
+                if (PROGRAM_STARTED.matches(command.toString())) {
+                    triggerProcessAction(START);
+                } else if (PROGRAM_STOPPED.matches(command.toString())) {
+                    triggerProcessAction(STOP);
+                }
+                break;
+
+            case PROGRAM_START_STOP_PAUSE:
+                if (PROGRAM_STARTED.matches(command.toString())) {
+                    triggerProcessAction(START);
+                } else if (PROGRAM_STOPPED.matches(command.toString())) {
+                    triggerProcessAction(STOP);
+                } else if (PROGRAM_PAUSED.matches(command.toString())) {
+                    triggerProcessAction(PAUSE);
+                }
+                break;
+
+            case LIGHT_SWITCH:
+                if (command instanceof OnOffType) {
+                    triggerLight(OnOffType.ON.equals(command));
+                }
+                break;
+
+            case POWER_ON_OFF:
+                if (POWER_ON.matches(command.toString()) || POWER_OFF.matches(command.toString())) {
+                    triggerPowerState(OnOffType.ON.equals(OnOffType.from(command.toString())));
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void dispose() {
+    }
+
+    /**
+     * Invoked when an update of the available actions for the device managed by this handler is received from the Miele
+     * cloud.
+     */
+    public final void onProcessActionUpdated(ActionsState actionState) {
+        latestActionsState = actionState;
+        updateActionState(new ActionsChannelState(latestActionsState));
+    }
+
+    /**
+     * Invoked when the device managed by this handler was removed from the Miele cloud.
+     */
+    public final void onDeviceRemoved() {
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, I18NKeys.THING_STATUS_DESCRIPTION_REMOVED);
+    }
+
+    /**
+     * Invoked when a device state update for the device managed by this handler is received from the Miele cloud.
+     */
+    public final void onDeviceStateUpdated(DeviceState deviceState) {
+        actionFetcher.onDeviceStateUpdated(deviceState);
+
+        latestTransitionState = new TransitionState(latestTransitionState, deviceState);
+        latestDeviceState = deviceState;
+
+        updateThingProperties(deviceState);
+        updateDeviceState(new DeviceChannelState(latestDeviceState));
+        updateTransitionState(new TransitionChannelState(latestTransitionState));
+        updateThingStatus(latestDeviceState);
+    }
+
+    protected void triggerProcessAction(final ProcessAction processAction) {
+        performPutAction(() -> getWebservice().putProcessAction(getDeviceId(), processAction),
+                t -> logger.warn("Failed to perform '{}' operation for device '{}'.", processAction, getDeviceId(), t));
+    }
+
+    protected void triggerLight(final boolean on) {
+        performPutAction(() -> getWebservice().putLight(getDeviceId(), on),
+                t -> logger.warn("Failed to set light state to '{}' for device '{}'.", on, getDeviceId(), t));
+    }
+
+    protected void triggerPowerState(final boolean on) {
+        performPutAction(() -> getWebservice().putPowerState(getDeviceId(), on),
+                t -> logger.warn("Failed to set the power state to '{}' for device '{}'.", on, getDeviceId(), t));
+    }
+
+    protected void triggerProgram(final long programId) {
+        performPutAction(() -> getWebservice().putProgram(getDeviceId(), programId), t -> logger
+                .warn("Failed to activate program with ID '{}' for device '{}'.", programId, getDeviceId(), t));
+    }
+
+    private void performPutAction(Runnable action, Consumer<Exception> onError) {
+        scheduler.execute(() -> {
+            try {
+                action.run();
+            } catch (TooManyRequestsException e) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                        I18NKeys.THING_STATUS_DESCRIPTION_RATELIMIT);
+                onError.accept(e);
+            } catch (Exception e) {
+                onError.accept(e);
+            }
+        });
+    }
+
+    protected final String getDeviceId() {
+        return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER).toString();
+    }
+
+    /**
+     * Creates a {@link ChannelUID} from the given name.
+     *
+     * @param name channel name
+     * @return {@link ChannelUID}
+     */
+    protected ChannelUID channel(String name) {
+        return new ChannelUID(getThing().getUID(), name);
+    }
+
+    /**
+     * Updates the thing status depending on whether the managed device is connected and reachable.
+     */
+    private void updateThingStatus(DeviceState deviceState) {
+        if (deviceState.isInState(StateType.NOT_CONNECTED)) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    I18NKeys.THING_STATUS_DESCRIPTION_DISCONNECTED);
+        } else {
+            updateStatus(ThingStatus.ONLINE);
+        }
+    }
+
+    /**
+     * Determines the status of the currently selected program.
+     */
+    protected ProgramStatus getProgramStatus(StateType rawStatus) {
+        if (rawStatus.equals(StateType.RUNNING)) {
+            return PROGRAM_STARTED;
+        }
+        return PROGRAM_STOPPED;
+    }
+
+    /**
+     * Determines the power status of the managed device.
+     */
+    protected PowerStatus getPowerStatus(StateType rawStatus) {
+        if (rawStatus.equals(StateType.OFF) || rawStatus.equals(StateType.NOT_CONNECTED)) {
+            return POWER_OFF;
+        }
+        return POWER_ON;
+    }
+
+    /**
+     * Updates the thing properties. This is necessary if properties have not been set during discovery.
+     */
+    private void updateThingProperties(DeviceState deviceState) {
+        var properties = editProperties();
+        properties.putAll(ThingInformationExtractor.extractProperties(getThing().getThingTypeUID(), deviceState));
+        updateProperties(properties);
+    }
+
+    /**
+     * Updates the device state channels.
+     *
+     * @param device The {@link DeviceChannelState} information to update the device channel states with.
+     */
+    protected abstract void updateDeviceState(DeviceChannelState device);
+
+    /**
+     * Updates the transition state channels.
+     *
+     * @param transition The {@link TransitionChannelState} information to update the transition channel states with.
+     */
+    protected abstract void updateTransitionState(TransitionChannelState transition);
+
+    /**
+     * Updates the device action state channels.
+     *
+     * @param action The {@link ActionsChannelState} information to update the action channel states with.
+     */
+    protected abstract void updateActionState(ActionsChannelState actions);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoffeeSystemThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoffeeSystemThingHandler.java
new file mode 100644 (file)
index 0000000..ca4557c
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele coffee devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Switch from polling to SSE, add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class CoffeeSystemThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link CoffeeSystemThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public CoffeeSystemThingHandler(Thing thing) {
+        super(thing);
+
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+        updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+        updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+        updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+        if (transition.hasFinishedChanged()) {
+            updateState(channel(FINISH_STATE), transition.getFinishState());
+        }
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+        updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoolingDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoolingDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..03539f1
--- /dev/null
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+
+/**
+ * ThingHandler implementation for the Miele cooling devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add door state and door alarm, add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class CoolingDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link CoolingDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public CoolingDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        super.handleCommand(channelUID, command);
+
+        if (!OnOffType.ON.equals(command) && !OnOffType.OFF.equals(command)) {
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case FRIDGE_SUPER_COOL:
+                triggerProcessAction(OnOffType.ON.equals(command) ? ProcessAction.START_SUPERCOOLING
+                        : ProcessAction.STOP_SUPERCOOLING);
+                break;
+
+            case FREEZER_SUPER_FREEZE:
+                triggerProcessAction(OnOffType.ON.equals(command) ? ProcessAction.START_SUPERFREEZING
+                        : ProcessAction.STOP_SUPERFREEZING);
+                break;
+        }
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(FRIDGE_SUPER_COOL), device.getFridgeSuperCool());
+        updateState(channel(FREEZER_SUPER_FREEZE), device.getFreezerSuperFreeze());
+        updateState(channel(FRIDGE_TEMPERATURE_TARGET), device.getFridgeTemperatureTarget());
+        updateState(channel(FREEZER_TEMPERATURE_TARGET), device.getFreezerTemperatureTarget());
+        updateState(channel(FRIDGE_TEMPERATURE_CURRENT), device.getFridgeTemperatureCurrent());
+        updateState(channel(FREEZER_TEMPERATURE_CURRENT), device.getFreezerTemperatureCurrent());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(DOOR_STATE), device.getDoorState());
+        updateState(channel(DOOR_ALARM), device.getDoorAlarm());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(SUPER_COOL_CAN_BE_CONTROLLED), actions.getSuperCoolCanBeControlled());
+        updateState(channel(SUPER_FREEZE_CAN_BE_CONTROLLED), actions.getSuperFreezeCanBeControlled());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishWarmerDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishWarmerDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..62da3a1
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * ThingHandler implementation for Miele dish warmers.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DishWarmerDeviceThingHandler extends AbstractMieleThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /**
+     * Creates a new {@link DishWarmerDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public DishWarmerDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        super.handleCommand(channelUID, command);
+
+        if (DISH_WARMER_PROGRAM_ACTIVE.equals(channelUID.getId()) && command instanceof StringType) {
+            try {
+                triggerProgram(Long.parseLong(command.toString()));
+            } catch (NumberFormatException e) {
+                logger.warn("Failed to activate program: '{}' is not a valid program ID", command.toString());
+            }
+        }
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(DISH_WARMER_PROGRAM_ACTIVE), device.getProgramActiveId());
+        updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+        updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
+        updateState(channel(TEMPERATURE_CURRENT), device.getTemperatureCurrent());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(DOOR_STATE), device.getDoorState());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+        updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+        if (transition.hasFinishedChanged()) {
+            updateState(channel(FINISH_STATE), transition.getFinishState());
+        }
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishwasherDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishwasherDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..5c48e78
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele dishwasher devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DishwasherDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link DishwasherDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public DishwasherDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+        updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+        updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+        updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+        updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(DOOR_STATE), device.getDoorState());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+        updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+        if (transition.hasFinishedChanged()) {
+            updateState(channel(FINISH_STATE), transition.getFinishState());
+        }
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DryerDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/DryerDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..7c53541
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele dryer and washingDryer devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DryerDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link DryerDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public DryerDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+        updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+        updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+        updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+        updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+        updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+        updateState(channel(DRYING_TARGET), device.getDryingTarget());
+        updateState(channel(DRYING_TARGET_RAW), device.getDryingTargetRaw());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+        updateState(channel(DOOR_STATE), device.getDoorState());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+        updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+        if (transition.hasFinishedChanged()) {
+            updateState(channel(FINISH_STATE), transition.getFinishState());
+        }
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+        updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/HobDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/HobDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..0c3739c
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele hob devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add plate step, add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class HobDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link HobDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public HobDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(PLATE_1_POWER_STEP), device.getPlateStep(0));
+        updateState(channel(PLATE_1_POWER_STEP_RAW), device.getPlateStepRaw(0));
+        updateState(channel(PLATE_2_POWER_STEP), device.getPlateStep(1));
+        updateState(channel(PLATE_2_POWER_STEP_RAW), device.getPlateStepRaw(1));
+        updateState(channel(PLATE_3_POWER_STEP), device.getPlateStep(2));
+        updateState(channel(PLATE_3_POWER_STEP_RAW), device.getPlateStepRaw(2));
+        updateState(channel(PLATE_4_POWER_STEP), device.getPlateStep(3));
+        updateState(channel(PLATE_4_POWER_STEP_RAW), device.getPlateStepRaw(3));
+        updateState(channel(PLATE_5_POWER_STEP), device.getPlateStep(4));
+        updateState(channel(PLATE_5_POWER_STEP_RAW), device.getPlateStepRaw(4));
+        updateState(channel(PLATE_6_POWER_STEP), device.getPlateStep(5));
+        updateState(channel(PLATE_6_POWER_STEP_RAW), device.getPlateStepRaw(5));
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        // No state transition required
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        // The Hob device has no trigger actions
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/HoodDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/HoodDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..631f999
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele Hood devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class HoodDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link HoodDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public HoodDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+        updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(VENTILATION_POWER), device.getVentilationPower());
+        updateState(channel(VENTILATION_POWER_RAW), device.getVentilationPowerRaw());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleBridgeHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleBridgeHandler.java
new file mode 100644 (file)
index 0000000..03d93ee
--- /dev/null
@@ -0,0 +1,362 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefreshListener;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.discovery.ThingDiscoveryService;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionStatusListener;
+import org.openhab.binding.mielecloud.internal.webservice.DeviceStateListener;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * BridgeHandler implementation for the Miele cloud account.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Introduced CombiningLanguageProvider field and interactions, added LanguageProvider super
+ *         interface, switched from polling to SSE, added support for multiple bridges
+ */
+@NonNullByDefault
+public class MieleBridgeHandler extends BaseBridgeHandler
+        implements OAuthTokenRefreshListener, LanguageProvider, ConnectionStatusListener, DeviceStateListener {
+    private static final int NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED = 6;
+
+    private final Supplier<MieleWebservice> webserviceFactory;
+
+    private final OAuthTokenRefresher tokenRefresher;
+    private final CombiningLanguageProvider languageProvider;
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private @Nullable CompletableFuture<@Nullable Void> logoutFuture;
+    private @Nullable MieleWebservice webService;
+    private @Nullable ThingDiscoveryService discoveryService;
+
+    /**
+     * Creates a new {@link MieleBridgeHandler}.
+     *
+     * @param bridge The bridge to handle.
+     * @param webserviceFactory Factory for creating {@link MieleWebservice} instances.
+     * @param tokenRefresher Token refresher.
+     * @param languageProvider Language provider.
+     */
+    public MieleBridgeHandler(Bridge bridge, Function<ScheduledExecutorService, MieleWebservice> webserviceFactory,
+            OAuthTokenRefresher tokenRefresher, CombiningLanguageProvider languageProvider) {
+        super(bridge);
+        this.webserviceFactory = () -> webserviceFactory.apply(scheduler);
+        this.tokenRefresher = tokenRefresher;
+        this.languageProvider = languageProvider;
+    }
+
+    public void setDiscoveryService(@Nullable ThingDiscoveryService discoveryService) {
+        this.discoveryService = discoveryService;
+    }
+
+    /**
+     * Gets the current webservice instance for communication with the Miele service.
+     *
+     * This function may return an {@link UnavailableMieleWebservice} in case no webservice is available at the moment.
+     */
+    public MieleWebservice getWebservice() {
+        MieleWebservice webservice = webService;
+        if (webservice != null) {
+            return webservice;
+        } else {
+            return UnavailableMieleWebservice.INSTANCE;
+        }
+    }
+
+    private String getOAuthServiceHandle() {
+        return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString();
+    }
+
+    @Override
+    public void initialize() {
+        // It is required to set a status in this method as stated in the Javadoc of ThingHandler.initialize
+        updateStatus(ThingStatus.UNKNOWN);
+
+        initializeWebservice();
+    }
+
+    public void initializeWebservice() {
+        if (!EmailValidator.isValid(getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString())) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
+            // When the e-mail configuration is changed a new initialization will be triggered. Therefore, we can leave
+            // the bridge in this state.
+            return;
+        }
+
+        try {
+            webService = webserviceFactory.get();
+        } catch (MieleWebserviceInitializationException e) {
+            logger.warn("Failed to initialize webservice.", e);
+            updateStatus(ThingStatus.OFFLINE);
+            return;
+        }
+
+        try {
+            tokenRefresher.setRefreshListener(this, getOAuthServiceHandle());
+        } catch (OAuthException e) {
+            logger.debug("Could not initialize Miele Cloud bridge.", e);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
+            // When the authorization takes place a new initialization will be triggered. Therefore, we can leave the
+            // bridge in this state.
+            return;
+        }
+        languageProvider.setPrioritizedLanguageProvider(this);
+        tryInitializeWebservice();
+
+        MieleWebservice webservice = getWebservice();
+        webservice.addConnectionStatusListener(this);
+        webservice.addDeviceStateListener(this);
+        if (webservice.hasAccessToken()) {
+            webservice.connectSse();
+        }
+    }
+
+    @Override
+    public void handleRemoval() {
+        performLogout();
+        tokenRefresher.removeTokensFromStorage(getOAuthServiceHandle());
+        super.handleRemoval();
+    }
+
+    @Override
+    public void dispose() {
+        logger.debug("Disposing {}", this.getClass().getName());
+        disposeWebservice();
+    }
+
+    public void disposeWebservice() {
+        getWebservice().removeConnectionStatusListener(this);
+        getWebservice().removeDeviceStateListener(this);
+        getWebservice().disconnectSse();
+        languageProvider.unsetPrioritizedLanguageProvider();
+        tokenRefresher.unsetRefreshListener(getOAuthServiceHandle());
+
+        stopWebservice();
+    }
+
+    private void stopWebservice() {
+        final MieleWebservice webService = this.webService;
+        this.webService = null;
+        if (webService == null) {
+            return;
+        }
+
+        scheduler.submit(() -> {
+            CompletableFuture<@Nullable Void> logoutFuture = this.logoutFuture;
+            if (logoutFuture != null) {
+                try {
+                    logoutFuture.get();
+                } catch (InterruptedException e) {
+                    logger.warn("Interrupted while waiting for logout!");
+                } catch (ExecutionException e) {
+                    logger.warn("Failed to wait for logout.", e);
+                }
+            }
+
+            try {
+                webService.close();
+            } catch (Exception e) {
+                logger.warn("Failed to close webservice.", e);
+            }
+        });
+    }
+
+    @Override
+    public void onNewAccessToken(String accessToken) {
+        logger.debug("Setting new access token for webservice access.");
+        updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken);
+
+        // Without this the retry would fail causing the thing to go OFFLINE
+        getWebservice().setAccessToken(accessToken);
+
+        // If there was no access token during initialization then the SSE connection was not established.
+        getWebservice().connectSse();
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+
+    private void performLogout() {
+        logoutFuture = new CompletableFuture<>();
+        scheduler.execute(() -> {
+            try {
+                getWebservice().logout();
+            } catch (Exception e) {
+                logger.warn("Failed to logout from Miele cloud.", e);
+            }
+            Optional.ofNullable(logoutFuture).map(future -> future.complete(null));
+        });
+    }
+
+    private void tryInitializeWebservice() {
+        Optional<String> accessToken = tokenRefresher.getAccessTokenFromStorage(getOAuthServiceHandle());
+        if (!accessToken.isPresent()) {
+            logger.debug("No OAuth2 access token available. Retrying later.");
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                    I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
+            return;
+        }
+        getWebservice().setAccessToken(accessToken.get());
+        updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken.get());
+    }
+
+    @Override
+    public void onConnectionAlive() {
+        updateStatus(ThingStatus.ONLINE);
+    }
+
+    @Override
+    public void onConnectionError(ConnectionError connectionError, int failedReconnectionAttempts) {
+        if (connectionError == ConnectionError.AUTHORIZATION_FAILED) {
+            tryToRefreshAccessToken();
+            return;
+        }
+
+        if (failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED
+                && getThing().getStatus() != ThingStatus.UNKNOWN) {
+            return;
+        }
+
+        if (getThing().getStatus() == ThingStatus.UNKNOWN && connectionError == ConnectionError.REQUEST_INTERRUPTED
+                && failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED) {
+            return;
+        }
+
+        switch (connectionError) {
+            case AUTHORIZATION_FAILED:
+                // Handled above.
+                break;
+
+            case REQUEST_EXECUTION_FAILED:
+            case SERVICE_UNAVAILABLE:
+            case RESPONSE_MALFORMED:
+            case TIMEOUT:
+            case TOO_MANY_RERQUESTS:
+            case SSE_STREAM_ENDED:
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+                break;
+
+            case SERVER_ERROR:
+            case REQUEST_INTERRUPTED:
+            case OTHER_HTTP_ERROR:
+            default:
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+                        I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+                break;
+        }
+    }
+
+    private void tryToRefreshAccessToken() {
+        try {
+            tokenRefresher.refreshToken(getOAuthServiceHandle());
+            getWebservice().connectSse();
+        } catch (OAuthException e) {
+            logger.debug("Failed to refresh OAuth token!", e);
+            getWebservice().disconnectSse();
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
+        }
+    }
+
+    @Override
+    public Optional<String> getLanguage() {
+        Object languageObject = thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
+        if (languageObject instanceof String) {
+            String language = (String) languageObject;
+            if (language.isEmpty() || !LocaleValidator.isValidLanguage(language)) {
+                return Optional.empty();
+            } else {
+                return Optional.of(language);
+            }
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public void onDeviceStateUpdated(DeviceState deviceState) {
+        ThingDiscoveryService discoveryService = this.discoveryService;
+        if (discoveryService != null) {
+            discoveryService.onDeviceStateUpdated(deviceState);
+        }
+
+        invokeOnThingHandlers(deviceState.getDeviceIdentifier(), handler -> handler.onDeviceStateUpdated(deviceState));
+    }
+
+    @Override
+    public void onProcessActionUpdated(ActionsState actionState) {
+        invokeOnThingHandlers(actionState.getDeviceIdentifier(),
+                handler -> handler.onProcessActionUpdated(actionState));
+    }
+
+    @Override
+    public void onDeviceRemoved(String deviceIdentifier) {
+        ThingDiscoveryService discoveryService = this.discoveryService;
+        if (discoveryService != null) {
+            discoveryService.onDeviceRemoved(deviceIdentifier);
+        }
+
+        invokeOnThingHandlers(deviceIdentifier, handler -> handler.onDeviceRemoved());
+    }
+
+    private void invokeOnThingHandlers(String deviceIdentifier, Consumer<AbstractMieleThingHandler> action) {
+        getThing().getThings().stream().map(Thing::getHandler)
+                .filter(handler -> handler instanceof AbstractMieleThingHandler)
+                .map(handler -> (AbstractMieleThingHandler) handler)
+                .filter(handler -> deviceIdentifier.equals(handler.getDeviceId())).forEach(action);
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(ThingDiscoveryService.class);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleHandlerFactory.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleHandlerFactory.java
new file mode 100644 (file)
index 0000000..e9f6438
--- /dev/null
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.*;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.DefaultMieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceConfiguration;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.OpenHabLanguageProvider;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Factory producing the {@link ThingHandler}s for all things supported by this binding.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Added language provider, added support for multiple bridges
+ */
+@NonNullByDefault
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.mielecloud")
+public class MieleHandlerFactory extends BaseThingHandlerFactory {
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE, THING_TYPE_WASHING_MACHINE,
+            THING_TYPE_WASHER_DRYER, THING_TYPE_COFFEE_SYSTEM, THING_TYPE_FRIDGE_FREEZER, THING_TYPE_FRIDGE,
+            THING_TYPE_FREEZER, THING_TYPE_OVEN, THING_TYPE_WINE_STORAGE, THING_TYPE_HOB, THING_TYPE_DRYER,
+            THING_TYPE_DISHWASHER, THING_TYPE_HOOD, THING_TYPE_DISH_WARMER, THING_TYPE_ROBOTIC_VACUUM_CLEANER);
+
+    private final HttpClientFactory httpClientFactory;
+    private final OAuthTokenRefresher tokenRefresher;
+    private final LocaleProvider localeProvider;
+
+    private final MieleWebserviceFactory webserviceFactory = new DefaultMieleWebserviceFactory();
+
+    @Activate
+    public MieleHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+            @Reference OAuthTokenRefresher tokenRefresher, @Reference LocaleProvider localeProvider) {
+        this.httpClientFactory = httpClientFactory;
+        this.tokenRefresher = tokenRefresher;
+        this.localeProvider = localeProvider;
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES.contains(thingTypeUID);
+    }
+
+    @Override
+    @Nullable
+    protected ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (thingTypeUID.equals(THING_TYPE_BRIDGE)) {
+            return createBridgeHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_WASHING_MACHINE) || thingTypeUID.equals(THING_TYPE_WASHER_DRYER)) {
+            return new WashingDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_COFFEE_SYSTEM)) {
+            return new CoffeeSystemThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_FRIDGE_FREEZER) || thingTypeUID.equals(THING_TYPE_FRIDGE)
+                || thingTypeUID.equals(THING_TYPE_FREEZER)) {
+            return new CoolingDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_WINE_STORAGE)) {
+            return new WineStorageDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_OVEN)) {
+            return new OvenDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_HOB)) {
+            return new HobDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_DISHWASHER)) {
+            return new DishwasherDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_DRYER)) {
+            return new DryerDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_HOOD)) {
+            return new HoodDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_DISH_WARMER)) {
+            return new DishWarmerDeviceThingHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_ROBOTIC_VACUUM_CLEANER)) {
+            return new RoboticVacuumCleanerDeviceThingHandler(thing);
+        }
+
+        return null;
+    }
+
+    private ThingHandler createBridgeHandler(Thing thing) {
+        CombiningLanguageProvider languageProvider = getLanguageProvider();
+        Function<ScheduledExecutorService, MieleWebservice> webserviceFactoryFunction = scheduler -> webserviceFactory
+                .create(MieleWebserviceConfiguration.builder().withHttpClientFactory(httpClientFactory)
+                        .withLanguageProvider(languageProvider).withTokenRefresher(tokenRefresher)
+                        .withServiceHandle(thing.getUID().getAsString()).withScheduler(scheduler).build());
+
+        return new MieleBridgeHandler((Bridge) thing, webserviceFactoryFunction, tokenRefresher, languageProvider);
+    }
+
+    private CombiningLanguageProvider getLanguageProvider() {
+        return new CombiningLanguageProvider(null, new OpenHabLanguageProvider(localeProvider));
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/OvenDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/OvenDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..6e6f765
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele oven devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add pre-heat finished channel, add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class OvenDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link OvenDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public OvenDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+        updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+        updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+        updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+        updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+        updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+        updateState(channel(PRE_HEAT_FINISHED), device.hasPreHeatFinished());
+        updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
+        updateState(channel(TEMPERATURE_CURRENT), device.getTemperatureCurrent());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+        updateState(channel(DOOR_STATE), device.getDoorState());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+        updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+        if (transition.hasFinishedChanged()) {
+            updateState(channel(FINISH_STATE), transition.getFinishState());
+        }
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+        updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/RoboticVacuumCleanerDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/RoboticVacuumCleanerDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..3b36a0f
--- /dev/null
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * ThingHandler implementation for Miele robotic vacuum cleaners.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RoboticVacuumCleanerDeviceThingHandler extends AbstractMieleThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /**
+     * Creates a new {@link RoboticVacuumCleanerDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public RoboticVacuumCleanerDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        super.handleCommand(channelUID, command);
+
+        if (VACUUM_CLEANER_PROGRAM_ACTIVE.equals(channelUID.getId()) && command instanceof StringType) {
+            try {
+                triggerProgram(Long.parseLong(command.toString()));
+            } catch (NumberFormatException e) {
+                logger.warn("Failed to activate program: '{}' is not a valid program ID", command.toString());
+            }
+        }
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(VACUUM_CLEANER_PROGRAM_ACTIVE), device.getProgramActiveId());
+        updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(PROGRAM_START_STOP_PAUSE), device.getProgramStartStopPause());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(BATTERY_LEVEL), device.getBatteryLevel());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        if (transition.hasFinishedChanged()) {
+            updateState(channel(FINISH_STATE), transition.getFinishState());
+        }
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_PAUSED), actions.getRemoteControlCanBePaused());
+        updateState(channel(REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE), actions.getRemoteControlCanSetProgramActive());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/WashingDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/WashingDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..dff2448
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele washing devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class WashingDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link WashingDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public WashingDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(SPINNING_SPEED), device.getSpinningSpeed());
+        updateState(channel(SPINNING_SPEED_RAW), device.getSpinningSpeedRaw());
+        updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+        updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+        updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+        updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+        updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+        updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+        updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+        updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+        updateState(channel(DOOR_STATE), device.getDoorState());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+        updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+        updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+        if (transition.hasFinishedChanged()) {
+            updateState(channel(FINISH_STATE), transition.getFinishState());
+        }
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+        updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/WineStorageDeviceThingHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/WineStorageDeviceThingHandler.java
new file mode 100644 (file)
index 0000000..e883e45
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele wine storage devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class WineStorageDeviceThingHandler extends AbstractMieleThingHandler {
+    /**
+     * Creates a new {@link WineStorageDeviceThingHandler}.
+     *
+     * @param thing The thing to handle.
+     */
+    public WineStorageDeviceThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
+        updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
+    }
+
+    @Override
+    protected void updateDeviceState(DeviceChannelState device) {
+        updateState(channel(OPERATION_STATE), device.getOperationState());
+        updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+        updateState(channel(TEMPERATURE_TARGET), device.getWineTemperatureTarget());
+        updateState(channel(TEMPERATURE_CURRENT), device.getWineTemperatureCurrent());
+        updateState(channel(TOP_TEMPERATURE_TARGET), device.getWineTopTemperatureTarget());
+        updateState(channel(TOP_TEMPERATURE_CURRENT), device.getWineTopTemperatureCurrent());
+        updateState(channel(MIDDLE_TEMPERATURE_TARGET), device.getWineMiddleTemperatureTarget());
+        updateState(channel(MIDDLE_TEMPERATURE_CURRENT), device.getWineMiddleTemperatureCurrent());
+        updateState(channel(BOTTOM_TEMPERATURE_TARGET), device.getWineBottomTemperatureTarget());
+        updateState(channel(BOTTOM_TEMPERATURE_CURRENT), device.getWineBottomTemperatureCurrent());
+        updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+        updateState(channel(ERROR_STATE), device.getErrorState());
+        updateState(channel(INFO_STATE), device.getInfoState());
+    }
+
+    @Override
+    protected void updateTransitionState(TransitionChannelState transition) {
+    }
+
+    @Override
+    protected void updateActionState(ActionsChannelState actions) {
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+        updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/ActionsChannelState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/ActionsChannelState.java
new file mode 100644 (file)
index 0000000..9e4c108
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.State;
+
+/**
+ * Wrapper for {@link ActionsState} handling the type conversion to {@link State} for directly filling channels.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ActionsChannelState {
+    private final ActionsState actions;
+
+    public ActionsChannelState(ActionsState actions) {
+        this.actions = actions;
+    }
+
+    public State getRemoteControlCanBeSwitchedOn() {
+        return OnOffType.from(actions.canBeSwitchedOn());
+    }
+
+    public State getRemoteControlCanBeSwitchedOff() {
+        return OnOffType.from(actions.canBeSwitchedOff());
+    }
+
+    public State getLightCanBeControlled() {
+        return OnOffType.from(actions.canControlLight());
+    }
+
+    public State getSuperCoolCanBeControlled() {
+        return OnOffType.from(actions.canContolSupercooling());
+    }
+
+    public State getSuperFreezeCanBeControlled() {
+        return OnOffType.from(actions.canControlSuperfreezing());
+    }
+
+    public State getRemoteControlCanBeStarted() {
+        return OnOffType.from(actions.canBeStarted());
+    }
+
+    public State getRemoteControlCanBeStopped() {
+        return OnOffType.from(actions.canBeStopped());
+    }
+
+    public State getRemoteControlCanBePaused() {
+        return OnOffType.from(actions.canBePaused());
+    }
+
+    public State getRemoteControlCanSetProgramActive() {
+        return OnOffType.from(actions.canSetActiveProgramId());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/ChannelTypeUtil.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/ChannelTypeUtil.java
new file mode 100644 (file)
index 0000000..6a9b47f
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Utility class handling type conversions from Java types to channel types.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ChannelTypeUtil {
+    private ChannelTypeUtil() {
+        throw new IllegalStateException("ChannelTypeUtil cannot be instantiated.");
+    }
+
+    /**
+     * Converts an {@link Optional} of {@link String} to {@link State}.
+     */
+    public static State stringToState(Optional<String> value) {
+        return value.filter(v -> !v.isEmpty()).filter(v -> !v.equals("null")).map(v -> (State) new StringType(v))
+                .orElse(UnDefType.UNDEF);
+    }
+
+    /**
+     * Converts an {@link Optional} of {@link Boolean} to {@link State}.
+     */
+    public static State booleanToState(Optional<Boolean> value) {
+        return value.map(v -> (State) OnOffType.from(v)).orElse(UnDefType.UNDEF);
+    }
+
+    /**
+     * Converts an {@link Optional} of {@link Integer} to {@link State}.
+     */
+    public static State intToState(Optional<Integer> value) {
+        return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
+    }
+
+    /**
+     * Converts an {@link Optional} of {@link Long} to {@link State}.
+     */
+    public static State longToState(Optional<Long> value) {
+        return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
+    }
+
+    /**
+     * Converts an {@link Optional} of {@link Integer} to {@link State} representing a temperature.
+     */
+    public static State intToTemperatureState(Optional<Integer> value) {
+        // The Miele 3rd Party API always provides temperatures in °C (even if the device uses another unit).
+        return value.map(v -> (State) new QuantityType<>(v, SIUnits.CELSIUS)).orElse(UnDefType.UNDEF);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/DeviceChannelState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/DeviceChannelState.java
new file mode 100644 (file)
index 0000000..e075ced
--- /dev/null
@@ -0,0 +1,269 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import static org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus.*;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.CoolingDeviceTemperatureState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.WineStorageDeviceTemperatureState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+
+/**
+ * Wrapper for {@link DeviceState} handling the type conversion to {@link State} for directly filling channels.
+ *
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm and info state channel and map
+ *         signal flags from API
+ * @author Björn Lange - Add elapsed time channel, dish warmer and robotic vacuum cleaner thing
+ */
+@NonNullByDefault
+public final class DeviceChannelState {
+    private final DeviceState device;
+    private final CoolingDeviceTemperatureState coolingTemperature;
+    private final WineStorageDeviceTemperatureState wineTemperature;
+
+    public DeviceChannelState(DeviceState device) {
+        this.device = device;
+        this.coolingTemperature = new CoolingDeviceTemperatureState(device);
+        this.wineTemperature = new WineStorageDeviceTemperatureState(device);
+    }
+
+    public State getLightSwitch() {
+        return ChannelTypeUtil.booleanToState(device.getLightState());
+    }
+
+    public State getDoorState() {
+        return ChannelTypeUtil.booleanToState(device.getDoorState());
+    }
+
+    public State getDoorAlarm() {
+        return ChannelTypeUtil.booleanToState(device.getDoorAlarm());
+    }
+
+    public State getErrorState() {
+        return OnOffType.from(device.hasError());
+    }
+
+    public State getInfoState() {
+        return OnOffType.from(device.hasInfo());
+    }
+
+    public State getPowerOnOff() {
+        return new StringType(getPowerStatus().getState());
+    }
+
+    public State getProgramElapsedTime() {
+        return ChannelTypeUtil.intToState(device.getElapsedTime());
+    }
+
+    public State getOperationState() {
+        return ChannelTypeUtil.stringToState(device.getStatus());
+    }
+
+    public State getOperationStateRaw() {
+        return ChannelTypeUtil.intToState(device.getStatusRaw());
+    }
+
+    public State getProgramPhase() {
+        return ChannelTypeUtil.stringToState(device.getProgramPhase());
+    }
+
+    public State getProgramPhaseRaw() {
+        return ChannelTypeUtil.intToState(device.getProgramPhaseRaw());
+    }
+
+    public State getProgramActive() {
+        return ChannelTypeUtil.stringToState(device.getSelectedProgram());
+    }
+
+    public State getProgramActiveRaw() {
+        return ChannelTypeUtil.longToState(device.getSelectedProgramId());
+    }
+
+    public State getProgramActiveId() {
+        return ChannelTypeUtil.stringToState(device.getSelectedProgramId().map(Object::toString));
+    }
+
+    public State getFridgeSuperCool() {
+        return ChannelTypeUtil.booleanToState(isInState(StateType.SUPERCOOLING, StateType.SUPERCOOLING_SUPERFREEZING));
+    }
+
+    public State getFreezerSuperFreeze() {
+        return ChannelTypeUtil.booleanToState(isInState(StateType.SUPERFREEZING, StateType.SUPERCOOLING_SUPERFREEZING));
+    }
+
+    public State getFridgeTemperatureTarget() {
+        return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFridgeTargetTemperature());
+    }
+
+    public State getFreezerTemperatureTarget() {
+        return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFreezerTargetTemperature());
+    }
+
+    public State getFridgeTemperatureCurrent() {
+        return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFridgeTemperature());
+    }
+
+    public State getFreezerTemperatureCurrent() {
+        return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFreezerTemperature());
+    }
+
+    public State getProgramStartStop() {
+        return new StringType(getProgramStartStopStatus().getState());
+    }
+
+    public State getProgramStartStopPause() {
+        return new StringType(getProgramStartStopPauseStatus().getState());
+    }
+
+    public State getDelayedStartTime() {
+        return ChannelTypeUtil.intToState(device.getStartTime());
+    }
+
+    public State getDryingTarget() {
+        return ChannelTypeUtil.stringToState(device.getDryingTarget());
+    }
+
+    public State getDryingTargetRaw() {
+        return ChannelTypeUtil.intToState(device.getDryingTargetRaw());
+    }
+
+    public State hasPreHeatFinished() {
+        return ChannelTypeUtil.booleanToState(device.hasPreHeatFinished());
+    }
+
+    public State getTemperatureTarget() {
+        return ChannelTypeUtil.intToTemperatureState(device.getTargetTemperature(0));
+    }
+
+    public State getVentilationPower() {
+        return ChannelTypeUtil.stringToState(device.getVentilationStep());
+    }
+
+    public State getVentilationPowerRaw() {
+        return ChannelTypeUtil.intToState(device.getVentilationStepRaw());
+    }
+
+    public State getPlateStep(int index) {
+        return ChannelTypeUtil.stringToState(device.getPlateStep(index));
+    }
+
+    public State getPlateStepRaw(int index) {
+        return ChannelTypeUtil.intToState(device.getPlateStepRaw(index));
+    }
+
+    public State getTemperatureCurrent() {
+        return ChannelTypeUtil.intToTemperatureState(device.getTemperature(0));
+    }
+
+    public State getSpinningSpeed() {
+        return ChannelTypeUtil.stringToState(device.getSpinningSpeed());
+    }
+
+    public State getSpinningSpeedRaw() {
+        return ChannelTypeUtil.intToState(device.getSpinningSpeedRaw());
+    }
+
+    public State getBatteryLevel() {
+        return ChannelTypeUtil.intToState(device.getBatteryLevel());
+    }
+
+    public State getWineTemperatureTarget() {
+        return ChannelTypeUtil.intToState(wineTemperature.getTargetTemperature());
+    }
+
+    public State getWineTemperatureCurrent() {
+        return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTemperature());
+    }
+
+    public State getWineTopTemperatureTarget() {
+        return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTopTargetTemperature());
+    }
+
+    public State getWineTopTemperatureCurrent() {
+        return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTopTemperature());
+    }
+
+    public State getWineMiddleTemperatureTarget() {
+        return ChannelTypeUtil.intToTemperatureState(wineTemperature.getMiddleTargetTemperature());
+    }
+
+    public State getWineMiddleTemperatureCurrent() {
+        return ChannelTypeUtil.intToTemperatureState(wineTemperature.getMiddleTemperature());
+    }
+
+    public State getWineBottomTemperatureTarget() {
+        return ChannelTypeUtil.intToTemperatureState(wineTemperature.getBottomTargetTemperature());
+    }
+
+    public State getWineBottomTemperatureCurrent() {
+        return ChannelTypeUtil.intToTemperatureState(wineTemperature.getBottomTemperature());
+    }
+
+    /**
+     * Determines the status of the currently selected program.
+     */
+    private PowerStatus getPowerStatus() {
+        if (device.isInState(StateType.OFF) || device.isInState(StateType.NOT_CONNECTED)) {
+            return POWER_OFF;
+        } else {
+            return POWER_ON;
+        }
+    }
+
+    /**
+     * Determines the status of the currently selected program respecting the possibilities started and stopped.
+     */
+    protected ProgramStatus getProgramStartStopStatus() {
+        if (device.isInState(StateType.RUNNING)) {
+            return PROGRAM_STARTED;
+        } else {
+            return PROGRAM_STOPPED;
+        }
+    }
+
+    /**
+     * Determines the status of the currently selected program respecting the possibilities started, stopped and paused.
+     */
+    protected ProgramStatus getProgramStartStopPauseStatus() {
+        if (device.isInState(StateType.RUNNING)) {
+            return PROGRAM_STARTED;
+        } else if (device.isInState(StateType.PAUSE)) {
+            return PROGRAM_PAUSED;
+        } else {
+            return PROGRAM_STOPPED;
+        }
+    }
+
+    /**
+     * Gets whether the device is in one of the given states.
+     *
+     * @param stateType The states to check.
+     * @return An empty {@link Optional} if the raw status is unknown, otherwise an {@link Optional} with a value
+     *         indicating whether the device is in one of the given states.
+     */
+    private Optional<Boolean> isInState(StateType... stateType) {
+        return device.getStateType().map(it -> Arrays.asList(stateType).contains(it));
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/TransitionChannelState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/handler/channel/TransitionChannelState.java
new file mode 100644 (file)
index 0000000..ceb430b
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.TransitionState;
+import org.openhab.core.types.State;
+
+/**
+ * Wrapper for {@link TransitionState} handling the type conversion to {@link State} for directly filling channels.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class TransitionChannelState {
+    private final TransitionState transition;
+
+    public TransitionChannelState(TransitionState transition) {
+        this.transition = transition;
+    }
+
+    public boolean hasFinishedChanged() {
+        return transition.hasFinishedChanged();
+    }
+
+    public State getFinishState() {
+        return ChannelTypeUtil.booleanToState(transition.isFinished());
+    }
+
+    public State getProgramRemainingTime() {
+        return ChannelTypeUtil.intToState(transition.getRemainingTime());
+    }
+
+    public State getProgramProgress() {
+        return ChannelTypeUtil.intToState(transition.getProgress());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/util/EmailValidator.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/util/EmailValidator.java
new file mode 100644 (file)
index 0000000..4e4083f
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility for validating e-mail addresses.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class EmailValidator {
+    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w-_\\.+]*[\\w-_\\.]\\@([\\w]+\\.)+[\\w]+[\\w]$");
+
+    private EmailValidator() {
+        throw new UnsupportedOperationException();
+    }
+
+    public static boolean isValid(String emailAddress) {
+        return EMAIL_PATTERN.matcher(emailAddress).matches();
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/util/LocaleValidator.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/util/LocaleValidator.java
new file mode 100644 (file)
index 0000000..6d213d5
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility for validating locales.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class LocaleValidator {
+    private LocaleValidator() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Checks whether the given string is a valid two letter language code.
+     *
+     * @param language The string to check.
+     * @return Whether it is a valid language.
+     */
+    public static boolean isValidLanguage(String language) {
+        try {
+            String iso3Language = new Locale(language).getISO3Language();
+            return iso3Language != null && !iso3Language.isEmpty();
+        } catch (MissingResourceException e) {
+            return false;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ActionStateFetcher.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ActionStateFetcher.java
new file mode 100644 (file)
index 0000000..47c851a
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ActionStateFetcher} fetches the updated actions state for a device from the {@link MieleWebservice} if
+ * the state of that device changed.
+ *
+ * Note that an instance of this class is required for each device.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Make calls to webservice asynchronous
+ */
+@NonNullByDefault
+public class ActionStateFetcher {
+    private Optional<DeviceState> lastDeviceState = Optional.empty();
+    private final Supplier<MieleWebservice> webserviceSupplier;
+    private final ScheduledExecutorService scheduler;
+
+    private final Logger logger = LoggerFactory.getLogger(ActionStateFetcher.class);
+
+    /**
+     * Creates a new {@link ActionStateFetcher}.
+     *
+     * @param webserviceSupplier Getter function for access to the {@link MieleWebservice}.
+     * @param scheduler System-wide scheduler.
+     */
+    public ActionStateFetcher(Supplier<MieleWebservice> webserviceSupplier, ScheduledExecutorService scheduler) {
+        this.webserviceSupplier = webserviceSupplier;
+        this.scheduler = scheduler;
+    }
+
+    /**
+     * Invoked when the state of a device was updated.
+     */
+    public void onDeviceStateUpdated(DeviceState deviceState) {
+        if (hasDeviceStatusChanged(deviceState)) {
+            scheduler.submit(() -> fetchActions(deviceState));
+        }
+        lastDeviceState = Optional.of(deviceState);
+    }
+
+    private boolean hasDeviceStatusChanged(DeviceState newDeviceState) {
+        return lastDeviceState.map(DeviceState::getStateType)
+                .map(rawStatus -> !newDeviceState.getStateType().equals(rawStatus)).orElse(true);
+    }
+
+    private void fetchActions(DeviceState deviceState) {
+        try {
+            webserviceSupplier.get().fetchActions(deviceState.getDeviceIdentifier());
+        } catch (MieleWebserviceException e) {
+            logger.warn("Failed to fetch action state for device {}: {} - {}", deviceState.getDeviceIdentifier(),
+                    e.getConnectionError(), e.getMessage());
+        } catch (AuthorizationFailedException | TooManyRequestsException e) {
+            logger.warn("Failed to fetch action state for device {}: {}", deviceState.getDeviceIdentifier(),
+                    e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ConnectionError.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ConnectionError.java
new file mode 100644 (file)
index 0000000..471f6cd
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ConnectionError} enumeration represents the error state of a connection to the Miele cloud.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum ConnectionError {
+    SERVER_ERROR,
+    SERVICE_UNAVAILABLE,
+    OTHER_HTTP_ERROR,
+    REQUEST_INTERRUPTED,
+    TIMEOUT,
+    REQUEST_EXECUTION_FAILED,
+    RESPONSE_MALFORMED,
+    AUTHORIZATION_FAILED,
+    TOO_MANY_RERQUESTS,
+    SSE_STREAM_ENDED,
+    UNKNOWN,
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ConnectionStatusListener.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/ConnectionStatusListener.java
new file mode 100644 (file)
index 0000000..444252d
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Listener for the connection status.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface ConnectionStatusListener {
+    /**
+     * Called regularly while the connection is up and running.
+     */
+    void onConnectionAlive();
+
+    /**
+     * Called when a connection error is encountered.
+     *
+     * @param connectionError The error.
+     * @param failedReconnectAttempts The number of failed attempts to reconnect.
+     */
+    void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebservice.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebservice.java
new file mode 100644 (file)
index 0000000..a3ab848
--- /dev/null
@@ -0,0 +1,355 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactory;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactoryImpl;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
+import org.openhab.binding.mielecloud.internal.webservice.sse.ServerSentEvent;
+import org.openhab.binding.mielecloud.internal.webservice.sse.SseConnection;
+import org.openhab.binding.mielecloud.internal.webservice.sse.SseListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Default implementation of the {@link MieleWebservice}.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class DefaultMieleWebservice implements MieleWebservice, SseListener {
+    private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
+    public static final String THIRD_PARTY_ENDPOINTS_BASENAME = SERVER_ADDRESS + "/thirdparty";
+    private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
+    private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + "%s" + "/actions";
+    private static final String ENDPOINT_LOGOUT = THIRD_PARTY_ENDPOINTS_BASENAME + "/logout";
+    private static final String ENDPOINT_ALL_SSE_EVENTS = ENDPOINT_DEVICES + "all/events";
+
+    private static final String SSE_EVENT_TYPE_DEVICES = "devices";
+
+    private static final Gson GSON = new Gson();
+
+    private final Logger logger = LoggerFactory.getLogger(DefaultMieleWebservice.class);
+
+    private Optional<String> accessToken = Optional.empty();
+    private final RequestFactory requestFactory;
+
+    private final DeviceStateDispatcher deviceStateDispatcher;
+    private final List<ConnectionStatusListener> connectionStatusListeners = new ArrayList<>();
+
+    private final RetryStrategy retryStrategy;
+
+    private final SseConnection sseConnection;
+
+    /**
+     * Creates a new {@link DefaultMieleWebservice} with default retry configuration which is to retry failed operations
+     * once on a transient error. In case an authorization error occurs, a new access token is requested and a retry of
+     * the failed request is executed.
+     *
+     * @param configuration The configuration holding all parameters for constructing the instance.
+     * @throws MieleWebserviceInitializationException if initializing the HTTP client fails.
+     */
+    public DefaultMieleWebservice(MieleWebserviceConfiguration configuration) {
+        this(new RequestFactoryImpl(configuration.getHttpClientFactory(), configuration.getLanguageProvider()),
+                new RetryStrategyCombiner(new NTimesRetryStrategy(1),
+                        new AuthorizationFailedRetryStrategy(configuration.getTokenRefresher(),
+                                configuration.getServiceHandle())),
+                new DeviceStateDispatcher(), configuration.getScheduler());
+    }
+
+    /**
+     * This constructor only exists for testing.
+     */
+    DefaultMieleWebservice(RequestFactory requestFactory, RetryStrategy retryStrategy,
+            DeviceStateDispatcher deviceStateDispatcher, ScheduledExecutorService scheduler) {
+        this.requestFactory = requestFactory;
+        this.retryStrategy = retryStrategy;
+        this.deviceStateDispatcher = deviceStateDispatcher;
+        this.sseConnection = new SseConnection(ENDPOINT_ALL_SSE_EVENTS, this::createSseRequest, scheduler);
+        this.sseConnection.addSseListener(this);
+    }
+
+    @Override
+    public void setAccessToken(String accessToken) {
+        this.accessToken = Optional.of(accessToken);
+    }
+
+    @Override
+    public boolean hasAccessToken() {
+        return accessToken.isPresent();
+    }
+
+    @Override
+    public synchronized void connectSse() {
+        sseConnection.connect();
+    }
+
+    @Override
+    public synchronized void disconnectSse() {
+        sseConnection.disconnect();
+    }
+
+    @Nullable
+    private Request createSseRequest(String endpoint) {
+        Optional<String> accessToken = this.accessToken;
+        if (!accessToken.isPresent()) {
+            logger.warn("No access token present.");
+            return null;
+        }
+
+        return requestFactory.createSseRequest(endpoint, accessToken.get());
+    }
+
+    @Override
+    public void onServerSentEvent(ServerSentEvent event) {
+        fireConnectionAlive();
+
+        if (!SSE_EVENT_TYPE_DEVICES.equals(event.getEvent())) {
+            return;
+        }
+
+        try {
+            deviceStateDispatcher.dispatchDeviceStateUpdates(DeviceCollection.fromJson(event.getData()));
+        } catch (MieleSyntaxException e) {
+            logger.warn("SSE payload is not valid Json: {}", event.getData());
+        }
+    }
+
+    private void fireConnectionAlive() {
+        connectionStatusListeners.forEach(ConnectionStatusListener::onConnectionAlive);
+    }
+
+    @Override
+    public void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts) {
+        connectionStatusListeners.forEach(l -> l.onConnectionError(connectionError, failedReconnectAttempts));
+    }
+
+    @Override
+    public void fetchActions(String deviceId) {
+        Actions actions = retryStrategy.performRetryableOperation(() -> getActions(deviceId),
+                e -> logger.warn("Cannot poll action state: {}. Retrying...", e.getMessage()));
+        if (actions != null) {
+            deviceStateDispatcher.dispatchActionStateUpdates(deviceId, actions);
+        } else {
+            logger.warn("Cannot poll action state. Response is missing actions.");
+        }
+    }
+
+    @Override
+    public void putProcessAction(String deviceId, ProcessAction processAction) {
+        if (processAction.equals(ProcessAction.UNKNOWN)) {
+            throw new IllegalArgumentException("Process action must not be UNKNOWN.");
+        }
+
+        String formattedProcessAction = GSON.toJson(processAction, ProcessAction.class);
+        formattedProcessAction = formattedProcessAction.substring(1, formattedProcessAction.length() - 1);
+        String json = "{\"processAction\":" + formattedProcessAction + "}";
+
+        logger.debug("Activate process action {} of Miele device {}", processAction.toString(), deviceId);
+        putActions(deviceId, json);
+    }
+
+    @Override
+    public void putLight(String deviceId, boolean enabled) {
+        Light light = enabled ? Light.ENABLE : Light.DISABLE;
+        String json = "{\"light\":" + light.format() + "}";
+
+        logger.debug("Set light of Miele device {} to {}", deviceId, enabled);
+        putActions(deviceId, json);
+    }
+
+    @Override
+    public void putPowerState(String deviceId, boolean enabled) {
+        String action = enabled ? "powerOn" : "powerOff";
+        String json = "{\"" + action + "\":true}";
+
+        logger.debug("Set power state of Miele device {} to {}", deviceId, action);
+        putActions(deviceId, json);
+    }
+
+    @Override
+    public void putProgram(String deviceId, long programId) {
+        String json = "{\"programId\":" + programId + "}";
+
+        logger.debug("Activate program with ID {} of Miele device {}", programId, deviceId);
+        putActions(deviceId, json);
+    }
+
+    @Override
+    public void logout() {
+        Optional<String> accessToken = this.accessToken;
+        if (!accessToken.isPresent()) {
+            logger.debug("No access token present.");
+            return;
+        }
+
+        try {
+            logger.debug("Invalidating Miele webservice access token.");
+            Request request = requestFactory.createPostRequest(ENDPOINT_LOGOUT, accessToken.get());
+            this.accessToken = Optional.empty();
+            sendRequest(request);
+        } catch (MieleWebserviceTransientException e) {
+            throw new MieleWebserviceException("Transient error occurred during logout.", e, e.getConnectionError());
+        }
+    }
+
+    /**
+     * Sends the given request and wraps the possible exceptions in Miele exception types.
+     *
+     * @param request The {@link Request} to send.
+     * @return The obtained {@link ContentResponse}.
+     * @throws MieleWebserviceException if an irrecoverable error occurred.
+     * @throws MieleWebserviceTransientException if a recoverable error occurred.
+     */
+    private ContentResponse sendRequest(Request request) {
+        try {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Send {} request to Miele webservice on uri {}",
+                        Optional.ofNullable(request).map(Request::getMethod).orElse("null"),
+                        Optional.ofNullable(request).map(Request::getURI).map(URI::toString).orElse("null"));
+            }
+
+            ContentResponse response = request.send();
+            logger.debug("Received response with status code {}", response.getStatus());
+            return response;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new MieleWebserviceException("Interrupted.", e, ConnectionError.REQUEST_INTERRUPTED);
+        } catch (TimeoutException e) {
+            throw new MieleWebserviceTransientException("Request timed out.", e, ConnectionError.TIMEOUT);
+        } catch (ExecutionException e) {
+            throw new MieleWebserviceException("Request execution failed.", e,
+                    ConnectionError.REQUEST_EXECUTION_FAILED);
+        }
+    }
+
+    /**
+     * Gets all available device actions.
+     *
+     * @param deviceId The unique device ID.
+     *
+     * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+     * @throws MieleWebserviceTransientException if an error occurs during webservice requests or content parsing that
+     *             is recoverable by retrying the operation.
+     * @throws AuthorizationFailedException if the authorization against the webservice failed.
+     * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+     */
+    private Actions getActions(String deviceId) {
+        Optional<String> accessToken = this.accessToken;
+        if (!accessToken.isPresent()) {
+            throw new MieleWebserviceException("Missing access token.", ConnectionError.AUTHORIZATION_FAILED);
+        }
+
+        try {
+            logger.debug("Fetch action state description for Miele device {}", deviceId);
+            Request request = requestFactory.createGetRequest(String.format(ENDPOINT_ACTIONS, deviceId),
+                    accessToken.get());
+            ContentResponse response = sendRequest(request);
+            HttpUtil.checkHttpSuccess(response);
+            Actions actions = GSON.fromJson(response.getContentAsString(), Actions.class);
+            if (actions == null) {
+                throw new MieleWebserviceTransientException("Failed to parse response message.",
+                        ConnectionError.RESPONSE_MALFORMED);
+            }
+            return actions;
+        } catch (JsonSyntaxException e) {
+            throw new MieleWebserviceTransientException("Failed to parse response message.", e,
+                    ConnectionError.RESPONSE_MALFORMED);
+        }
+    }
+
+    /**
+     * Performs a PUT request to the actions endpoint for the specified device.
+     *
+     * @param deviceId The ID of the device to PUT for.
+     * @param json The Json body to send with the request.
+     * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+     * @throws MieleWebserviceTransientException if an error occurs during webservice requests or content parsing that
+     *             is recoverable by retrying the operation.
+     * @throws AuthorizationFailedException if the authorization against the webservice failed.
+     * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+     */
+    private void putActions(String deviceId, String json) {
+        retryStrategy.performRetryableOperation(() -> {
+            Optional<String> accessToken = this.accessToken;
+            if (!accessToken.isPresent()) {
+                throw new MieleWebserviceException("Missing access token.", ConnectionError.AUTHORIZATION_FAILED);
+            }
+
+            Request request = requestFactory.createPutRequest(String.format(ENDPOINT_ACTIONS, deviceId),
+                    accessToken.get(), json);
+            ContentResponse response = sendRequest(request);
+            HttpUtil.checkHttpSuccess(response);
+        }, e -> {
+            logger.warn("Failed to perform PUT request: {}. Retrying...", e.getMessage());
+        });
+    }
+
+    @Override
+    public void dispatchDeviceState(String deviceIdentifier) {
+        deviceStateDispatcher.dispatchDeviceState(deviceIdentifier);
+    }
+
+    @Override
+    public void addDeviceStateListener(DeviceStateListener listener) {
+        deviceStateDispatcher.addListener(listener);
+    }
+
+    @Override
+    public void removeDeviceStateListener(DeviceStateListener listener) {
+        deviceStateDispatcher.removeListener(listener);
+    }
+
+    @Override
+    public void addConnectionStatusListener(ConnectionStatusListener listener) {
+        connectionStatusListeners.add(listener);
+    }
+
+    @Override
+    public void removeConnectionStatusListener(ConnectionStatusListener listener) {
+        connectionStatusListeners.remove(listener);
+    }
+
+    @Override
+    public void close() throws Exception {
+        requestFactory.close();
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebserviceFactory.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebserviceFactory.java
new file mode 100644 (file)
index 0000000..57e3fb1
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Factory creating {@link DefaultMieleWebservice} instances.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class DefaultMieleWebserviceFactory implements MieleWebserviceFactory {
+    @Override
+    public MieleWebservice create(MieleWebserviceConfiguration configuration) {
+        return new DefaultMieleWebservice(configuration);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceCache.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceCache.java
new file mode 100644 (file)
index 0000000..d0ac8cd
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+
+/**
+ * A cache for {@link Device} objects associated with unique identifiers.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+class DeviceCache {
+    private final Map<String, Device> entries = new HashMap<>();
+
+    public void replaceAllDevices(DeviceCollection deviceCollection) {
+        clear();
+        deviceCollection.getDeviceIdentifiers().stream().forEach(i -> entries.put(i, deviceCollection.getDevice(i)));
+    }
+
+    public void clear() {
+        entries.clear();
+    }
+
+    public Set<String> getDeviceIds() {
+        return entries.keySet();
+    }
+
+    public Optional<Device> getDevice(String deviceIdentifier) {
+        return Optional.ofNullable(entries.get(deviceIdentifier));
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateDispatcher.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateDispatcher.java
new file mode 100644 (file)
index 0000000..02950bc
--- /dev/null
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles event dispatching to {@link DeviceStateListener}s.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceStateDispatcher {
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final List<DeviceStateListener> listeners = new CopyOnWriteArrayList<>();
+    private Set<String> previousDeviceIdentifiers = new HashSet<>();
+    private final DeviceCache cache = new DeviceCache();
+
+    /**
+     * Adds a listener. The listener will be immediately invoked with the current status of all known devices.
+     *
+     * @param listener The listener to add.
+     */
+    public void addListener(DeviceStateListener listener) {
+        if (listeners.contains(listener)) {
+            logger.warn("Listener '{}' was registered multiple times.", listener);
+        }
+        listeners.add(listener);
+
+        cache.getDeviceIds().forEach(deviceIdentifier -> cache.getDevice(deviceIdentifier)
+                .ifPresent(device -> listener.onDeviceStateUpdated(new DeviceState(deviceIdentifier, device))));
+    }
+
+    /**
+     * Removes a listener.
+     */
+    public void removeListener(DeviceStateListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Clears the internal device state cache.
+     */
+    public void clearCache() {
+        cache.clear();
+    }
+
+    /**
+     * Dispatches device status updates to all registered {@link DeviceStateListener}. This includes device removal.
+     *
+     * @param devices {@link DeviceCollection} which contains the state information to dispatch.
+     */
+    public void dispatchDeviceStateUpdates(DeviceCollection devices) {
+        cache.replaceAllDevices(devices);
+        dispatchDevicesRemoved(devices);
+        cache.getDeviceIds().forEach(this::dispatchDeviceState);
+    }
+
+    /**
+     * Dispatches the cached state of the device identified by the given device identifier.
+     */
+    public void dispatchDeviceState(String deviceIdentifier) {
+        cache.getDevice(deviceIdentifier).ifPresent(device -> listeners
+                .forEach(listener -> listener.onDeviceStateUpdated(new DeviceState(deviceIdentifier, device))));
+    }
+
+    /**
+     * Dispatches device action updates to all registered {@link DeviceStateListener}.
+     *
+     * @param deviceId ID of the device to dispatch the {@link Actions} for.
+     * @param actions {@link Actions} to dispatch.
+     */
+    public void dispatchActionStateUpdates(String deviceId, Actions actions) {
+        listeners.forEach(listener -> listener.onProcessActionUpdated(new ActionsState(deviceId, actions)));
+    }
+
+    private void dispatchDevicesRemoved(DeviceCollection devices) {
+        Set<String> presentDeviceIdentifiers = devices.getDeviceIdentifiers();
+        Set<String> removedDeviceIdentifiers = previousDeviceIdentifiers;
+        removedDeviceIdentifiers.removeAll(presentDeviceIdentifiers);
+
+        previousDeviceIdentifiers = devices.getDeviceIdentifiers();
+
+        removedDeviceIdentifiers
+                .forEach(deviceIdentifier -> listeners.forEach(listener -> listener.onDeviceRemoved(deviceIdentifier)));
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateListener.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateListener.java
new file mode 100644 (file)
index 0000000..b7400c6
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+
+/**
+ * Listener for the device states.
+ *
+ * @author Björn Lange and Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public interface DeviceStateListener {
+    /**
+     * Invoked when new status information is available for a device.
+     *
+     * @param deviceState The device state information.
+     */
+    void onDeviceStateUpdated(DeviceState deviceState);
+
+    /**
+     * Invoked when a new process action is available for a device.
+     *
+     * @param ActionsState The action state information.
+     */
+    void onProcessActionUpdated(ActionsState actionState);
+
+    /**
+     * Invoked when a device got removed from the Miele cloud and no information is available about it.
+     *
+     * @param deviceIdentifier The identifier of the removed device.
+     */
+    void onDeviceRemoved(String deviceIdentifier);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/HttpUtil.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/HttpUtil.java
new file mode 100644 (file)
index 0000000..e8ccae3
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Response;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ErrorMessage;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * Holds utility functions for working with HTTP.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class HttpUtil {
+    private static final String RETRY_AFTER_HEADER_FIELD_NAME = "Retry-After";
+
+    private HttpUtil() {
+        throw new IllegalStateException("This class must not be instantiated");
+    }
+
+    /**
+     * Checks whether the HTTP status given in {@code response} is a success state. In case an error state is obtained,
+     * exceptions are thrown.
+     *
+     * @param response The response to check.
+     * @throws MieleWebserviceTransientException if the status indicates a transient HTTP error.
+     * @throws MieleWebserviceException if the status indicates another HTTP error.
+     * @throws AuthorizationFailedException if the status indicates an authorization failure.
+     * @throws TooManyRequestsException if the status indicates that too many requests have been made against the remote
+     *             endpoint.
+     */
+    public static void checkHttpSuccess(Response response) {
+        if (isHttpSuccessStatus(response.getStatus())) {
+            return;
+        }
+
+        String exceptionMessage = getHttpErrorMessageFromCloudResponse(response);
+
+        switch (response.getStatus()) {
+            case 401:
+                throw new AuthorizationFailedException(exceptionMessage);
+            case 429:
+                String retryAfter = null;
+                if (response.getHeaders().containsKey(RETRY_AFTER_HEADER_FIELD_NAME)) {
+                    retryAfter = response.getHeaders().get(RETRY_AFTER_HEADER_FIELD_NAME);
+                }
+                throw new TooManyRequestsException(exceptionMessage, retryAfter);
+            case 500:
+                throw new MieleWebserviceTransientException(exceptionMessage, ConnectionError.SERVER_ERROR);
+            case 503:
+                throw new MieleWebserviceTransientException(exceptionMessage, ConnectionError.SERVICE_UNAVAILABLE);
+            default:
+                throw new MieleWebserviceException(exceptionMessage, ConnectionError.OTHER_HTTP_ERROR);
+        }
+    }
+
+    /**
+     * Gets whether {@code httpStatus} is a HTTP error code from the 200 range (success).
+     */
+    private static boolean isHttpSuccessStatus(int httpStatus) {
+        return httpStatus / 100 == 2;
+    }
+
+    private static String getHttpErrorMessageFromCloudResponse(Response response) {
+        String exceptionMessage = "HTTP error " + response.getStatus() + ": " + response.getReason();
+
+        if (response instanceof ContentResponse) {
+            try {
+                ErrorMessage errorMessage = ErrorMessage.fromJson(((ContentResponse) response).getContentAsString());
+                exceptionMessage += "\nCloud returned message: " + errorMessage.getMessage();
+            } catch (MieleSyntaxException e) {
+                exceptionMessage += "\nCloud returned invalid message.";
+            }
+        }
+        return exceptionMessage;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebservice.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebservice.java
new file mode 100644 (file)
index 0000000..a25027b
--- /dev/null
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * The {@link MieleWebservice} serves as an interface to the Miele REST API and wraps all calls to it.
+ *
+ * @author Björn Lange and Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public interface MieleWebservice extends AutoCloseable {
+    /**
+     * Sets the OAuth2 access token to use.
+     */
+    void setAccessToken(String accessToken);
+
+    /**
+     * Returns whether an access token is available.
+     */
+    boolean hasAccessToken();
+
+    /**
+     * Connects to the Miele webservice SSE endpoint and starts receiving events.
+     */
+    void connectSse();
+
+    /**
+     * Disconnects a running connection from the Miele SSE endpoint.
+     */
+    void disconnectSse();
+
+    /**
+     * Fetches the available actions for the device with the given {@code deviceId}.
+     *
+     * @param deviceId The unique ID of the device to fetch the available actions for.
+     * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+     * @throws AuthorizationFailedException if the authorization against the webservice failed.
+     * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+     */
+    void fetchActions(String deviceId);
+
+    /**
+     * Performs a PUT operation with the given {@code processAction}.
+     *
+     * @param deviceId ID of the device to trigger the action for.
+     * @param processAction The action to perform.
+     * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+     * @throws AuthorizationFailedException if the authorization against the webservice failed.
+     * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+     */
+    void putProcessAction(String deviceId, ProcessAction processAction);
+
+    /**
+     * Performs a PUT operation enabling or disabling the device's light.
+     *
+     * @param deviceId ID of the device to trigger the action for.
+     * @param enabled {@code true} to enable or {@code false} to disable the light.
+     * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+     * @throws AuthorizationFailedException if the authorization against the webservice failed.
+     * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+     */
+    void putLight(String deviceId, boolean enabled);
+
+    /**
+     * Performs a PUT operation switching the device on or off.
+     *
+     * @param deviceId ID of the device to trigger the action for.
+     * @param enabled {@code true} to switch on or {@code false} to switch off the device.
+     * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+     * @throws AuthorizationFailedException if the authorization against the webservice failed.
+     * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+     */
+    void putPowerState(String deviceId, boolean enabled);
+
+    /**
+     * Performs a PUT operation setting the active program.
+     *
+     * @param deviceId ID of the device to trigger the action for.
+     * @param program The program to activate.
+     * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+     * @throws AuthorizationFailedException if the authorization against the webservice failed.
+     * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+     */
+    void putProgram(String deviceId, long programId);
+
+    /**
+     * Performs a logout and invalidates the current OAuth2 token. This operation is assumed to work on the first try
+     * and is never retried. HTTP errors are ignored.
+     *
+     * @throws MieleWebserviceException if the request operation fails.
+     */
+    void logout();
+
+    /**
+     * Dispatches the cached state of the device identified by the given device identifier.
+     */
+    void dispatchDeviceState(String deviceIdentifier);
+
+    /**
+     * Adds a {@link DeviceStateListener}.
+     *
+     * @param listener The listener to add.
+     */
+    void addDeviceStateListener(DeviceStateListener listener);
+
+    /**
+     * Removes a {@link DeviceStateListener}.
+     *
+     * @param listener The listener to remove.
+     */
+    void removeDeviceStateListener(DeviceStateListener listener);
+
+    /**
+     * Adds a {@link ConnectionStatusListener}.
+     *
+     * @param listener The listener to add.
+     */
+    void addConnectionStatusListener(ConnectionStatusListener listener);
+
+    /**
+     * Removes a {@link ConnectionStatusListener}.
+     *
+     * @param listener The listener to remove.
+     */
+    void removeConnectionStatusListener(ConnectionStatusListener listener);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebserviceConfiguration.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebserviceConfiguration.java
new file mode 100644 (file)
index 0000000..08b23d3
--- /dev/null
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Represents a webservice configuration.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MieleWebserviceConfiguration {
+    private final HttpClientFactory httpClientFactory;
+    private final LanguageProvider languageProvider;
+    private final OAuthTokenRefresher tokenRefresher;
+    private final String serviceHandle;
+    private final ScheduledExecutorService scheduler;
+
+    private MieleWebserviceConfiguration(MieleWebserviceConfigurationBuilder builder) {
+        this.httpClientFactory = getOrThrow(builder.httpClientFactory, "httpClientFactory");
+        this.languageProvider = getOrThrow(builder.languageProvider, "languageProvider");
+        this.tokenRefresher = getOrThrow(builder.tokenRefresher, "tokenRefresher");
+        this.serviceHandle = getOrThrow(builder.serviceHandle, "serviceHandle");
+        this.scheduler = getOrThrow(builder.scheduler, "scheduler");
+    }
+
+    private static <T> T getOrThrow(@Nullable T object, String objectName) {
+        if (object == null) {
+            throw new IllegalArgumentException(objectName + " must not be null");
+        }
+        return object;
+    }
+
+    /**
+     * Gets the factory to use for HttpClient construction.
+     */
+    public HttpClientFactory getHttpClientFactory() {
+        return httpClientFactory;
+    }
+
+    /**
+     * Gets the provider for the language to use when making requests to the API.
+     */
+    public LanguageProvider getLanguageProvider() {
+        return languageProvider;
+    }
+
+    /**
+     * Gets the refresher for OAuth tokens.
+     */
+    public OAuthTokenRefresher getTokenRefresher() {
+        return tokenRefresher;
+    }
+
+    /**
+     * Gets the handle referring to the OAuth tokens in the framework's persistent storage.
+     */
+    public String getServiceHandle() {
+        return serviceHandle;
+    }
+
+    /**
+     * Gets the system wide scheduler.
+     */
+    public ScheduledExecutorService getScheduler() {
+        return scheduler;
+    }
+
+    public static MieleWebserviceConfigurationBuilder builder() {
+        return new MieleWebserviceConfigurationBuilder();
+    }
+
+    public static final class MieleWebserviceConfigurationBuilder {
+        @Nullable
+        private HttpClientFactory httpClientFactory;
+        @Nullable
+        private LanguageProvider languageProvider;
+        @Nullable
+        private OAuthTokenRefresher tokenRefresher;
+        @Nullable
+        private String serviceHandle;
+        @Nullable
+        private ScheduledExecutorService scheduler;
+
+        private MieleWebserviceConfigurationBuilder() {
+        }
+
+        public MieleWebserviceConfigurationBuilder withHttpClientFactory(HttpClientFactory httpClientFactory) {
+            this.httpClientFactory = httpClientFactory;
+            return this;
+        }
+
+        public MieleWebserviceConfigurationBuilder withLanguageProvider(LanguageProvider languageProvider) {
+            this.languageProvider = languageProvider;
+            return this;
+        }
+
+        public MieleWebserviceConfigurationBuilder withTokenRefresher(OAuthTokenRefresher tokenRefresher) {
+            this.tokenRefresher = tokenRefresher;
+            return this;
+        }
+
+        public MieleWebserviceConfigurationBuilder withServiceHandle(String serviceHandle) {
+            this.serviceHandle = serviceHandle;
+            return this;
+        }
+
+        public MieleWebserviceConfigurationBuilder withScheduler(ScheduledExecutorService scheduler) {
+            this.scheduler = scheduler;
+            return this;
+        }
+
+        public MieleWebserviceConfiguration build() {
+            return new MieleWebserviceConfiguration(this);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebserviceFactory.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/MieleWebserviceFactory.java
new file mode 100644 (file)
index 0000000..576c3d8
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Factory for creating {@link MieleWebservice} instances.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface MieleWebserviceFactory {
+    /**
+     * Creates a new {@link MieleWebservice}.
+     *
+     * @param configuration The configuration holding all required parameters to construct the instance.
+     * @return A new {@link MieleWebservice}.
+     */
+    public MieleWebservice create(MieleWebserviceConfiguration configuration);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/UnavailableMieleWebservice.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/UnavailableMieleWebservice.java
new file mode 100644 (file)
index 0000000..71d7df3
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of {@link MieleWebservice} that serves as a replacement when no webservice is available.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class UnavailableMieleWebservice implements MieleWebservice {
+    public static final UnavailableMieleWebservice INSTANCE = new UnavailableMieleWebservice();
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private UnavailableMieleWebservice() {
+    }
+
+    @Override
+    public void setAccessToken(String accessToken) {
+        logger.warn("Cannot set access token: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public boolean hasAccessToken() {
+        logger.warn("There is no access token: The Miele cloud service is not available.");
+        return false;
+    }
+
+    @Override
+    public void connectSse() {
+        logger.warn("Cannot connect to SSE stream: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public void disconnectSse() {
+        logger.warn("Cannot disconnect from SSE stream: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public void fetchActions(String deviceId) {
+        logger.warn("Cannot fetch actions for device '{}': The Miele cloud service is not available.", deviceId);
+    }
+
+    @Override
+    public void putProcessAction(String deviceId, ProcessAction processAction) {
+        logger.warn("Cannot perform '{}' operation for device '{}': The Miele cloud service is not available.",
+                processAction, deviceId);
+    }
+
+    @Override
+    public void putLight(String deviceId, boolean enabled) {
+        logger.warn("Cannot set light state to '{}' for device '{}': The Miele cloud service is not available.",
+                enabled ? "ON" : "OFF", deviceId);
+    }
+
+    @Override
+    public void putPowerState(String deviceId, boolean enabled) {
+        logger.warn("Cannot set power state to '{}' for device '{}': The Miele cloud service is not available.",
+                enabled ? "ON" : "OFF", deviceId);
+    }
+
+    @Override
+    public void putProgram(String deviceId, long programId) {
+        logger.warn("Cannot activate program with ID '{}' for device '{}': The Miele cloud service is not available.",
+                programId, deviceId);
+    }
+
+    @Override
+    public void logout() {
+        logger.warn("Cannot logout: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public void dispatchDeviceState(String deviceIdentifier) {
+        logger.warn("Cannot re-emit device state for device '{}': The Miele cloud service is not available.",
+                deviceIdentifier);
+    }
+
+    @Override
+    public void addDeviceStateListener(DeviceStateListener listener) {
+        logger.warn("Cannot add listener for all devices: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public void removeDeviceStateListener(DeviceStateListener listener) {
+        logger.warn("Cannot remove listener: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public void addConnectionStatusListener(ConnectionStatusListener listener) {
+        logger.warn("Cannot add connection error listener: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public void removeConnectionStatusListener(ConnectionStatusListener listener) {
+        logger.warn("Cannot remove listener: The Miele cloud service is not available.");
+    }
+
+    @Override
+    public void close() throws Exception {
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/ActionsState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/ActionsState.java
new file mode 100644 (file)
index 0000000..e29be3c
--- /dev/null
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+
+/**
+ * Provides convenient access to the list of actions that can be performed with a device.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class ActionsState {
+
+    private final String deviceIdentifier;
+    private final Optional<Actions> actions;
+
+    public ActionsState(String deviceIdentifier, @Nullable Actions actions) {
+        this.deviceIdentifier = deviceIdentifier;
+        this.actions = Optional.ofNullable(actions);
+    }
+
+    /**
+     * Gets the unique identifier of the device to which this state refers.
+     */
+    public String getDeviceIdentifier() {
+        return deviceIdentifier;
+    }
+
+    /**
+     * Gets whether the device can be started.
+     */
+    public boolean canBeStarted() {
+        return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START)).orElse(false);
+    }
+
+    /**
+     * Gets whether the device can be stopped.
+     */
+    public boolean canBeStopped() {
+        return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP)).orElse(false);
+    }
+
+    /**
+     * Gets whether the device can be paused.
+     */
+    public boolean canBePaused() {
+        return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.PAUSE)).orElse(false);
+    }
+
+    /**
+     * Gets whether supercooling can be controlled.
+     */
+    public boolean canContolSupercooling() {
+        return canStartSupercooling() || canStopSupercooling();
+    }
+
+    /**
+     * Gets whether supercooling can be started.
+     */
+    public boolean canStartSupercooling() {
+        return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START_SUPERCOOLING))
+                .orElse(false);
+    }
+
+    /**
+     * Gets whether supercooling can be stopped.
+     */
+    public boolean canStopSupercooling() {
+        return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP_SUPERCOOLING))
+                .orElse(false);
+    }
+
+    /**
+     * Gets whether superfreezing can be controlled.
+     */
+    public boolean canControlSuperfreezing() {
+        return canStartSuperfreezing() || canStopSuperfreezing();
+    }
+
+    /**
+     * Gets whether superfreezing can be started.
+     */
+    public boolean canStartSuperfreezing() {
+        return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START_SUPERFREEZING))
+                .orElse(false);
+    }
+
+    /**
+     * Gets whether superfreezing can be stopped.
+     */
+    public boolean canStopSuperfreezing() {
+        return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP_SUPERFREEZING))
+                .orElse(false);
+    }
+
+    /**
+     * Gets whether light can be enabled.
+     */
+    public boolean canEnableLight() {
+        return actions.map(Actions::getLight).map(a -> a.contains(Light.ENABLE)).orElse(false);
+    }
+
+    /**
+     * Gets whether light can be disabled.
+     */
+    public boolean canDisableLight() {
+        return actions.map(Actions::getLight).map(a -> a.contains(Light.DISABLE)).orElse(false);
+    }
+
+    /**
+     * Gets whether the device can be switched on.
+     */
+    public boolean canBeSwitchedOn() {
+        return actions.flatMap(Actions::getPowerOn).map(Boolean.TRUE::equals).orElse(false);
+    }
+
+    /**
+     * Gets whether the device can be switched off.
+     */
+    public boolean canBeSwitchedOff() {
+        return actions.flatMap(Actions::getPowerOff).map(Boolean.TRUE::equals).orElse(false);
+    }
+
+    /**
+     * Gets whether the light can be controlled.
+     */
+    public boolean canControlLight() {
+        return canEnableLight() || canDisableLight();
+    }
+
+    /**
+     * Gets whether the active program can be set.
+     */
+    public boolean canSetActiveProgramId() {
+        return !actions.map(Actions::getProgramId).map(List::isEmpty).orElse(true);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(actions, deviceIdentifier);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ActionsState other = (ActionsState) obj;
+        return Objects.equals(actions, other.actions) && Objects.equals(deviceIdentifier, other.deviceIdentifier);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/CoolingDeviceTemperatureState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/CoolingDeviceTemperatureState.java
new file mode 100644 (file)
index 0000000..2e22ac2
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Provides easy access to temperature values mapped for cooling devices.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CoolingDeviceTemperatureState {
+    private final DeviceState deviceState;
+
+    public CoolingDeviceTemperatureState(DeviceState deviceState) {
+        this.deviceState = deviceState;
+    }
+
+    /**
+     * Gets the current temperature of the fridge part of the device.
+     *
+     * @return The current temperature of the fridge part of the device.
+     */
+    public Optional<Integer> getFridgeTemperature() {
+        switch (deviceState.getRawType()) {
+            case FRIDGE:
+                return deviceState.getTemperature(0);
+
+            case FRIDGE_FREEZER_COMBINATION:
+                return deviceState.getTemperature(0);
+
+            default:
+                return Optional.empty();
+        }
+    }
+
+    /**
+     * Gets the target temperature of the fridge part of the device.
+     *
+     * @return The target temperature of the fridge part of the device.
+     */
+    public Optional<Integer> getFridgeTargetTemperature() {
+        switch (deviceState.getRawType()) {
+            case FRIDGE:
+                return deviceState.getTargetTemperature(0);
+
+            case FRIDGE_FREEZER_COMBINATION:
+                return deviceState.getTargetTemperature(0);
+
+            default:
+                return Optional.empty();
+        }
+    }
+
+    /**
+     * Gets the current temperature of the freezer part of the device.
+     *
+     * @return The current temperature of the freezer part of the device.
+     */
+    public Optional<Integer> getFreezerTemperature() {
+        switch (deviceState.getRawType()) {
+            case FREEZER:
+                return deviceState.getTemperature(0);
+
+            case FRIDGE_FREEZER_COMBINATION:
+                return deviceState.getTemperature(1);
+
+            default:
+                return Optional.empty();
+        }
+    }
+
+    /**
+     * Gets the target temperature of the freezer part of the device.
+     *
+     * @return The target temperature of the freezer part of the device.
+     */
+    public Optional<Integer> getFreezerTargetTemperature() {
+        switch (deviceState.getRawType()) {
+            case FREEZER:
+                return deviceState.getTargetTemperature(0);
+
+            case FRIDGE_FREEZER_COMBINATION:
+                return deviceState.getTargetTemperature(1);
+
+            default:
+                return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/DeviceState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/DeviceState.java
new file mode 100644 (file)
index 0000000..c4ab6d2
--- /dev/null
@@ -0,0 +1,558 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DryingStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.PlateStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramId;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramPhase;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.RemoteEnable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.SpinningSpeed;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.State;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Status;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Temperature;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.VentilationStep;
+
+/**
+ * This immutable class provides methods to extract the device state information in a comfortable way.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Introduced null handling
+ * @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm, info state channel and map signal
+ *         flags from API
+ * @author Björn Lange - Add elapsed time channel, dish warmer and robotic vacuum cleaner things
+ */
+@NonNullByDefault
+public class DeviceState {
+
+    private final String deviceIdentifier;
+
+    private final Optional<Device> device;
+
+    public DeviceState(String deviceIdentifier, @Nullable Device device) {
+        this.deviceIdentifier = deviceIdentifier;
+        this.device = Optional.ofNullable(device);
+    }
+
+    /**
+     * Gets the unique identifier for this device.
+     *
+     * @return The unique identifier for this device.
+     */
+    public String getDeviceIdentifier() {
+        return deviceIdentifier;
+    }
+
+    /**
+     * Gets the main operation status of the device.
+     *
+     * @return The main operation status of the device.
+     */
+    public Optional<String> getStatus() {
+        return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueLocalized);
+    }
+
+    /**
+     * Gets the raw main operation status of the device.
+     *
+     * @return The raw main operation status of the device.
+     */
+    public Optional<Integer> getStatusRaw() {
+        return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw);
+    }
+
+    /**
+     * Gets the raw operation status of the device parsed to a {@link StateType}.
+     *
+     * @return The raw operation status of the device parsed to a {@link StateType}.
+     */
+    public Optional<StateType> getStateType() {
+        return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw)
+                .flatMap(StateType::fromCode);
+    }
+
+    /**
+     * Gets the currently selected program type of the device.
+     *
+     * @return The currently selected program type of the device.
+     */
+    public Optional<String> getSelectedProgram() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueLocalized);
+    }
+
+    /**
+     * Gets the selected program ID.
+     *
+     * @return The selected program ID.
+     */
+    public Optional<Long> getSelectedProgramId() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueRaw);
+    }
+
+    /**
+     * Gets the currently active phase of the active program.
+     *
+     * @return The currently active phase of the active program.
+     */
+    public Optional<String> getProgramPhase() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getProgramPhase)
+                .flatMap(ProgramPhase::getValueLocalized);
+    }
+
+    /**
+     * Gets the currently active raw phase of the active program.
+     *
+     * @return The currently active raw phase of the active program.
+     */
+    public Optional<Integer> getProgramPhaseRaw() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getProgramPhase).flatMap(ProgramPhase::getValueRaw);
+    }
+
+    /**
+     * Gets the currently selected drying step.
+     *
+     * @return The currently selected drying step.
+     */
+    public Optional<String> getDryingTarget() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueLocalized);
+    }
+
+    /**
+     * Gets the currently selected raw drying step.
+     *
+     * @return The currently selected raw drying step.
+     */
+    public Optional<Integer> getDryingTargetRaw() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueRaw);
+    }
+
+    /**
+     * Calculates if pre-heating the oven has finished.
+     *
+     * @return Whether pre-heating the oven has finished.
+     */
+    public Optional<Boolean> hasPreHeatFinished() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+
+        Optional<Integer> targetTemperature = getTargetTemperature(0);
+        Optional<Integer> currentTemperature = getTemperature(0);
+
+        if (!targetTemperature.isPresent() || !currentTemperature.isPresent()) {
+            return Optional.empty();
+        }
+
+        return Optional.of(isInState(StateType.RUNNING) && currentTemperature.get() >= targetTemperature.get());
+    }
+
+    /**
+     * Gets the target temperature with the given index.
+     *
+     * @return The target temperature with the given index.
+     */
+    public Optional<Integer> getTargetTemperature(int index) {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).map(State::getTargetTemperature).flatMap(l -> getOrNull(l, index))
+                .flatMap(Temperature::getValueLocalized);
+    }
+
+    /**
+     * Gets the current temperature of the device for the given index.
+     *
+     * @param index The index of the device zone for which the temperature shall be obtained.
+     * @return The target temperature if available.
+     */
+    public Optional<Integer> getTemperature(int index) {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+
+        return device.flatMap(Device::getState).map(State::getTemperature).flatMap(l -> getOrNull(l, index))
+                .flatMap(Temperature::getValueLocalized);
+    }
+
+    /**
+     * Gets the remaining time of the active program.
+     *
+     * @return The remaining time in seconds.
+     */
+    public Optional<Integer> getRemainingTime() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getRemainingTime).flatMap(this::toSeconds);
+    }
+
+    /**
+     * Gets the elapsed time of the active program.
+     *
+     * @return The elapsed time in seconds.
+     */
+    public Optional<Integer> getElapsedTime() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getElapsedTime).flatMap(this::toSeconds);
+    }
+
+    /**
+     * Gets the relative start time of the active program.
+     *
+     * @return The delayed start time in seconds.
+     */
+    public Optional<Integer> getStartTime() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getStartTime).flatMap(this::toSeconds);
+    }
+
+    /**
+     * Gets the "fullRemoteControl" state information of the device. If this flag is true ALL remote control actions
+     * of the device can be triggered.
+     *
+     * @return Whether the device can be remote controlled.
+     */
+    public Optional<Boolean> isRemoteControlEnabled() {
+        return device.flatMap(Device::getState).flatMap(State::getRemoteEnable)
+                .flatMap(RemoteEnable::getFullRemoteControl);
+    }
+
+    /**
+     * Calculates the program process.
+     *
+     * @return The progress of the active program in percent.
+     */
+    public Optional<Integer> getProgress() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+
+        Optional<Double> elapsedTime = device.flatMap(Device::getState).flatMap(State::getElapsedTime)
+                .flatMap(this::toSeconds).map(Integer::doubleValue);
+        Optional<Double> remainingTime = device.flatMap(Device::getState).flatMap(State::getRemainingTime)
+                .flatMap(this::toSeconds).map(Integer::doubleValue);
+
+        if (elapsedTime.isPresent() && remainingTime.isPresent()
+                && (elapsedTime.get() != 0 || remainingTime.get() != 0)) {
+            return Optional.of((int) ((elapsedTime.get() / (elapsedTime.get() + remainingTime.get())) * 100.0));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    private Optional<Integer> toSeconds(List<Integer> time) {
+        if (time.size() != 2) {
+            return Optional.empty();
+        }
+        return Optional.of((time.get(0) * 60 + time.get(1)) * 60);
+    }
+
+    /**
+     * Gets the spinning speed.
+     *
+     * @return The spinning speed.
+     */
+    public Optional<String> getSpinningSpeed() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw)
+                .map(String::valueOf);
+    }
+
+    /**
+     * Gets the raw spinning speed.
+     *
+     * @return The raw spinning speed.
+     */
+    public Optional<Integer> getSpinningSpeedRaw() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw);
+    }
+
+    /**
+     * Gets the ventilation step.
+     *
+     * @return The ventilation step.
+     */
+    public Optional<String> getVentilationStep() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
+                .flatMap(VentilationStep::getValueLocalized).map(Object::toString);
+    }
+
+    /**
+     * Gets the raw ventilation step.
+     *
+     * @return The raw ventilation step.
+     */
+    public Optional<Integer> getVentilationStepRaw() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
+                .flatMap(VentilationStep::getValueRaw);
+    }
+
+    /**
+     * Gets the plate power step of the device for the given index.
+     *
+     * @param index The index of the device plate for which the power step shall be obtained.
+     * @return The plate power step if available.
+     */
+    public Optional<String> getPlateStep(int index) {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
+                .flatMap(PlateStep::getValueLocalized);
+    }
+
+    /**
+     * Gets the raw plate power step of the device for the given index.
+     *
+     * @param index The index of the device plate for which the power step shall be obtained.
+     * @return The raw plate power step if available.
+     */
+    public Optional<Integer> getPlateStepRaw(int index) {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+        return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
+                .flatMap(PlateStep::getValueRaw);
+    }
+
+    /**
+     * Gets the number of available plate steps.
+     *
+     * @return The number of available plate steps.
+     */
+    public Optional<Integer> getPlateStepCount() {
+        return device.flatMap(Device::getState).map(State::getPlateStep).map(List::size);
+    }
+
+    /**
+     * Indicates if the device has an error that requires a user action.
+     *
+     * @return Whether the device has an error that requires a user action.
+     */
+    public boolean hasError() {
+        return isInState(StateType.FAILURE)
+                || device.flatMap(Device::getState).flatMap(State::getSignalFailure).orElse(false);
+    }
+
+    /**
+     * Indicates if the device has a user information.
+     *
+     * @return Whether the device has a user information.
+     */
+    public boolean hasInfo() {
+        if (deviceIsInOffState()) {
+            return false;
+        }
+        return device.flatMap(Device::getState).flatMap(State::getSignalInfo).orElse(false);
+    }
+
+    /**
+     * Gets the state of the light attached to the device.
+     *
+     * @return An {@link Optional} with value {@code true} if the light is turned on, {@code false} if the light is
+     *         turned off or an empty {@link Optional} if light is not supported or no state is available.
+     */
+    public Optional<Boolean> getLightState() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+
+        Optional<Light> light = device.flatMap(Device::getState).map(State::getLight);
+        if (light.isPresent()) {
+            if (light.get().equals(Light.ENABLE)) {
+                return Optional.of(true);
+            } else if (light.get().equals(Light.DISABLE)) {
+                return Optional.of(false);
+            }
+        }
+
+        return Optional.empty();
+    }
+
+    /**
+     * Gets the state of the door attached to the device.
+     *
+     * @return Whether the device door is open.
+     */
+    public Optional<Boolean> getDoorState() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+
+        return device.flatMap(Device::getState).flatMap(State::getSignalDoor);
+    }
+
+    /**
+     * Gets the state of the device's door alarm.
+     *
+     * @return Whether the device door alarm was triggered.
+     */
+    public Optional<Boolean> getDoorAlarm() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+
+        Optional<Boolean> doorState = getDoorState();
+        Optional<Boolean> failure = device.flatMap(Device::getState).flatMap(State::getSignalFailure);
+
+        if (!doorState.isPresent() || !failure.isPresent()) {
+            return Optional.empty();
+        }
+
+        return Optional.of(doorState.get() && failure.get());
+    }
+
+    /**
+     * Gets the battery level.
+     *
+     * @return The battery level.
+     */
+    public Optional<Integer> getBatteryLevel() {
+        if (deviceIsInOffState()) {
+            return Optional.empty();
+        }
+
+        return device.flatMap(Device::getState).flatMap(State::getBatteryLevel);
+    }
+
+    /**
+     * Gets the device type.
+     *
+     * @return The device type as human readable value.
+     */
+    public Optional<String> getType() {
+        return device.flatMap(Device::getIdent).flatMap(Ident::getType).flatMap(Type::getValueLocalized)
+                .filter(type -> !type.isEmpty());
+    }
+
+    /**
+     * Gets the raw device type.
+     *
+     * @return The raw device type.
+     */
+    public DeviceType getRawType() {
+        return device.flatMap(Device::getIdent).flatMap(Ident::getType).map(Type::getValueRaw)
+                .orElse(DeviceType.UNKNOWN);
+    }
+
+    /**
+     * Gets the user-defined name of the device.
+     *
+     * @return The user-defined name of the device.
+     */
+    public Optional<String> getDeviceName() {
+        return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceName).filter(name -> !name.isEmpty());
+    }
+
+    /**
+     * Gets the fabrication (=serial) number of the device.
+     *
+     * @return The serial number of the device.
+     */
+    public Optional<String> getFabNumber() {
+        return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
+                .flatMap(DeviceIdentLabel::getFabNumber).filter(fabNumber -> !fabNumber.isEmpty());
+    }
+
+    /**
+     * Gets the tech type of the device.
+     *
+     * @return The tech type of the device.
+     */
+    public Optional<String> getTechType() {
+        return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
+                .flatMap(DeviceIdentLabel::getTechType).filter(techType -> !techType.isEmpty());
+    }
+
+    private <T> Optional<T> getOrNull(List<T> list, int index) {
+        if (index < 0 || index >= list.size()) {
+            return Optional.empty();
+        }
+
+        return Optional.ofNullable(list.get(index));
+    }
+
+    private boolean deviceIsInOffState() {
+        return getStateType().map(StateType.OFF::equals).orElse(true);
+    }
+
+    public boolean isInState(StateType stateType) {
+        return getStateType().map(stateType::equals).orElse(false);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(device, deviceIdentifier);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        DeviceState other = (DeviceState) obj;
+        return Objects.equals(device, other.device) && Objects.equals(deviceIdentifier, other.deviceIdentifier);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/PowerStatus.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/PowerStatus.java
new file mode 100644 (file)
index 0000000..a68fce9
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Represents the power status of the device, i.e. whether it is powered on, off or in standby.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum PowerStatus {
+    POWER_ON("on"),
+    POWER_OFF("off"),
+    STANDBY("standby");
+
+    /**
+     * Corresponding state of the ChannelTypeDefinition
+     */
+    private String state;
+
+    PowerStatus(String value) {
+        this.state = value;
+    }
+
+    /**
+     * Checks whether the given value is the raw state represented by this enum instance.
+     */
+    public boolean matches(String passedValue) {
+        return state.equalsIgnoreCase(passedValue);
+    }
+
+    /**
+     * Gets the raw state.
+     */
+    public String getState() {
+        return state;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/ProgramStatus.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/ProgramStatus.java
new file mode 100644 (file)
index 0000000..bb73af4
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Represents the status of a program.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum ProgramStatus {
+    PROGRAM_STARTED("start"),
+    PROGRAM_STOPPED("stop"),
+    PROGRAM_PAUSED("pause");
+
+    /**
+     * Corresponding state of the ChannelTypeDefinition
+     */
+    private String state;
+
+    ProgramStatus(String value) {
+        this.state = value;
+    }
+
+    /**
+     * Checks whether the given value is the raw state represented by this enum instance.
+     */
+    public boolean matches(String passedValue) {
+        return state.equalsIgnoreCase(passedValue);
+    }
+
+    /**
+     * Gets the raw state.
+     */
+    public String getState() {
+        return state;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/TransitionState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/TransitionState.java
new file mode 100644 (file)
index 0000000..d2dd05c
--- /dev/null
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+
+/**
+ * This immutable class provides methods to extract the state information related to state transitions in a comfortable
+ * way.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TransitionState {
+    private final boolean remainingTimeWasSetInCurrentProgram;
+    private final Optional<DeviceState> previousState;
+    private final DeviceState nextState;
+
+    /**
+     * Creates a new {@link TransitionState}.
+     *
+     * Note: {@code previousState} <b>must not</b> be saved in a field in this class as this will create a linked list
+     * and cause memory issues. The constructor only serves the purpose of unpacking state that must be carried on.
+     *
+     * @param previousTransitionState The previous transition state if it exists.
+     * @param nextState The device state which the device is transitioning to.
+     */
+    public TransitionState(@Nullable TransitionState previousTransitionState, DeviceState nextState) {
+        this.remainingTimeWasSetInCurrentProgram = wasRemainingTimeSetInCurrentProgram(previousTransitionState,
+                nextState);
+        this.previousState = Optional.ofNullable(previousTransitionState).map(it -> it.nextState);
+        this.nextState = nextState;
+    }
+
+    /**
+     * Gets whether the finish state changed due to the transition form the previous to the current state.
+     *
+     * @return Whether the finish state changed due to the transition form the previous to the current state.
+     */
+    public boolean hasFinishedChanged() {
+        return previousState.map(this::hasFinishedChangedFromPreviousState).orElse(true);
+    }
+
+    private boolean hasFinishedChangedFromPreviousState(DeviceState previous) {
+        if (previous.getStateType().equals(nextState.getStateType())) {
+            return false;
+        }
+
+        if (isInRunningState(previous) && nextState.isInState(StateType.FAILURE)) {
+            return false;
+        }
+
+        if (isInRunningState(previous) != isInRunningState(nextState)) {
+            return true;
+        }
+
+        if (nextState.isInState(StateType.OFF)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Gets whether a program finished.
+     *
+     * @return Whether a program finished.
+     */
+    public Optional<Boolean> isFinished() {
+        return previousState.flatMap(this::hasFinishedFromPreviousState);
+    }
+
+    private Optional<Boolean> hasFinishedFromPreviousState(DeviceState prevState) {
+        if (!prevState.getStateType().isPresent()) {
+            return Optional.empty();
+        }
+
+        if (nextState.isInState(StateType.OFF)) {
+            return Optional.of(false);
+        }
+
+        if (nextState.isInState(StateType.FAILURE)) {
+            return Optional.of(false);
+        }
+
+        return Optional.of(!isInRunningState(nextState));
+    }
+
+    /**
+     * Gets the remaining time of the active program.
+     *
+     * Note: Tracking changes in the remaining time is a workaround for the Miele API not properly distinguishing
+     * between "there is no remaining time set" and "the remaining time is zero". If the remaining time is zero when a
+     * program is started then we assume that no timer was set / program with remaining time is active. This may be
+     * changed later by the user which is detected by the remaining time changing from 0 to some larger value.
+     *
+     * @return The remaining time in seconds.
+     */
+    public Optional<Integer> getRemainingTime() {
+        if (!remainingTimeWasSetInCurrentProgram && isInRunningState(nextState)) {
+            return nextState.getRemainingTime().filter(it -> it != 0);
+        } else {
+            return nextState.getRemainingTime();
+        }
+    }
+
+    /**
+     * Gets the program progress.
+     *
+     * @return The progress of the active program in percent.
+     */
+    public Optional<Integer> getProgress() {
+        if (getRemainingTime().isPresent()) {
+            return nextState.getProgress();
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    private static boolean wasRemainingTimeSetInCurrentProgram(@Nullable TransitionState previousTransitionState,
+            DeviceState nextState) {
+        if (previousTransitionState != null && isInRunningState(previousTransitionState.nextState)) {
+            return previousTransitionState.remainingTimeWasSetInCurrentProgram
+                    || previousTransitionState.getRemainingTime().isPresent();
+        } else {
+            return false;
+        }
+    }
+
+    private static boolean isInRunningState(DeviceState device) {
+        return device.isInState(StateType.RUNNING) || device.isInState(StateType.PAUSE);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/WineStorageDeviceTemperatureState.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/WineStorageDeviceTemperatureState.java
new file mode 100644 (file)
index 0000000..5b8915e
--- /dev/null
@@ -0,0 +1,206 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+
+/**
+ * Provides easy access to temperature values mapped for wine storage devices.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class WineStorageDeviceTemperatureState {
+    private static final Set<DeviceType> ALL_WINE_STORAGES = Set.of(DeviceType.WINE_CABINET,
+            DeviceType.WINE_CABINET_FREEZER_COMBINATION, DeviceType.WINE_CONDITIONING_UNIT,
+            DeviceType.WINE_STORAGE_CONDITIONING_UNIT);
+
+    private final DeviceState deviceState;
+    private final List<Integer> effectiveTemperatures;
+    private final List<Integer> effectiveTargetTemperatures;
+
+    /**
+     * Creates a new {@link WineStorageDeviceTemperatureState}.
+     *
+     * @param deviceState Device state to query extended state information from.
+     */
+    public WineStorageDeviceTemperatureState(DeviceState deviceState) {
+        this.deviceState = deviceState;
+        effectiveTemperatures = getEffectiveTemperatures();
+        effectiveTargetTemperatures = getEffectiveTargetTemperatures();
+    }
+
+    private List<Integer> getEffectiveTemperatures() {
+        return Arrays
+                .asList(deviceState.getTemperature(0), deviceState.getTemperature(1), deviceState.getTemperature(2))
+                .stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+    }
+
+    private List<Integer> getEffectiveTargetTemperatures() {
+        return Arrays
+                .asList(deviceState.getTargetTemperature(0), deviceState.getTargetTemperature(1),
+                        deviceState.getTargetTemperature(2))
+                .stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+    }
+
+    /**
+     * Gets the current main temperature of the wine storage.
+     *
+     * @return The current main temperature of the wine storage.
+     */
+    public Optional<Integer> getTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getTemperatureFromList(effectiveTemperatures);
+    }
+
+    /**
+     * Gets the target main temperature of the wine storage.
+     *
+     * @return The target main temperature of the wine storage.
+     */
+    public Optional<Integer> getTargetTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getTemperatureFromList(effectiveTargetTemperatures);
+    }
+
+    private Optional<Integer> getTemperatureFromList(List<Integer> temperatures) {
+        if (temperatures.isEmpty()) {
+            return Optional.empty();
+        }
+
+        if (temperatures.size() > 1) {
+            return Optional.empty();
+        }
+
+        return Optional.of(temperatures.get(0));
+    }
+
+    /**
+     * Gets the current top temperature of the wine storage.
+     *
+     * @return The current top temperature of the wine storage.
+     */
+    public Optional<Integer> getTopTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getTopTemperatureFromList(effectiveTemperatures);
+    }
+
+    /**
+     * Gets the target top temperature of the wine storage.
+     *
+     * @return The target top temperature of the wine storage.
+     */
+    public Optional<Integer> getTopTargetTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getTopTemperatureFromList(effectiveTargetTemperatures);
+    }
+
+    private Optional<Integer> getTopTemperatureFromList(List<Integer> temperatures) {
+        if (temperatures.size() <= 1) {
+            return Optional.empty();
+        }
+
+        return Optional.of(temperatures.get(0));
+    }
+
+    /**
+     * Gets the current middle temperature of the wine storage.
+     *
+     * @return The current middle temperature of the wine storage.
+     */
+    public Optional<Integer> getMiddleTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getMiddleTemperatureFromList(effectiveTemperatures);
+    }
+
+    /**
+     * Gets the target middle temperature of the wine storage.
+     *
+     * @return The target middle temperature of the wine storage.
+     */
+    public Optional<Integer> getMiddleTargetTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getMiddleTemperatureFromList(effectiveTargetTemperatures);
+    }
+
+    private Optional<Integer> getMiddleTemperatureFromList(List<Integer> temperatures) {
+        if (temperatures.size() != 3) {
+            return Optional.empty();
+        }
+
+        return Optional.of(temperatures.get(1));
+    }
+
+    /**
+     * Gets the current bottom temperature of the wine storage.
+     *
+     * @return The current bottom temperature of the wine storage.
+     */
+    public Optional<Integer> getBottomTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getBottomTemperatureFromList(effectiveTemperatures);
+    }
+
+    /**
+     * Gets the target bottom temperature of the wine storage.
+     *
+     * @return The target bottom temperature of the wine storage.
+     */
+    public Optional<Integer> getBottomTargetTemperature() {
+        if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+            return Optional.empty();
+        }
+
+        return getBottomTemperatureFromList(effectiveTargetTemperatures);
+    }
+
+    private Optional<Integer> getBottomTemperatureFromList(List<Integer> temperatures) {
+        if (temperatures.size() == 3) {
+            return Optional.of(temperatures.get(2));
+        }
+
+        if (temperatures.size() == 2) {
+            return Optional.of(temperatures.get(1));
+        }
+
+        return Optional.empty();
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Actions.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Actions.java
new file mode 100644 (file)
index 0000000..835b0fd
--- /dev/null
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the device actions queried from the Miele REST API.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class Actions {
+    @SerializedName("processAction")
+    @Nullable
+    private final List<ProcessAction> processAction = null;
+    @SerializedName("light")
+    @Nullable
+    private final List<Integer> light = null;
+    @SerializedName("startTime")
+    @Nullable
+    private final List<List<Integer>> startTime = null;
+    @SerializedName("programId")
+    @Nullable
+    private final List<Integer> programId = null;
+    @SerializedName("deviceName")
+    @Nullable
+    private String deviceName;
+    @SerializedName("powerOff")
+    @Nullable
+    private Boolean powerOff;
+    @SerializedName("powerOn")
+    @Nullable
+    private Boolean powerOn;
+
+    public List<ProcessAction> getProcessAction() {
+        if (processAction == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(processAction);
+    }
+
+    public List<Light> getLight() {
+        final List<Integer> lightRefCopy = light;
+        if (lightRefCopy == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(lightRefCopy.stream().map(Light::fromId).collect(Collectors.toList()));
+    }
+
+    /**
+     * Gets the start time encoded as {@link List} of {@link List} of {@link Integer} values.
+     * The first list entry defines the lower time constraint for setting the delayed start time. The second list
+     * entry defines the upper time constraint. The time constraints are defined as a list of integers with the full
+     * hour as first and minutes as second element.
+     *
+     * @return The possible start time interval encoded as described above.
+     */
+    public Optional<List<List<Integer>>> getStartTime() {
+        if (startTime == null) {
+            return Optional.empty();
+        }
+
+        return Optional.of(Collections.unmodifiableList(startTime));
+    }
+
+    public List<Integer> getProgramId() {
+        if (programId == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(programId);
+    }
+
+    public Optional<String> getDeviceName() {
+        return Optional.ofNullable(deviceName);
+    }
+
+    public Optional<Boolean> getPowerOn() {
+        return Optional.ofNullable(powerOn);
+    }
+
+    public Optional<Boolean> getPowerOff() {
+        return Optional.ofNullable(powerOff);
+    }
+
+    @Override
+    public String toString() {
+        return "ActionState [processAction=" + processAction + ", light=" + light + ", startTime=" + startTime
+                + ", programId=" + programId + ", deviceName=" + deviceName + ", powerOff=" + powerOff + ", powerOn="
+                + powerOn + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceName, light, powerOn, powerOff, processAction, startTime, programId);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Actions other = (Actions) obj;
+        return Objects.equals(deviceName, other.deviceName) && Objects.equals(light, other.light)
+                && Objects.equals(powerOn, other.powerOn) && Objects.equals(powerOff, other.powerOff)
+                && Objects.equals(processAction, other.processAction) && Objects.equals(startTime, other.startTime)
+                && Objects.equals(programId, other.programId);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Device.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Device.java
new file mode 100644 (file)
index 0000000..4fcf801
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing a device queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Device {
+    @Nullable
+    private Ident ident;
+    @Nullable
+    private State state;
+
+    public Optional<Ident> getIdent() {
+        return Optional.ofNullable(ident);
+    }
+
+    public Optional<State> getState() {
+        return Optional.ofNullable(state);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ident, state);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Device other = (Device) obj;
+        return Objects.equals(ident, other.ident) && Objects.equals(state, other.state);
+    }
+
+    @Override
+    public String toString() {
+        return "Device [ident=" + ident + ", state=" + state + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceCollection.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceCollection.java
new file mode 100644 (file)
index 0000000..da56e6d
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Immutable POJO representing a collection of devices queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceCollection {
+    private static final java.lang.reflect.Type STRING_DEVICE_MAP_TYPE = new TypeToken<Map<String, Device>>() {
+    }.getType();
+
+    private final Map<String, Device> devices;
+
+    DeviceCollection(Map<String, Device> devices) {
+        this.devices = devices;
+    }
+
+    /**
+     * Creates a new {@link DeviceCollection} from the given Json text.
+     *
+     * @param json The Json text.
+     * @return The created {@link DeviceCollection}.
+     * @throws MieleSyntaxException if parsing the data from {@code json} fails.
+     */
+    public static DeviceCollection fromJson(String json) {
+        try {
+            Map<String, Device> devices = new Gson().fromJson(json, STRING_DEVICE_MAP_TYPE);
+            if (devices == null) {
+                throw new MieleSyntaxException("Failed to parse Json.");
+            }
+            return new DeviceCollection(devices);
+        } catch (JsonSyntaxException e) {
+            throw new MieleSyntaxException("Failed to parse Json.", e);
+        }
+    }
+
+    public Set<String> getDeviceIdentifiers() {
+        return devices.keySet();
+    }
+
+    public Device getDevice(String identifier) {
+        Device device = devices.get(identifier);
+        if (device == null) {
+            throw new IllegalArgumentException("There is no device for identifier " + identifier);
+        }
+        return device;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(devices);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        DeviceCollection other = (DeviceCollection) obj;
+        return Objects.equals(devices, other.devices);
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceCollection [devices=" + devices + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceIdentLabel.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceIdentLabel.java
new file mode 100644 (file)
index 0000000..26857fe
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the full device identification queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceIdentLabel {
+    @Nullable
+    private String fabNumber;
+    @Nullable
+    private String fabIndex;
+    @Nullable
+    private String techType;
+    @Nullable
+    private String matNumber;
+    @Nullable
+    private final List<String> swids = null;
+
+    public Optional<String> getFabNumber() {
+        return Optional.ofNullable(fabNumber);
+    }
+
+    public Optional<String> getFabIndex() {
+        return Optional.ofNullable(fabIndex);
+    }
+
+    public Optional<String> getTechType() {
+        return Optional.ofNullable(techType);
+    }
+
+    public Optional<String> getMatNumber() {
+        return Optional.ofNullable(matNumber);
+    }
+
+    public List<String> getSwids() {
+        if (swids == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(swids);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fabIndex, fabNumber, matNumber, swids, techType);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        DeviceIdentLabel other = (DeviceIdentLabel) obj;
+        return Objects.equals(fabIndex, other.fabIndex) && Objects.equals(fabNumber, other.fabNumber)
+                && Objects.equals(matNumber, other.matNumber) && Objects.equals(swids, other.swids)
+                && Objects.equals(techType, other.techType);
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceIdentLabel [fabNumber=" + fabNumber + ", fabIndex=" + fabIndex + ", techType=" + techType
+                + ", matNumber=" + matNumber + ", swids=" + swids + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceType.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceType.java
new file mode 100644 (file)
index 0000000..d25f9ad
--- /dev/null
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents the Miele device type.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum DeviceType {
+    /**
+     * {@link DeviceType} for unknown devices.
+     */
+    UNKNOWN,
+
+    @SerializedName("1")
+    WASHING_MACHINE,
+
+    @SerializedName("2")
+    TUMBLE_DRYER,
+
+    @SerializedName("7")
+    DISHWASHER,
+
+    @SerializedName("8")
+    DISHWASHER_SEMI_PROF,
+
+    @SerializedName("12")
+    OVEN,
+
+    @SerializedName("13")
+    OVEN_MICROWAVE,
+
+    @SerializedName("14")
+    HOB_HIGHLIGHT,
+
+    @SerializedName("15")
+    STEAM_OVEN,
+
+    @SerializedName("16")
+    MICROWAVE,
+
+    @SerializedName("17")
+    COFFEE_SYSTEM,
+
+    @SerializedName("18")
+    HOOD,
+
+    @SerializedName("19")
+    FRIDGE,
+
+    @SerializedName("20")
+    FREEZER,
+
+    @SerializedName("21")
+    FRIDGE_FREEZER_COMBINATION,
+
+    /**
+     * Might also be AUTOMATIC ROBOTIC VACUUM CLEANER.
+     */
+    @SerializedName("23")
+    VACUUM_CLEANER,
+
+    @SerializedName("24")
+    WASHER_DRYER,
+
+    @SerializedName("25")
+    DISH_WARMER,
+
+    @SerializedName("27")
+    HOB_INDUCTION,
+
+    @SerializedName("28")
+    HOB_GAS,
+
+    @SerializedName("31")
+    STEAM_OVEN_COMBINATION,
+
+    @SerializedName("32")
+    WINE_CABINET,
+
+    @SerializedName("33")
+    WINE_CONDITIONING_UNIT,
+
+    @SerializedName("34")
+    WINE_STORAGE_CONDITIONING_UNIT,
+
+    @SerializedName("39")
+    DOUBLE_OVEN,
+
+    @SerializedName("40")
+    DOUBLE_STEAM_OVEN,
+
+    @SerializedName("41")
+    DOUBLE_STEAM_OVEN_COMBINATION,
+
+    @SerializedName("42")
+    DOUBLE_MICROWAVE,
+
+    @SerializedName("43")
+    DOUBLE_MICROWAVE_OVEN,
+
+    @SerializedName("45")
+    STEAM_OVEN_MICROWAVE_COMBINATION,
+
+    @SerializedName("48")
+    VACUUM_DRAWER,
+
+    @SerializedName("67")
+    DIALOGOVEN,
+
+    @SerializedName("68")
+    WINE_CABINET_FREEZER_COMBINATION,
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DryingStep.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DryingStep.java
new file mode 100644 (file)
index 0000000..e7c55bb
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current drying step, queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DryingStep {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        DryingStep other = (DryingStep) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "DryingStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+                + keyLocalized + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ErrorMessage.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ErrorMessage.java
new file mode 100644 (file)
index 0000000..c72bb80
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Immutable POJO representing an error message. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ErrorMessage {
+    @Nullable
+    private String message;
+
+    /**
+     * Creates a new {@link ErrorMessage} from the given Json text.
+     *
+     * @param json The Json text.
+     * @return The created {@link ErrorMessage}.
+     * @throws MieleSyntaxException if parsing the data from {@code json} fails.
+     */
+    public static ErrorMessage fromJson(String json) {
+        try {
+            ErrorMessage errorMessage = new Gson().fromJson(json, ErrorMessage.class);
+            if (errorMessage == null) {
+                throw new MieleSyntaxException("Failed to parse Json.");
+            }
+            return errorMessage;
+        } catch (JsonSyntaxException e) {
+            throw new MieleSyntaxException("Failed to parse Json.", e);
+        }
+    }
+
+    public Optional<String> getMessage() {
+        return Optional.ofNullable(message);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(message);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ErrorMessage other = (ErrorMessage) obj;
+        return Objects.equals(message, other.message);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Ident.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Ident.java
new file mode 100644 (file)
index 0000000..dbbc763
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the device identification queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Ident {
+    @Nullable
+    private Type type;
+    @Nullable
+    private String deviceName;
+    @Nullable
+    private DeviceIdentLabel deviceIdentLabel;
+    @Nullable
+    private XkmIdentLabel xkmIdentLabel;
+
+    public Optional<Type> getType() {
+        return Optional.ofNullable(type);
+    }
+
+    public Optional<String> getDeviceName() {
+        return Optional.ofNullable(deviceName);
+    }
+
+    public Optional<DeviceIdentLabel> getDeviceIdentLabel() {
+        return Optional.ofNullable(deviceIdentLabel);
+    }
+
+    public Optional<XkmIdentLabel> getXkmIdentLabel() {
+        return Optional.ofNullable(xkmIdentLabel);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceIdentLabel, deviceName, type, xkmIdentLabel);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Ident other = (Ident) obj;
+        return Objects.equals(deviceIdentLabel, other.deviceIdentLabel) && Objects.equals(deviceName, other.deviceName)
+                && Objects.equals(type, other.type) && Objects.equals(xkmIdentLabel, other.xkmIdentLabel);
+    }
+
+    @Override
+    public String toString() {
+        return "Ident [type=" + type + ", deviceName=" + deviceName + ", deviceIdentLabel=" + deviceIdentLabel
+                + ", xkmIdentLabel=" + xkmIdentLabel + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Light.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Light.java
new file mode 100644 (file)
index 0000000..db91171
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Represents the state of a light on a Miele device.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Added NOT_SUPPORTED entry
+ */
+@NonNullByDefault
+public enum Light {
+    /**
+     * {Light} for unknown states.
+     */
+    UNKNOWN(),
+
+    ENABLE(1),
+
+    DISABLE(2),
+
+    NOT_SUPPORTED(0, 255);
+
+    private List<Integer> ids;
+
+    Light(int... ids) {
+        this.ids = Collections.unmodifiableList(Arrays.stream(ids).boxed().collect(Collectors.toList()));
+    }
+
+    /**
+     * Gets the {@link Light} state matching the given ID.
+     * 
+     * @param id The ID.
+     * @return The matching {@link Light} or {@code UNKNOWN} if no ID matches.
+     */
+    public static Light fromId(@Nullable Integer id) {
+        for (Light light : Light.values()) {
+            if (light.ids.contains(id)) {
+                return light;
+            }
+        }
+
+        return Light.UNKNOWN;
+    }
+
+    /**
+     * Formats this instance for interaction with the Miele webservice.
+     */
+    public String format() {
+        if (ids.isEmpty()) {
+            return "";
+        } else {
+            return Integer.toString(ids.get(0));
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/MieleSyntaxException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/MieleSyntaxException.java
new file mode 100644 (file)
index 0000000..b6f563e
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link RuntimeException} thrown when the syntax of a message received from the Miele REST API does not match and
+ * cannot be interpreted as the expected syntax (e.g. by ignoring entries).
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleSyntaxException extends RuntimeException {
+    private static final long serialVersionUID = 8253804935427566729L;
+
+    public MieleSyntaxException(String message) {
+        super(message);
+    }
+
+    public MieleSyntaxException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/PlateStep.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/PlateStep.java
new file mode 100644 (file)
index 0000000..0392661
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing a plate power state. Queried from the Miele REST API.
+ *
+ * @author Benjamin Bolte - Initial contribution
+ */
+@NonNullByDefault
+public class PlateStep {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        PlateStep other = (PlateStep) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "PlateStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", key_localized="
+                + keyLocalized + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProcessAction.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProcessAction.java
new file mode 100644 (file)
index 0000000..80c08bf
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents a process action.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public enum ProcessAction {
+    /**
+     * {@StateType} for unknown states.
+     */
+    UNKNOWN,
+
+    @SerializedName("1")
+    START,
+
+    @SerializedName("2")
+    STOP,
+
+    @SerializedName("3")
+    PAUSE,
+
+    @SerializedName("4")
+    START_SUPERFREEZING,
+
+    @SerializedName("5")
+    STOP_SUPERFREEZING,
+
+    @SerializedName("6")
+    START_SUPERCOOLING,
+
+    @SerializedName("7")
+    STOP_SUPERCOOLING,
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramId.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramId.java
new file mode 100644 (file)
index 0000000..b9d665d
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the program type that is currently running. Queried from the Miele REST API.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class ProgramId {
+    @SerializedName("value_raw")
+    @Nullable
+    private Long valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+
+    public Optional<Long> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ProgramId other = (ProgramId) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "ProgramType [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+                + keyLocalized + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramPhase.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramPhase.java
new file mode 100644 (file)
index 0000000..1b3d931
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current program's phase. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ProgramPhase {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ProgramPhase other = (ProgramPhase) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "ProgramPhase [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+                + keyLocalized + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramType.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ProgramType.java
new file mode 100644 (file)
index 0000000..2dac3fd
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the type of program currently running. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ProgramType {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ProgramType other = (ProgramType) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "ProgramType [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+                + keyLocalized + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/RemoteEnable.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/RemoteEnable.java
new file mode 100644 (file)
index 0000000..ffa34fa
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the remote control capabilities of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RemoteEnable {
+    @Nullable
+    private Boolean fullRemoteControl;
+    @Nullable
+    private Boolean smartGrid;
+
+    public Optional<Boolean> getFullRemoteControl() {
+        return Optional.ofNullable(fullRemoteControl);
+    }
+
+    public Optional<Boolean> getSmartGrid() {
+        return Optional.ofNullable(smartGrid);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fullRemoteControl, smartGrid);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        RemoteEnable other = (RemoteEnable) obj;
+        return Objects.equals(fullRemoteControl, other.fullRemoteControl) && Objects.equals(smartGrid, other.smartGrid);
+    }
+
+    @Override
+    public String toString() {
+        return "RemoteEnable [fullRemoteControl=" + fullRemoteControl + ", smartGrid=" + smartGrid + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/SpinningSpeed.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/SpinningSpeed.java
new file mode 100644 (file)
index 0000000..bdaca12
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current spinning speed, queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class SpinningSpeed {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("unit")
+    @Nullable
+    private String unit;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getUnit() {
+        return Optional.ofNullable(unit);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(unit, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        SpinningSpeed other = (SpinningSpeed) obj;
+        return Objects.equals(unit, other.unit) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "SpinningSpeed [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", unit=" + unit + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/State.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/State.java
new file mode 100644 (file)
index 0000000..c9f6d11
--- /dev/null
@@ -0,0 +1,238 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the state of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add plate step
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class State {
+    @Nullable
+    private Status status;
+    /**
+     * Currently used by Miele webservice.
+     */
+    @Nullable
+    private ProgramId ProgramID;
+    /**
+     * Planned to be used in the future.
+     */
+    @Nullable
+    private ProgramId programId;
+    @Nullable
+    private ProgramType programType;
+    @Nullable
+    private ProgramPhase programPhase;
+    @Nullable
+    private final List<Integer> remainingTime = null;
+    @Nullable
+    private final List<Integer> startTime = null;
+    @Nullable
+    private final List<Temperature> targetTemperature = null;
+    @Nullable
+    private final List<Temperature> temperature = null;
+    @Nullable
+    private Boolean signalInfo;
+    @Nullable
+    private Boolean signalFailure;
+    @Nullable
+    private Boolean signalDoor;
+    @Nullable
+    private RemoteEnable remoteEnable;
+    @Nullable
+    private Integer light;
+    @Nullable
+    private final List<Integer> elapsedTime = null;
+    @Nullable
+    private SpinningSpeed spinningSpeed;
+    @Nullable
+    private DryingStep dryingStep;
+    @Nullable
+    private VentilationStep ventilationStep;
+    @Nullable
+    private final List<PlateStep> plateStep = null;
+    @Nullable
+    private Integer batteryLevel;
+
+    public Optional<Status> getStatus() {
+        return Optional.ofNullable(status);
+    }
+
+    public Optional<ProgramId> getProgramId() {
+        // There is a typo for the program ID in the Miele Cloud API, which will be corrected in the future.
+        // For the sake of robustness, we currently support both upper and lower case.
+        return Optional.ofNullable(programId != null ? programId : ProgramID);
+    }
+
+    public Optional<ProgramType> getProgramType() {
+        return Optional.ofNullable(programType);
+    }
+
+    public Optional<ProgramPhase> getProgramPhase() {
+        return Optional.ofNullable(programPhase);
+    }
+
+    /**
+     * Gets the remaining time encoded as {@link List} of {@link Integer} values.
+     *
+     * @return The remaining time encoded as {@link List} of {@link Integer} values.
+     */
+    public Optional<List<Integer>> getRemainingTime() {
+        if (remainingTime == null) {
+            return Optional.empty();
+        }
+
+        return Optional.ofNullable(Collections.unmodifiableList(remainingTime));
+    }
+
+    /**
+     * Gets the start time encoded as {@link List} of {@link Integer} values.
+     *
+     * @return The start time encoded as {@link List} of {@link Integer} values.
+     */
+    public Optional<List<Integer>> getStartTime() {
+        if (startTime == null) {
+            return Optional.empty();
+        }
+
+        return Optional.ofNullable(Collections.unmodifiableList(startTime));
+    }
+
+    public List<Temperature> getTargetTemperature() {
+        if (targetTemperature == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(targetTemperature);
+    }
+
+    public List<Temperature> getTemperature() {
+        if (temperature == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(temperature);
+    }
+
+    public Optional<Boolean> getSignalInfo() {
+        return Optional.ofNullable(signalInfo);
+    }
+
+    public Optional<Boolean> getSignalFailure() {
+        return Optional.ofNullable(signalFailure);
+    }
+
+    public Optional<Boolean> getSignalDoor() {
+        return Optional.ofNullable(signalDoor);
+    }
+
+    public Optional<RemoteEnable> getRemoteEnable() {
+        return Optional.ofNullable(remoteEnable);
+    }
+
+    public Light getLight() {
+        return Light.fromId(light);
+    }
+
+    /**
+     * Gets the elapsed time encoded as {@link List} of {@link Integer} values.
+     *
+     * @return The elapsed time encoded as {@link List} of {@link Integer} values.
+     */
+    public Optional<List<Integer>> getElapsedTime() {
+        if (elapsedTime == null) {
+            return Optional.empty();
+        }
+
+        return Optional.ofNullable(Collections.unmodifiableList(elapsedTime));
+    }
+
+    public Optional<SpinningSpeed> getSpinningSpeed() {
+        return Optional.ofNullable(spinningSpeed);
+    }
+
+    public Optional<DryingStep> getDryingStep() {
+        return Optional.ofNullable(dryingStep);
+    }
+
+    public Optional<VentilationStep> getVentilationStep() {
+        return Optional.ofNullable(ventilationStep);
+    }
+
+    public List<PlateStep> getPlateStep() {
+        if (plateStep == null) {
+            return Collections.emptyList();
+        }
+
+        return Collections.unmodifiableList(plateStep);
+    }
+
+    public Optional<Integer> getBatteryLevel() {
+        return Optional.ofNullable(batteryLevel);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(dryingStep, elapsedTime, light, programPhase, ProgramID, programId, programType,
+                remainingTime, remoteEnable, signalDoor, signalFailure, signalInfo, startTime, status,
+                targetTemperature, temperature, ventilationStep, plateStep, batteryLevel);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        State other = (State) obj;
+        return Objects.equals(dryingStep, other.dryingStep) && Objects.equals(elapsedTime, other.elapsedTime)
+                && Objects.equals(light, other.light) && Objects.equals(programPhase, other.programPhase)
+                && Objects.equals(ProgramID, other.ProgramID) && Objects.equals(programId, other.programId)
+                && Objects.equals(programType, other.programType) && Objects.equals(remainingTime, other.remainingTime)
+                && Objects.equals(remoteEnable, other.remoteEnable) && Objects.equals(signalDoor, other.signalDoor)
+                && Objects.equals(signalFailure, other.signalFailure) && Objects.equals(signalInfo, other.signalInfo)
+                && Objects.equals(startTime, other.startTime) && Objects.equals(status, other.status)
+                && Objects.equals(targetTemperature, other.targetTemperature)
+                && Objects.equals(temperature, other.temperature)
+                && Objects.equals(ventilationStep, other.ventilationStep) && Objects.equals(plateStep, other.plateStep)
+                && Objects.equals(batteryLevel, other.batteryLevel);
+    }
+
+    @Override
+    public String toString() {
+        return "State [status=" + status + ", programId=" + getProgramId() + ", programType=" + programType
+                + ", programPhase=" + programPhase + ", remainingTime=" + remainingTime + ", startTime=" + startTime
+                + ", targetTemperature=" + targetTemperature + ", temperature=" + temperature + ", signalInfo="
+                + signalInfo + ", signalFailure=" + signalFailure + ", signalDoor=" + signalDoor + ", remoteEnable="
+                + remoteEnable + ", light=" + light + ", elapsedTime=" + elapsedTime + ", dryingStep=" + dryingStep
+                + ", ventilationStep=" + ventilationStep + ", plateStep=" + plateStep + ", batteryLevel=" + batteryLevel
+                + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StateType.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StateType.java
new file mode 100644 (file)
index 0000000..a3058e0
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Represents the Miele device state.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public enum StateType {
+    OFF(1),
+    ON(2),
+    PROGRAMMED(3),
+    PROGRAMMED_WAITING_TO_START(4),
+    RUNNING(5),
+    PAUSE(6),
+    END_PROGRAMMED(7),
+    FAILURE(8),
+    PROGRAMME_INTERRUPTED(9),
+    IDLE(10),
+    RINSE_HOLD(11),
+    SERVICE(12),
+    SUPERFREEZING(13),
+    SUPERCOOLING(14),
+    SUPERHEATING(15),
+    SUPERCOOLING_SUPERFREEZING(146),
+    NOT_CONNECTED(255);
+
+    private static final Map<Integer, StateType> STATE_TYPE_BY_CODE;
+
+    static {
+        Map<Integer, StateType> stateTypeByCode = new HashMap<>();
+        for (StateType stateType : values()) {
+            stateTypeByCode.put(stateType.code, stateType);
+        }
+        STATE_TYPE_BY_CODE = Collections.unmodifiableMap(stateTypeByCode);
+    }
+
+    private final int code;
+
+    private StateType(int code) {
+        this.code = code;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public static Optional<StateType> fromCode(int code) {
+        return Optional.ofNullable(STATE_TYPE_BY_CODE.get(code));
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Status.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Status.java
new file mode 100644 (file)
index 0000000..f5c5c6e
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the actual status of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Status {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Status other = (Status) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "Status [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized=" + keyLocalized
+                + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Temperature.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Temperature.java
new file mode 100644 (file)
index 0000000..8ddae8f
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing a temperature value. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Temperature {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private Double valueLocalized;
+    @SerializedName("unit")
+    @Nullable
+    private String unit;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<Integer> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized).map(Double::intValue);
+    }
+
+    public Optional<String> getUnit() {
+        return Optional.ofNullable(unit);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(unit, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Temperature other = (Temperature) obj;
+        return Objects.equals(unit, other.unit) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "Temperature [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", unit=" + unit + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Type.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/Type.java
new file mode 100644 (file)
index 0000000..0ed6891
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the type of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Type {
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+    @SerializedName("value_raw")
+    @Nullable
+    private DeviceType valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    public DeviceType getValueRaw() {
+        return Optional.ofNullable(valueRaw).orElse(DeviceType.UNKNOWN);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        Type other = (Type) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && valueRaw == other.valueRaw;
+    }
+
+    @Override
+    public String toString() {
+        return "Type [keyLocalized=" + keyLocalized + ", valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized
+                + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/VentilationStep.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/VentilationStep.java
new file mode 100644 (file)
index 0000000..ea7a4b9
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current ventilation step. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class VentilationStep {
+    @SerializedName("value_raw")
+    @Nullable
+    private Integer valueRaw;
+    @SerializedName("value_localized")
+    @Nullable
+    private String valueLocalized;
+    @SerializedName("key_localized")
+    @Nullable
+    private String keyLocalized;
+
+    public Optional<Integer> getValueRaw() {
+        return Optional.ofNullable(valueRaw);
+    }
+
+    public Optional<String> getValueLocalized() {
+        return Optional.ofNullable(valueLocalized);
+    }
+
+    public Optional<String> getKeyLocalized() {
+        return Optional.ofNullable(keyLocalized);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        VentilationStep other = (VentilationStep) obj;
+        return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+                && Objects.equals(valueRaw, other.valueRaw);
+    }
+
+    @Override
+    public String toString() {
+        return "VentilationStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+                + keyLocalized + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/XkmIdentLabel.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/api/json/XkmIdentLabel.java
new file mode 100644 (file)
index 0000000..5b126ea
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the XKM (Miele communication module) identification. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class XkmIdentLabel {
+    @Nullable
+    private String techType;
+    @Nullable
+    private String releaseVersion;
+
+    public Optional<String> getTechType() {
+        return Optional.ofNullable(techType);
+    }
+
+    public Optional<String> getReleaseVersion() {
+        return Optional.ofNullable(releaseVersion);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(releaseVersion, techType);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        XkmIdentLabel other = (XkmIdentLabel) obj;
+        return Objects.equals(releaseVersion, other.releaseVersion) && Objects.equals(techType, other.techType);
+    }
+
+    @Override
+    public String toString() {
+        return "XkmIdentLabel [techType=" + techType + ", releaseVersion=" + releaseVersion + "]";
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/AuthorizationFailedException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/AuthorizationFailedException.java
new file mode 100644 (file)
index 0000000..f958ec1
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This {@link RuntimeException} is thrown if an error occurred due to authorization failure.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class AuthorizationFailedException extends RuntimeException {
+    private static final long serialVersionUID = 963609531804668970L;
+
+    public AuthorizationFailedException(final String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceDisconnectSseException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceDisconnectSseException.java
new file mode 100644 (file)
index 0000000..5d42428
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Used as a notification to close SSE connections.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceDisconnectSseException extends RuntimeException {
+    private static final long serialVersionUID = 607435177026345387L;
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceException.java
new file mode 100644 (file)
index 0000000..4523f6f
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+
+/**
+ * {@link RuntimeException} thrown if the Miele service is not available or unable to handle requests.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceException extends RuntimeException {
+
+    private static final long serialVersionUID = 6268725866086530042L;
+
+    private final ConnectionError connectionError;
+
+    public MieleWebserviceException(final String message, final ConnectionError connectionError) {
+        super(message);
+        this.connectionError = connectionError;
+    }
+
+    public MieleWebserviceException(final String message, @Nullable final Throwable cause,
+            final ConnectionError connectionError) {
+        super(message, cause);
+        this.connectionError = connectionError;
+    }
+
+    public ConnectionError getConnectionError() {
+        return connectionError;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceInitializationException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceInitializationException.java
new file mode 100644 (file)
index 0000000..c9de5ea
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when the Miele webservice fails to initialize.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceInitializationException extends RuntimeException {
+    private static final long serialVersionUID = -3778846331483843234L;
+
+    public MieleWebserviceInitializationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceTransientException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/MieleWebserviceTransientException.java
new file mode 100644 (file)
index 0000000..ce00575
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+
+/**
+ * {@link RuntimeException} thrown if a transient error occurred which the binding can recover from by retrying.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceTransientException extends RuntimeException {
+    private static final long serialVersionUID = -1863609233382694104L;
+
+    private final ConnectionError connectionError;
+
+    public MieleWebserviceTransientException(final String message, final ConnectionError connectionError) {
+        super(message);
+        this.connectionError = connectionError;
+    }
+
+    public MieleWebserviceTransientException(final String message, final Throwable cause,
+            final ConnectionError connectionError) {
+        super(message, cause);
+        this.connectionError = connectionError;
+    }
+
+    public ConnectionError getConnectionError() {
+        return connectionError;
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/TooManyRequestsException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/exception/TooManyRequestsException.java
new file mode 100644 (file)
index 0000000..afb8e7f
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link RuntimeException} indicating that too many requests have been made against the cloud service.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TooManyRequestsException extends RuntimeException {
+    private static final long serialVersionUID = 3393292912418862566L;
+
+    @Nullable
+    private final String retryAfter;
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    public TooManyRequestsException(String message, @Nullable String retryAfter) {
+        super(message);
+        this.retryAfter = retryAfter;
+    }
+
+    /**
+     * Gets whether a hint on when to retry the operation is available.
+     *
+     * @return Whether a hint on when to retry the operation is available.
+     */
+    public boolean hasRetryAfterHint() {
+        return retryAfter != null;
+    }
+
+    /**
+     * Gets the number of seconds until the operation may be retried.
+     *
+     * @return The number of seconds until the operation may be retried. This will return -1 if no Retry-After header
+     *         was present or parsing the data from the header fails.
+     */
+    public long getSecondsUntilRetry() {
+        String retryAfter = this.retryAfter;
+        if (retryAfter == null) {
+            logger.debug("Received no Retry-After header.");
+            return -1;
+        }
+
+        logger.debug("Received Retry-After header: {}", retryAfter);
+        try {
+            long seconds = Long.parseLong(retryAfter);
+            logger.debug("Interpreted Retry-After header value: {} seconds", seconds);
+            return seconds;
+        } catch (NumberFormatException e) {
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("ccc, d MMM yyyy HH:mm:ss z", Locale.US);
+
+            try {
+                LocalDateTime dateTime = LocalDateTime.parse(retryAfter, formatter);
+                logger.debug("Interpreted Retry-After header value: {}", dateTime);
+
+                Duration duration = Duration.between(LocalDateTime.now(), dateTime);
+
+                long seconds = Math.max(0, duration.toMillis() / 1000);
+                logger.debug("Interpreted Retry-After header value: {} seconds.", seconds);
+                return seconds;
+            } catch (DateTimeParseException dateTimeParseException) {
+                logger.warn("Unable to parse Retry-After header: {}", retryAfter);
+                return -1;
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/CombiningLanguageProvider.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/CombiningLanguageProvider.java
new file mode 100644 (file)
index 0000000..e470e0f
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link LanguageProvider} combining two {@link LanguageProvider}s, a prioritized and a fallback provider.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CombiningLanguageProvider implements LanguageProvider {
+    private @Nullable LanguageProvider prioritizedLanguageProvider;
+    private @Nullable LanguageProvider fallbackLanguageProvider;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param prioritizedLanguageProvider Primary {@link LanguageProvider} to use. May be {@code null}, in that case the
+     *            {@code fallbackLanguageProvider} will be used.
+     * @param fallbackLanguageProvider {@link LanguageProvider} to fall back to if the
+     *            {@code prioritizedLanguageProvider} is {@code null} or provides no language. May be
+     *            {@code null}, in case the fallback is used and returns no language then no language will be returned.
+     */
+    public CombiningLanguageProvider(@Nullable LanguageProvider prioritizedLanguageProvider,
+            @Nullable LanguageProvider fallbackLanguageProvider) {
+        this.prioritizedLanguageProvider = prioritizedLanguageProvider;
+        this.fallbackLanguageProvider = fallbackLanguageProvider;
+    }
+
+    public void setPrioritizedLanguageProvider(LanguageProvider prioritizedLanguageProvider) {
+        this.prioritizedLanguageProvider = prioritizedLanguageProvider;
+    }
+
+    public void unsetPrioritizedLanguageProvider() {
+        this.prioritizedLanguageProvider = null;
+    }
+
+    public void setFallbackLanguageProvider(LanguageProvider fallbackLanguageProvider) {
+        this.fallbackLanguageProvider = fallbackLanguageProvider;
+    }
+
+    public void unsetFallbackLanguageProvider() {
+        this.fallbackLanguageProvider = null;
+    }
+
+    @Override
+    public Optional<String> getLanguage() {
+        Optional<String> prioritizedLanguage = Optional.ofNullable(prioritizedLanguageProvider)
+                .flatMap(LanguageProvider::getLanguage);
+        if (prioritizedLanguage.isPresent()) {
+            return prioritizedLanguage;
+        } else {
+            return Optional.ofNullable(fallbackLanguageProvider).flatMap(LanguageProvider::getLanguage);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/JvmLanguageProvider.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/JvmLanguageProvider.java
new file mode 100644 (file)
index 0000000..c16c070
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Locale;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link LanguageProvider} returning the default JVM language.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class JvmLanguageProvider implements LanguageProvider {
+    @Override
+    public Optional<String> getLanguage() {
+        return Optional.ofNullable(Locale.getDefault()).map(Locale::getLanguage).filter(l -> !l.isEmpty());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/LanguageProvider.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/LanguageProvider.java
new file mode 100644 (file)
index 0000000..86e1259
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Interface for providing language code information.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface LanguageProvider {
+    /**
+     * Gets a language represented as 2-letter language code.
+     *
+     * @return The language represented as 2-letter language code.
+     */
+    Optional<String> getLanguage();
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/OpenHabLanguageProvider.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/language/OpenHabLanguageProvider.java
new file mode 100644 (file)
index 0000000..7c7436d
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.i18n.LocaleProvider;
+
+/**
+ * Language provider relying on the openHAB runtime to provide a locale which is converted to a language.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class OpenHabLanguageProvider implements LanguageProvider {
+    private final LocaleProvider localeProvider;
+
+    public OpenHabLanguageProvider(LocaleProvider localeProvider) {
+        this.localeProvider = localeProvider;
+    }
+
+    @Override
+    public Optional<String> getLanguage() {
+        return Optional.of(localeProvider.getLocale().getLanguage());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/request/RequestFactory.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/request/RequestFactory.java
new file mode 100644 (file)
index 0000000..f1c8d5e
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.request;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Request;
+
+/**
+ * Factory for {@link Request} objects.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface RequestFactory extends AutoCloseable {
+    /**
+     * Creates a GET {@link Request} for the given URL decorated with all required headers to interact with the Miele
+     * cloud.
+     *
+     * @param url The URL to GET.
+     * @param accessToken The OAuth2 access token for bearer authentication.
+     * @return The {@link Request}.
+     */
+    Request createGetRequest(String url, String accessToken);
+
+    /**
+     * Creates a PUT {@link Request} for the given URL decorated with all required headers to interact with the Miele
+     * cloud.
+     *
+     * @param url The URL to PUT.
+     * @param accessToken The OAuth2 access token for bearer authentication.
+     * @param jsonContent Json content to send in the body of the request.
+     * @return The {@link Request}.
+     */
+    Request createPutRequest(String url, String accessToken, String jsonContent);
+
+    /**
+     * Creates a POST {@link Request} for the given URL decorated with all required headers to interact with the Miele
+     * cloud.
+     *
+     * @param url The URL to POST.
+     * @param accessToken The OAuth2 access token for bearer authentication.
+     * @return The {@link Request}.
+     */
+    Request createPostRequest(String url, String accessToken);
+
+    /**
+     * Creates a GET request prepared for HTTP event stream data (also referred to as Server Sent Events, SSE).
+     *
+     * @param url The URL to subscribe to.
+     * @param accessToken The OAuth2 access token for bearer authentication.
+     * @return The {@link Request}.
+     */
+    Request createSseRequest(String url, String accessToken);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/request/RequestFactoryImpl.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/request/RequestFactoryImpl.java
new file mode 100644 (file)
index 0000000..2f557ec
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.request;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Default implementation of {@link RequestFactory}.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RequestFactoryImpl implements RequestFactory {
+    private static final long REQUEST_TIMEOUT = 5;
+    private static final long EXTENDED_REQUEST_TIMEOUT = 10;
+    private static final TimeUnit REQUEST_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+    private final HttpClient httpClient;
+    private final LanguageProvider languageProvider;
+
+    /**
+     * Creates a new {@link RequestFactoryImpl}.
+     *
+     * @param httpClientFactory Factory for obtaining a {@link HttpClient}.
+     * @param languageProvider Provider for the language to use for new requests.
+     * @throws MieleWebserviceInitializationException if creating and starting a new {@link HttpClient} fails.
+     */
+    public RequestFactoryImpl(HttpClientFactory httpClientFactory, LanguageProvider languageProvider) {
+        this.httpClient = httpClientFactory.createHttpClient("mielecloud");
+        try {
+            this.httpClient.start();
+        } catch (Exception e) {
+            throw new MieleWebserviceInitializationException("Failed to start HttpClient", e);
+        }
+        this.languageProvider = languageProvider;
+    }
+
+    private Request createRequestWithDefaultHeaders(String url, String accessToken) {
+        return httpClient.newRequest(url).header("Content-type", "application/json").header("Authorization",
+                "Bearer " + accessToken);
+    }
+
+    private Request decorateWithLanguageParameter(Request request) {
+        Optional<String> language = languageProvider.getLanguage();
+        if (language.isPresent() && !language.get().isEmpty()) {
+            return request.param("language", language.get());
+        } else {
+            return request;
+        }
+    }
+
+    private Request decorateWithAcceptLanguageHeader(Request request) {
+        Optional<String> language = languageProvider.getLanguage();
+        if (language.isPresent() && !language.get().isEmpty()) {
+            return request.header("Accept-Language", language.get());
+        } else {
+            return request;
+        }
+    }
+
+    private Request createDefaultHttpRequest(String url, String accessToken, long timeout) {
+        return decorateWithLanguageParameter(createRequestWithDefaultHeaders(url, accessToken)).header("Accept", "*/*")
+                .timeout(timeout, REQUEST_TIMEOUT_UNIT);
+    }
+
+    @Override
+    public Request createGetRequest(String url, String accessToken) {
+        return createDefaultHttpRequest(url, accessToken, REQUEST_TIMEOUT).method(HttpMethod.GET);
+    }
+
+    @Override
+    public Request createPutRequest(String url, String accessToken, String jsonContent) {
+        return createDefaultHttpRequest(url, accessToken, EXTENDED_REQUEST_TIMEOUT).method(HttpMethod.PUT)
+                .content(new StringContentProvider("application/json", jsonContent, StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public Request createPostRequest(String url, String accessToken) {
+        return createDefaultHttpRequest(url, accessToken, REQUEST_TIMEOUT).method(HttpMethod.POST);
+    }
+
+    @Override
+    public Request createSseRequest(String url, String accessToken) {
+        return decorateWithAcceptLanguageHeader(createRequestWithDefaultHeaders(url, accessToken)).header("Accept",
+                "text/event-stream");
+    }
+
+    @Override
+    public void close() throws Exception {
+        httpClient.stop();
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/AuthorizationFailedRetryStrategy.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/AuthorizationFailedRetryStrategy.java
new file mode 100644 (file)
index 0000000..aa56865
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AuthorizationFailedRetryStrategy} retries an operation after refreshing the access token in case of an
+ * authorization failure.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class AuthorizationFailedRetryStrategy implements RetryStrategy {
+    /**
+     * Message of exception thrown by the Jetty client in case of unmatching header fields and body content. E.g.
+     * application/json header with HTML body content. Mostly thrown when an invalid 401 response is received.
+     */
+    public static final String JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE = "org.eclipse.jetty.client.HttpResponseException: HTTP protocol violation: Authentication challenge without WWW-Authenticate header";
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final OAuthTokenRefresher tokenRefresher;
+    private final String serviceHandle;
+
+    public AuthorizationFailedRetryStrategy(OAuthTokenRefresher tokenRefresher, String serviceHandle) {
+        this.tokenRefresher = tokenRefresher;
+        this.serviceHandle = serviceHandle;
+    }
+
+    private void refreshToken() {
+        try {
+            logger.debug("Refreshing Miele OAuth access token.");
+            tokenRefresher.refreshToken(serviceHandle);
+            logger.debug("Miele OAuth access token has successfully been refreshed.");
+        } catch (OAuthException e) {
+            throw new MieleWebserviceException("Failed to refresh access token.", e,
+                    ConnectionError.AUTHORIZATION_FAILED);
+        }
+    }
+
+    @Override
+    public <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException) {
+        try {
+            return operation.get();
+        } catch (AuthorizationFailedException e) {
+            onException.accept(e);
+            refreshToken();
+        } catch (MieleWebserviceException e) {
+            // Workaround for HTML response from cloud in case of a 401 HTTP error.
+            var cause = e.getCause();
+            if (cause == null || !(cause instanceof ExecutionException)) {
+                throw e;
+            }
+
+            if (!JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE.equals(cause.getMessage())) {
+                throw e;
+            }
+
+            onException.accept(e);
+            refreshToken();
+        }
+
+        try {
+            return operation.get();
+        } catch (AuthorizationFailedException e) {
+            throw new MieleWebserviceException("Request failed after access token renewal.", e,
+                    ConnectionError.AUTHORIZATION_FAILED);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/NTimesRetryStrategy.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/NTimesRetryStrategy.java
new file mode 100644 (file)
index 0000000..9e957c8
--- /dev/null
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+
+/**
+ * {@link RetryStrategy} retrying a failing operation for a number of times.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class NTimesRetryStrategy implements RetryStrategy {
+    private final int numberOfRetries;
+
+    /**
+     * Creates a new {@link NTimesRetryStrategy}.
+     *
+     * @param numberOfRetries The number of retries to make.
+     * @throws IllegalArgumentException if {@code numberOfRetries} is smaller than zero.
+     */
+    public NTimesRetryStrategy(int numberOfRetries) {
+        if (numberOfRetries < 0) {
+            throw new IllegalArgumentException("Number of retries must not be negative.");
+        }
+
+        this.numberOfRetries = numberOfRetries;
+    }
+
+    @Override
+    public <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException) {
+        boolean obtainedReturnValue = false;
+        T returnValue = null;
+        MieleWebserviceTransientException lastException = null;
+        for (int i = 0; !obtainedReturnValue && i < numberOfRetries + 1; i++) {
+            try {
+                returnValue = operation.get();
+                obtainedReturnValue = true;
+            } catch (MieleWebserviceTransientException e) {
+                lastException = e;
+                if (i < numberOfRetries) {
+                    onException.accept(e);
+                }
+            }
+        }
+
+        if (!obtainedReturnValue) {
+            throw new MieleWebserviceException(
+                    "Unable to perform operation. Operation failed " + (numberOfRetries + 1) + " times.", lastException,
+                    lastException == null ? ConnectionError.UNKNOWN : lastException.getConnectionError());
+        } else {
+            return returnValue;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategy.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategy.java
new file mode 100644 (file)
index 0000000..3aae0dc
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Interface for strategies implementing the retry behavior of requests against the Miele cloud.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface RetryStrategy {
+    /**
+     * Performs an operation which may be retried several times.
+     *
+     * If retrying fails or a critical error occurred, this method may throw {@link Exception}s of any type.
+     *
+     * @param operation The operation to perform. To signal that an error can be resolved by retrying this operation it
+     *            should throw an {@link Exception}. Whether the operation is retried is up to the {@link RetryStrategy}
+     *            implementation.
+     * @param onException Handler to invoke when an {@link Exception} is handled by retrying the {@code operation}. This
+     *            handler should at least log a message. It must not throw any exception.
+     * @return The object returned by {@code operation} if it completed successfully.
+     */
+    <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException);
+
+    /**
+     * Performs an operation which may be retried several times.
+     *
+     * If retrying fails or a critical error occurred, this method may throw {@link Exception}s of any type.
+     *
+     * @param operation The operation to perform. To signal that an error can be resolved by retrying this operation it
+     *            should throw an {@link Exception}. Whether the operation is retried is up to the {@link RetryStrategy}
+     *            implementation
+     * @param onException Handler to invoke when an {@link Exception} is handled by retrying the {@code operation}. This
+     *            handler should at least log a message. It may not throw any exception.
+     */
+    default void performRetryableOperation(Runnable operation, Consumer<Exception> onException) {
+        performRetryableOperation(new Supplier<@Nullable Void>() {
+            @Override
+            public @Nullable Void get() {
+                operation.run();
+                return null;
+            }
+        }, onException);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategyCombiner.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategyCombiner.java
new file mode 100644 (file)
index 0000000..374e11b
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link RetryStrategy} implementation wrapping the consecutive execution of two retry strategies.
+ *
+ * @author Björn Lange and Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class RetryStrategyCombiner implements RetryStrategy {
+    private final RetryStrategy first;
+    private final RetryStrategy second;
+
+    /**
+     * Creates a new {@link RetryStrategy} combining the given ones.
+     *
+     * @param first First strategy to execute.
+     * @param second Strategy to execute in each execution of {@code first}.
+     */
+    public RetryStrategyCombiner(RetryStrategy first, RetryStrategy second) {
+        this.first = first;
+        this.second = second;
+    }
+
+    @Override
+    public <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException) {
+        return first.performRetryableOperation(() -> second.performRetryableOperation(operation, onException),
+                onException);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/BackoffStrategy.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/BackoffStrategy.java
new file mode 100644 (file)
index 0000000..dace1fe
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A strategy computing the wait time between multiple connection attempts.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+interface BackoffStrategy {
+    /**
+     * Gets the minimal number of seconds to wait until retrying an operation. This is the lower bound of the value
+     * returned by {@link #getSecondsUntilRetry(int)}.
+     *
+     * @return The minimal number of seconds to wait until retrying an operation. Always larger or equal to zero, always
+     *         smaller than {@link #getMaximumSecondsUntilRetry()}.
+     */
+    long getMinimumSecondsUntilRetry();
+
+    /**
+     * Gets the maximal number of seconds to wait until retrying an operation. This is the upper bound of the value
+     * returned by {@link #getSecondsUntilRetry(int)}.
+     *
+     * @return The maximal number of seconds to wait until retrying an operation. Always larger or equal to zero, always
+     *         larger than {@link #getMinimumSecondsUntilRetry()}.
+     */
+    long getMaximumSecondsUntilRetry();
+
+    /**
+     * Gets the number of seconds until a retryable operation is performed. The value returned by this method is within
+     * the interval defined by {@link #getMinimumSecondsUntilRetry()} and {@link #getMaximumSecondsUntilRetry()}.
+     *
+     * @param failedConnectionAttempts The number of failed attempts.
+     * @return The number of seconds to wait before making the next attempt.
+     */
+    long getSecondsUntilRetry(int failedAttempts);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/ExponentialBackoffWithJitter.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/ExponentialBackoffWithJitter.java
new file mode 100644 (file)
index 0000000..200ff4b
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.util.Random;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements the exponential backoff with jitter backoff strategy.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+class ExponentialBackoffWithJitter implements BackoffStrategy {
+    private static final long INITIAL_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS = 5;
+    private static final long MAXIMUM_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS = 3600;
+
+    private final long minimumWaitTimeInSeconds;
+    private final long maximumWaitTimeInSeconds;
+    private final long retryIntervalInSeconds;
+    private final Random random;
+
+    private final Logger logger = LoggerFactory.getLogger(ExponentialBackoffWithJitter.class);
+
+    /**
+     * Creates a new {@link ExponentialBackoffWithJitter}.
+     */
+    public ExponentialBackoffWithJitter() {
+        this(INITIAL_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS, MAXIMUM_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS,
+                INITIAL_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS);
+    }
+
+    ExponentialBackoffWithJitter(long minimumWaitTimeInSeconds, long maximumWaitTimeInSeconds,
+            long retryIntervalInSeconds) {
+        this(minimumWaitTimeInSeconds, maximumWaitTimeInSeconds, retryIntervalInSeconds, new Random());
+    }
+
+    ExponentialBackoffWithJitter(long minimumWaitTimeInSeconds, long maximumWaitTimeInSeconds,
+            long retryIntervalInSeconds, Random random) {
+        if (minimumWaitTimeInSeconds < 0) {
+            throw new IllegalArgumentException("minimumWaitTimeInSeconds must not be smaller than zero");
+        }
+        if (maximumWaitTimeInSeconds < 0) {
+            throw new IllegalArgumentException("maximumWaitTimeInSeconds must not be smaller than zero");
+        }
+        if (retryIntervalInSeconds < 0) {
+            throw new IllegalArgumentException("retryIntervalInSeconds must not be smaller than zero");
+        }
+        if (maximumWaitTimeInSeconds < minimumWaitTimeInSeconds) {
+            throw new IllegalArgumentException(
+                    "maximumWaitTimeInSeconds must not be smaller than minimumWaitTimeInSeconds");
+        }
+        if (maximumWaitTimeInSeconds < retryIntervalInSeconds) {
+            throw new IllegalArgumentException(
+                    "maximumWaitTimeInSeconds must not be smaller than retryIntervalInSeconds");
+        }
+
+        this.minimumWaitTimeInSeconds = minimumWaitTimeInSeconds;
+        this.maximumWaitTimeInSeconds = maximumWaitTimeInSeconds;
+        this.retryIntervalInSeconds = retryIntervalInSeconds;
+        this.random = random;
+    }
+
+    @Override
+    public long getMinimumSecondsUntilRetry() {
+        return minimumWaitTimeInSeconds;
+    }
+
+    @Override
+    public long getMaximumSecondsUntilRetry() {
+        return maximumWaitTimeInSeconds;
+    }
+
+    @Override
+    public long getSecondsUntilRetry(int failedAttempts) {
+        if (failedAttempts < 0) {
+            logger.warn("The number of failed attempts must not be smaller than zero, was {}.", failedAttempts);
+        }
+
+        return minimumWaitTimeInSeconds
+                + getRandomLongWithUpperLimit(Math.min(maximumWaitTimeInSeconds - minimumWaitTimeInSeconds,
+                        retryIntervalInSeconds * (long) Math.pow(2, Math.max(0, failedAttempts))));
+    }
+
+    private long getRandomLongWithUpperLimit(long upperLimit) {
+        return Math.abs(random.nextLong()) % (upperLimit + 1);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/ServerSentEvent.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/ServerSentEvent.java
new file mode 100644 (file)
index 0000000..3848abf
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * An event emitted by an SSE connection.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ServerSentEvent {
+    private final String event;
+    private final String data;
+
+    ServerSentEvent(String event, String data) {
+        this.event = event;
+        this.data = data;
+    }
+
+    public String getEvent() {
+        return event;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(event, data);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ServerSentEvent other = (ServerSentEvent) obj;
+        return Objects.equals(event, other.event) && Objects.equals(data, other.data);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseConnection.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseConnection.java
new file mode 100644 (file)
index 0000000..c250c94
--- /dev/null
@@ -0,0 +1,240 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.InputStreamResponseListener;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.HttpUtil;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An active or inactive SSE connection emitting a stream of events.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class SseConnection {
+    private static final long CONNECTION_TIMEOUT = 30;
+    private static final TimeUnit CONNECTION_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+    private final Logger logger = LoggerFactory.getLogger(SseConnection.class);
+
+    private final String endpoint;
+    private final SseRequestFactory requestFactory;
+    private final ScheduledExecutorService scheduler;
+    private final BackoffStrategy backoffStrategy;
+
+    private final List<SseListener> listeners = new ArrayList<>();
+
+    private boolean active = false;
+
+    private int failedConnectionAttempts = 0;
+
+    @Nullable
+    private Request sseRequest;
+
+    /**
+     * Creates a new {@link SseConnection} to the given endpoint.
+     *
+     * Note: It is required to call {@link #connect()} in order to open the connection and start receiving events.
+     *
+     * @param endpoint The endpoint to connect to.
+     * @param requestFactory Factory for creating requests.
+     * @param scheduler Scheduler to run scheduled and concurrent tasks on.
+     */
+    public SseConnection(String endpoint, SseRequestFactory requestFactory, ScheduledExecutorService scheduler) {
+        this(endpoint, requestFactory, scheduler, new ExponentialBackoffWithJitter());
+    }
+
+    /**
+     * Creates a new {@link SseConnection} to the given endpoint.
+     *
+     * Note: It is required to call {@link #connect()} in order to open the connection and start receiving events.
+     *
+     * @param endpoint The endpoint to connect to.
+     * @param requestFactory Factory for creating requests.
+     * @param scheduler Scheduler to run scheduled and concurrent tasks on.
+     * @param backoffStrategy Strategy for deriving the wait time between connection attempts.
+     */
+    SseConnection(String endpoint, SseRequestFactory requestFactory, ScheduledExecutorService scheduler,
+            BackoffStrategy backoffStrategy) {
+        this.endpoint = endpoint;
+        this.requestFactory = requestFactory;
+        this.scheduler = scheduler;
+        this.backoffStrategy = backoffStrategy;
+    }
+
+    public synchronized void connect() {
+        active = true;
+        connectInternal();
+    }
+
+    private synchronized void connectInternal() {
+        if (!active) {
+            return;
+        }
+
+        Request runningRequest = this.sseRequest;
+        if (runningRequest != null) {
+            return;
+        }
+
+        logger.debug("Opening SSE connection...");
+        Request sseRequest = createRequest();
+        if (sseRequest == null) {
+            logger.warn("Could not create SSE request, not opening SSE connection.");
+            return;
+        }
+
+        final InputStreamResponseListener stream = new InputStreamResponseListener();
+        SseStreamParser eventStreamParser = new SseStreamParser(stream.getInputStream(), this::onServerSentEvent,
+                this::onSseStreamClosed);
+
+        sseRequest = sseRequest
+                .onResponseHeaders(
+                        response -> scheduler.schedule(eventStreamParser::parseAndDispatchEvents, 0, TimeUnit.SECONDS))
+                .onComplete(result -> onConnectionComplete(result));
+        sseRequest.send(stream);
+        this.sseRequest = sseRequest;
+    }
+
+    @Nullable
+    private Request createRequest() {
+        Request sseRequest = requestFactory.createSseRequest(endpoint);
+        if (sseRequest == null) {
+            return null;
+        }
+
+        return sseRequest.timeout(0, TimeUnit.SECONDS).idleTimeout(CONNECTION_TIMEOUT, CONNECTION_TIMEOUT_UNIT);
+    }
+
+    private synchronized void onSseStreamClosed(@Nullable Throwable exception) {
+        if (exception != null && AuthorizationFailedRetryStrategy.JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE
+                .equals(exception.getMessage())) {
+            onConnectionError(ConnectionError.AUTHORIZATION_FAILED);
+        } else if (exception instanceof TimeoutException) {
+            onConnectionError(ConnectionError.TIMEOUT);
+        } else {
+            onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+        }
+    }
+
+    private synchronized void onConnectionComplete(@Nullable Result result) {
+        sseRequest = null;
+
+        if (result == null) {
+            logger.warn("SSE stream was closed but there was no result delivered.");
+            onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+            return;
+        }
+
+        Response response = result.getResponse();
+        if (response == null) {
+            logger.warn("SSE stream was closed without response.");
+            onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+            return;
+        }
+
+        onConnectionClosed(response);
+    }
+
+    private void onConnectionClosed(Response response) {
+        try {
+            HttpUtil.checkHttpSuccess(response);
+            onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+        } catch (AuthorizationFailedException e) {
+            onConnectionError(ConnectionError.AUTHORIZATION_FAILED);
+        } catch (TooManyRequestsException e) {
+            long secondsUntilRetry = e.getSecondsUntilRetry();
+            if (secondsUntilRetry < 0) {
+                onConnectionError(ConnectionError.TOO_MANY_RERQUESTS);
+            } else {
+                onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, secondsUntilRetry);
+            }
+        } catch (MieleWebserviceTransientException e) {
+            onConnectionError(e.getConnectionError(), 0);
+        } catch (MieleWebserviceException e) {
+            onConnectionError(e.getConnectionError());
+        }
+    }
+
+    private void onConnectionError(ConnectionError connectionError) {
+        onConnectionError(connectionError, backoffStrategy.getSecondsUntilRetry(failedConnectionAttempts));
+    }
+
+    private synchronized void onConnectionError(ConnectionError connectionError, long secondsUntilRetry) {
+        if (!active) {
+            return;
+        }
+
+        if (connectionError != ConnectionError.AUTHORIZATION_FAILED) {
+            scheduleReconnect(secondsUntilRetry);
+        }
+
+        fireConnectionError(connectionError);
+        failedConnectionAttempts++;
+    }
+
+    private void scheduleReconnect(long secondsUntilRetry) {
+        long retryInSeconds = Math.max(backoffStrategy.getMinimumSecondsUntilRetry(),
+                Math.min(secondsUntilRetry, backoffStrategy.getMaximumSecondsUntilRetry()));
+        scheduler.schedule(this::connectInternal, retryInSeconds, TimeUnit.SECONDS);
+        logger.debug("Scheduled reconnect attempt for Miele webservice to take place in {} seconds", retryInSeconds);
+    }
+
+    public synchronized void disconnect() {
+        active = false;
+
+        Request runningRequest = sseRequest;
+        if (runningRequest == null) {
+            logger.debug("SSE connection is not established, skipping SSE disconnect.");
+            return;
+        }
+
+        logger.debug("Disconnecting SSE");
+        runningRequest.abort(new MieleWebserviceDisconnectSseException());
+        sseRequest = null;
+        logger.debug("Disconnected");
+    }
+
+    private void onServerSentEvent(ServerSentEvent event) {
+        failedConnectionAttempts = 0;
+        listeners.forEach(l -> l.onServerSentEvent(event));
+    }
+
+    private void fireConnectionError(ConnectionError connectionError) {
+        listeners.forEach(l -> l.onConnectionError(connectionError, failedConnectionAttempts));
+    }
+
+    public void addSseListener(SseListener listener) {
+        listeners.add(listener);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseListener.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseListener.java
new file mode 100644 (file)
index 0000000..cb39a47
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+
+/**
+ * Listens to events received via a SSE connection and errors concerning that connection.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface SseListener {
+    /**
+     * Called when an event is received via a SSE connection.
+     *
+     * @param event The received event.
+     */
+    void onServerSentEvent(ServerSentEvent event);
+
+    /**
+     * Called when an error occurs that is related to the connection and cannot be handled automatically.
+     *
+     * @param connectionError The connection error.
+     * @param failedReconnectAttempts The number of attempts that were made to reconnect to the event stream.
+     */
+    void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseRequestFactory.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseRequestFactory.java
new file mode 100644 (file)
index 0000000..b509e9e
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+
+/**
+ * Factory that produces configured {@link Request} instances for usage with SSE.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+@FunctionalInterface
+public interface SseRequestFactory {
+    /**
+     * Produces a {@link Request} which is decorated with all required headers.
+     *
+     * @param endpoint The endpoint to connect to.
+     * @return The created {@link Request} or {@code null} if no request can be created due to lacking request
+     *         information. If this method returns {@code null} then all connection attempts will be cancelled.
+     */
+    @Nullable
+    Request createSseRequest(String endpoint);
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseStreamParser.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseStreamParser.java
new file mode 100644 (file)
index 0000000..9260758
--- /dev/null
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parses events from the SSE event stream and emits them via the given dispatcher.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+class SseStreamParser {
+    private static final String SSE_KEY_EVENT = "event:";
+    private static final String SSE_KEY_DATA = "data:";
+
+    private final Logger logger = LoggerFactory.getLogger(SseStreamParser.class);
+
+    private final BufferedReader reader;
+    private final Consumer<ServerSentEvent> onServerSentEventCallback;
+    private final Consumer<@Nullable Throwable> onStreamClosedCallback;
+
+    private @Nullable String event;
+
+    SseStreamParser(InputStream inputStream, Consumer<ServerSentEvent> onServerSentEventCallback,
+            Consumer<@Nullable Throwable> onStreamClosedCallback) {
+        this.reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+        this.onServerSentEventCallback = onServerSentEventCallback;
+        this.onStreamClosedCallback = onStreamClosedCallback;
+    }
+
+    void parseAndDispatchEvents() {
+        try {
+            String line = null;
+            while ((line = reader.readLine()) != null) {
+                onLineReceived(line);
+            }
+
+            silentlyCloseReader();
+            logger.debug("SSE stream ended. Closing stream.");
+            onStreamClosedCallback.accept(null);
+        } catch (IOException exception) {
+            silentlyCloseReader();
+
+            if (!(exception.getCause() instanceof MieleWebserviceDisconnectSseException)) {
+                logger.warn("SSE connection failed unexpectedly: {}", exception.getMessage());
+                onStreamClosedCallback.accept(exception.getCause());
+            }
+        }
+        logger.debug("SSE stream closed.");
+    }
+
+    private void silentlyCloseReader() {
+        try {
+            reader.close();
+        } catch (IOException e) {
+            logger.warn("Failed to clean up SSE connection resources!", e);
+        }
+    }
+
+    private void onLineReceived(String line) {
+        if (line.isEmpty()) {
+            return;
+        }
+
+        if (line.startsWith(SSE_KEY_EVENT)) {
+            event = line.substring(SSE_KEY_EVENT.length()).trim();
+        } else if (line.startsWith(SSE_KEY_DATA)) {
+            String event = this.event;
+            String data = line.substring(SSE_KEY_DATA.length()).trim();
+
+            if (event == null) {
+                logger.warn("Received data payload without prior event payload.");
+            } else {
+                onServerSentEventCallback.accept(new ServerSentEvent(event, data));
+            }
+        } else {
+            logger.warn("Unable to parse line from SSE stream: {}", line);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..a9dc15f
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="mielecloud" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>@text/binding.mielecloud.name</name>
+       <description>@text/binding.mielecloud.description</description>
+</binding:binding>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/config/configDescription.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/config/configDescription.xml
new file mode 100644 (file)
index 0000000..e2f9d5d
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0">
+
+       <config-description uri="thing-type:mielecloud:device">
+               <parameter name="deviceIdentifier" type="text" required="true">
+                       <label>@text/thing-type.config.mielecloud.device.deviceIdentifier.label</label>
+                       <description>@text/thing-type.config.mielecloud.device.deviceIdentifier.description</description>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/i18n/mielecloud.properties b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/i18n/mielecloud.properties
new file mode 100644 (file)
index 0000000..7452986
--- /dev/null
@@ -0,0 +1,253 @@
+# Binding related texts
+binding.mielecloud.name=Miele@home Cloud Binding
+binding.mielecloud.description=This is the cloud-based Miele@home binding.
+
+# Thing related texts
+thing-type.mielecloud.account.label=Miele@home Account
+thing-type.mielecloud.account.description=The Miele@home Account is used to access linked Miele Conn@ct smart home devices.
+
+thing-type.config.mielecloud.account.locale.label=E-mail
+thing-type.config.mielecloud.account.locale.description=E-mail address associated with the Miele Cloud account.
+
+thing-type.config.mielecloud.account.locale.label=Locale
+thing-type.config.mielecloud.account.locale.description=Locale to be used for API calls.
+
+thing-type.config.mielecloud.device.deviceIdentifier.label=Device identifier
+thing-type.config.mielecloud.device.deviceIdentifier.description=Technical device identifier used to identify the Miele device.
+
+thing-type.mielecloud.coffee_system.label=Coffee System
+thing-type.mielecloud.coffee_system.description=The generic thing type for all Miele coffee systems.
+
+thing-type.mielecloud.dishwasher.label=Dishwasher
+thing-type.mielecloud.dishwasher.description=The generic thing type for all Miele dish washing devices.
+
+thing-type.mielecloud.dish_warmer.label=Dish Warmer
+thing-type.mielecloud.dish_warmer.description=The generic thing type for all Miele dish warmer devices.
+
+thing-type.mielecloud.dryer.label=Tumble Dryer
+thing-type.mielecloud.dryer.description=The generic thing type for all Miele drying devices.
+
+thing-type.mielecloud.freezer.label=Freezer
+thing-type.mielecloud.freezer.description=The generic thing type for all Miele freezer devices.
+
+thing-type.mielecloud.fridge.label=Fridge
+thing-type.mielecloud.fridge.description=The generic thing type for all Miele fridge devices.
+
+thing-type.mielecloud.fridge_freezer.label=Fridge Freezer
+thing-type.mielecloud.fridge_freezer.description=The generic thing type for all Miele fridge freezer devices.
+
+thing-type.mielecloud.hob.label=Hob
+thing-type.mielecloud.hob.description=The generic thing type for all Miele hob devices.
+
+thing-type.mielecloud.hood.label=Hood
+thing-type.mielecloud.hood.description=The generic thing type for all Miele hood devices.
+
+thing-type.mielecloud.oven.label=Oven
+thing-type.mielecloud.oven.description=The generic thing type for all Miele oven devices. Includes also Steam Ovens and Dialog Oven.
+
+thing-type.mielecloud.robotic_vacuum_cleaner.label=Robotic Vacuum Cleaner
+thing-type.mielecloud.robotic_vacuum_cleaner.description=The generic thing type for all Miele robotic vacuum cleaner devices.
+
+thing-type.mielecloud.washer_dryer.label=Washer Dryer
+thing-type.mielecloud.washer_dryer.description=The generic thing type for all Miele washer dryer devices.
+
+thing-type.mielecloud.washing_machine.label=Washing Machine
+thing-type.mielecloud.washing_machine.description=The generic thing type for all Miele washing devices.
+
+thing-type.mielecloud.wine_storage.label=Wine Storage
+thing-type.mielecloud.wine_storage.description=The generic thing type for all Miele wine storage devices.
+
+# Channel related texts
+channel-type.mielecloud.remote_control_can_be_started.label=Can Be Started
+channel-type.mielecloud.remote_control_can_be_started.description=Indicates if this device can be started remotely.
+
+channel-type.mielecloud.remote_control_can_be_stopped.label=Can Be Stopped
+channel-type.mielecloud.remote_control_can_be_stopped.description=Indicates if this device can be stopped remotely.
+
+channel-type.mielecloud.remote_control_can_be_paused.label=Can Be Paused
+channel-type.mielecloud.remote_control_can_be_paused.description=Indicates if this device can be paused remotely.
+
+channel-type.mielecloud.remote_control_can_be_switched_on.label=Can Be Switched On
+channel-type.mielecloud.remote_control_can_be_switched_on.description=Indicates if the device can be switched on remotely.
+
+channel-type.mielecloud.remote_control_can_be_switched_off.label=Can Be Switched Off
+channel-type.mielecloud.remote_control_can_be_switched_off.description=Indicates if the device can be switched off remotely.
+
+channel-type.mielecloud.remote_control_can_set_program_active.label=Can Set Active Program
+channel-type.mielecloud.remote_control_can_set_program_active.description=Indicates if the active program of the device can be set remotely.
+
+channel-type.mielecloud.spinning_speed.label=Spinning Speed
+channel-type.mielecloud.spinning_speed.description=The spinning speed of the active program.
+
+channel-type.mielecloud.spinning_speed_raw.label=Raw Spinning Speed
+channel-type.mielecloud.spinning_speed_raw.description=The raw spinning speed of the active program.
+
+channel-type.mielecloud.program_active.label=Active Program
+channel-type.mielecloud.program_active.description=The active program of the device.
+
+channel-type.mielecloud.program_active_raw.label=Raw Active Program
+channel-type.mielecloud.program_active_raw.description=The raw active program of the device.
+
+channel-type.mielecloud.dish_warmer_program_active.label=Active Program
+channel-type.mielecloud.dish_warmer_program_active.description=The active program of the device.
+channel-option.mielecloud.dish_warmer_program_active.warming_cups_glasses=Warming cups/glasses
+channel-option.mielecloud.dish_warmer_program_active.warming_dishes_plates=Warming dishes/plates
+channel-option.mielecloud.dish_warmer_program_active.keeping_food_warm=Keeping food warm
+channel-option.mielecloud.dish_warmer_program_active.low_temperature_cooking=Low temperature cooking
+
+channel-type.mielecloud.vacuum_cleaner_program_active.label=Active Program
+channel-type.mielecloud.vacuum_cleaner_program_active.description=The active program of the device.
+channel-option.mielecloud.vacuum_cleaner_program_active.auto=Auto
+channel-option.mielecloud.vacuum_cleaner_program_active.spot=Spot
+channel-option.mielecloud.vacuum_cleaner_program_active.turbo=Turbo
+channel-option.mielecloud.vacuum_cleaner_program_active.silent=Silent
+
+channel-type.mielecloud.program_phase.label=Program Phase
+channel-type.mielecloud.program_phase.description=The phase of the active program.
+
+channel-type.mielecloud.program_phase_raw.label=Raw Program Phase
+channel-type.mielecloud.program_phase_raw.description=The raw phase of the active program.
+
+channel-type.mielecloud.operation_state.label=Operation State
+channel-type.mielecloud.operation_state.description=The operation state of the device.
+
+channel-type.mielecloud.operation_state_raw.label=Raw Operation State
+channel-type.mielecloud.operation_state_raw.description=The raw operation state of the device.
+
+channel-type.mielecloud.program_start.label=Start
+channel-type.mielecloud.program_start.description=Starts the currently selected program.
+
+channel-type.mielecloud.program_stop.label=Stop
+channel-type.mielecloud.program_stop.description=Stops the currently selected program.
+
+channel-type.mielecloud.program_start_stop.label=Start Stop
+channel-type.mielecloud.program_start_stop.description=Starts or stops the currently selected program.
+channel-option.mielecloud.program_start_stop.start=Start
+channel-option.mielecloud.program_start_stop.stop=Stop
+
+channel-type.mielecloud.program_start_stop_pause.label=Start Stop Pause
+channel-type.mielecloud.program_start_stop_pause.description=Starts, stops or pauses the currently selected program.
+channel-option.mielecloud.program_start_stop_pause.start=Start
+channel-option.mielecloud.program_start_stop_pause.stop=Stop
+channel-option.mielecloud.program_start_stop_pause.pause=Pause
+
+channel-type.mielecloud.power_state_on_off.label=Power
+channel-type.mielecloud.power_state_on_off.description=Switches the device On or Off.
+channel-option.mielecloud.power_state_on_off.on=On
+channel-option.mielecloud.power_state_on_off.off=Off
+
+channel-type.mielecloud.finish_state.label=Finished
+channel-type.mielecloud.finish_state.description=Indicates whether the most recent program finished.
+
+channel-type.mielecloud.delayed_start_time.label=Delayed Start Time
+channel-type.mielecloud.delayed_start_time.description=The delayed start time of the selected program.
+
+channel-type.mielecloud.program_remaining_time.label=Program Remaining Time
+channel-type.mielecloud.program_remaining_time.description=The remaining time of the active program.
+
+channel-type.mielecloud.program_elapsed_time.label=Program Elapsed Time
+channel-type.mielecloud.program_elapsed_time.description=The elapsed time of the active program.
+
+channel-type.mielecloud.program_progress.label=Program Progress
+channel-type.mielecloud.program_progress.description=The progress of the active program.
+
+channel-type.mielecloud.drying_target.label=Drying Target
+channel-type.mielecloud.drying_target.description=The target drying step of the laundry.
+
+channel-type.mielecloud.drying_target_raw.label=Raw Drying Target
+channel-type.mielecloud.drying_target_raw.description=The raw target drying step of the laundry.
+
+channel-type.mielecloud.pre_heat_finished.label=Pre-heat Finished
+channel-type.mielecloud.pre_heat_finished.description=Indicates whether the pre-heating finished.
+
+channel-type.mielecloud.temperature_target.label=Target Temperature
+channel-type.mielecloud.temperature_target.description=The target temperature of the device.
+
+channel-type.mielecloud.temperature_current.label=Current Temperature
+channel-type.mielecloud.temperature_current.description=The currently measured temperature of the device.
+
+channel-type.mielecloud.ventilation_power.label=Ventilation Power
+channel-type.mielecloud.ventilation_power.description=The current ventilation power of the hood.
+
+channel-type.mielecloud.ventilation_power_raw.label=Raw Ventilation Power
+channel-type.mielecloud.ventilation_power_raw.description=The current raw ventilation power of the hood.
+
+channel-type.mielecloud.error_state.label=Error
+channel-type.mielecloud.error_state.description=Indication flag which signals an error state for the device.
+
+channel-type.mielecloud.info_state.label=Info
+channel-type.mielecloud.info_state.description=Indication flag which signals an information of the device.
+
+channel-type.mielecloud.fridge_super_cool.label=Supercool
+channel-type.mielecloud.fridge_super_cool.description=Start the super cooling mode of the fridge.
+
+channel-type.mielecloud.freezer_super_freeze.label=Superfreeze
+channel-type.mielecloud.freezer_super_freeze.description=Start the super freezing mode of the freezer.
+
+channel-type.mielecloud.super_cool_can_be_controlled.label=Can Control Supercool
+channel-type.mielecloud.super_cool_can_be_controlled.description=Indicates if super cooling can be toggled.
+
+channel-type.mielecloud.super_freeze_can_be_controlled.label=Can Control Superfreeze
+channel-type.mielecloud.super_freeze_can_be_controlled.description=Indicates if super freezing can be toggled
+
+channel-type.mielecloud.fridge_temperature_target.label=Fridge Target Temperature
+channel-type.mielecloud.fridge_temperature_target.description=The target temperature of the fridge.
+
+channel-type.mielecloud.fridge_temperature_current.label=Current Fridge Temperature
+channel-type.mielecloud.fridge_temperature_current.description=The currently measured temperature of the fridge.
+
+channel-type.mielecloud.freezer_temperature_target.label=Freezer Target Temperature
+channel-type.mielecloud.freezer_temperature_target.description=The target temperature of the freezer.
+
+channel-type.mielecloud.freezer_temperature_current.label=Current Freezer Temperature
+channel-type.mielecloud.freezer_temperature_current.description=The currently measured temperature of the freezer.
+
+channel-type.mielecloud.top_temperature_target.label=Top Target Temperature
+channel-type.mielecloud.top_temperature_target.description=The target temperature of the top area.
+
+channel-type.mielecloud.top_temperature_current.label=Current Top Temperature
+channel-type.mielecloud.top_temperature_current.description=The currently measured temperature of the top area.
+
+channel-type.mielecloud.middle_temperature_target.label=Middle Target Temperature
+channel-type.mielecloud.middle_temperature_target.description=The target temperature of the middle area.
+
+channel-type.mielecloud.middle_temperature_current.label=Current Middle Temperature
+channel-type.mielecloud.middle_temperature_current.description=The currently measured temperature of the middle area.
+
+channel-type.mielecloud.bottom_temperature_target.label=Bottom Target Temperature
+channel-type.mielecloud.bottom_temperature_target.description=The target temperature of the bottom area.
+
+channel-type.mielecloud.bottom_temperature_current.label=Current Bottom Temperature
+channel-type.mielecloud.bottom_temperature_current.description=The currently measured temperature of the bottom area.
+
+channel-type.mielecloud.light_switch.label=Light Enabled
+channel-type.mielecloud.light_switch.description=Indicates if the light of the device is enabled.
+
+channel-type.mielecloud.light_can_be_controlled.label=Can Control Light
+channel-type.mielecloud.light_can_be_controlled.description=Indicates if the light of the device can be controlled.
+
+channel-type.mielecloud.plate_power_step.label=Plate Power Step
+channel-type.mielecloud.plate_power_step.description=The power level of the heating plate.
+
+channel-type.mielecloud.plate_power_step_raw.label=Raw Plate Power Step
+channel-type.mielecloud.plate_power_step_raw.description=The raw power level of the heating plate.
+
+channel-type.mielecloud.door_state.label=Door Signal
+channel-type.mielecloud.door_state.description=Indicates if the door of the device is open.
+
+channel-type.mielecloud.door_alarm.label=Door Alarm
+channel-type.mielecloud.door_alarm.description=Indicates if the door alarm of the device is active.
+
+channel-type.mielecloud.battery_level.label=Battery Level
+channel-type.mielecloud.battery_level.description=The battery level of the robotic vacuum cleaner.
+
+# Error message texts
+mielecloud.bridge.status.access.token.not.configured=The OAuth2 access token is not configured.
+mielecloud.bridge.status.account.not.authorized=The account has not been authorized. Please consult the documentation on how to do that.
+mielecloud.bridge.status.access.token.refresh.failed=Failed to refresh the OAuth2 access token. Please authorize the account again.
+mielecloud.bridge.status.invalid.email=The configured e-mail address has an invalid format.
+mielecloud.bridge.status.transient.http.error=An unexpected HTTP error occurred. Check the logs if this error persists.
+mielecloud.thing.status.webservice.missing=The Miele webservice cannot be accessed over the bridge. Check the bridge configuration.
+mielecloud.thing.status.removed=This Miele device has been removed from the Miele@home account.
+mielecloud.thing.status.ratelimit=The rate limit of the Miele cloud has been exceeded.
+mielecloud.thing.status.disconnected=This Miele device is not connected to the internet.
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644 (file)
index 0000000..9da9db3
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Miele Cloud Connector Bridge -->
+       <bridge-type id="account">
+               <label>@text/thing-type.mielecloud.account.label</label>
+               <description>@text/thing-type.mielecloud.account.description</description>
+               <category>WebService</category>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+                       <property name="modelId">Cloud Connector</property>
+                       <property name="connection">INTERNET</property>
+                       <!-- accessToken property is set on creation. -->
+               </properties>
+
+               <config-description>
+                       <parameter name="email" type="text" required="true">
+                               <context>email</context>
+                               <label>@text/thing-type.config.mielecloud.account.email.label</label>
+                               <description>@text/thing-type.config.mielecloud.account.email.description</description>
+                       </parameter>
+                       <parameter name="locale" type="text">
+                               <label>@text/thing-type.config.mielecloud.account.locale.label</label>
+                               <description>@text/thing-type.config.mielecloud.account.locale.description</description>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/channelTypes.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/channelTypes.xml
new file mode 100644 (file)
index 0000000..e1cb16c
--- /dev/null
@@ -0,0 +1,448 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <channel-type id="remote_control_can_be_started">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.remote_control_can_be_started.label</label>
+               <description>@text/channel-type.mielecloud.remote_control_can_be_started.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="remote_control_can_be_stopped">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.remote_control_can_be_stopped.label</label>
+               <description>@text/channel-type.mielecloud.remote_control_can_be_stopped.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="remote_control_can_be_paused">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.remote_control_can_be_paused.label</label>
+               <description>@text/channel-type.mielecloud.remote_control_can_be_paused.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="remote_control_can_be_switched_on">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.remote_control_can_be_switched_on.label</label>
+               <description>@text/channel-type.mielecloud.remote_control_can_be_switched_on.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="remote_control_can_be_switched_off">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.remote_control_can_be_switched_off.label</label>
+               <description>@text/channel-type.mielecloud.remote_control_can_be_switched_off.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="remote_control_can_set_program_active">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.remote_control_can_set_program_active.label</label>
+               <description>@text/channel-type.mielecloud.remote_control_can_set_program_active.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="spinning_speed">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.spinning_speed.label</label>
+               <description>@text/channel-type.mielecloud.spinning_speed.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="spinning_speed_raw">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.spinning_speed_raw.label</label>
+               <description>@text/channel-type.mielecloud.spinning_speed_raw.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="program_active">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.program_active.label</label>
+               <description>@text/channel-type.mielecloud.program_active.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="program_active_raw">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.program_active_raw.label</label>
+               <description>@text/channel-type.mielecloud.program_active_raw.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="dish_warmer_program_active">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.dish_warmer_program_active.label</label>
+               <description>@text/channel-type.mielecloud.dish_warmer_program_active.description</description>
+               <state>
+                       <options>
+                               <option value="1">@text/channel-option.mielecloud.dish_warmer_program_active.warming_cups_glasses</option>
+                               <option value="2">@text/channel-option.mielecloud.dish_warmer_program_active.warming_dishes_plates</option>
+                               <option value="3">@text/channel-option.mielecloud.dish_warmer_program_active.keeping_food_warm</option>
+                               <option value="4">@text/channel-option.mielecloud.dish_warmer_program_active.low_temperature_cooking</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="vacuum_cleaner_program_active">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.vacuum_cleaner_program_active.label</label>
+               <description>@text/channel-type.mielecloud.vacuum_cleaner_program_active.description</description>
+               <state>
+                       <options>
+                               <option value="1">@text/channel-option.mielecloud.vacuum_cleaner_program_active.auto</option>
+                               <option value="2">@text/channel-option.mielecloud.vacuum_cleaner_program_active.spot</option>
+                               <option value="3">@text/channel-option.mielecloud.vacuum_cleaner_program_active.turbo</option>
+                               <option value="4">@text/channel-option.mielecloud.vacuum_cleaner_program_active.silent</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="program_phase">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.program_phase.label</label>
+               <description>@text/channel-type.mielecloud.program_phase.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="program_phase_raw">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.program_phase_raw.label</label>
+               <description>@text/channel-type.mielecloud.program_phase_raw.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="operation_state">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.operation_state.label</label>
+               <description>@text/channel-type.mielecloud.operation_state.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="operation_state_raw">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.operation_state_raw.label</label>
+               <description>@text/channel-type.mielecloud.operation_state_raw.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="program_start">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.program_start.label</label>
+               <description>@text/channel-type.mielecloud.program_start.description</description>
+       </channel-type>
+
+       <channel-type id="program_stop">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.program_stop.label</label>
+               <description>@text/channel-type.mielecloud.program_stop.description</description>
+       </channel-type>
+
+       <channel-type id="program_start_stop">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.program_start_stop.label</label>
+               <description>@text/channel-type.mielecloud.program_start_stop.description</description>
+               <state>
+                       <options>
+                               <option value="start">@text/channel-option.mielecloud.program_start_stop.start</option>
+                               <option value="stop">@text/channel-option.mielecloud.program_start_stop.stop</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="program_start_stop_pause">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.program_start_stop_pause.label</label>
+               <description>@text/channel-type.mielecloud.program_start_stop_pause.description</description>
+               <state>
+                       <options>
+                               <option value="start">@text/channel-option.mielecloud.program_start_stop_pause.start</option>
+                               <option value="stop">@text/channel-option.mielecloud.program_start_stop_pause.stop</option>
+                               <option value="pause">@text/channel-option.mielecloud.program_start_stop_pause.pause</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="power_state_on_off">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.power_state_on_off.label</label>
+               <description>@text/channel-type.mielecloud.power_state_on_off.description</description>
+               <state>
+                       <options>
+                               <option value="on">@text/channel-option.mielecloud.power_state_on_off.on</option>
+                               <option value="off">@text/channel-option.mielecloud.power_state_on_off.off</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="finish_state">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.finish_state.label</label>
+               <description>@text/channel-type.mielecloud.finish_state.description</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="delayed_start_time">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.delayed_start_time.label</label>
+               <description>@text/channel-type.mielecloud.delayed_start_time.description</description>
+               <category>Number</category>
+               <state pattern="%d sec" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="program_remaining_time">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.program_remaining_time.label</label>
+               <description>@text/channel-type.mielecloud.program_remaining_time.description</description>
+               <category>Number</category>
+               <state pattern="%d sec" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="program_elapsed_time">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.program_elapsed_time.label</label>
+               <description>@text/channel-type.mielecloud.program_elapsed_time.description</description>
+               <category>Number</category>
+               <state pattern="%d sec" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="program_progress">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.program_progress.label</label>
+               <description>@text/channel-type.mielecloud.program_progress.description</description>
+               <category>Number</category>
+               <state min="0" max="100" step="1" pattern="%d %%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="drying_target">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.drying_target.label</label>
+               <description>@text/channel-type.mielecloud.drying_target.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="drying_target_raw">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.drying_target_raw.label</label>
+               <description>@text/channel-type.mielecloud.drying_target_raw.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="pre_heat_finished">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.pre_heat_finished.label</label>
+               <description>@text/channel-type.mielecloud.pre_heat_finished.description</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="temperature_target">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.temperature_target.label</label>
+               <description>@text/channel-type.mielecloud.temperature_target.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="temperature_current">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.temperature_current.label</label>
+               <description>@text/channel-type.mielecloud.temperature_current.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="ventilation_power">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.ventilation_power.label</label>
+               <description>@text/channel-type.mielecloud.ventilation_power.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="ventilation_power_raw">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.ventilation_power_raw.label</label>
+               <description>@text/channel-type.mielecloud.ventilation_power_raw.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="error_state">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.error_state.label</label>
+               <description>@text/channel-type.mielecloud.error_state.description</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="info_state">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.info_state.label</label>
+               <description>@text/channel-type.mielecloud.info_state.description</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="fridge_super_cool">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.fridge_super_cool.label</label>
+               <description>@text/channel-type.mielecloud.fridge_super_cool.description</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="freezer_super_freeze">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.freezer_super_freeze.label</label>
+               <description>@text/channel-type.mielecloud.freezer_super_freeze.description</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="super_cool_can_be_controlled">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.super_cool_can_be_controlled.label</label>
+               <description>@text/channel-type.mielecloud.super_cool_can_be_controlled.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="super_freeze_can_be_controlled">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.super_freeze_can_be_controlled.label</label>
+               <description>@text/channel-type.mielecloud.super_freeze_can_be_controlled.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="fridge_temperature_target">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.fridge_temperature_target.label</label>
+               <description>@text/channel-type.mielecloud.fridge_temperature_target.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="fridge_temperature_current">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.fridge_temperature_current.label</label>
+               <description>@text/channel-type.mielecloud.fridge_temperature_current.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="freezer_temperature_target">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.freezer_temperature_target.label</label>
+               <description>@text/channel-type.mielecloud.freezer_temperature_target.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="freezer_temperature_current">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.freezer_temperature_current.label</label>
+               <description>@text/channel-type.mielecloud.freezer_temperature_current.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="top_temperature_target">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.top_temperature_target.label</label>
+               <description>@text/channel-type.mielecloud.top_temperature_target.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="top_temperature_current">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.top_temperature_current.label</label>
+               <description>@text/channel-type.mielecloud.top_temperature_current.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="middle_temperature_target">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.middle_temperature_target.label</label>
+               <description>@text/channel-type.mielecloud.middle_temperature_target.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="middle_temperature_current">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.middle_temperature_current.label</label>
+               <description>@text/channel-type.mielecloud.middle_temperature_current.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="bottom_temperature_target">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.bottom_temperature_target.label</label>
+               <description>@text/channel-type.mielecloud.bottom_temperature_target.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="bottom_temperature_current">
+               <item-type>Number:Temperature</item-type>
+               <label>@text/channel-type.mielecloud.bottom_temperature_current.label</label>
+               <description>@text/channel-type.mielecloud.bottom_temperature_current.description</description>
+               <category>Number:Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="light_switch">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.light_switch.label</label>
+               <description>@text/channel-type.mielecloud.light_switch.description</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="light_can_be_controlled">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.light_can_be_controlled.label</label>
+               <description>@text/channel-type.mielecloud.light_can_be_controlled.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="plate_power_step">
+               <item-type>String</item-type>
+               <label>@text/channel-type.mielecloud.plate_power_step.label</label>
+               <description>@text/channel-type.mielecloud.plate_power_step.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="plate_power_step_raw">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.plate_power_step_raw.label</label>
+               <description>@text/channel-type.mielecloud.plate_power_step_raw.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="door_state">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.door_state.label</label>
+               <description>@text/channel-type.mielecloud.door_state.description</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="door_alarm">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.mielecloud.door_alarm.label</label>
+               <description>@text/channel-type.mielecloud.door_alarm.description</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="battery_level">
+               <item-type>Number</item-type>
+               <label>@text/channel-type.mielecloud.battery_level.label</label>
+               <description>@text/channel-type.mielecloud.battery_level.description</description>
+               <category>Battery</category>
+               <state readOnly="true"/>
+       </channel-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/coffeeSystem.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/coffeeSystem.xml
new file mode 100644 (file)
index 0000000..dae30e6
--- /dev/null
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="coffee_system">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.coffee_system.label</label>
+               <description>@text/thing-type.mielecloud.coffee_system.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="program_active" typeId="program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="program_phase" typeId="program_phase"/>
+                       <channel id="program_phase_raw" typeId="program_phase_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="program_remaining_time" typeId="program_remaining_time"/>
+                       <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="light_switch" typeId="light_switch"/>
+                       <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dishWarmerDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dishWarmerDevice.xml
new file mode 100644 (file)
index 0000000..7ddf1e3
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="dish_warmer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.dish_warmer.label</label>
+               <description>@text/thing-type.mielecloud.dish_warmer.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="dish_warmer_program_active" typeId="dish_warmer_program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="program_remaining_time" typeId="program_remaining_time"/>
+                       <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+                       <channel id="program_progress" typeId="program_progress"/>
+                       <channel id="temperature_target" typeId="temperature_target"/>
+                       <channel id="temperature_current" typeId="temperature_current"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="door_state" typeId="door_state"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dishwasherDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dishwasherDevice.xml
new file mode 100644 (file)
index 0000000..2d97427
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="dishwasher">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.dishwasher.label</label>
+               <description>@text/thing-type.mielecloud.dishwasher.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="program_active" typeId="program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="program_phase" typeId="program_phase"/>
+                       <channel id="program_phase_raw" typeId="program_phase_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="program_start_stop" typeId="program_start_stop"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="delayed_start_time" typeId="delayed_start_time"/>
+                       <channel id="program_remaining_time" typeId="program_remaining_time"/>
+                       <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+                       <channel id="program_progress" typeId="program_progress"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="door_state" typeId="door_state"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dryerDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/dryerDevice.xml
new file mode 100644 (file)
index 0000000..0522f00
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="dryer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.dryer.label</label>
+               <description>@text/thing-type.mielecloud.dryer.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="program_active" typeId="program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="program_phase" typeId="program_phase"/>
+                       <channel id="program_phase_raw" typeId="program_phase_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="program_start_stop" typeId="program_start_stop"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="delayed_start_time" typeId="delayed_start_time"/>
+                       <channel id="program_remaining_time" typeId="program_remaining_time"/>
+                       <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+                       <channel id="program_progress" typeId="program_progress"/>
+                       <channel id="drying_target" typeId="drying_target"/>
+                       <channel id="drying_target_raw" typeId="drying_target_raw"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="light_switch" typeId="light_switch"/>
+                       <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+                       <channel id="door_state" typeId="door_state"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/freezer.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/freezer.xml
new file mode 100644 (file)
index 0000000..6f331fc
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="freezer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.freezer.label</label>
+               <description>@text/thing-type.mielecloud.freezer.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="freezer_super_freeze" typeId="freezer_super_freeze"/>
+                       <channel id="super_freeze_can_be_controlled" typeId="super_freeze_can_be_controlled"/>
+                       <channel id="freezer_temperature_target" typeId="freezer_temperature_target"/>
+                       <channel id="freezer_temperature_current" typeId="freezer_temperature_current"/>
+                       <channel id="door_state" typeId="door_state"/>
+                       <channel id="door_alarm" typeId="door_alarm"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/fridge.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/fridge.xml
new file mode 100644 (file)
index 0000000..6fe4f85
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="fridge">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.fridge.label</label>
+               <description>@text/thing-type.mielecloud.fridge.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="fridge_super_cool" typeId="fridge_super_cool"/>
+                       <channel id="super_cool_can_be_controlled" typeId="super_cool_can_be_controlled"/>
+                       <channel id="fridge_temperature_target" typeId="fridge_temperature_target"/>
+                       <channel id="fridge_temperature_current" typeId="fridge_temperature_current"/>
+                       <channel id="door_state" typeId="door_state"/>
+                       <channel id="door_alarm" typeId="door_alarm"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/fridgeFreezer.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/fridgeFreezer.xml
new file mode 100644 (file)
index 0000000..2e8d56b
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="fridge_freezer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.fridge_freezer.label</label>
+               <description>@text/thing-type.mielecloud.fridge_freezer.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="fridge_super_cool" typeId="fridge_super_cool"/>
+                       <channel id="freezer_super_freeze" typeId="freezer_super_freeze"/>
+                       <channel id="super_cool_can_be_controlled" typeId="super_cool_can_be_controlled"/>
+                       <channel id="super_freeze_can_be_controlled" typeId="super_freeze_can_be_controlled"/>
+                       <channel id="fridge_temperature_target" typeId="fridge_temperature_target"/>
+                       <channel id="fridge_temperature_current" typeId="fridge_temperature_current"/>
+                       <channel id="freezer_temperature_target" typeId="freezer_temperature_target"/>
+                       <channel id="freezer_temperature_current" typeId="freezer_temperature_current"/>
+                       <channel id="door_state" typeId="door_state"/>
+                       <channel id="door_alarm" typeId="door_alarm"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/hobDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/hobDevice.xml
new file mode 100644 (file)
index 0000000..83db9fc
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="hob">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.hob.label</label>
+               <description>@text/thing-type.mielecloud.hob.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="plate_1_power_step" typeId="plate_power_step"/>
+                       <channel id="plate_1_power_step_raw" typeId="plate_power_step_raw"/>
+                       <channel id="plate_2_power_step" typeId="plate_power_step"/>
+                       <channel id="plate_2_power_step_raw" typeId="plate_power_step_raw"/>
+                       <channel id="plate_3_power_step" typeId="plate_power_step"/>
+                       <channel id="plate_3_power_step_raw" typeId="plate_power_step_raw"/>
+                       <channel id="plate_4_power_step" typeId="plate_power_step"/>
+                       <channel id="plate_4_power_step_raw" typeId="plate_power_step_raw"/>
+                       <channel id="plate_5_power_step" typeId="plate_power_step"/>
+                       <channel id="plate_5_power_step_raw" typeId="plate_power_step_raw"/>
+                       <channel id="plate_6_power_step" typeId="plate_power_step"/>
+                       <channel id="plate_6_power_step_raw" typeId="plate_power_step_raw"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/hoodDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/hoodDevice.xml
new file mode 100644 (file)
index 0000000..78c36f1
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="hood">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.hood.label</label>
+               <description>@text/thing-type.mielecloud.hood.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="program_phase" typeId="program_phase"/>
+                       <channel id="program_phase_raw" typeId="program_phase_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="ventilation_power" typeId="ventilation_power"/>
+                       <channel id="ventilation_power_raw" typeId="ventilation_power_raw"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="light_switch" typeId="light_switch"/>
+                       <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/ovenDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/ovenDevice.xml
new file mode 100644 (file)
index 0000000..abedb9f
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="oven">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.oven.label</label>
+               <description>@text/thing-type.mielecloud.oven.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="program_active" typeId="program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="program_phase" typeId="program_phase"/>
+                       <channel id="program_phase_raw" typeId="program_phase_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="program_start_stop" typeId="program_start_stop"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="delayed_start_time" typeId="delayed_start_time"/>
+                       <channel id="program_remaining_time" typeId="program_remaining_time"/>
+                       <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+                       <channel id="program_progress" typeId="program_progress"/>
+                       <channel id="pre_heat_finished" typeId="pre_heat_finished"/>
+                       <channel id="temperature_target" typeId="temperature_target"/>
+                       <channel id="temperature_current" typeId="temperature_current"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="light_switch" typeId="light_switch"/>
+                       <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+                       <channel id="door_state" typeId="door_state"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/roboticVacuumCleanerDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/roboticVacuumCleanerDevice.xml
new file mode 100644 (file)
index 0000000..ec01510
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="robotic_vacuum_cleaner">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.robotic_vacuum_cleaner.label</label>
+               <description>@text/thing-type.mielecloud.robotic_vacuum_cleaner.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_paused" typeId="remote_control_can_be_paused"/>
+                       <channel id="remote_control_can_set_program_active" typeId="remote_control_can_set_program_active"/>
+                       <channel id="vacuum_cleaner_program_active" typeId="vacuum_cleaner_program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="program_start_stop_pause" typeId="program_start_stop_pause"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="battery_level" typeId="battery_level"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/washerDryer.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/washerDryer.xml
new file mode 100644 (file)
index 0000000..21f21cb
--- /dev/null
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="washer_dryer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.washer_dryer.label</label>
+               <description>@text/thing-type.mielecloud.washer_dryer.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="spinning_speed" typeId="spinning_speed"/>
+                       <channel id="spinning_speed_raw" typeId="spinning_speed_raw"/>
+                       <channel id="program_active" typeId="program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="program_phase" typeId="program_phase"/>
+                       <channel id="program_phase_raw" typeId="program_phase_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="program_start_stop" typeId="program_start_stop"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="delayed_start_time" typeId="delayed_start_time"/>
+                       <channel id="program_remaining_time" typeId="program_remaining_time"/>
+                       <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+                       <channel id="program_progress" typeId="program_progress"/>
+                       <channel id="drying_target" typeId="drying_target"/>
+                       <channel id="drying_target_raw" typeId="drying_target_raw"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="temperature_target" typeId="temperature_target"/>
+                       <channel id="light_switch" typeId="light_switch"/>
+                       <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+                       <channel id="door_state" typeId="door_state"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/washingMachine.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/washingMachine.xml
new file mode 100644 (file)
index 0000000..8143ebb
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="washing_machine">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.washing_machine.label</label>
+               <description>@text/thing-type.mielecloud.washing_machine.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="spinning_speed" typeId="spinning_speed"/>
+                       <channel id="spinning_speed_raw" typeId="spinning_speed_raw"/>
+                       <channel id="program_active" typeId="program_active"/>
+                       <channel id="program_active_raw" typeId="program_active_raw"/>
+                       <channel id="program_phase" typeId="program_phase"/>
+                       <channel id="program_phase_raw" typeId="program_phase_raw"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="program_start_stop" typeId="program_start_stop"/>
+                       <channel id="finish_state" typeId="finish_state"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="delayed_start_time" typeId="delayed_start_time"/>
+                       <channel id="program_remaining_time" typeId="program_remaining_time"/>
+                       <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+                       <channel id="program_progress" typeId="program_progress"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="temperature_target" typeId="temperature_target"/>
+                       <channel id="light_switch" typeId="light_switch"/>
+                       <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+                       <channel id="door_state" typeId="door_state"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/wineStorageDevice.xml b/bundles/org.openhab.binding.mielecloud/src/main/resources/OH-INF/thing/wineStorageDevice.xml
new file mode 100644 (file)
index 0000000..cf98960
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="wine_storage">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+
+               <label>@text/thing-type.mielecloud.wine_storage.label</label>
+               <description>@text/thing-type.mielecloud.wine_storage.description</description>
+               <category>WhiteGood</category>
+
+               <channels>
+                       <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+                       <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+                       <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+                       <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+                       <channel id="operation_state" typeId="operation_state"/>
+                       <channel id="operation_state_raw" typeId="operation_state_raw"/>
+                       <channel id="power_state_on_off" typeId="power_state_on_off"/>
+                       <channel id="error_state" typeId="error_state"/>
+                       <channel id="info_state" typeId="info_state"/>
+                       <channel id="temperature_target" typeId="temperature_target"/>
+                       <channel id="temperature_current" typeId="temperature_current"/>
+                       <channel id="top_temperature_target" typeId="top_temperature_target"/>
+                       <channel id="top_temperature_current" typeId="top_temperature_current"/>
+                       <channel id="middle_temperature_target" typeId="middle_temperature_target"/>
+                       <channel id="middle_temperature_current" typeId="middle_temperature_current"/>
+                       <channel id="bottom_temperature_target" typeId="bottom_temperature_target"/>
+                       <channel id="bottom_temperature_current" typeId="bottom_temperature_current"/>
+               </channels>
+
+               <properties>
+                       <property name="vendor">Miele</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:mielecloud:device"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/css/main.css b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/css/main.css
new file mode 100755 (executable)
index 0000000..e51da11
--- /dev/null
@@ -0,0 +1,15023 @@
+/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
+/**\r
+ * 1. Change the default font family in all browsers (opinionated).\r
+ * 2. Correct the line height in all browsers.\r
+ * 3. Prevent adjustments of font size after orientation changes in\r
+ *    IE on Windows Phone and in iOS.\r
+ */
+/* Document\r
+   ========================================================================== */
+/* line 13, src/assets/scss/vendors/_normalize.scss */
+html {
+  font-family: sans-serif;
+  /* 1 */
+  line-height: 1.15;
+  /* 2 */
+  -ms-text-size-adjust: 100%;
+  /* 3 */
+  -webkit-text-size-adjust: 100%;
+  /* 3 */
+}
+
+/* Sections\r
+   ========================================================================== */
+/**\r
+ * Remove the margin in all browsers (opinionated).\r
+ */
+/* line 27, src/assets/scss/vendors/_normalize.scss */
+body {
+  margin: 0;
+}
+
+/**\r
+ * Add the correct display in IE 9-.\r
+ */
+/* line 35, src/assets/scss/vendors/_normalize.scss */
+article,
+aside,
+footer,
+header,
+nav,
+section {
+  display: block;
+}
+
+/**\r
+ * Correct the font size and margin on `h1` elements within `section` and\r
+ * `article` contexts in Chrome, Firefox, and Safari.\r
+ */
+/* line 49, src/assets/scss/vendors/_normalize.scss */
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content\r
+   ========================================================================== */
+/**\r
+ * Add the correct display in IE 9-.\r
+ * 1. Add the correct display in IE.\r
+ */
+/* line 62, src/assets/scss/vendors/_normalize.scss */
+figcaption,
+figure,
+main {
+  /* 1 */
+  display: block;
+}
+
+/**\r
+ * Add the correct margin in IE 8.\r
+ */
+/* line 72, src/assets/scss/vendors/_normalize.scss */
+figure {
+  margin: 1em 40px;
+}
+
+/**\r
+ * 1. Add the correct box sizing in Firefox.\r
+ * 2. Show the overflow in Edge and IE.\r
+ */
+/* line 81, src/assets/scss/vendors/_normalize.scss */
+hr {
+  -webkit-box-sizing: content-box;
+          box-sizing: content-box;
+  /* 1 */
+  height: 0;
+  /* 1 */
+  overflow: visible;
+  /* 2 */
+}
+
+/**\r
+ * 1. Correct the inheritance and scaling of font size in all browsers.\r
+ * 2. Correct the odd `em` font sizing in all browsers.\r
+ */
+/* line 92, src/assets/scss/vendors/_normalize.scss */
+pre {
+  font-family: monospace, monospace;
+  /* 1 */
+  font-size: 1em;
+  /* 2 */
+}
+
+/* Text-level semantics\r
+   ========================================================================== */
+/**\r
+ * 1. Remove the gray background on active links in IE 10.\r
+ * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.\r
+ */
+/* line 105, src/assets/scss/vendors/_normalize.scss */
+a {
+  background-color: transparent;
+  /* 1 */
+  -webkit-text-decoration-skip: objects;
+  /* 2 */
+}
+
+/**\r
+ * Remove the outline on focused links when they are also active or hovered\r
+ * in all browsers (opinionated).\r
+ */
+/* line 115, src/assets/scss/vendors/_normalize.scss */
+a:active,
+a:hover {
+  outline-width: 0;
+}
+
+/**\r
+ * 1. Remove the bottom border in Firefox 39-.\r
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\r
+ */
+/* line 125, src/assets/scss/vendors/_normalize.scss */
+abbr[title] {
+  border-bottom: none;
+  /* 1 */
+  text-decoration: underline;
+  /* 2 */
+  -webkit-text-decoration: underline dotted;
+          text-decoration: underline dotted;
+  /* 2 */
+}
+
+/**\r
+ * Prevent the duplicate application of `bolder` by the next rule in Safari 6.\r
+ */
+/* line 135, src/assets/scss/vendors/_normalize.scss */
+b,
+strong {
+  font-weight: inherit;
+}
+
+/**\r
+ * Add the correct font weight in Chrome, Edge, and Safari.\r
+ */
+/* line 144, src/assets/scss/vendors/_normalize.scss */
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**\r
+ * 1. Correct the inheritance and scaling of font size in all browsers.\r
+ * 2. Correct the odd `em` font sizing in all browsers.\r
+ */
+/* line 154, src/assets/scss/vendors/_normalize.scss */
+code,
+kbd,
+samp {
+  font-family: monospace, monospace;
+  /* 1 */
+  font-size: 1em;
+  /* 2 */
+}
+
+/**\r
+ * Add the correct font style in Android 4.3-.\r
+ */
+/* line 165, src/assets/scss/vendors/_normalize.scss */
+dfn {
+  font-style: italic;
+}
+
+/**\r
+ * Add the correct background and color in IE 9-.\r
+ */
+/* line 173, src/assets/scss/vendors/_normalize.scss */
+mark {
+  background-color: #ff0;
+  color: #000;
+}
+
+/**\r
+ * Add the correct font size in all browsers.\r
+ */
+/* line 182, src/assets/scss/vendors/_normalize.scss */
+small {
+  font-size: 80%;
+}
+
+/**\r
+ * Prevent `sub` and `sup` elements from affecting the line height in\r
+ * all browsers.\r
+ */
+/* line 191, src/assets/scss/vendors/_normalize.scss */
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+/* line 199, src/assets/scss/vendors/_normalize.scss */
+sub {
+  bottom: -0.25em;
+}
+
+/* line 203, src/assets/scss/vendors/_normalize.scss */
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content\r
+   ========================================================================== */
+/**\r
+ * Add the correct display in IE 9-.\r
+ */
+/* line 214, src/assets/scss/vendors/_normalize.scss */
+audio,
+video {
+  display: inline-block;
+}
+
+/**\r
+ * Add the correct display in iOS 4-7.\r
+ */
+/* line 223, src/assets/scss/vendors/_normalize.scss */
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+/**\r
+ * Remove the border on images inside links in IE 10-.\r
+ */
+/* line 232, src/assets/scss/vendors/_normalize.scss */
+img {
+  border-style: none;
+}
+
+/**\r
+ * Hide the overflow in IE.\r
+ */
+/* line 240, src/assets/scss/vendors/_normalize.scss */
+svg:not(:root) {
+  overflow: hidden;
+}
+
+/* Forms\r
+   ========================================================================== */
+/**\r
+ * 1. Change the font styles in all browsers (opinionated).\r
+ * 2. Remove the margin in Firefox and Safari.\r
+ */
+/* line 252, src/assets/scss/vendors/_normalize.scss */
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: sans-serif;
+  /* 1 */
+  font-size: 100%;
+  /* 1 */
+  line-height: 1.15;
+  /* 1 */
+  margin: 0;
+  /* 2 */
+}
+
+/**\r
+ * Show the overflow in IE.\r
+ * 1. Show the overflow in Edge.\r
+ */
+/* line 268, src/assets/scss/vendors/_normalize.scss */
+button,
+input {
+  /* 1 */
+  overflow: visible;
+}
+
+/**\r
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.\r
+ * 1. Remove the inheritance of text transform in Firefox.\r
+ */
+/* line 278, src/assets/scss/vendors/_normalize.scss */
+button,
+select {
+  /* 1 */
+  text-transform: none;
+}
+
+/**\r
+ * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\r
+ *    controls in Android 4.\r
+ * 2. Correct the inability to style clickable types in iOS and Safari.\r
+ */
+/* line 289, src/assets/scss/vendors/_normalize.scss */
+button,
+html [type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+  /* 2 */
+}
+
+/**\r
+ * Remove the inner border and padding in Firefox.\r
+ */
+/* line 300, src/assets/scss/vendors/_normalize.scss */
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**\r
+ * Restore the focus styles unset by the previous rule.\r
+ */
+/* line 312, src/assets/scss/vendors/_normalize.scss */
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**\r
+ * Change the border, margin, and padding in all browsers (opinionated).\r
+ */
+/* line 323, src/assets/scss/vendors/_normalize.scss */
+fieldset {
+  border: 1px solid #c0c0c0;
+  margin: 0 2px;
+  padding: 0.35em 0.625em 0.75em;
+}
+
+/**\r
+ * 1. Correct the text wrapping in Edge and IE.\r
+ * 2. Correct the color inheritance from `fieldset` elements in IE.\r
+ * 3. Remove the padding so developers are not caught out when they zero out\r
+ *    `fieldset` elements in all browsers.\r
+ */
+/* line 336, src/assets/scss/vendors/_normalize.scss */
+legend {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  /* 1 */
+  color: inherit;
+  /* 2 */
+  display: table;
+  /* 1 */
+  max-width: 100%;
+  /* 1 */
+  padding: 0;
+  /* 3 */
+  white-space: normal;
+  /* 1 */
+}
+
+/**\r
+ * 1. Add the correct display in IE 9-.\r
+ * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.\r
+ */
+/* line 350, src/assets/scss/vendors/_normalize.scss */
+progress {
+  display: inline-block;
+  /* 1 */
+  vertical-align: baseline;
+  /* 2 */
+}
+
+/**\r
+ * Remove the default vertical scrollbar in IE.\r
+ */
+/* line 359, src/assets/scss/vendors/_normalize.scss */
+textarea {
+  overflow: auto;
+}
+
+/**\r
+ * 1. Add the correct box sizing in IE 10-.\r
+ * 2. Remove the padding in IE 10-.\r
+ */
+/* line 368, src/assets/scss/vendors/_normalize.scss */
+[type="checkbox"],
+[type="radio"] {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  /* 1 */
+  padding: 0;
+  /* 2 */
+}
+
+/**\r
+ * Correct the cursor style of increment and decrement buttons in Chrome.\r
+ */
+/* line 378, src/assets/scss/vendors/_normalize.scss */
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**\r
+ * 1. Correct the odd appearance in Chrome and Safari.\r
+ * 2. Correct the outline style in Safari.\r
+ */
+/* line 388, src/assets/scss/vendors/_normalize.scss */
+[type="search"] {
+  -webkit-appearance: textfield;
+  /* 1 */
+  outline-offset: -2px;
+  /* 2 */
+}
+
+/**\r
+ * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.\r
+ */
+/* line 397, src/assets/scss/vendors/_normalize.scss */
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**\r
+ * 1. Correct the inability to style clickable types in iOS and Safari.\r
+ * 2. Change font properties to `inherit` in Safari.\r
+ */
+/* line 407, src/assets/scss/vendors/_normalize.scss */
+::-webkit-file-upload-button {
+  -webkit-appearance: button;
+  /* 1 */
+  font: inherit;
+  /* 2 */
+}
+
+/* Interactive\r
+   ========================================================================== */
+/*\r
+ * Add the correct display in IE 9-.\r
+ * 1. Add the correct display in Edge, IE, and Firefox.\r
+ */
+/* line 420, src/assets/scss/vendors/_normalize.scss */
+details,
+menu {
+  display: block;
+}
+
+/*\r
+ * Add the correct display in all browsers.\r
+ */
+/* line 429, src/assets/scss/vendors/_normalize.scss */
+summary {
+  display: list-item;
+}
+
+/* Scripting\r
+   ========================================================================== */
+/**\r
+ * Add the correct display in IE 9-.\r
+ */
+/* line 440, src/assets/scss/vendors/_normalize.scss */
+canvas {
+  display: inline-block;
+}
+
+/**\r
+ * Add the correct display in IE.\r
+ */
+/* line 448, src/assets/scss/vendors/_normalize.scss */
+template {
+  display: none;
+}
+
+/* Hidden\r
+   ========================================================================== */
+/**\r
+ * Add the correct display in IE 10-.\r
+ */
+/* line 459, src/assets/scss/vendors/_normalize.scss */
+[hidden] {
+  display: none;
+}
+
+/*!
+ * Bootstrap v4.5.2 (https://getbootstrap.com/)
+ * Copyright 2011-2020 The Bootstrap Authors
+ * Copyright 2011-2020 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+/* line 2, node_modules/bootstrap/scss/_root.scss */
+:root {
+  --blue: #6fa7fd;
+  --indigo: #6610f2;
+  --purple: #6f42c1;
+  --pink: #e83e8c;
+  --red: #e54a19;
+  --orange: #fd7e14;
+  --yellow: #ffc107;
+  --green: #28a745;
+  --teal: #20c997;
+  --cyan: #17a2b8;
+  --white: #ffffff;
+  --gray: #6c757d;
+  --gray-dark: #343a40;
+  --primary: #464746;
+  --secondary: #f0ebe3;
+  --success: #f0ebe3;
+  --info: #464746;
+  --warning: #464746;
+  --danger: #e54a19;
+  --light: #f7f7f7;
+  --dark: #343a40;
+  --breakpoint-xs: 0;
+  --breakpoint-sm: 576px;
+  --breakpoint-md: 768px;
+  --breakpoint-lg: 1024px;
+  --breakpoint-xl: 1280px;
+  --breakpoint-xxl: 1440px;
+  --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+/* line 19, node_modules/bootstrap/scss/_reboot.scss */
+*,
+*::before,
+*::after {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+}
+
+/* line 25, node_modules/bootstrap/scss/_reboot.scss */
+html {
+  font-family: sans-serif;
+  line-height: 1.15;
+  -webkit-text-size-adjust: 100%;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+/* line 35, node_modules/bootstrap/scss/_reboot.scss */
+article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
+  display: block;
+}
+
+/* line 46, node_modules/bootstrap/scss/_reboot.scss */
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  font-size: 1rem;
+  font-weight: 300;
+  line-height: 1.5;
+  color: #464746;
+  text-align: left;
+  background-color: #ffffff;
+}
+
+/* line 66, node_modules/bootstrap/scss/_reboot.scss */
+[tabindex="-1"]:focus:not(:focus-visible) {
+  outline: 0 !important;
+}
+
+/* line 76, node_modules/bootstrap/scss/_reboot.scss */
+hr {
+  -webkit-box-sizing: content-box;
+          box-sizing: content-box;
+  height: 0;
+  overflow: visible;
+}
+
+/* line 92, node_modules/bootstrap/scss/_reboot.scss */
+h1, h2, h3, h4, h5, h6 {
+  margin-top: 0;
+  margin-bottom: 0.5rem;
+}
+
+/* line 101, node_modules/bootstrap/scss/_reboot.scss */
+p {
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+/* line 114, node_modules/bootstrap/scss/_reboot.scss */
+abbr[title],
+abbr[data-original-title] {
+  text-decoration: underline;
+  -webkit-text-decoration: underline dotted;
+          text-decoration: underline dotted;
+  cursor: help;
+  border-bottom: 0;
+  -webkit-text-decoration-skip-ink: none;
+          text-decoration-skip-ink: none;
+}
+
+/* line 123, node_modules/bootstrap/scss/_reboot.scss */
+address {
+  margin-bottom: 1rem;
+  font-style: normal;
+  line-height: inherit;
+}
+
+/* line 129, node_modules/bootstrap/scss/_reboot.scss */
+ol,
+ul,
+dl {
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+/* line 136, node_modules/bootstrap/scss/_reboot.scss */
+ol ol,
+ul ul,
+ol ul,
+ul ol {
+  margin-bottom: 0;
+}
+
+/* line 143, node_modules/bootstrap/scss/_reboot.scss */
+dt {
+  font-weight: 700;
+}
+
+/* line 147, node_modules/bootstrap/scss/_reboot.scss */
+dd {
+  margin-bottom: .5rem;
+  margin-left: 0;
+}
+
+/* line 152, node_modules/bootstrap/scss/_reboot.scss */
+blockquote {
+  margin: 0 0 1rem;
+}
+
+/* line 156, node_modules/bootstrap/scss/_reboot.scss */
+b,
+strong {
+  font-weight: 800;
+}
+
+/* line 161, node_modules/bootstrap/scss/_reboot.scss */
+small {
+  font-size: 80%;
+}
+
+/* line 170, node_modules/bootstrap/scss/_reboot.scss */
+sub,
+sup {
+  position: relative;
+  font-size: 75%;
+  line-height: 0;
+  vertical-align: baseline;
+}
+
+/* line 178, node_modules/bootstrap/scss/_reboot.scss */
+sub {
+  bottom: -.25em;
+}
+
+/* line 179, node_modules/bootstrap/scss/_reboot.scss */
+sup {
+  top: -.5em;
+}
+
+/* line 186, node_modules/bootstrap/scss/_reboot.scss */
+a {
+  color: #464746;
+  text-decoration: none;
+  background-color: transparent;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+a:hover {
+  color: #202020;
+  text-decoration: underline;
+}
+
+/* line 202, node_modules/bootstrap/scss/_reboot.scss */
+a:not([href]):not([class]) {
+  color: inherit;
+  text-decoration: none;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+a:not([href]):not([class]):hover {
+  color: inherit;
+  text-decoration: none;
+}
+
+/* line 217, node_modules/bootstrap/scss/_reboot.scss */
+pre,
+code,
+kbd,
+samp {
+  font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  font-size: 1em;
+}
+
+/* line 225, node_modules/bootstrap/scss/_reboot.scss */
+pre {
+  margin-top: 0;
+  margin-bottom: 1rem;
+  overflow: auto;
+  -ms-overflow-style: scrollbar;
+}
+
+/* line 242, node_modules/bootstrap/scss/_reboot.scss */
+figure {
+  margin: 0 0 1rem;
+}
+
+/* line 252, node_modules/bootstrap/scss/_reboot.scss */
+img {
+  vertical-align: middle;
+  border-style: none;
+}
+
+/* line 257, node_modules/bootstrap/scss/_reboot.scss */
+svg {
+  overflow: hidden;
+  vertical-align: middle;
+}
+
+/* line 269, node_modules/bootstrap/scss/_reboot.scss */
+table {
+  border-collapse: collapse;
+}
+
+/* line 273, node_modules/bootstrap/scss/_reboot.scss */
+caption {
+  padding-top: 0.75rem;
+  padding-bottom: 0.75rem;
+  color: #6c757d;
+  text-align: left;
+  caption-side: bottom;
+}
+
+/* line 281, node_modules/bootstrap/scss/_reboot.scss */
+th {
+  text-align: inherit;
+}
+
+/* line 292, node_modules/bootstrap/scss/_reboot.scss */
+label {
+  display: inline-block;
+  margin-bottom: 0.5rem;
+}
+
+/* line 301, node_modules/bootstrap/scss/_reboot.scss */
+button {
+  border-radius: 0;
+}
+
+/* line 310, node_modules/bootstrap/scss/_reboot.scss */
+button:focus {
+  outline: 1px dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+}
+
+/* line 315, node_modules/bootstrap/scss/_reboot.scss */
+input,
+button,
+select,
+optgroup,
+textarea {
+  margin: 0;
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+/* line 326, node_modules/bootstrap/scss/_reboot.scss */
+button,
+input {
+  overflow: visible;
+}
+
+/* line 331, node_modules/bootstrap/scss/_reboot.scss */
+button,
+select {
+  text-transform: none;
+}
+
+/* line 339, node_modules/bootstrap/scss/_reboot.scss */
+[role="button"] {
+  cursor: pointer;
+}
+
+/* line 346, node_modules/bootstrap/scss/_reboot.scss */
+select {
+  word-wrap: normal;
+}
+
+/* line 354, node_modules/bootstrap/scss/_reboot.scss */
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/* line 367, node_modules/bootstrap/scss/_reboot.scss */
+button:not(:disabled),
+[type="button"]:not(:disabled),
+[type="reset"]:not(:disabled),
+[type="submit"]:not(:disabled) {
+  cursor: pointer;
+}
+
+/* line 374, node_modules/bootstrap/scss/_reboot.scss */
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  padding: 0;
+  border-style: none;
+}
+
+/* line 382, node_modules/bootstrap/scss/_reboot.scss */
+input[type="radio"],
+input[type="checkbox"] {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  padding: 0;
+}
+
+/* line 389, node_modules/bootstrap/scss/_reboot.scss */
+textarea {
+  overflow: auto;
+  resize: vertical;
+}
+
+/* line 395, node_modules/bootstrap/scss/_reboot.scss */
+fieldset {
+  min-width: 0;
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+
+/* line 410, node_modules/bootstrap/scss/_reboot.scss */
+legend {
+  display: block;
+  width: 100%;
+  max-width: 100%;
+  padding: 0;
+  margin-bottom: .5rem;
+  font-size: 1.5rem;
+  line-height: inherit;
+  color: inherit;
+  white-space: normal;
+}
+
+/* line 422, node_modules/bootstrap/scss/_reboot.scss */
+progress {
+  vertical-align: baseline;
+}
+
+/* line 427, node_modules/bootstrap/scss/_reboot.scss */
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/* line 432, node_modules/bootstrap/scss/_reboot.scss */
+[type="search"] {
+  outline-offset: -2px;
+  -webkit-appearance: none;
+}
+
+/* line 445, node_modules/bootstrap/scss/_reboot.scss */
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/* line 454, node_modules/bootstrap/scss/_reboot.scss */
+::-webkit-file-upload-button {
+  font: inherit;
+  -webkit-appearance: button;
+}
+
+/* line 463, node_modules/bootstrap/scss/_reboot.scss */
+output {
+  display: inline-block;
+}
+
+/* line 467, node_modules/bootstrap/scss/_reboot.scss */
+summary {
+  display: list-item;
+  cursor: pointer;
+}
+
+/* line 472, node_modules/bootstrap/scss/_reboot.scss */
+template {
+  display: none;
+}
+
+/* line 478, node_modules/bootstrap/scss/_reboot.scss */
+[hidden] {
+  display: none !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/_type.scss */
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+  margin-bottom: 0.5rem;
+  font-weight: 500;
+  line-height: 1.2;
+}
+
+/* line 16, node_modules/bootstrap/scss/_type.scss */
+h1, .h1 {
+  font-size: 4rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/_type.scss */
+h2, .h2 {
+  font-size: 2.4375rem;
+}
+
+/* line 18, node_modules/bootstrap/scss/_type.scss */
+h3, .h3 {
+  font-size: 1.5rem;
+}
+
+/* line 19, node_modules/bootstrap/scss/_type.scss */
+h4, .h4 {
+  font-size: 0.9375rem;
+}
+
+/* line 20, node_modules/bootstrap/scss/_type.scss */
+h5, .h5 {
+  font-size: 0.75rem;
+}
+
+/* line 21, node_modules/bootstrap/scss/_type.scss */
+h6, .h6 {
+  font-size: 0.6875rem;
+}
+
+/* line 23, node_modules/bootstrap/scss/_type.scss */
+.lead {
+  font-size: 1.25rem;
+  font-weight: 300;
+}
+
+/* line 29, node_modules/bootstrap/scss/_type.scss */
+.display-1 {
+  font-size: 6rem;
+  font-weight: 300;
+  line-height: 1.2;
+}
+
+/* line 34, node_modules/bootstrap/scss/_type.scss */
+.display-2 {
+  font-size: 5.5rem;
+  font-weight: 300;
+  line-height: 1.2;
+}
+
+/* line 39, node_modules/bootstrap/scss/_type.scss */
+.display-3 {
+  font-size: 4.5rem;
+  font-weight: 300;
+  line-height: 1.2;
+}
+
+/* line 44, node_modules/bootstrap/scss/_type.scss */
+.display-4 {
+  font-size: 3.5rem;
+  font-weight: 300;
+  line-height: 1.2;
+}
+
+/* line 55, node_modules/bootstrap/scss/_type.scss */
+hr {
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+  border: 0;
+  border-top: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+/* line 67, node_modules/bootstrap/scss/_type.scss */
+small,
+.small {
+  font-size: 80%;
+  font-weight: 400;
+}
+
+/* line 73, node_modules/bootstrap/scss/_type.scss */
+mark,
+.mark {
+  padding: 0.2em;
+  background-color: #fcf8e3;
+}
+
+/* line 84, node_modules/bootstrap/scss/_type.scss */
+.list-unstyled {
+  padding-left: 0;
+  list-style: none;
+}
+
+/* line 89, node_modules/bootstrap/scss/_type.scss */
+.list-inline {
+  padding-left: 0;
+  list-style: none;
+}
+
+/* line 92, node_modules/bootstrap/scss/_type.scss */
+.list-inline-item {
+  display: inline-block;
+}
+
+/* line 95, node_modules/bootstrap/scss/_type.scss */
+.list-inline-item:not(:last-child) {
+  margin-right: 0.5rem;
+}
+
+/* line 106, node_modules/bootstrap/scss/_type.scss */
+.initialism {
+  font-size: 90%;
+  text-transform: uppercase;
+}
+
+/* line 112, node_modules/bootstrap/scss/_type.scss */
+.blockquote {
+  margin-bottom: 1rem;
+  font-size: 1.25rem;
+}
+
+/* line 117, node_modules/bootstrap/scss/_type.scss */
+.blockquote-footer {
+  display: block;
+  font-size: 80%;
+  color: #6c757d;
+}
+
+/* line 122, node_modules/bootstrap/scss/_type.scss */
+.blockquote-footer::before {
+  content: "\2014\00A0";
+}
+
+/* line 8, node_modules/bootstrap/scss/_images.scss */
+.img-fluid {
+  max-width: 100%;
+  height: auto;
+}
+
+/* line 14, node_modules/bootstrap/scss/_images.scss */
+.img-thumbnail {
+  padding: 0.25rem;
+  background-color: #ffffff;
+  border: 1px solid #dee2e6;
+  border-radius: 0.25rem;
+  max-width: 100%;
+  height: auto;
+}
+
+/* line 29, node_modules/bootstrap/scss/_images.scss */
+.figure {
+  display: inline-block;
+}
+
+/* line 34, node_modules/bootstrap/scss/_images.scss */
+.figure-img {
+  margin-bottom: 0.5rem;
+  line-height: 1;
+}
+
+/* line 39, node_modules/bootstrap/scss/_images.scss */
+.figure-caption {
+  font-size: 90%;
+  color: #6c757d;
+}
+
+/* line 2, node_modules/bootstrap/scss/_code.scss */
+code {
+  font-size: 87.5%;
+  color: #e83e8c;
+  word-wrap: break-word;
+}
+
+/* line 8, node_modules/bootstrap/scss/_code.scss */
+a > code {
+  color: inherit;
+}
+
+/* line 14, node_modules/bootstrap/scss/_code.scss */
+kbd {
+  padding: 0.2rem 0.4rem;
+  font-size: 87.5%;
+  color: #ffffff;
+  background-color: #464746;
+  border-radius: 0.2rem;
+}
+
+/* line 22, node_modules/bootstrap/scss/_code.scss */
+kbd kbd {
+  padding: 0;
+  font-size: 100%;
+  font-weight: 700;
+}
+
+/* line 31, node_modules/bootstrap/scss/_code.scss */
+pre {
+  display: block;
+  font-size: 87.5%;
+  color: #464746;
+}
+
+/* line 37, node_modules/bootstrap/scss/_code.scss */
+pre code {
+  font-size: inherit;
+  color: inherit;
+  word-break: normal;
+}
+
+/* line 45, node_modules/bootstrap/scss/_code.scss */
+.pre-scrollable {
+  max-height: 340px;
+  overflow-y: scroll;
+}
+
+/* line 7, node_modules/bootstrap/scss/_grid.scss */
+.container,
+.container-fluid,
+.container-sm,
+.container-md,
+.container-lg,
+.container-xl,
+.container-xxl {
+  width: 100%;
+  padding-right: 15px;
+  padding-left: 15px;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+@media (min-width: 576px) {
+  /* line 20, node_modules/bootstrap/scss/_grid.scss */
+  .container, .container-sm {
+    max-width: 100%;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 20, node_modules/bootstrap/scss/_grid.scss */
+  .container, .container-sm, .container-md {
+    max-width: 100%;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 20, node_modules/bootstrap/scss/_grid.scss */
+  .container, .container-sm, .container-md, .container-lg {
+    max-width: 100%;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 20, node_modules/bootstrap/scss/_grid.scss */
+  .container, .container-sm, .container-md, .container-lg, .container-xl {
+    max-width: 1150px;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 20, node_modules/bootstrap/scss/_grid.scss */
+  .container, .container-sm, .container-md, .container-lg, .container-xl, .container-xxl {
+    max-width: 1310px;
+  }
+}
+
+/* line 49, node_modules/bootstrap/scss/_grid.scss */
+.row {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  margin-right: -15px;
+  margin-left: -15px;
+}
+
+/* line 55, node_modules/bootstrap/scss/_grid.scss */
+.no-gutters {
+  margin-right: 0;
+  margin-left: 0;
+}
+
+/* line 59, node_modules/bootstrap/scss/_grid.scss */
+.no-gutters > .col,
+.no-gutters > [class*="col-"] {
+  padding-right: 0;
+  padding-left: 0;
+}
+
+/* line 8, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,
+.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,
+.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,
+.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,
+.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,
+.col-xl-auto, .col-xxl-1, .col-xxl-2, .col-xxl-3, .col-xxl-4, .col-xxl-5, .col-xxl-6, .col-xxl-7, .col-xxl-8, .col-xxl-9, .col-xxl-10, .col-xxl-11, .col-xxl-12, .col-xxl,
+.col-xxl-auto {
+  position: relative;
+  width: 100%;
+  padding-right: 15px;
+  padding-left: 15px;
+}
+
+/* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col {
+  -ms-flex-preferred-size: 0;
+      flex-basis: 0;
+  -webkit-box-flex: 1;
+      -ms-flex-positive: 1;
+          flex-grow: 1;
+  max-width: 100%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-1 > * {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 100%;
+          flex: 0 0 100%;
+  max-width: 100%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-2 > * {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 50%;
+          flex: 0 0 50%;
+  max-width: 50%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-3 > * {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 33.33333%;
+          flex: 0 0 33.33333%;
+  max-width: 33.33333%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-4 > * {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 25%;
+          flex: 0 0 25%;
+  max-width: 25%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-5 > * {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 20%;
+          flex: 0 0 20%;
+  max-width: 20%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-6 > * {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 16.66667%;
+          flex: 0 0 16.66667%;
+  max-width: 16.66667%;
+}
+
+/* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-auto {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 auto;
+          flex: 0 0 auto;
+  width: auto;
+  max-width: 100%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-1 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 8.33333%;
+          flex: 0 0 8.33333%;
+  max-width: 8.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-2 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 16.66667%;
+          flex: 0 0 16.66667%;
+  max-width: 16.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-3 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 25%;
+          flex: 0 0 25%;
+  max-width: 25%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-4 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 33.33333%;
+          flex: 0 0 33.33333%;
+  max-width: 33.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-5 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 41.66667%;
+          flex: 0 0 41.66667%;
+  max-width: 41.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-6 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 50%;
+          flex: 0 0 50%;
+  max-width: 50%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-7 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 58.33333%;
+          flex: 0 0 58.33333%;
+  max-width: 58.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-8 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 66.66667%;
+          flex: 0 0 66.66667%;
+  max-width: 66.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-9 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 75%;
+          flex: 0 0 75%;
+  max-width: 75%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-10 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 83.33333%;
+          flex: 0 0 83.33333%;
+  max-width: 83.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-11 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 91.66667%;
+          flex: 0 0 91.66667%;
+  max-width: 91.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-12 {
+  -webkit-box-flex: 0;
+      -ms-flex: 0 0 100%;
+          flex: 0 0 100%;
+  max-width: 100%;
+}
+
+/* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-first {
+  -webkit-box-ordinal-group: 0;
+      -ms-flex-order: -1;
+          order: -1;
+}
+
+/* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-last {
+  -webkit-box-ordinal-group: 14;
+      -ms-flex-order: 13;
+          order: 13;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-0 {
+  -webkit-box-ordinal-group: 1;
+      -ms-flex-order: 0;
+          order: 0;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-1 {
+  -webkit-box-ordinal-group: 2;
+      -ms-flex-order: 1;
+          order: 1;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-2 {
+  -webkit-box-ordinal-group: 3;
+      -ms-flex-order: 2;
+          order: 2;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-3 {
+  -webkit-box-ordinal-group: 4;
+      -ms-flex-order: 3;
+          order: 3;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-4 {
+  -webkit-box-ordinal-group: 5;
+      -ms-flex-order: 4;
+          order: 4;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-5 {
+  -webkit-box-ordinal-group: 6;
+      -ms-flex-order: 5;
+          order: 5;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-6 {
+  -webkit-box-ordinal-group: 7;
+      -ms-flex-order: 6;
+          order: 6;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-7 {
+  -webkit-box-ordinal-group: 8;
+      -ms-flex-order: 7;
+          order: 7;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-8 {
+  -webkit-box-ordinal-group: 9;
+      -ms-flex-order: 8;
+          order: 8;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-9 {
+  -webkit-box-ordinal-group: 10;
+      -ms-flex-order: 9;
+          order: 9;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-10 {
+  -webkit-box-ordinal-group: 11;
+      -ms-flex-order: 10;
+          order: 10;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-11 {
+  -webkit-box-ordinal-group: 12;
+      -ms-flex-order: 11;
+          order: 11;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-12 {
+  -webkit-box-ordinal-group: 13;
+      -ms-flex-order: 12;
+          order: 12;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-1 {
+  margin-left: 8.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-2 {
+  margin-left: 16.66667%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-3 {
+  margin-left: 25%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-4 {
+  margin-left: 33.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-5 {
+  margin-left: 41.66667%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-6 {
+  margin-left: 50%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-7 {
+  margin-left: 58.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-8 {
+  margin-left: 66.66667%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-9 {
+  margin-left: 75%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-10 {
+  margin-left: 83.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-11 {
+  margin-left: 91.66667%;
+}
+
+@media (min-width: 576px) {
+  /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm {
+    -ms-flex-preferred-size: 0;
+        flex-basis: 0;
+    -webkit-box-flex: 1;
+        -ms-flex-positive: 1;
+            flex-grow: 1;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-sm-1 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-sm-2 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-sm-3 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-sm-4 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-sm-5 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 20%;
+            flex: 0 0 20%;
+    max-width: 20%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-sm-6 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-auto {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 auto;
+            flex: 0 0 auto;
+    width: auto;
+    max-width: 100%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-1 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 8.33333%;
+            flex: 0 0 8.33333%;
+    max-width: 8.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-2 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-3 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-4 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-5 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 41.66667%;
+            flex: 0 0 41.66667%;
+    max-width: 41.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-6 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-7 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 58.33333%;
+            flex: 0 0 58.33333%;
+    max-width: 58.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-8 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 66.66667%;
+            flex: 0 0 66.66667%;
+    max-width: 66.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-9 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 75%;
+            flex: 0 0 75%;
+    max-width: 75%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-10 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 83.33333%;
+            flex: 0 0 83.33333%;
+    max-width: 83.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-11 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 91.66667%;
+            flex: 0 0 91.66667%;
+    max-width: 91.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-sm-12 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-first {
+    -webkit-box-ordinal-group: 0;
+        -ms-flex-order: -1;
+            order: -1;
+  }
+  /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-last {
+    -webkit-box-ordinal-group: 14;
+        -ms-flex-order: 13;
+            order: 13;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-0 {
+    -webkit-box-ordinal-group: 1;
+        -ms-flex-order: 0;
+            order: 0;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-1 {
+    -webkit-box-ordinal-group: 2;
+        -ms-flex-order: 1;
+            order: 1;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-2 {
+    -webkit-box-ordinal-group: 3;
+        -ms-flex-order: 2;
+            order: 2;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-3 {
+    -webkit-box-ordinal-group: 4;
+        -ms-flex-order: 3;
+            order: 3;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-4 {
+    -webkit-box-ordinal-group: 5;
+        -ms-flex-order: 4;
+            order: 4;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-5 {
+    -webkit-box-ordinal-group: 6;
+        -ms-flex-order: 5;
+            order: 5;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-6 {
+    -webkit-box-ordinal-group: 7;
+        -ms-flex-order: 6;
+            order: 6;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-7 {
+    -webkit-box-ordinal-group: 8;
+        -ms-flex-order: 7;
+            order: 7;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-8 {
+    -webkit-box-ordinal-group: 9;
+        -ms-flex-order: 8;
+            order: 8;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-9 {
+    -webkit-box-ordinal-group: 10;
+        -ms-flex-order: 9;
+            order: 9;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-10 {
+    -webkit-box-ordinal-group: 11;
+        -ms-flex-order: 10;
+            order: 10;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-11 {
+    -webkit-box-ordinal-group: 12;
+        -ms-flex-order: 11;
+            order: 11;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-sm-12 {
+    -webkit-box-ordinal-group: 13;
+        -ms-flex-order: 12;
+            order: 12;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-0 {
+    margin-left: 0;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-1 {
+    margin-left: 8.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-2 {
+    margin-left: 16.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-3 {
+    margin-left: 25%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-4 {
+    margin-left: 33.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-5 {
+    margin-left: 41.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-6 {
+    margin-left: 50%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-7 {
+    margin-left: 58.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-8 {
+    margin-left: 66.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-9 {
+    margin-left: 75%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-10 {
+    margin-left: 83.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-sm-11 {
+    margin-left: 91.66667%;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md {
+    -ms-flex-preferred-size: 0;
+        flex-basis: 0;
+    -webkit-box-flex: 1;
+        -ms-flex-positive: 1;
+            flex-grow: 1;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-md-1 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-md-2 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-md-3 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-md-4 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-md-5 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 20%;
+            flex: 0 0 20%;
+    max-width: 20%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-md-6 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-auto {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 auto;
+            flex: 0 0 auto;
+    width: auto;
+    max-width: 100%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-1 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 8.33333%;
+            flex: 0 0 8.33333%;
+    max-width: 8.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-2 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-3 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-4 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-5 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 41.66667%;
+            flex: 0 0 41.66667%;
+    max-width: 41.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-6 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-7 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 58.33333%;
+            flex: 0 0 58.33333%;
+    max-width: 58.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-8 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 66.66667%;
+            flex: 0 0 66.66667%;
+    max-width: 66.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-9 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 75%;
+            flex: 0 0 75%;
+    max-width: 75%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-10 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 83.33333%;
+            flex: 0 0 83.33333%;
+    max-width: 83.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-11 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 91.66667%;
+            flex: 0 0 91.66667%;
+    max-width: 91.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-md-12 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-first {
+    -webkit-box-ordinal-group: 0;
+        -ms-flex-order: -1;
+            order: -1;
+  }
+  /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-last {
+    -webkit-box-ordinal-group: 14;
+        -ms-flex-order: 13;
+            order: 13;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-0 {
+    -webkit-box-ordinal-group: 1;
+        -ms-flex-order: 0;
+            order: 0;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-1 {
+    -webkit-box-ordinal-group: 2;
+        -ms-flex-order: 1;
+            order: 1;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-2 {
+    -webkit-box-ordinal-group: 3;
+        -ms-flex-order: 2;
+            order: 2;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-3 {
+    -webkit-box-ordinal-group: 4;
+        -ms-flex-order: 3;
+            order: 3;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-4 {
+    -webkit-box-ordinal-group: 5;
+        -ms-flex-order: 4;
+            order: 4;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-5 {
+    -webkit-box-ordinal-group: 6;
+        -ms-flex-order: 5;
+            order: 5;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-6 {
+    -webkit-box-ordinal-group: 7;
+        -ms-flex-order: 6;
+            order: 6;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-7 {
+    -webkit-box-ordinal-group: 8;
+        -ms-flex-order: 7;
+            order: 7;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-8 {
+    -webkit-box-ordinal-group: 9;
+        -ms-flex-order: 8;
+            order: 8;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-9 {
+    -webkit-box-ordinal-group: 10;
+        -ms-flex-order: 9;
+            order: 9;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-10 {
+    -webkit-box-ordinal-group: 11;
+        -ms-flex-order: 10;
+            order: 10;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-11 {
+    -webkit-box-ordinal-group: 12;
+        -ms-flex-order: 11;
+            order: 11;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-md-12 {
+    -webkit-box-ordinal-group: 13;
+        -ms-flex-order: 12;
+            order: 12;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-0 {
+    margin-left: 0;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-1 {
+    margin-left: 8.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-2 {
+    margin-left: 16.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-3 {
+    margin-left: 25%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-4 {
+    margin-left: 33.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-5 {
+    margin-left: 41.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-6 {
+    margin-left: 50%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-7 {
+    margin-left: 58.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-8 {
+    margin-left: 66.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-9 {
+    margin-left: 75%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-10 {
+    margin-left: 83.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-md-11 {
+    margin-left: 91.66667%;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg {
+    -ms-flex-preferred-size: 0;
+        flex-basis: 0;
+    -webkit-box-flex: 1;
+        -ms-flex-positive: 1;
+            flex-grow: 1;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-lg-1 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-lg-2 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-lg-3 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-lg-4 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-lg-5 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 20%;
+            flex: 0 0 20%;
+    max-width: 20%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-lg-6 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-auto {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 auto;
+            flex: 0 0 auto;
+    width: auto;
+    max-width: 100%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-1 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 8.33333%;
+            flex: 0 0 8.33333%;
+    max-width: 8.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-2 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-3 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-4 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-5 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 41.66667%;
+            flex: 0 0 41.66667%;
+    max-width: 41.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-6 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-7 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 58.33333%;
+            flex: 0 0 58.33333%;
+    max-width: 58.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-8 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 66.66667%;
+            flex: 0 0 66.66667%;
+    max-width: 66.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-9 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 75%;
+            flex: 0 0 75%;
+    max-width: 75%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-10 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 83.33333%;
+            flex: 0 0 83.33333%;
+    max-width: 83.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-11 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 91.66667%;
+            flex: 0 0 91.66667%;
+    max-width: 91.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-lg-12 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-first {
+    -webkit-box-ordinal-group: 0;
+        -ms-flex-order: -1;
+            order: -1;
+  }
+  /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-last {
+    -webkit-box-ordinal-group: 14;
+        -ms-flex-order: 13;
+            order: 13;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-0 {
+    -webkit-box-ordinal-group: 1;
+        -ms-flex-order: 0;
+            order: 0;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-1 {
+    -webkit-box-ordinal-group: 2;
+        -ms-flex-order: 1;
+            order: 1;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-2 {
+    -webkit-box-ordinal-group: 3;
+        -ms-flex-order: 2;
+            order: 2;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-3 {
+    -webkit-box-ordinal-group: 4;
+        -ms-flex-order: 3;
+            order: 3;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-4 {
+    -webkit-box-ordinal-group: 5;
+        -ms-flex-order: 4;
+            order: 4;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-5 {
+    -webkit-box-ordinal-group: 6;
+        -ms-flex-order: 5;
+            order: 5;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-6 {
+    -webkit-box-ordinal-group: 7;
+        -ms-flex-order: 6;
+            order: 6;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-7 {
+    -webkit-box-ordinal-group: 8;
+        -ms-flex-order: 7;
+            order: 7;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-8 {
+    -webkit-box-ordinal-group: 9;
+        -ms-flex-order: 8;
+            order: 8;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-9 {
+    -webkit-box-ordinal-group: 10;
+        -ms-flex-order: 9;
+            order: 9;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-10 {
+    -webkit-box-ordinal-group: 11;
+        -ms-flex-order: 10;
+            order: 10;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-11 {
+    -webkit-box-ordinal-group: 12;
+        -ms-flex-order: 11;
+            order: 11;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-lg-12 {
+    -webkit-box-ordinal-group: 13;
+        -ms-flex-order: 12;
+            order: 12;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-0 {
+    margin-left: 0;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-1 {
+    margin-left: 8.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-2 {
+    margin-left: 16.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-3 {
+    margin-left: 25%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-4 {
+    margin-left: 33.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-5 {
+    margin-left: 41.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-6 {
+    margin-left: 50%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-7 {
+    margin-left: 58.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-8 {
+    margin-left: 66.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-9 {
+    margin-left: 75%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-10 {
+    margin-left: 83.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-lg-11 {
+    margin-left: 91.66667%;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl {
+    -ms-flex-preferred-size: 0;
+        flex-basis: 0;
+    -webkit-box-flex: 1;
+        -ms-flex-positive: 1;
+            flex-grow: 1;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xl-1 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xl-2 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xl-3 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xl-4 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xl-5 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 20%;
+            flex: 0 0 20%;
+    max-width: 20%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xl-6 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-auto {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 auto;
+            flex: 0 0 auto;
+    width: auto;
+    max-width: 100%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-1 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 8.33333%;
+            flex: 0 0 8.33333%;
+    max-width: 8.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-2 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-3 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-4 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-5 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 41.66667%;
+            flex: 0 0 41.66667%;
+    max-width: 41.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-6 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-7 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 58.33333%;
+            flex: 0 0 58.33333%;
+    max-width: 58.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-8 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 66.66667%;
+            flex: 0 0 66.66667%;
+    max-width: 66.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-9 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 75%;
+            flex: 0 0 75%;
+    max-width: 75%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-10 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 83.33333%;
+            flex: 0 0 83.33333%;
+    max-width: 83.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-11 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 91.66667%;
+            flex: 0 0 91.66667%;
+    max-width: 91.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xl-12 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-first {
+    -webkit-box-ordinal-group: 0;
+        -ms-flex-order: -1;
+            order: -1;
+  }
+  /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-last {
+    -webkit-box-ordinal-group: 14;
+        -ms-flex-order: 13;
+            order: 13;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-0 {
+    -webkit-box-ordinal-group: 1;
+        -ms-flex-order: 0;
+            order: 0;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-1 {
+    -webkit-box-ordinal-group: 2;
+        -ms-flex-order: 1;
+            order: 1;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-2 {
+    -webkit-box-ordinal-group: 3;
+        -ms-flex-order: 2;
+            order: 2;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-3 {
+    -webkit-box-ordinal-group: 4;
+        -ms-flex-order: 3;
+            order: 3;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-4 {
+    -webkit-box-ordinal-group: 5;
+        -ms-flex-order: 4;
+            order: 4;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-5 {
+    -webkit-box-ordinal-group: 6;
+        -ms-flex-order: 5;
+            order: 5;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-6 {
+    -webkit-box-ordinal-group: 7;
+        -ms-flex-order: 6;
+            order: 6;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-7 {
+    -webkit-box-ordinal-group: 8;
+        -ms-flex-order: 7;
+            order: 7;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-8 {
+    -webkit-box-ordinal-group: 9;
+        -ms-flex-order: 8;
+            order: 8;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-9 {
+    -webkit-box-ordinal-group: 10;
+        -ms-flex-order: 9;
+            order: 9;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-10 {
+    -webkit-box-ordinal-group: 11;
+        -ms-flex-order: 10;
+            order: 10;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-11 {
+    -webkit-box-ordinal-group: 12;
+        -ms-flex-order: 11;
+            order: 11;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xl-12 {
+    -webkit-box-ordinal-group: 13;
+        -ms-flex-order: 12;
+            order: 12;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-0 {
+    margin-left: 0;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-1 {
+    margin-left: 8.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-2 {
+    margin-left: 16.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-3 {
+    margin-left: 25%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-4 {
+    margin-left: 33.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-5 {
+    margin-left: 41.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-6 {
+    margin-left: 50%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-7 {
+    margin-left: 58.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-8 {
+    margin-left: 66.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-9 {
+    margin-left: 75%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-10 {
+    margin-left: 83.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xl-11 {
+    margin-left: 91.66667%;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl {
+    -ms-flex-preferred-size: 0;
+        flex-basis: 0;
+    -webkit-box-flex: 1;
+        -ms-flex-positive: 1;
+            flex-grow: 1;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xxl-1 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xxl-2 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xxl-3 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xxl-4 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xxl-5 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 20%;
+            flex: 0 0 20%;
+    max-width: 20%;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+  .row-cols-xxl-6 > * {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-auto {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 auto;
+            flex: 0 0 auto;
+    width: auto;
+    max-width: 100%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-1 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 8.33333%;
+            flex: 0 0 8.33333%;
+    max-width: 8.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-2 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 16.66667%;
+            flex: 0 0 16.66667%;
+    max-width: 16.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-3 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 25%;
+            flex: 0 0 25%;
+    max-width: 25%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-4 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 33.33333%;
+            flex: 0 0 33.33333%;
+    max-width: 33.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-5 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 41.66667%;
+            flex: 0 0 41.66667%;
+    max-width: 41.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-6 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 50%;
+            flex: 0 0 50%;
+    max-width: 50%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-7 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 58.33333%;
+            flex: 0 0 58.33333%;
+    max-width: 58.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-8 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 66.66667%;
+            flex: 0 0 66.66667%;
+    max-width: 66.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-9 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 75%;
+            flex: 0 0 75%;
+    max-width: 75%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-10 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 83.33333%;
+            flex: 0 0 83.33333%;
+    max-width: 83.33333%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-11 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 91.66667%;
+            flex: 0 0 91.66667%;
+    max-width: 91.66667%;
+  }
+  /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .col-xxl-12 {
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 100%;
+            flex: 0 0 100%;
+    max-width: 100%;
+  }
+  /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-first {
+    -webkit-box-ordinal-group: 0;
+        -ms-flex-order: -1;
+            order: -1;
+  }
+  /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-last {
+    -webkit-box-ordinal-group: 14;
+        -ms-flex-order: 13;
+            order: 13;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-0 {
+    -webkit-box-ordinal-group: 1;
+        -ms-flex-order: 0;
+            order: 0;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-1 {
+    -webkit-box-ordinal-group: 2;
+        -ms-flex-order: 1;
+            order: 1;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-2 {
+    -webkit-box-ordinal-group: 3;
+        -ms-flex-order: 2;
+            order: 2;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-3 {
+    -webkit-box-ordinal-group: 4;
+        -ms-flex-order: 3;
+            order: 3;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-4 {
+    -webkit-box-ordinal-group: 5;
+        -ms-flex-order: 4;
+            order: 4;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-5 {
+    -webkit-box-ordinal-group: 6;
+        -ms-flex-order: 5;
+            order: 5;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-6 {
+    -webkit-box-ordinal-group: 7;
+        -ms-flex-order: 6;
+            order: 6;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-7 {
+    -webkit-box-ordinal-group: 8;
+        -ms-flex-order: 7;
+            order: 7;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-8 {
+    -webkit-box-ordinal-group: 9;
+        -ms-flex-order: 8;
+            order: 8;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-9 {
+    -webkit-box-ordinal-group: 10;
+        -ms-flex-order: 9;
+            order: 9;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-10 {
+    -webkit-box-ordinal-group: 11;
+        -ms-flex-order: 10;
+            order: 10;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-11 {
+    -webkit-box-ordinal-group: 12;
+        -ms-flex-order: 11;
+            order: 11;
+  }
+  /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .order-xxl-12 {
+    -webkit-box-ordinal-group: 13;
+        -ms-flex-order: 12;
+            order: 12;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-0 {
+    margin-left: 0;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-1 {
+    margin-left: 8.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-2 {
+    margin-left: 16.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-3 {
+    margin-left: 25%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-4 {
+    margin-left: 33.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-5 {
+    margin-left: 41.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-6 {
+    margin-left: 50%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-7 {
+    margin-left: 58.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-8 {
+    margin-left: 66.66667%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-9 {
+    margin-left: 75%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-10 {
+    margin-left: 83.33333%;
+  }
+  /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+  .offset-xxl-11 {
+    margin-left: 91.66667%;
+  }
+}
+
+/* line 5, node_modules/bootstrap/scss/_tables.scss */
+.table {
+  width: 100%;
+  margin-bottom: 1rem;
+  color: #464746;
+}
+
+/* line 11, node_modules/bootstrap/scss/_tables.scss */
+.table th,
+.table td {
+  padding: 0.75rem;
+  vertical-align: top;
+  border-top: 1px solid #dee2e6;
+}
+
+/* line 18, node_modules/bootstrap/scss/_tables.scss */
+.table thead th {
+  vertical-align: bottom;
+  border-bottom: 2px solid #dee2e6;
+}
+
+/* line 23, node_modules/bootstrap/scss/_tables.scss */
+.table tbody + tbody {
+  border-top: 2px solid #dee2e6;
+}
+
+/* line 34, node_modules/bootstrap/scss/_tables.scss */
+.table-sm th,
+.table-sm td {
+  padding: 0.3rem;
+}
+
+/* line 45, node_modules/bootstrap/scss/_tables.scss */
+.table-bordered {
+  border: 1px solid #dee2e6;
+}
+
+/* line 48, node_modules/bootstrap/scss/_tables.scss */
+.table-bordered th,
+.table-bordered td {
+  border: 1px solid #dee2e6;
+}
+
+/* line 54, node_modules/bootstrap/scss/_tables.scss */
+.table-bordered thead th,
+.table-bordered thead td {
+  border-bottom-width: 2px;
+}
+
+/* line 62, node_modules/bootstrap/scss/_tables.scss */
+.table-borderless th,
+.table-borderless td,
+.table-borderless thead th,
+.table-borderless tbody + tbody {
+  border: 0;
+}
+
+/* line 75, node_modules/bootstrap/scss/_tables.scss */
+.table-striped tbody tr:nth-of-type(odd) {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover tbody tr:hover {
+  color: #464746;
+  background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-primary,
+.table-primary > th,
+.table-primary > td {
+  background-color: #cbcbcb;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-primary th,
+.table-primary td,
+.table-primary thead th,
+.table-primary tbody + tbody {
+  border-color: #9f9f9f;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-primary:hover {
+  background-color: #bebebe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-primary:hover > td,
+.table-hover .table-primary:hover > th {
+  background-color: #bebebe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-secondary,
+.table-secondary > th,
+.table-secondary > td {
+  background-color: #fbf9f7;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-secondary th,
+.table-secondary td,
+.table-secondary thead th,
+.table-secondary tbody + tbody {
+  border-color: #f7f5f0;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-secondary:hover {
+  background-color: #f3ece6;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-secondary:hover > td,
+.table-hover .table-secondary:hover > th {
+  background-color: #f3ece6;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-success,
+.table-success > th,
+.table-success > td {
+  background-color: #fbf9f7;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-success th,
+.table-success td,
+.table-success thead th,
+.table-success tbody + tbody {
+  border-color: #f7f5f0;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-success:hover {
+  background-color: #f3ece6;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-success:hover > td,
+.table-hover .table-success:hover > th {
+  background-color: #f3ece6;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-info,
+.table-info > th,
+.table-info > td {
+  background-color: #cbcbcb;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-info th,
+.table-info td,
+.table-info thead th,
+.table-info tbody + tbody {
+  border-color: #9f9f9f;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-info:hover {
+  background-color: #bebebe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-info:hover > td,
+.table-hover .table-info:hover > th {
+  background-color: #bebebe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-warning,
+.table-warning > th,
+.table-warning > td {
+  background-color: #cbcbcb;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-warning th,
+.table-warning td,
+.table-warning thead th,
+.table-warning tbody + tbody {
+  border-color: #9f9f9f;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-warning:hover {
+  background-color: #bebebe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-warning:hover > td,
+.table-hover .table-warning:hover > th {
+  background-color: #bebebe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-danger,
+.table-danger > th,
+.table-danger > td {
+  background-color: #f8ccbf;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-danger th,
+.table-danger td,
+.table-danger thead th,
+.table-danger tbody + tbody {
+  border-color: #f1a187;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-danger:hover {
+  background-color: #f5baa8;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-danger:hover > td,
+.table-hover .table-danger:hover > th {
+  background-color: #f5baa8;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-light,
+.table-light > th,
+.table-light > td {
+  background-color: #fdfdfd;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-light th,
+.table-light td,
+.table-light thead th,
+.table-light tbody + tbody {
+  border-color: #fbfbfb;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-light:hover {
+  background-color: #f0f0f0;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-light:hover > td,
+.table-hover .table-light:hover > th {
+  background-color: #f0f0f0;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-dark,
+.table-dark > th,
+.table-dark > td {
+  background-color: #c6c8ca;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-dark th,
+.table-dark td,
+.table-dark thead th,
+.table-dark tbody + tbody {
+  border-color: #95999c;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-dark:hover {
+  background-color: #b9bbbe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-dark:hover > td,
+.table-hover .table-dark:hover > th {
+  background-color: #b9bbbe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-active,
+.table-active > th,
+.table-active > td {
+  background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-active:hover {
+  background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-active:hover > td,
+.table-hover .table-active:hover > th {
+  background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 114, node_modules/bootstrap/scss/_tables.scss */
+.table .thead-dark th {
+  color: #ffffff;
+  background-color: #343a40;
+  border-color: #454d55;
+}
+
+/* line 122, node_modules/bootstrap/scss/_tables.scss */
+.table .thead-light th {
+  color: #495057;
+  background-color: #eaebea;
+  border-color: #dee2e6;
+}
+
+/* line 130, node_modules/bootstrap/scss/_tables.scss */
+.table-dark {
+  color: #ffffff;
+  background-color: #343a40;
+}
+
+/* line 134, node_modules/bootstrap/scss/_tables.scss */
+.table-dark th,
+.table-dark td,
+.table-dark thead th {
+  border-color: #454d55;
+}
+
+/* line 140, node_modules/bootstrap/scss/_tables.scss */
+.table-dark.table-bordered {
+  border: 0;
+}
+
+/* line 145, node_modules/bootstrap/scss/_tables.scss */
+.table-dark.table-striped tbody tr:nth-of-type(odd) {
+  background-color: rgba(255, 255, 255, 0.05);
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-dark.table-hover tbody tr:hover {
+  color: #ffffff;
+  background-color: rgba(255, 255, 255, 0.075);
+}
+
+@media (max-width: 575.98px) {
+  /* line 171, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-sm {
+    display: block;
+    width: 100%;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+  /* line 179, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-sm > .table-bordered {
+    border: 0;
+  }
+}
+
+@media (max-width: 767.98px) {
+  /* line 171, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-md {
+    display: block;
+    width: 100%;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+  /* line 179, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-md > .table-bordered {
+    border: 0;
+  }
+}
+
+@media (max-width: 1023.98px) {
+  /* line 171, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-lg {
+    display: block;
+    width: 100%;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+  /* line 179, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-lg > .table-bordered {
+    border: 0;
+  }
+}
+
+@media (max-width: 1279.98px) {
+  /* line 171, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-xl {
+    display: block;
+    width: 100%;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+  /* line 179, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-xl > .table-bordered {
+    border: 0;
+  }
+}
+
+@media (max-width: 1439.98px) {
+  /* line 171, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-xxl {
+    display: block;
+    width: 100%;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+  /* line 179, node_modules/bootstrap/scss/_tables.scss */
+  .table-responsive-xxl > .table-bordered {
+    border: 0;
+  }
+}
+
+/* line 171, node_modules/bootstrap/scss/_tables.scss */
+.table-responsive {
+  display: block;
+  width: 100%;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+/* line 179, node_modules/bootstrap/scss/_tables.scss */
+.table-responsive > .table-bordered {
+  border: 0;
+}
+
+/* line 7, node_modules/bootstrap/scss/_forms.scss */
+.form-control {
+  display: block;
+  width: 100%;
+  height: calc(1.5em + 0.75rem + 2px);
+  padding: 0.375rem 0.75rem;
+  font-size: 1rem;
+  font-weight: 300;
+  line-height: 1.5;
+  color: #495057;
+  background-color: #ffffff;
+  background-clip: padding-box;
+  border: 1px solid #ced4da;
+  border-radius: 0.25rem;
+  -webkit-transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 7, node_modules/bootstrap/scss/_forms.scss */
+  .form-control {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 28, node_modules/bootstrap/scss/_forms.scss */
+.form-control::-ms-expand {
+  background-color: transparent;
+  border: 0;
+}
+
+/* line 34, node_modules/bootstrap/scss/_forms.scss */
+.form-control:-moz-focusring {
+  color: transparent;
+  text-shadow: 0 0 0 #495057;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_forms.scss */
+.form-control:focus {
+  color: #495057;
+  background-color: #ffffff;
+  border-color: #858785;
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 43, node_modules/bootstrap/scss/_forms.scss */
+.form-control::-webkit-input-placeholder {
+  color: #6c757d;
+  opacity: 1;
+}
+.form-control::-moz-placeholder {
+  color: #6c757d;
+  opacity: 1;
+}
+.form-control:-ms-input-placeholder {
+  color: #6c757d;
+  opacity: 1;
+}
+.form-control::-ms-input-placeholder {
+  color: #6c757d;
+  opacity: 1;
+}
+.form-control::placeholder {
+  color: #6c757d;
+  opacity: 1;
+}
+
+/* line 54, node_modules/bootstrap/scss/_forms.scss */
+.form-control:disabled, .form-control[readonly] {
+  background-color: #eaebea;
+  opacity: 1;
+}
+
+/* line 66, node_modules/bootstrap/scss/_forms.scss */
+input[type="date"].form-control,
+input[type="time"].form-control,
+input[type="datetime-local"].form-control,
+input[type="month"].form-control {
+  -webkit-appearance: none;
+     -moz-appearance: none;
+          appearance: none;
+}
+
+/* line 72, node_modules/bootstrap/scss/_forms.scss */
+select.form-control:focus::-ms-value {
+  color: #495057;
+  background-color: #ffffff;
+}
+
+/* line 84, node_modules/bootstrap/scss/_forms.scss */
+.form-control-file,
+.form-control-range {
+  display: block;
+  width: 100%;
+}
+
+/* line 97, node_modules/bootstrap/scss/_forms.scss */
+.col-form-label {
+  padding-top: calc(0.375rem + 1px);
+  padding-bottom: calc(0.375rem + 1px);
+  margin-bottom: 0;
+  font-size: inherit;
+  line-height: 1.5;
+}
+
+/* line 105, node_modules/bootstrap/scss/_forms.scss */
+.col-form-label-lg {
+  padding-top: calc(0.5rem + 1px);
+  padding-bottom: calc(0.5rem + 1px);
+  font-size: 1.25rem;
+  line-height: 1.5;
+}
+
+/* line 112, node_modules/bootstrap/scss/_forms.scss */
+.col-form-label-sm {
+  padding-top: calc(0.25rem + 1px);
+  padding-bottom: calc(0.25rem + 1px);
+  font-size: 0.875rem;
+  line-height: 1.5;
+}
+
+/* line 125, node_modules/bootstrap/scss/_forms.scss */
+.form-control-plaintext {
+  display: block;
+  width: 100%;
+  padding: 0.375rem 0;
+  margin-bottom: 0;
+  font-size: 1rem;
+  line-height: 1.5;
+  color: #464746;
+  background-color: transparent;
+  border: solid transparent;
+  border-width: 1px 0;
+}
+
+/* line 137, node_modules/bootstrap/scss/_forms.scss */
+.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {
+  padding-right: 0;
+  padding-left: 0;
+}
+
+/* line 152, node_modules/bootstrap/scss/_forms.scss */
+.form-control-sm {
+  height: calc(1.5em + 0.5rem + 2px);
+  padding: 0.25rem 0.5rem;
+  font-size: 0.875rem;
+  line-height: 1.5;
+  border-radius: 0.2rem;
+}
+
+/* line 160, node_modules/bootstrap/scss/_forms.scss */
+.form-control-lg {
+  height: calc(1.5em + 1rem + 2px);
+  padding: 0.5rem 1rem;
+  font-size: 1.25rem;
+  line-height: 1.5;
+  border-radius: 0.3rem;
+}
+
+/* line 170, node_modules/bootstrap/scss/_forms.scss */
+select.form-control[size], select.form-control[multiple] {
+  height: auto;
+}
+
+/* line 176, node_modules/bootstrap/scss/_forms.scss */
+textarea.form-control {
+  height: auto;
+}
+
+/* line 185, node_modules/bootstrap/scss/_forms.scss */
+.form-group {
+  margin-bottom: 1rem;
+}
+
+/* line 189, node_modules/bootstrap/scss/_forms.scss */
+.form-text {
+  display: block;
+  margin-top: 0.25rem;
+}
+
+/* line 199, node_modules/bootstrap/scss/_forms.scss */
+.form-row {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  margin-right: -5px;
+  margin-left: -5px;
+}
+
+/* line 205, node_modules/bootstrap/scss/_forms.scss */
+.form-row > .col,
+.form-row > [class*="col-"] {
+  padding-right: 5px;
+  padding-left: 5px;
+}
+
+/* line 217, node_modules/bootstrap/scss/_forms.scss */
+.form-check {
+  position: relative;
+  display: block;
+  padding-left: 1.25rem;
+}
+
+/* line 223, node_modules/bootstrap/scss/_forms.scss */
+.form-check-input {
+  position: absolute;
+  margin-top: 0.3rem;
+  margin-left: -1.25rem;
+}
+
+/* line 229, node_modules/bootstrap/scss/_forms.scss */
+.form-check-input[disabled] ~ .form-check-label,
+.form-check-input:disabled ~ .form-check-label {
+  color: #6c757d;
+}
+
+/* line 235, node_modules/bootstrap/scss/_forms.scss */
+.form-check-label {
+  margin-bottom: 0;
+}
+
+/* line 239, node_modules/bootstrap/scss/_forms.scss */
+.form-check-inline {
+  display: -webkit-inline-box;
+  display: -ms-inline-flexbox;
+  display: inline-flex;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  padding-left: 0;
+  margin-right: 0.75rem;
+}
+
+/* line 246, node_modules/bootstrap/scss/_forms.scss */
+.form-check-inline .form-check-input {
+  position: static;
+  margin-top: 0;
+  margin-right: 0.3125rem;
+  margin-left: 0;
+}
+
+/* line 45, node_modules/bootstrap/scss/mixins/_forms.scss */
+.valid-feedback {
+  display: none;
+  width: 100%;
+  margin-top: 0.25rem;
+  font-size: 80%;
+  color: #f0ebe3;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_forms.scss */
+.valid-tooltip {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 5;
+  display: none;
+  max-width: 100%;
+  padding: 0.25rem 0.5rem;
+  margin-top: .1rem;
+  font-size: 0.875rem;
+  line-height: 1.5;
+  color: #464746;
+  background-color: rgba(240, 235, 227, 0.9);
+  border-radius: 0.25rem;
+}
+
+/* line 70, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated :valid ~ .valid-feedback,
+.was-validated :valid ~ .valid-tooltip,
+.is-valid ~ .valid-feedback,
+.is-valid ~ .valid-tooltip {
+  display: block;
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:valid, .form-control.is-valid {
+  border-color: #f0ebe3;
+  padding-right: calc(1.5em + 0.75rem);
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23f0ebe3' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+  background-repeat: no-repeat;
+  background-position: right calc(0.375em + 0.1875rem) center;
+  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 88, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:valid:focus, .form-control.is-valid:focus {
+  border-color: #f0ebe3;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated textarea.form-control:valid, textarea.form-control.is-valid {
+  padding-right: calc(1.5em + 0.75rem);
+  background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:valid, .custom-select.is-valid {
+  border-color: #f0ebe3;
+  padding-right: calc(0.75em + 2.3125rem);
+  background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23f0ebe3' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #ffffff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 114, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {
+  border-color: #f0ebe3;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 123, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {
+  color: #f0ebe3;
+}
+
+/* line 127, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:valid ~ .valid-feedback,
+.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,
+.form-check-input.is-valid ~ .valid-tooltip {
+  display: block;
+}
+
+/* line 136, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {
+  color: #f0ebe3;
+}
+
+/* line 139, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {
+  border-color: #f0ebe3;
+}
+
+/* line 145, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {
+  border-color: white;
+  background-color: white;
+}
+
+/* line 152, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 156, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {
+  border-color: #f0ebe3;
+}
+
+/* line 166, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {
+  border-color: #f0ebe3;
+}
+
+/* line 171, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {
+  border-color: #f0ebe3;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 45, node_modules/bootstrap/scss/mixins/_forms.scss */
+.invalid-feedback {
+  display: none;
+  width: 100%;
+  margin-top: 0.25rem;
+  font-size: 80%;
+  color: #e54a19;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_forms.scss */
+.invalid-tooltip {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 5;
+  display: none;
+  max-width: 100%;
+  padding: 0.25rem 0.5rem;
+  margin-top: .1rem;
+  font-size: 0.875rem;
+  line-height: 1.5;
+  color: #ffffff;
+  background-color: rgba(229, 74, 25, 0.9);
+  border-radius: 0.25rem;
+}
+
+/* line 70, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated :invalid ~ .invalid-feedback,
+.was-validated :invalid ~ .invalid-tooltip,
+.is-invalid ~ .invalid-feedback,
+.is-invalid ~ .invalid-tooltip {
+  display: block;
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:invalid, .form-control.is-invalid {
+  border-color: #e54a19;
+  padding-right: calc(1.5em + 0.75rem);
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23e54a19' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23e54a19' stroke='none'/%3e%3c/svg%3e");
+  background-repeat: no-repeat;
+  background-position: right calc(0.375em + 0.1875rem) center;
+  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 88, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {
+  border-color: #e54a19;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {
+  padding-right: calc(1.5em + 0.75rem);
+  background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:invalid, .custom-select.is-invalid {
+  border-color: #e54a19;
+  padding-right: calc(0.75em + 2.3125rem);
+  background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23e54a19' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23e54a19' stroke='none'/%3e%3c/svg%3e") #ffffff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 114, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {
+  border-color: #e54a19;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 123, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {
+  color: #e54a19;
+}
+
+/* line 127, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:invalid ~ .invalid-feedback,
+.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,
+.form-check-input.is-invalid ~ .invalid-tooltip {
+  display: block;
+}
+
+/* line 136, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {
+  color: #e54a19;
+}
+
+/* line 139, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {
+  border-color: #e54a19;
+}
+
+/* line 145, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {
+  border-color: #eb6e46;
+  background-color: #eb6e46;
+}
+
+/* line 152, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 156, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {
+  border-color: #e54a19;
+}
+
+/* line 166, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {
+  border-color: #e54a19;
+}
+
+/* line 171, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {
+  border-color: #e54a19;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 275, node_modules/bootstrap/scss/_forms.scss */
+.form-inline {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: horizontal;
+  -webkit-box-direction: normal;
+      -ms-flex-flow: row wrap;
+          flex-flow: row wrap;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+}
+
+/* line 283, node_modules/bootstrap/scss/_forms.scss */
+.form-inline .form-check {
+  width: 100%;
+}
+
+@media (min-width: 576px) {
+  /* line 289, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline label {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-align: center;
+        -ms-flex-align: center;
+            align-items: center;
+    -webkit-box-pack: center;
+        -ms-flex-pack: center;
+            justify-content: center;
+    margin-bottom: 0;
+  }
+  /* line 297, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .form-group {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-flex: 0;
+        -ms-flex: 0 0 auto;
+            flex: 0 0 auto;
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row wrap;
+            flex-flow: row wrap;
+    -webkit-box-align: center;
+        -ms-flex-align: center;
+            align-items: center;
+    margin-bottom: 0;
+  }
+  /* line 306, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .form-control {
+    display: inline-block;
+    width: auto;
+    vertical-align: middle;
+  }
+  /* line 313, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .form-control-plaintext {
+    display: inline-block;
+  }
+  /* line 317, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .input-group,
+  .form-inline .custom-select {
+    width: auto;
+  }
+  /* line 324, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .form-check {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-align: center;
+        -ms-flex-align: center;
+            align-items: center;
+    -webkit-box-pack: center;
+        -ms-flex-pack: center;
+            justify-content: center;
+    width: auto;
+    padding-left: 0;
+  }
+  /* line 331, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .form-check-input {
+    position: relative;
+    -ms-flex-negative: 0;
+        flex-shrink: 0;
+    margin-top: 0;
+    margin-right: 0.25rem;
+    margin-left: 0;
+  }
+  /* line 339, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .custom-control {
+    -webkit-box-align: center;
+        -ms-flex-align: center;
+            align-items: center;
+    -webkit-box-pack: center;
+        -ms-flex-pack: center;
+            justify-content: center;
+  }
+  /* line 343, node_modules/bootstrap/scss/_forms.scss */
+  .form-inline .custom-control-label {
+    margin-bottom: 0;
+  }
+}
+
+/* line 7, node_modules/bootstrap/scss/_buttons.scss */
+.btn {
+  display: inline-block;
+  font-weight: 400;
+  color: #464746;
+  text-align: center;
+  vertical-align: middle;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+  background-color: transparent;
+  border: 1px solid transparent;
+  padding: 0.375rem 0.75rem;
+  font-size: 1rem;
+  line-height: 1.5;
+  border-radius: 0.25rem;
+  -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 7, node_modules/bootstrap/scss/_buttons.scss */
+  .btn {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn:hover {
+  color: #464746;
+  text-decoration: none;
+}
+
+/* line 27, node_modules/bootstrap/scss/_buttons.scss */
+.btn:focus, .btn.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 34, node_modules/bootstrap/scss/_buttons.scss */
+.btn.disabled, .btn:disabled {
+  opacity: 0.65;
+}
+
+/* line 40, node_modules/bootstrap/scss/_buttons.scss */
+.btn:not(:disabled):not(.disabled) {
+  cursor: pointer;
+}
+
+/* line 55, node_modules/bootstrap/scss/_buttons.scss */
+a.btn.disabled,
+fieldset:disabled a.btn {
+  pointer-events: none;
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-primary {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-primary:hover {
+  color: #ffffff;
+  background-color: #333433;
+  border-color: #2d2d2d;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary:focus, .btn-primary.focus {
+  color: #ffffff;
+  background-color: #333433;
+  border-color: #2d2d2d;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary.disabled, .btn-primary:disabled {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,
+.show > .btn-primary.dropdown-toggle {
+  color: #ffffff;
+  background-color: #2d2d2d;
+  border-color: #262726;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-primary.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-secondary {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-secondary:hover {
+  color: #464746;
+  background-color: #e3d9ca;
+  border-color: #ded3c2;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary:focus, .btn-secondary.focus {
+  color: #464746;
+  background-color: #e3d9ca;
+  border-color: #ded3c2;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary.disabled, .btn-secondary:disabled {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,
+.show > .btn-secondary.dropdown-toggle {
+  color: #464746;
+  background-color: #ded3c2;
+  border-color: #dacdb9;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-secondary.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-success {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-success:hover {
+  color: #464746;
+  background-color: #e3d9ca;
+  border-color: #ded3c2;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success:focus, .btn-success.focus {
+  color: #464746;
+  background-color: #e3d9ca;
+  border-color: #ded3c2;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success.disabled, .btn-success:disabled {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,
+.show > .btn-success.dropdown-toggle {
+  color: #464746;
+  background-color: #ded3c2;
+  border-color: #dacdb9;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,
+.show > .btn-success.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-info {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-info:hover {
+  color: #ffffff;
+  background-color: #333433;
+  border-color: #2d2d2d;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info:focus, .btn-info.focus {
+  color: #ffffff;
+  background-color: #333433;
+  border-color: #2d2d2d;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info.disabled, .btn-info:disabled {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,
+.show > .btn-info.dropdown-toggle {
+  color: #ffffff;
+  background-color: #2d2d2d;
+  border-color: #262726;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,
+.show > .btn-info.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-warning {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-warning:hover {
+  color: #ffffff;
+  background-color: #333433;
+  border-color: #2d2d2d;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning:focus, .btn-warning.focus {
+  color: #ffffff;
+  background-color: #333433;
+  border-color: #2d2d2d;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning.disabled, .btn-warning:disabled {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,
+.show > .btn-warning.dropdown-toggle {
+  color: #ffffff;
+  background-color: #2d2d2d;
+  border-color: #262726;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,
+.show > .btn-warning.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-danger {
+  color: #ffffff;
+  background-color: #e54a19;
+  border-color: #e54a19;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-danger:hover {
+  color: #ffffff;
+  background-color: #c33f15;
+  border-color: #b73b14;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger:focus, .btn-danger.focus {
+  color: #ffffff;
+  background-color: #c33f15;
+  border-color: #b73b14;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger.disabled, .btn-danger:disabled {
+  color: #ffffff;
+  background-color: #e54a19;
+  border-color: #e54a19;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,
+.show > .btn-danger.dropdown-toggle {
+  color: #ffffff;
+  background-color: #b73b14;
+  border-color: #ac3713;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,
+.show > .btn-danger.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-light {
+  color: #464746;
+  background-color: #f7f7f7;
+  border-color: #f7f7f7;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-light:hover {
+  color: #464746;
+  background-color: #e4e4e4;
+  border-color: #dedede;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light:focus, .btn-light.focus {
+  color: #464746;
+  background-color: #e4e4e4;
+  border-color: #dedede;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light.disabled, .btn-light:disabled {
+  color: #464746;
+  background-color: #f7f7f7;
+  border-color: #f7f7f7;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,
+.show > .btn-light.dropdown-toggle {
+  color: #464746;
+  background-color: #dedede;
+  border-color: #d7d7d7;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,
+.show > .btn-light.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-dark {
+  color: #ffffff;
+  background-color: #343a40;
+  border-color: #343a40;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-dark:hover {
+  color: #ffffff;
+  background-color: #23272b;
+  border-color: #1d2124;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark:focus, .btn-dark.focus {
+  color: #ffffff;
+  background-color: #23272b;
+  border-color: #1d2124;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark.disabled, .btn-dark:disabled {
+  color: #ffffff;
+  background-color: #343a40;
+  border-color: #343a40;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,
+.show > .btn-dark.dropdown-toggle {
+  color: #ffffff;
+  background-color: #1d2124;
+  border-color: #171a1d;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,
+.show > .btn-dark.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-primary {
+  color: #464746;
+  border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-primary:hover {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary:focus, .btn-outline-primary.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary.disabled, .btn-outline-primary:disabled {
+  color: #464746;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,
+.show > .btn-outline-primary.dropdown-toggle {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-primary.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-secondary {
+  color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-secondary:hover {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary:focus, .btn-outline-secondary.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {
+  color: #f0ebe3;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,
+.show > .btn-outline-secondary.dropdown-toggle {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-secondary.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-success {
+  color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-success:hover {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success:focus, .btn-outline-success.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success.disabled, .btn-outline-success:disabled {
+  color: #f0ebe3;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,
+.show > .btn-outline-success.dropdown-toggle {
+  color: #464746;
+  background-color: #f0ebe3;
+  border-color: #f0ebe3;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-success.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-info {
+  color: #464746;
+  border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-info:hover {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info:focus, .btn-outline-info.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info.disabled, .btn-outline-info:disabled {
+  color: #464746;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,
+.show > .btn-outline-info.dropdown-toggle {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-info.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-warning {
+  color: #464746;
+  border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-warning:hover {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning:focus, .btn-outline-warning.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning.disabled, .btn-outline-warning:disabled {
+  color: #464746;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,
+.show > .btn-outline-warning.dropdown-toggle {
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-warning.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-danger {
+  color: #e54a19;
+  border-color: #e54a19;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-danger:hover {
+  color: #ffffff;
+  background-color: #e54a19;
+  border-color: #e54a19;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger:focus, .btn-outline-danger.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger.disabled, .btn-outline-danger:disabled {
+  color: #e54a19;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,
+.show > .btn-outline-danger.dropdown-toggle {
+  color: #ffffff;
+  background-color: #e54a19;
+  border-color: #e54a19;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-danger.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-light {
+  color: #f7f7f7;
+  border-color: #f7f7f7;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-light:hover {
+  color: #464746;
+  background-color: #f7f7f7;
+  border-color: #f7f7f7;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light:focus, .btn-outline-light.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light.disabled, .btn-outline-light:disabled {
+  color: #f7f7f7;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,
+.show > .btn-outline-light.dropdown-toggle {
+  color: #464746;
+  background-color: #f7f7f7;
+  border-color: #f7f7f7;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-light.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-dark {
+  color: #343a40;
+  border-color: #343a40;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-dark:hover {
+  color: #ffffff;
+  background-color: #343a40;
+  border-color: #343a40;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark:focus, .btn-outline-dark.focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark.disabled, .btn-outline-dark:disabled {
+  color: #343a40;
+  background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,
+.show > .btn-outline-dark.dropdown-toggle {
+  color: #ffffff;
+  background-color: #343a40;
+  border-color: #343a40;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-dark.dropdown-toggle:focus {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+/* line 83, node_modules/bootstrap/scss/_buttons.scss */
+.btn-link {
+  font-weight: 400;
+  color: #464746;
+  text-decoration: none;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-link:hover {
+  color: #202020;
+  text-decoration: underline;
+}
+
+/* line 93, node_modules/bootstrap/scss/_buttons.scss */
+.btn-link:focus, .btn-link.focus {
+  text-decoration: underline;
+}
+
+/* line 98, node_modules/bootstrap/scss/_buttons.scss */
+.btn-link:disabled, .btn-link.disabled {
+  color: #6c757d;
+  pointer-events: none;
+}
+
+/* line 112, node_modules/bootstrap/scss/_buttons.scss */
+.btn-lg, .btn-group-lg > .btn {
+  padding: 0.5rem 1rem;
+  font-size: 1.25rem;
+  line-height: 1.5;
+  border-radius: 0.3rem;
+}
+
+/* line 116, node_modules/bootstrap/scss/_buttons.scss */
+.btn-sm, .btn-group-sm > .btn {
+  padding: 0.25rem 0.5rem;
+  font-size: 0.875rem;
+  line-height: 1.5;
+  border-radius: 0.2rem;
+}
+
+/* line 125, node_modules/bootstrap/scss/_buttons.scss */
+.btn-block {
+  display: block;
+  width: 100%;
+}
+
+/* line 130, node_modules/bootstrap/scss/_buttons.scss */
+.btn-block + .btn-block {
+  margin-top: 0.5rem;
+}
+
+/* line 139, node_modules/bootstrap/scss/_buttons.scss */
+input[type="submit"].btn-block,
+input[type="reset"].btn-block,
+input[type="button"].btn-block {
+  width: 100%;
+}
+
+/* line 1, node_modules/bootstrap/scss/_transitions.scss */
+.fade {
+  -webkit-transition: opacity 0.15s linear;
+  transition: opacity 0.15s linear;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 1, node_modules/bootstrap/scss/_transitions.scss */
+  .fade {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 4, node_modules/bootstrap/scss/_transitions.scss */
+.fade:not(.show) {
+  opacity: 0;
+}
+
+/* line 10, node_modules/bootstrap/scss/_transitions.scss */
+.collapse:not(.show) {
+  display: none;
+}
+
+/* line 15, node_modules/bootstrap/scss/_transitions.scss */
+.collapsing {
+  position: relative;
+  height: 0;
+  overflow: hidden;
+  -webkit-transition: height 0.35s ease;
+  transition: height 0.35s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 15, node_modules/bootstrap/scss/_transitions.scss */
+  .collapsing {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 2, node_modules/bootstrap/scss/_dropdown.scss */
+.dropup,
+.dropright,
+.dropdown,
+.dropleft {
+  position: relative;
+}
+
+/* line 9, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-toggle {
+  white-space: nowrap;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropdown-toggle::after {
+  display: inline-block;
+  margin-left: 0.255em;
+  vertical-align: 0.255em;
+  content: "";
+  border-top: 0.3em solid;
+  border-right: 0.3em solid transparent;
+  border-bottom: 0;
+  border-left: 0.3em solid transparent;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropdown-toggle:empty::after {
+  margin-left: 0;
+}
+
+/* line 17, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  display: none;
+  float: left;
+  min-width: 10rem;
+  padding: 0.5rem 0;
+  margin: 0.125rem 0 0;
+  font-size: 1rem;
+  color: #464746;
+  text-align: left;
+  list-style: none;
+  background-color: #ffffff;
+  background-clip: padding-box;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  border-radius: 0.25rem;
+}
+
+/* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu-left {
+  right: auto;
+  left: 0;
+}
+
+/* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu-right {
+  right: 0;
+  left: auto;
+}
+
+@media (min-width: 576px) {
+  /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-sm-left {
+    right: auto;
+    left: 0;
+  }
+  /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-sm-right {
+    right: 0;
+    left: auto;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-md-left {
+    right: auto;
+    left: 0;
+  }
+  /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-md-right {
+    right: 0;
+    left: auto;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-lg-left {
+    right: auto;
+    left: 0;
+  }
+  /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-lg-right {
+    right: 0;
+    left: auto;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-xl-left {
+    right: auto;
+    left: 0;
+  }
+  /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-xl-right {
+    right: 0;
+    left: auto;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-xxl-left {
+    right: auto;
+    left: 0;
+  }
+  /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+  .dropdown-menu-xxl-right {
+    right: 0;
+    left: auto;
+  }
+}
+
+/* line 57, node_modules/bootstrap/scss/_dropdown.scss */
+.dropup .dropdown-menu {
+  top: auto;
+  bottom: 100%;
+  margin-top: 0;
+  margin-bottom: 0.125rem;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropup .dropdown-toggle::after {
+  display: inline-block;
+  margin-left: 0.255em;
+  vertical-align: 0.255em;
+  content: "";
+  border-top: 0;
+  border-right: 0.3em solid transparent;
+  border-bottom: 0.3em solid;
+  border-left: 0.3em solid transparent;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropup .dropdown-toggle:empty::after {
+  margin-left: 0;
+}
+
+/* line 70, node_modules/bootstrap/scss/_dropdown.scss */
+.dropright .dropdown-menu {
+  top: 0;
+  right: auto;
+  left: 100%;
+  margin-top: 0;
+  margin-left: 0.125rem;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropright .dropdown-toggle::after {
+  display: inline-block;
+  margin-left: 0.255em;
+  vertical-align: 0.255em;
+  content: "";
+  border-top: 0.3em solid transparent;
+  border-right: 0;
+  border-bottom: 0.3em solid transparent;
+  border-left: 0.3em solid;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropright .dropdown-toggle:empty::after {
+  margin-left: 0;
+}
+
+/* line 80, node_modules/bootstrap/scss/_dropdown.scss */
+.dropright .dropdown-toggle::after {
+  vertical-align: 0;
+}
+
+/* line 87, node_modules/bootstrap/scss/_dropdown.scss */
+.dropleft .dropdown-menu {
+  top: 0;
+  right: 100%;
+  left: auto;
+  margin-top: 0;
+  margin-right: 0.125rem;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle::after {
+  display: inline-block;
+  margin-left: 0.255em;
+  vertical-align: 0.255em;
+  content: "";
+}
+
+/* line 45, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle::after {
+  display: none;
+}
+
+/* line 49, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle::before {
+  display: inline-block;
+  margin-right: 0.255em;
+  vertical-align: 0.255em;
+  content: "";
+  border-top: 0.3em solid transparent;
+  border-right: 0.3em solid;
+  border-bottom: 0.3em solid transparent;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle:empty::after {
+  margin-left: 0;
+}
+
+/* line 97, node_modules/bootstrap/scss/_dropdown.scss */
+.dropleft .dropdown-toggle::before {
+  vertical-align: 0;
+}
+
+/* line 106, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] {
+  right: auto;
+  bottom: auto;
+}
+
+/* line 116, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-divider {
+  height: 0;
+  margin: 0.5rem 0;
+  overflow: hidden;
+  border-top: 1px solid #eaebea;
+}
+
+/* line 123, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item {
+  display: block;
+  width: 100%;
+  padding: 0.25rem 1.5rem;
+  clear: both;
+  font-weight: 400;
+  color: #464746;
+  text-align: inherit;
+  white-space: nowrap;
+  background-color: transparent;
+  border: 0;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.dropdown-item:hover, .dropdown-item:focus {
+  color: #393a39;
+  text-decoration: none;
+  background-color: #f7f7f7;
+}
+
+/* line 154, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item.active, .dropdown-item:active {
+  color: #ffffff;
+  text-decoration: none;
+  background-color: #464746;
+}
+
+/* line 161, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item.disabled, .dropdown-item:disabled {
+  color: #6c757d;
+  pointer-events: none;
+  background-color: transparent;
+}
+
+/* line 173, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu.show {
+  display: block;
+}
+
+/* line 178, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-header {
+  display: block;
+  padding: 0.5rem 1.5rem;
+  margin-bottom: 0;
+  font-size: 0.875rem;
+  color: #6c757d;
+  white-space: nowrap;
+}
+
+/* line 188, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item-text {
+  display: block;
+  padding: 0.25rem 1.5rem;
+  color: #464746;
+}
+
+/* line 4, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group,
+.btn-group-vertical {
+  position: relative;
+  display: -webkit-inline-box;
+  display: -ms-inline-flexbox;
+  display: inline-flex;
+  vertical-align: middle;
+}
+
+/* line 10, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+  position: relative;
+  -webkit-box-flex: 1;
+      -ms-flex: 1 1 auto;
+          flex: 1 1 auto;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-group > .btn:hover,
+.btn-group-vertical > .btn:hover {
+  z-index: 1;
+}
+
+/* line 19, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,
+.btn-group-vertical > .btn:focus,
+.btn-group-vertical > .btn:active,
+.btn-group-vertical > .btn.active {
+  z-index: 1;
+}
+
+/* line 28, node_modules/bootstrap/scss/_button-group.scss */
+.btn-toolbar {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  -webkit-box-pack: start;
+      -ms-flex-pack: start;
+          justify-content: flex-start;
+}
+
+/* line 33, node_modules/bootstrap/scss/_button-group.scss */
+.btn-toolbar .input-group {
+  width: auto;
+}
+
+/* line 40, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:not(:first-child),
+.btn-group > .btn-group:not(:first-child) {
+  margin-left: -1px;
+}
+
+/* line 46, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group > .btn-group:not(:last-child) > .btn {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+/* line 51, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:not(:first-child),
+.btn-group > .btn-group:not(:first-child) > .btn {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+/* line 69, node_modules/bootstrap/scss/_button-group.scss */
+.dropdown-toggle-split {
+  padding-right: 0.5625rem;
+  padding-left: 0.5625rem;
+}
+
+/* line 73, node_modules/bootstrap/scss/_button-group.scss */
+.dropdown-toggle-split::after,
+.dropup .dropdown-toggle-split::after,
+.dropright .dropdown-toggle-split::after {
+  margin-left: 0;
+}
+
+/* line 79, node_modules/bootstrap/scss/_button-group.scss */
+.dropleft .dropdown-toggle-split::before {
+  margin-right: 0;
+}
+
+/* line 84, node_modules/bootstrap/scss/_button-group.scss */
+.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {
+  padding-right: 0.375rem;
+  padding-left: 0.375rem;
+}
+
+/* line 89, node_modules/bootstrap/scss/_button-group.scss */
+.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {
+  padding-right: 0.75rem;
+  padding-left: 0.75rem;
+}
+
+/* line 111, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical {
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: column;
+          flex-direction: column;
+  -webkit-box-align: start;
+      -ms-flex-align: start;
+          align-items: flex-start;
+  -webkit-box-pack: center;
+      -ms-flex-pack: center;
+          justify-content: center;
+}
+
+/* line 116, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group {
+  width: 100%;
+}
+
+/* line 121, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn:not(:first-child),
+.btn-group-vertical > .btn-group:not(:first-child) {
+  margin-top: -1px;
+}
+
+/* line 127, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group-vertical > .btn-group:not(:last-child) > .btn {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+/* line 132, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn:not(:first-child),
+.btn-group-vertical > .btn-group:not(:first-child) > .btn {
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+/* line 152, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-toggle > .btn,
+.btn-group-toggle > .btn-group > .btn {
+  margin-bottom: 0;
+}
+
+/* line 156, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-toggle > .btn input[type="radio"],
+.btn-group-toggle > .btn input[type="checkbox"],
+.btn-group-toggle > .btn-group > .btn input[type="radio"],
+.btn-group-toggle > .btn-group > .btn input[type="checkbox"] {
+  position: absolute;
+  clip: rect(0, 0, 0, 0);
+  pointer-events: none;
+}
+
+/* line 7, node_modules/bootstrap/scss/_input-group.scss */
+.input-group {
+  position: relative;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  -webkit-box-align: stretch;
+      -ms-flex-align: stretch;
+          align-items: stretch;
+  width: 100%;
+}
+
+/* line 14, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control,
+.input-group > .form-control-plaintext,
+.input-group > .custom-select,
+.input-group > .custom-file {
+  position: relative;
+  -webkit-box-flex: 1;
+      -ms-flex: 1 1 auto;
+          flex: 1 1 auto;
+  width: 1%;
+  min-width: 0;
+  margin-bottom: 0;
+}
+
+/* line 24, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control + .form-control,
+.input-group > .form-control + .custom-select,
+.input-group > .form-control + .custom-file,
+.input-group > .form-control-plaintext + .form-control,
+.input-group > .form-control-plaintext + .custom-select,
+.input-group > .form-control-plaintext + .custom-file,
+.input-group > .custom-select + .form-control,
+.input-group > .custom-select + .custom-select,
+.input-group > .custom-select + .custom-file,
+.input-group > .custom-file + .form-control,
+.input-group > .custom-file + .custom-select,
+.input-group > .custom-file + .custom-file {
+  margin-left: -1px;
+}
+
+/* line 32, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control:focus,
+.input-group > .custom-select:focus,
+.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {
+  z-index: 3;
+}
+
+/* line 39, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file .custom-file-input:focus {
+  z-index: 4;
+}
+
+/* line 45, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control:not(:last-child),
+.input-group > .custom-select:not(:last-child) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+/* line 46, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control:not(:first-child),
+.input-group > .custom-select:not(:first-child) {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+/* line 51, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+}
+
+/* line 55, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file:not(:last-child) .custom-file-label,
+.input-group > .custom-file:not(:last-child) .custom-file-label::after {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+/* line 57, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file:not(:first-child) .custom-file-label {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+/* line 68, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend,
+.input-group-append {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+}
+
+/* line 75, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend .btn,
+.input-group-append .btn {
+  position: relative;
+  z-index: 2;
+}
+
+/* line 79, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend .btn:focus,
+.input-group-append .btn:focus {
+  z-index: 3;
+}
+
+/* line 84, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend .btn + .btn,
+.input-group-prepend .btn + .input-group-text,
+.input-group-prepend .input-group-text + .input-group-text,
+.input-group-prepend .input-group-text + .btn,
+.input-group-append .btn + .btn,
+.input-group-append .btn + .input-group-text,
+.input-group-append .input-group-text + .input-group-text,
+.input-group-append .input-group-text + .btn {
+  margin-left: -1px;
+}
+
+/* line 92, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend {
+  margin-right: -1px;
+}
+
+/* line 93, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-append {
+  margin-left: -1px;
+}
+
+/* line 101, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-text {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  padding: 0.375rem 0.75rem;
+  margin-bottom: 0;
+  font-size: 1rem;
+  font-weight: 400;
+  line-height: 1.5;
+  color: #495057;
+  text-align: center;
+  white-space: nowrap;
+  background-color: #eaebea;
+  border: 1px solid #ced4da;
+  border-radius: 0.25rem;
+}
+
+/* line 117, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-text input[type="radio"],
+.input-group-text input[type="checkbox"] {
+  margin-top: 0;
+}
+
+/* line 129, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-lg > .form-control:not(textarea),
+.input-group-lg > .custom-select {
+  height: calc(1.5em + 1rem + 2px);
+}
+
+/* line 134, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-lg > .form-control,
+.input-group-lg > .custom-select,
+.input-group-lg > .input-group-prepend > .input-group-text,
+.input-group-lg > .input-group-append > .input-group-text,
+.input-group-lg > .input-group-prepend > .btn,
+.input-group-lg > .input-group-append > .btn {
+  padding: 0.5rem 1rem;
+  font-size: 1.25rem;
+  line-height: 1.5;
+  border-radius: 0.3rem;
+}
+
+/* line 146, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-sm > .form-control:not(textarea),
+.input-group-sm > .custom-select {
+  height: calc(1.5em + 0.5rem + 2px);
+}
+
+/* line 151, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-sm > .form-control,
+.input-group-sm > .custom-select,
+.input-group-sm > .input-group-prepend > .input-group-text,
+.input-group-sm > .input-group-append > .input-group-text,
+.input-group-sm > .input-group-prepend > .btn,
+.input-group-sm > .input-group-append > .btn {
+  padding: 0.25rem 0.5rem;
+  font-size: 0.875rem;
+  line-height: 1.5;
+  border-radius: 0.2rem;
+}
+
+/* line 163, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-lg > .custom-select,
+.input-group-sm > .custom-select {
+  padding-right: 1.75rem;
+}
+
+/* line 176, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .input-group-prepend > .btn,
+.input-group > .input-group-prepend > .input-group-text,
+.input-group > .input-group-append:not(:last-child) > .btn,
+.input-group > .input-group-append:not(:last-child) > .input-group-text,
+.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),
+.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+/* line 185, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .input-group-append > .btn,
+.input-group > .input-group-append > .input-group-text,
+.input-group > .input-group-prepend:not(:first-child) > .btn,
+.input-group > .input-group-prepend:not(:first-child) > .input-group-text,
+.input-group > .input-group-prepend:first-child > .btn:not(:first-child),
+.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+/* line 10, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control {
+  position: relative;
+  z-index: 1;
+  display: block;
+  min-height: 1.5rem;
+  padding-left: 1.5rem;
+}
+
+/* line 18, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-inline {
+  display: -webkit-inline-box;
+  display: -ms-inline-flexbox;
+  display: inline-flex;
+  margin-right: 1rem;
+}
+
+/* line 23, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input {
+  position: absolute;
+  left: 0;
+  z-index: -1;
+  width: 1rem;
+  height: 1.25rem;
+  opacity: 0;
+}
+
+/* line 31, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:checked ~ .custom-control-label::before {
+  color: #ffffff;
+  border-color: #464746;
+  background-color: #464746;
+}
+
+/* line 38, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:focus ~ .custom-control-label::before {
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 47, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
+  border-color: #858785;
+}
+
+/* line 51, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
+  color: #ffffff;
+  background-color: #9fa09f;
+  border-color: #9fa09f;
+}
+
+/* line 61, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {
+  color: #6c757d;
+}
+
+/* line 64, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before {
+  background-color: #eaebea;
+}
+
+/* line 75, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label {
+  position: relative;
+  margin-bottom: 0;
+  vertical-align: top;
+}
+
+/* line 83, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label::before {
+  position: absolute;
+  top: 0.25rem;
+  left: -1.5rem;
+  display: block;
+  width: 1rem;
+  height: 1rem;
+  pointer-events: none;
+  content: "";
+  background-color: #ffffff;
+  border: #adb5bd solid 1px;
+}
+
+/* line 98, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label::after {
+  position: absolute;
+  top: 0.25rem;
+  left: -1.5rem;
+  display: block;
+  width: 1rem;
+  height: 1rem;
+  content: "";
+  background: no-repeat 50% / 50% 50%;
+}
+
+/* line 116, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-label::before {
+  border-radius: 0.25rem;
+}
+
+/* line 121, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
+}
+
+/* line 127, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {
+  border-color: #464746;
+  background-color: #464746;
+}
+
+/* line 132, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23ffffff' d='M0 2h4'/%3e%3c/svg%3e");
+}
+
+/* line 138, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {
+  background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 141, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {
+  background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 152, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-radio .custom-control-label::before {
+  border-radius: 50%;
+}
+
+/* line 158, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e");
+}
+
+/* line 164, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {
+  background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 175, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch {
+  padding-left: 2.25rem;
+}
+
+/* line 179, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-label::before {
+  left: -2.25rem;
+  width: 1.75rem;
+  pointer-events: all;
+  border-radius: 0.5rem;
+}
+
+/* line 187, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-label::after {
+  top: calc(0.25rem + 2px);
+  left: calc(-2.25rem + 2px);
+  width: calc(1rem - 4px);
+  height: calc(1rem - 4px);
+  background-color: #adb5bd;
+  border-radius: 0.5rem;
+  -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 187, node_modules/bootstrap/scss/_custom-forms.scss */
+  .custom-switch .custom-control-label::after {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 200, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-input:checked ~ .custom-control-label::after {
+  background-color: #ffffff;
+  -webkit-transform: translateX(0.75rem);
+          transform: translateX(0.75rem);
+}
+
+/* line 207, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {
+  background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 220, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select {
+  display: inline-block;
+  width: 100%;
+  height: calc(1.5em + 0.75rem + 2px);
+  padding: 0.375rem 1.75rem 0.375rem 0.75rem;
+  font-size: 1rem;
+  font-weight: 300;
+  line-height: 1.5;
+  color: #495057;
+  vertical-align: middle;
+  background: #ffffff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;
+  border: 1px solid #ced4da;
+  border-radius: 0.25rem;
+  -webkit-appearance: none;
+     -moz-appearance: none;
+          appearance: none;
+}
+
+/* line 237, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:focus {
+  border-color: #858785;
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 247, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:focus::-ms-value {
+  color: #495057;
+  background-color: #ffffff;
+}
+
+/* line 258, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select[multiple], .custom-select[size]:not([size="1"]) {
+  height: auto;
+  padding-right: 0.75rem;
+  background-image: none;
+}
+
+/* line 265, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:disabled {
+  color: #6c757d;
+  background-color: #eaebea;
+}
+
+/* line 271, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select::-ms-expand {
+  display: none;
+}
+
+/* line 276, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:-moz-focusring {
+  color: transparent;
+  text-shadow: 0 0 0 #495057;
+}
+
+/* line 282, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select-sm {
+  height: calc(1.5em + 0.5rem + 2px);
+  padding-top: 0.25rem;
+  padding-bottom: 0.25rem;
+  padding-left: 0.5rem;
+  font-size: 0.875rem;
+}
+
+/* line 290, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select-lg {
+  height: calc(1.5em + 1rem + 2px);
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+  padding-left: 1rem;
+  font-size: 1.25rem;
+}
+
+/* line 303, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file {
+  position: relative;
+  display: inline-block;
+  width: 100%;
+  height: calc(1.5em + 0.75rem + 2px);
+  margin-bottom: 0;
+}
+
+/* line 311, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input {
+  position: relative;
+  z-index: 2;
+  width: 100%;
+  height: calc(1.5em + 0.75rem + 2px);
+  margin: 0;
+  opacity: 0;
+}
+
+/* line 319, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input:focus ~ .custom-file-label {
+  border-color: #858785;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 325, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input[disabled] ~ .custom-file-label,
+.custom-file-input:disabled ~ .custom-file-label {
+  background-color: #eaebea;
+}
+
+/* line 331, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input:lang(en) ~ .custom-file-label::after {
+  content: "Browse";
+}
+
+/* line 336, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input ~ .custom-file-label[data-browse]::after {
+  content: attr(data-browse);
+}
+
+/* line 341, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-label {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  z-index: 1;
+  height: calc(1.5em + 0.75rem + 2px);
+  padding: 0.375rem 0.75rem;
+  font-weight: 300;
+  line-height: 1.5;
+  color: #495057;
+  background-color: #ffffff;
+  border: 1px solid #ced4da;
+  border-radius: 0.25rem;
+}
+
+/* line 358, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-label::after {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 3;
+  display: block;
+  height: calc(1.5em + 0.75rem);
+  padding: 0.375rem 0.75rem;
+  line-height: 1.5;
+  color: #495057;
+  content: "Browse";
+  background-color: #eaebea;
+  border-left: inherit;
+  border-radius: 0 0.25rem 0.25rem 0;
+}
+
+/* line 382, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range {
+  width: 100%;
+  height: 1.4rem;
+  padding: 0;
+  background-color: transparent;
+  -webkit-appearance: none;
+     -moz-appearance: none;
+          appearance: none;
+}
+
+/* line 389, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus {
+  outline: none;
+}
+
+/* line 394, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus::-webkit-slider-thumb {
+  -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+          box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 395, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus::-moz-range-thumb {
+  box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 396, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus::-ms-thumb {
+  box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 399, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-focus-outer {
+  border: 0;
+}
+
+/* line 403, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-webkit-slider-thumb {
+  width: 1rem;
+  height: 1rem;
+  margin-top: -0.25rem;
+  background-color: #464746;
+  border: 0;
+  border-radius: 1rem;
+  -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  -webkit-appearance: none;
+          appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 403, node_modules/bootstrap/scss/_custom-forms.scss */
+  .custom-range::-webkit-slider-thumb {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 414, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-webkit-slider-thumb:active {
+  background-color: #9fa09f;
+}
+
+/* line 419, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-webkit-slider-runnable-track {
+  width: 100%;
+  height: 0.5rem;
+  color: transparent;
+  cursor: pointer;
+  background-color: #dee2e6;
+  border-color: transparent;
+  border-radius: 1rem;
+}
+
+/* line 430, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-range-thumb {
+  width: 1rem;
+  height: 1rem;
+  background-color: #464746;
+  border: 0;
+  border-radius: 1rem;
+  -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  -moz-appearance: none;
+       appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 430, node_modules/bootstrap/scss/_custom-forms.scss */
+  .custom-range::-moz-range-thumb {
+    -moz-transition: none;
+    transition: none;
+  }
+}
+
+/* line 440, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-range-thumb:active {
+  background-color: #9fa09f;
+}
+
+/* line 445, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-range-track {
+  width: 100%;
+  height: 0.5rem;
+  color: transparent;
+  cursor: pointer;
+  background-color: #dee2e6;
+  border-color: transparent;
+  border-radius: 1rem;
+}
+
+/* line 456, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-thumb {
+  width: 1rem;
+  height: 1rem;
+  margin-top: 0;
+  margin-right: 0.2rem;
+  margin-left: 0.2rem;
+  background-color: #464746;
+  border: 0;
+  border-radius: 1rem;
+  -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 456, node_modules/bootstrap/scss/_custom-forms.scss */
+  .custom-range::-ms-thumb {
+    -ms-transition: none;
+    transition: none;
+  }
+}
+
+/* line 469, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-thumb:active {
+  background-color: #9fa09f;
+}
+
+/* line 474, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-track {
+  width: 100%;
+  height: 0.5rem;
+  color: transparent;
+  cursor: pointer;
+  background-color: transparent;
+  border-color: transparent;
+  border-width: 0.5rem;
+}
+
+/* line 485, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-fill-lower {
+  background-color: #dee2e6;
+  border-radius: 1rem;
+}
+
+/* line 490, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-fill-upper {
+  margin-right: 15px;
+  background-color: #dee2e6;
+  border-radius: 1rem;
+}
+
+/* line 497, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-webkit-slider-thumb {
+  background-color: #adb5bd;
+}
+
+/* line 501, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-webkit-slider-runnable-track {
+  cursor: default;
+}
+
+/* line 505, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-moz-range-thumb {
+  background-color: #adb5bd;
+}
+
+/* line 509, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-moz-range-track {
+  cursor: default;
+}
+
+/* line 513, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-ms-thumb {
+  background-color: #adb5bd;
+}
+
+/* line 519, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label::before,
+.custom-file-label,
+.custom-select {
+  -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 519, node_modules/bootstrap/scss/_custom-forms.scss */
+  .custom-control-label::before,
+  .custom-file-label,
+  .custom-select {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 6, node_modules/bootstrap/scss/_nav.scss */
+.nav {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  padding-left: 0;
+  margin-bottom: 0;
+  list-style: none;
+}
+
+/* line 14, node_modules/bootstrap/scss/_nav.scss */
+.nav-link {
+  display: block;
+  padding: 0.5rem 1rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.nav-link:hover, .nav-link:focus {
+  text-decoration: none;
+}
+
+/* line 24, node_modules/bootstrap/scss/_nav.scss */
+.nav-link.disabled {
+  color: #6c757d;
+  pointer-events: none;
+  cursor: default;
+}
+
+/* line 35, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs {
+  border-bottom: 1px solid #dee2e6;
+}
+
+/* line 38, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-item {
+  margin-bottom: -1px;
+}
+
+/* line 42, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-link {
+  border: 1px solid transparent;
+  border-top-left-radius: 0.25rem;
+  border-top-right-radius: 0.25rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
+  border-color: #eaebea #eaebea #dee2e6;
+}
+
+/* line 50, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-link.disabled {
+  color: #6c757d;
+  background-color: transparent;
+  border-color: transparent;
+}
+
+/* line 57, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-link.active,
+.nav-tabs .nav-item.show .nav-link {
+  color: #495057;
+  background-color: #ffffff;
+  border-color: #dee2e6 #dee2e6 #ffffff;
+}
+
+/* line 64, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .dropdown-menu {
+  margin-top: -1px;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+/* line 78, node_modules/bootstrap/scss/_nav.scss */
+.nav-pills .nav-link {
+  border-radius: 0.25rem;
+}
+
+/* line 82, node_modules/bootstrap/scss/_nav.scss */
+.nav-pills .nav-link.active,
+.nav-pills .show > .nav-link {
+  color: #ffffff;
+  background-color: #464746;
+}
+
+/* line 95, node_modules/bootstrap/scss/_nav.scss */
+.nav-fill > .nav-link,
+.nav-fill .nav-item {
+  -webkit-box-flex: 1;
+      -ms-flex: 1 1 auto;
+          flex: 1 1 auto;
+  text-align: center;
+}
+
+/* line 103, node_modules/bootstrap/scss/_nav.scss */
+.nav-justified > .nav-link,
+.nav-justified .nav-item {
+  -ms-flex-preferred-size: 0;
+      flex-basis: 0;
+  -webkit-box-flex: 1;
+      -ms-flex-positive: 1;
+          flex-grow: 1;
+  text-align: center;
+}
+
+/* line 117, node_modules/bootstrap/scss/_nav.scss */
+.tab-content > .tab-pane {
+  display: none;
+}
+
+/* line 120, node_modules/bootstrap/scss/_nav.scss */
+.tab-content > .active {
+  display: block;
+}
+
+/* line 18, node_modules/bootstrap/scss/_navbar.scss */
+.navbar {
+  position: relative;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  -webkit-box-pack: justify;
+      -ms-flex-pack: justify;
+          justify-content: space-between;
+  padding: 0.5rem 1rem;
+}
+
+/* line 28, node_modules/bootstrap/scss/_navbar.scss */
+.navbar .container,
+.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl, .navbar .container-xxl {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  -webkit-box-pack: justify;
+      -ms-flex-pack: justify;
+          justify-content: space-between;
+}
+
+/* line 52, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-brand {
+  display: inline-block;
+  padding-top: 0.3125rem;
+  padding-bottom: 0.3125rem;
+  margin-right: 1rem;
+  font-size: 1.25rem;
+  line-height: inherit;
+  white-space: nowrap;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-brand:hover, .navbar-brand:focus {
+  text-decoration: none;
+}
+
+/* line 71, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-nav {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: column;
+          flex-direction: column;
+  padding-left: 0;
+  margin-bottom: 0;
+  list-style: none;
+}
+
+/* line 78, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-nav .nav-link {
+  padding-right: 0;
+  padding-left: 0;
+}
+
+/* line 83, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-nav .dropdown-menu {
+  position: static;
+  float: none;
+}
+
+/* line 94, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-text {
+  display: inline-block;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+/* line 109, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-collapse {
+  -ms-flex-preferred-size: 100%;
+      flex-basis: 100%;
+  -webkit-box-flex: 1;
+      -ms-flex-positive: 1;
+          flex-grow: 1;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+}
+
+/* line 118, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-toggler {
+  padding: 0.25rem 0.75rem;
+  font-size: 1.25rem;
+  line-height: 1;
+  background-color: transparent;
+  border: 1px solid transparent;
+  border-radius: 0.25rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-toggler:hover, .navbar-toggler:focus {
+  text-decoration: none;
+}
+
+/* line 133, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-toggler-icon {
+  display: inline-block;
+  width: 1.5em;
+  height: 1.5em;
+  vertical-align: middle;
+  content: "";
+  background: no-repeat center center;
+  background-size: 100% 100%;
+}
+
+@media (max-width: 575.98px) {
+  /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm > .container,
+  .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl, .navbar-expand-sm > .container-xxl {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+
+@media (min-width: 576px) {
+  /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row nowrap;
+            flex-flow: row nowrap;
+    -webkit-box-pack: start;
+        -ms-flex-pack: start;
+            justify-content: flex-start;
+  }
+  /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm .navbar-nav {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm .navbar-nav .dropdown-menu {
+    position: absolute;
+  }
+  /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm .navbar-nav .nav-link {
+    padding-right: 0.5rem;
+    padding-left: 0.5rem;
+  }
+  /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm > .container,
+  .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl, .navbar-expand-sm > .container-xxl {
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+  }
+  /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm .navbar-collapse {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+    -ms-flex-preferred-size: auto;
+        flex-basis: auto;
+  }
+  /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-sm .navbar-toggler {
+    display: none;
+  }
+}
+
+@media (max-width: 767.98px) {
+  /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md > .container,
+  .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl, .navbar-expand-md > .container-xxl {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row nowrap;
+            flex-flow: row nowrap;
+    -webkit-box-pack: start;
+        -ms-flex-pack: start;
+            justify-content: flex-start;
+  }
+  /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md .navbar-nav {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md .navbar-nav .dropdown-menu {
+    position: absolute;
+  }
+  /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md .navbar-nav .nav-link {
+    padding-right: 0.5rem;
+    padding-left: 0.5rem;
+  }
+  /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md > .container,
+  .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl, .navbar-expand-md > .container-xxl {
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+  }
+  /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md .navbar-collapse {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+    -ms-flex-preferred-size: auto;
+        flex-basis: auto;
+  }
+  /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-md .navbar-toggler {
+    display: none;
+  }
+}
+
+@media (max-width: 1023.98px) {
+  /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg > .container,
+  .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl, .navbar-expand-lg > .container-xxl {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row nowrap;
+            flex-flow: row nowrap;
+    -webkit-box-pack: start;
+        -ms-flex-pack: start;
+            justify-content: flex-start;
+  }
+  /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg .navbar-nav {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg .navbar-nav .dropdown-menu {
+    position: absolute;
+  }
+  /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg .navbar-nav .nav-link {
+    padding-right: 0.5rem;
+    padding-left: 0.5rem;
+  }
+  /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg > .container,
+  .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl, .navbar-expand-lg > .container-xxl {
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+  }
+  /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg .navbar-collapse {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+    -ms-flex-preferred-size: auto;
+        flex-basis: auto;
+  }
+  /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-lg .navbar-toggler {
+    display: none;
+  }
+}
+
+@media (max-width: 1279.98px) {
+  /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl > .container,
+  .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl, .navbar-expand-xl > .container-xxl {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row nowrap;
+            flex-flow: row nowrap;
+    -webkit-box-pack: start;
+        -ms-flex-pack: start;
+            justify-content: flex-start;
+  }
+  /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl .navbar-nav {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl .navbar-nav .dropdown-menu {
+    position: absolute;
+  }
+  /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl .navbar-nav .nav-link {
+    padding-right: 0.5rem;
+    padding-left: 0.5rem;
+  }
+  /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl > .container,
+  .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl, .navbar-expand-xl > .container-xxl {
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+  }
+  /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl .navbar-collapse {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+    -ms-flex-preferred-size: auto;
+        flex-basis: auto;
+  }
+  /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xl .navbar-toggler {
+    display: none;
+  }
+}
+
+@media (max-width: 1439.98px) {
+  /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl > .container,
+  .navbar-expand-xxl > .container-fluid, .navbar-expand-xxl > .container-sm, .navbar-expand-xxl > .container-md, .navbar-expand-xxl > .container-lg, .navbar-expand-xxl > .container-xl, .navbar-expand-xxl > .container-xxl {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row nowrap;
+            flex-flow: row nowrap;
+    -webkit-box-pack: start;
+        -ms-flex-pack: start;
+            justify-content: flex-start;
+  }
+  /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl .navbar-nav {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl .navbar-nav .dropdown-menu {
+    position: absolute;
+  }
+  /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl .navbar-nav .nav-link {
+    padding-right: 0.5rem;
+    padding-left: 0.5rem;
+  }
+  /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl > .container,
+  .navbar-expand-xxl > .container-fluid, .navbar-expand-xxl > .container-sm, .navbar-expand-xxl > .container-md, .navbar-expand-xxl > .container-lg, .navbar-expand-xxl > .container-xl, .navbar-expand-xxl > .container-xxl {
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+  }
+  /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl .navbar-collapse {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+    -ms-flex-preferred-size: auto;
+        flex-basis: auto;
+  }
+  /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+  .navbar-expand-xxl .navbar-toggler {
+    display: none;
+  }
+}
+
+/* line 150, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand {
+  -webkit-box-orient: horizontal;
+  -webkit-box-direction: normal;
+      -ms-flex-flow: row nowrap;
+          flex-flow: row nowrap;
+  -webkit-box-pack: start;
+      -ms-flex-pack: start;
+          justify-content: flex-start;
+}
+
+/* line 152, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand > .container,
+.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl, .navbar-expand > .container-xxl {
+  padding-right: 0;
+  padding-left: 0;
+}
+
+/* line 173, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-nav {
+  -webkit-box-orient: horizontal;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: row;
+          flex-direction: row;
+}
+
+/* line 176, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-nav .dropdown-menu {
+  position: absolute;
+}
+
+/* line 180, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-nav .nav-link {
+  padding-right: 0.5rem;
+  padding-left: 0.5rem;
+}
+
+/* line 187, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand > .container,
+.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl, .navbar-expand > .container-xxl {
+  -ms-flex-wrap: nowrap;
+      flex-wrap: nowrap;
+}
+
+/* line 202, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-collapse {
+  display: -webkit-box !important;
+  display: -ms-flexbox !important;
+  display: flex !important;
+  -ms-flex-preferred-size: auto;
+      flex-basis: auto;
+}
+
+/* line 209, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-toggler {
+  display: none;
+}
+
+/* line 224, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-brand {
+  color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {
+  color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 233, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-nav .nav-link {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {
+  color: rgba(0, 0, 0, 0.7);
+}
+
+/* line 240, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-nav .nav-link.disabled {
+  color: rgba(0, 0, 0, 0.3);
+}
+
+/* line 245, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-nav .show > .nav-link,
+.navbar-light .navbar-nav .active > .nav-link,
+.navbar-light .navbar-nav .nav-link.show,
+.navbar-light .navbar-nav .nav-link.active {
+  color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 253, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-toggler {
+  color: rgba(0, 0, 0, 0.5);
+  border-color: rgba(0, 0, 0, 0.1);
+}
+
+/* line 258, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-toggler-icon {
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+/* line 262, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-text {
+  color: rgba(0, 0, 0, 0.5);
+}
+
+/* line 264, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-text a {
+  color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {
+  color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 276, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-brand {
+  color: #ffffff;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {
+  color: #ffffff;
+}
+
+/* line 285, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-nav .nav-link {
+  color: rgba(255, 255, 255, 0.5);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
+  color: rgba(255, 255, 255, 0.75);
+}
+
+/* line 292, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-nav .nav-link.disabled {
+  color: rgba(255, 255, 255, 0.25);
+}
+
+/* line 297, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-nav .show > .nav-link,
+.navbar-dark .navbar-nav .active > .nav-link,
+.navbar-dark .navbar-nav .nav-link.show,
+.navbar-dark .navbar-nav .nav-link.active {
+  color: #ffffff;
+}
+
+/* line 305, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-toggler {
+  color: rgba(255, 255, 255, 0.5);
+  border-color: rgba(255, 255, 255, 0.1);
+}
+
+/* line 310, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-toggler-icon {
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+/* line 314, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-text {
+  color: rgba(255, 255, 255, 0.5);
+}
+
+/* line 316, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-text a {
+  color: #ffffff;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {
+  color: #ffffff;
+}
+
+/* line 5, node_modules/bootstrap/scss/_card.scss */
+.card {
+  position: relative;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: column;
+          flex-direction: column;
+  min-width: 0;
+  word-wrap: break-word;
+  background-color: #ffffff;
+  background-clip: border-box;
+  border: 1px solid rgba(0, 0, 0, 0.125);
+  border-radius: 0.25rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/_card.scss */
+.card > hr {
+  margin-right: 0;
+  margin-left: 0;
+}
+
+/* line 22, node_modules/bootstrap/scss/_card.scss */
+.card > .list-group {
+  border-top: inherit;
+  border-bottom: inherit;
+}
+
+/* line 26, node_modules/bootstrap/scss/_card.scss */
+.card > .list-group:first-child {
+  border-top-width: 0;
+  border-top-left-radius: calc(0.25rem - 1px);
+  border-top-right-radius: calc(0.25rem - 1px);
+}
+
+/* line 31, node_modules/bootstrap/scss/_card.scss */
+.card > .list-group:last-child {
+  border-bottom-width: 0;
+  border-bottom-right-radius: calc(0.25rem - 1px);
+  border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+/* line 39, node_modules/bootstrap/scss/_card.scss */
+.card > .card-header + .list-group,
+.card > .list-group + .card-footer {
+  border-top: 0;
+}
+
+/* line 45, node_modules/bootstrap/scss/_card.scss */
+.card-body {
+  -webkit-box-flex: 1;
+      -ms-flex: 1 1 auto;
+          flex: 1 1 auto;
+  min-height: 1px;
+  padding: 1.25rem;
+}
+
+/* line 56, node_modules/bootstrap/scss/_card.scss */
+.card-title {
+  margin-bottom: 0.75rem;
+}
+
+/* line 60, node_modules/bootstrap/scss/_card.scss */
+.card-subtitle {
+  margin-top: -0.375rem;
+  margin-bottom: 0;
+}
+
+/* line 65, node_modules/bootstrap/scss/_card.scss */
+.card-text:last-child {
+  margin-bottom: 0;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.card-link:hover {
+  text-decoration: none;
+}
+
+/* line 74, node_modules/bootstrap/scss/_card.scss */
+.card-link + .card-link {
+  margin-left: 1.25rem;
+}
+
+/* line 83, node_modules/bootstrap/scss/_card.scss */
+.card-header {
+  padding: 0.75rem 1.25rem;
+  margin-bottom: 0;
+  background-color: rgba(0, 0, 0, 0.03);
+  border-bottom: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+/* line 90, node_modules/bootstrap/scss/_card.scss */
+.card-header:first-child {
+  border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;
+}
+
+/* line 95, node_modules/bootstrap/scss/_card.scss */
+.card-footer {
+  padding: 0.75rem 1.25rem;
+  background-color: rgba(0, 0, 0, 0.03);
+  border-top: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+/* line 101, node_modules/bootstrap/scss/_card.scss */
+.card-footer:last-child {
+  border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);
+}
+
+/* line 111, node_modules/bootstrap/scss/_card.scss */
+.card-header-tabs {
+  margin-right: -0.625rem;
+  margin-bottom: -0.75rem;
+  margin-left: -0.625rem;
+  border-bottom: 0;
+}
+
+/* line 118, node_modules/bootstrap/scss/_card.scss */
+.card-header-pills {
+  margin-right: -0.625rem;
+  margin-left: -0.625rem;
+}
+
+/* line 124, node_modules/bootstrap/scss/_card.scss */
+.card-img-overlay {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  padding: 1.25rem;
+  border-radius: calc(0.25rem - 1px);
+}
+
+/* line 134, node_modules/bootstrap/scss/_card.scss */
+.card-img,
+.card-img-top,
+.card-img-bottom {
+  -ms-flex-negative: 0;
+      flex-shrink: 0;
+  width: 100%;
+}
+
+/* line 141, node_modules/bootstrap/scss/_card.scss */
+.card-img,
+.card-img-top {
+  border-top-left-radius: calc(0.25rem - 1px);
+  border-top-right-radius: calc(0.25rem - 1px);
+}
+
+/* line 146, node_modules/bootstrap/scss/_card.scss */
+.card-img,
+.card-img-bottom {
+  border-bottom-right-radius: calc(0.25rem - 1px);
+  border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+/* line 155, node_modules/bootstrap/scss/_card.scss */
+.card-deck .card {
+  margin-bottom: 15px;
+}
+
+@media (min-width: 576px) {
+  /* line 154, node_modules/bootstrap/scss/_card.scss */
+  .card-deck {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row wrap;
+            flex-flow: row wrap;
+    margin-right: -15px;
+    margin-left: -15px;
+  }
+  /* line 165, node_modules/bootstrap/scss/_card.scss */
+  .card-deck .card {
+    -webkit-box-flex: 1;
+        -ms-flex: 1 0 0%;
+            flex: 1 0 0%;
+    margin-right: 15px;
+    margin-bottom: 0;
+    margin-left: 15px;
+  }
+}
+
+/* line 183, node_modules/bootstrap/scss/_card.scss */
+.card-group > .card {
+  margin-bottom: 15px;
+}
+
+@media (min-width: 576px) {
+  /* line 180, node_modules/bootstrap/scss/_card.scss */
+  .card-group {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-flow: row wrap;
+            flex-flow: row wrap;
+  }
+  /* line 192, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card {
+    -webkit-box-flex: 1;
+        -ms-flex: 1 0 0%;
+            flex: 1 0 0%;
+    margin-bottom: 0;
+  }
+  /* line 197, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card + .card {
+    margin-left: 0;
+    border-left: 0;
+  }
+  /* line 204, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card:not(:last-child) {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+  /* line 207, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card:not(:last-child) .card-img-top,
+  .card-group > .card:not(:last-child) .card-header {
+    border-top-right-radius: 0;
+  }
+  /* line 212, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card:not(:last-child) .card-img-bottom,
+  .card-group > .card:not(:last-child) .card-footer {
+    border-bottom-right-radius: 0;
+  }
+  /* line 219, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card:not(:first-child) {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+  /* line 222, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card:not(:first-child) .card-img-top,
+  .card-group > .card:not(:first-child) .card-header {
+    border-top-left-radius: 0;
+  }
+  /* line 227, node_modules/bootstrap/scss/_card.scss */
+  .card-group > .card:not(:first-child) .card-img-bottom,
+  .card-group > .card:not(:first-child) .card-footer {
+    border-bottom-left-radius: 0;
+  }
+}
+
+/* line 244, node_modules/bootstrap/scss/_card.scss */
+.card-columns .card {
+  margin-bottom: 0.75rem;
+}
+
+@media (min-width: 576px) {
+  /* line 243, node_modules/bootstrap/scss/_card.scss */
+  .card-columns {
+    -webkit-column-count: 3;
+       -moz-column-count: 3;
+            column-count: 3;
+    -webkit-column-gap: 1.25rem;
+       -moz-column-gap: 1.25rem;
+            column-gap: 1.25rem;
+    orphans: 1;
+    widows: 1;
+  }
+  /* line 254, node_modules/bootstrap/scss/_card.scss */
+  .card-columns .card {
+    display: inline-block;
+    width: 100%;
+  }
+}
+
+/* line 266, node_modules/bootstrap/scss/_card.scss */
+.accordion {
+  overflow-anchor: none;
+}
+
+/* line 269, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card {
+  overflow: hidden;
+}
+
+/* line 272, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card:not(:last-of-type) {
+  border-bottom: 0;
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+/* line 277, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card:not(:first-of-type) {
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+/* line 281, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card > .card-header {
+  border-radius: 0;
+  margin-bottom: -1px;
+}
+
+/* line 1, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  padding: 0.75rem 1rem;
+  margin-bottom: 1rem;
+  list-style: none;
+  background-color: #eaebea;
+  border-radius: 0.25rem;
+}
+
+/* line 12, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+}
+
+/* line 16, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item {
+  padding-left: 0.5rem;
+}
+
+/* line 19, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item::before {
+  display: inline-block;
+  padding-right: 0.5rem;
+  color: #6c757d;
+  content: "/";
+}
+
+/* line 33, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item:hover::before {
+  text-decoration: underline;
+}
+
+/* line 37, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item:hover::before {
+  text-decoration: none;
+}
+
+/* line 41, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item.active {
+  color: #6c757d;
+}
+
+/* line 1, node_modules/bootstrap/scss/_pagination.scss */
+.pagination {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  padding-left: 0;
+  list-style: none;
+  border-radius: 0.25rem;
+}
+
+/* line 7, node_modules/bootstrap/scss/_pagination.scss */
+.page-link {
+  position: relative;
+  display: block;
+  padding: 0.5rem 0.75rem;
+  margin-left: -1px;
+  line-height: 1.25;
+  color: #464746;
+  background-color: #ffffff;
+  border: 1px solid #dee2e6;
+}
+
+/* line 18, node_modules/bootstrap/scss/_pagination.scss */
+.page-link:hover {
+  z-index: 2;
+  color: #202020;
+  text-decoration: none;
+  background-color: #eaebea;
+  border-color: #dee2e6;
+}
+
+/* line 26, node_modules/bootstrap/scss/_pagination.scss */
+.page-link:focus {
+  z-index: 3;
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 35, node_modules/bootstrap/scss/_pagination.scss */
+.page-item:first-child .page-link {
+  margin-left: 0;
+  border-top-left-radius: 0.25rem;
+  border-bottom-left-radius: 0.25rem;
+}
+
+/* line 41, node_modules/bootstrap/scss/_pagination.scss */
+.page-item:last-child .page-link {
+  border-top-right-radius: 0.25rem;
+  border-bottom-right-radius: 0.25rem;
+}
+
+/* line 46, node_modules/bootstrap/scss/_pagination.scss */
+.page-item.active .page-link {
+  z-index: 3;
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 53, node_modules/bootstrap/scss/_pagination.scss */
+.page-item.disabled .page-link {
+  color: #6c757d;
+  pointer-events: none;
+  cursor: auto;
+  background-color: #ffffff;
+  border-color: #dee2e6;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-lg .page-link {
+  padding: 0.75rem 1.5rem;
+  font-size: 1.25rem;
+  line-height: 1.5;
+}
+
+/* line 12, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-lg .page-item:first-child .page-link {
+  border-top-left-radius: 0.3rem;
+  border-bottom-left-radius: 0.3rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-lg .page-item:last-child .page-link {
+  border-top-right-radius: 0.3rem;
+  border-bottom-right-radius: 0.3rem;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-sm .page-link {
+  padding: 0.25rem 0.5rem;
+  font-size: 0.875rem;
+  line-height: 1.5;
+}
+
+/* line 12, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-sm .page-item:first-child .page-link {
+  border-top-left-radius: 0.2rem;
+  border-bottom-left-radius: 0.2rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-sm .page-item:last-child .page-link {
+  border-top-right-radius: 0.2rem;
+  border-bottom-right-radius: 0.2rem;
+}
+
+/* line 6, node_modules/bootstrap/scss/_badge.scss */
+.badge {
+  display: inline-block;
+  padding: 0.25em 0.4em;
+  font-size: 75%;
+  font-weight: 700;
+  line-height: 1;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  border-radius: 0.25rem;
+  -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 6, node_modules/bootstrap/scss/_badge.scss */
+  .badge {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge:hover, a.badge:focus {
+  text-decoration: none;
+}
+
+/* line 25, node_modules/bootstrap/scss/_badge.scss */
+.badge:empty {
+  display: none;
+}
+
+/* line 31, node_modules/bootstrap/scss/_badge.scss */
+.btn .badge {
+  position: relative;
+  top: -1px;
+}
+
+/* line 40, node_modules/bootstrap/scss/_badge.scss */
+.badge-pill {
+  padding-right: 0.6em;
+  padding-left: 0.6em;
+  border-radius: 10rem;
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-primary {
+  color: #ffffff;
+  background-color: #464746;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-primary:hover, a.badge-primary:focus {
+  color: #ffffff;
+  background-color: #2d2d2d;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-primary:focus, a.badge-primary.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-secondary {
+  color: #464746;
+  background-color: #f0ebe3;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-secondary:hover, a.badge-secondary:focus {
+  color: #464746;
+  background-color: #ded3c2;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-secondary:focus, a.badge-secondary.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-success {
+  color: #464746;
+  background-color: #f0ebe3;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-success:hover, a.badge-success:focus {
+  color: #464746;
+  background-color: #ded3c2;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-success:focus, a.badge-success.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-info {
+  color: #ffffff;
+  background-color: #464746;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-info:hover, a.badge-info:focus {
+  color: #ffffff;
+  background-color: #2d2d2d;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-info:focus, a.badge-info.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-warning {
+  color: #ffffff;
+  background-color: #464746;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-warning:hover, a.badge-warning:focus {
+  color: #ffffff;
+  background-color: #2d2d2d;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-warning:focus, a.badge-warning.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-danger {
+  color: #ffffff;
+  background-color: #e54a19;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-danger:hover, a.badge-danger:focus {
+  color: #ffffff;
+  background-color: #b73b14;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-danger:focus, a.badge-danger.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-light {
+  color: #464746;
+  background-color: #f7f7f7;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-light:hover, a.badge-light:focus {
+  color: #464746;
+  background-color: #dedede;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-light:focus, a.badge-light.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-dark {
+  color: #ffffff;
+  background-color: #343a40;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-dark:hover, a.badge-dark:focus {
+  color: #ffffff;
+  background-color: #1d2124;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-dark:focus, a.badge-dark.focus {
+  outline: 0;
+  -webkit-box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+          box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+/* line 1, node_modules/bootstrap/scss/_jumbotron.scss */
+.jumbotron {
+  padding: 2rem 1rem;
+  margin-bottom: 2rem;
+  background-color: #eaebea;
+  border-radius: 0.3rem;
+}
+
+@media (min-width: 576px) {
+  /* line 1, node_modules/bootstrap/scss/_jumbotron.scss */
+  .jumbotron {
+    padding: 4rem 2rem;
+  }
+}
+
+/* line 13, node_modules/bootstrap/scss/_jumbotron.scss */
+.jumbotron-fluid {
+  padding-right: 0;
+  padding-left: 0;
+  border-radius: 0;
+}
+
+/* line 5, node_modules/bootstrap/scss/_alert.scss */
+.alert {
+  position: relative;
+  padding: 0.75rem 1.25rem;
+  margin-bottom: 1rem;
+  border: 1px solid transparent;
+  border-radius: 0.25rem;
+}
+
+/* line 14, node_modules/bootstrap/scss/_alert.scss */
+.alert-heading {
+  color: inherit;
+}
+
+/* line 20, node_modules/bootstrap/scss/_alert.scss */
+.alert-link {
+  font-weight: 700;
+}
+
+/* line 29, node_modules/bootstrap/scss/_alert.scss */
+.alert-dismissible {
+  padding-right: 4rem;
+}
+
+/* line 33, node_modules/bootstrap/scss/_alert.scss */
+.alert-dismissible .close {
+  position: absolute;
+  top: 0;
+  right: 0;
+  padding: 0.75rem 1.25rem;
+  color: inherit;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-primary {
+  color: #242524;
+  background-color: #dadada;
+  border-color: #cbcbcb;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-primary hr {
+  border-top-color: #bebebe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-primary .alert-link {
+  color: #0b0b0b;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-secondary {
+  color: #7d7a76;
+  background-color: #fcfbf9;
+  border-color: #fbf9f7;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-secondary hr {
+  border-top-color: #f3ece6;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-secondary .alert-link {
+  color: #63605d;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-success {
+  color: #7d7a76;
+  background-color: #fcfbf9;
+  border-color: #fbf9f7;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-success hr {
+  border-top-color: #f3ece6;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-success .alert-link {
+  color: #63605d;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-info {
+  color: #242524;
+  background-color: #dadada;
+  border-color: #cbcbcb;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-info hr {
+  border-top-color: #bebebe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-info .alert-link {
+  color: #0b0b0b;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-warning {
+  color: #242524;
+  background-color: #dadada;
+  border-color: #cbcbcb;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-warning hr {
+  border-top-color: #bebebe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-warning .alert-link {
+  color: #0b0b0b;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-danger {
+  color: #77260d;
+  background-color: #fadbd1;
+  border-color: #f8ccbf;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-danger hr {
+  border-top-color: #f5baa8;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-danger .alert-link {
+  color: #491708;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-light {
+  color: gray;
+  background-color: #fdfdfd;
+  border-color: #fdfdfd;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-light hr {
+  border-top-color: #f0f0f0;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-light .alert-link {
+  color: #676767;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-dark {
+  color: #1b1e21;
+  background-color: #d6d8d9;
+  border-color: #c6c8ca;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-dark hr {
+  border-top-color: #b9bbbe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-dark .alert-link {
+  color: #040505;
+}
+
+@-webkit-keyframes progress-bar-stripes {
+  from {
+    background-position: 1rem 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+
+@keyframes progress-bar-stripes {
+  from {
+    background-position: 1rem 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+
+/* line 9, node_modules/bootstrap/scss/_progress.scss */
+.progress {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  height: 1rem;
+  overflow: hidden;
+  line-height: 0;
+  font-size: 0.75rem;
+  background-color: #eaebea;
+  border-radius: 0.25rem;
+}
+
+/* line 20, node_modules/bootstrap/scss/_progress.scss */
+.progress-bar {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: column;
+          flex-direction: column;
+  -webkit-box-pack: center;
+      -ms-flex-pack: center;
+          justify-content: center;
+  overflow: hidden;
+  color: #ffffff;
+  text-align: center;
+  white-space: nowrap;
+  background-color: #464746;
+  -webkit-transition: width 0.6s ease;
+  transition: width 0.6s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 20, node_modules/bootstrap/scss/_progress.scss */
+  .progress-bar {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 32, node_modules/bootstrap/scss/_progress.scss */
+.progress-bar-striped {
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-size: 1rem 1rem;
+}
+
+/* line 38, node_modules/bootstrap/scss/_progress.scss */
+.progress-bar-animated {
+  -webkit-animation: progress-bar-stripes 1s linear infinite;
+          animation: progress-bar-stripes 1s linear infinite;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 38, node_modules/bootstrap/scss/_progress.scss */
+  .progress-bar-animated {
+    -webkit-animation: none;
+            animation: none;
+  }
+}
+
+/* line 1, node_modules/bootstrap/scss/_media.scss */
+.media {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-align: start;
+      -ms-flex-align: start;
+          align-items: flex-start;
+}
+
+/* line 6, node_modules/bootstrap/scss/_media.scss */
+.media-body {
+  -webkit-box-flex: 1;
+      -ms-flex: 1;
+          flex: 1;
+}
+
+/* line 5, node_modules/bootstrap/scss/_list-group.scss */
+.list-group {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: column;
+          flex-direction: column;
+  padding-left: 0;
+  margin-bottom: 0;
+  border-radius: 0.25rem;
+}
+
+/* line 21, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item-action {
+  width: 100%;
+  color: #495057;
+  text-align: inherit;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-action:hover, .list-group-item-action:focus {
+  z-index: 1;
+  color: #495057;
+  text-decoration: none;
+  background-color: #f7f7f7;
+}
+
+/* line 34, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item-action:active {
+  color: #464746;
+  background-color: #eaebea;
+}
+
+/* line 45, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item {
+  position: relative;
+  display: block;
+  padding: 0.75rem 1.25rem;
+  background-color: #ffffff;
+  border: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+/* line 54, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item:first-child {
+  border-top-left-radius: inherit;
+  border-top-right-radius: inherit;
+}
+
+/* line 58, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item:last-child {
+  border-bottom-right-radius: inherit;
+  border-bottom-left-radius: inherit;
+}
+
+/* line 62, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item.disabled, .list-group-item:disabled {
+  color: #6c757d;
+  pointer-events: none;
+  background-color: #ffffff;
+}
+
+/* line 70, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item.active {
+  z-index: 2;
+  color: #ffffff;
+  background-color: #464746;
+  border-color: #464746;
+}
+
+/* line 77, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item + .list-group-item {
+  border-top-width: 0;
+}
+
+/* line 80, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item + .list-group-item.active {
+  margin-top: -1px;
+  border-top-width: 1px;
+}
+
+/* line 96, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal {
+  -webkit-box-orient: horizontal;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: row;
+          flex-direction: row;
+}
+
+/* line 100, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item:first-child {
+  border-bottom-left-radius: 0.25rem;
+  border-top-right-radius: 0;
+}
+
+/* line 105, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item:last-child {
+  border-top-right-radius: 0.25rem;
+  border-bottom-left-radius: 0;
+}
+
+/* line 110, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item.active {
+  margin-top: 0;
+}
+
+/* line 114, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item + .list-group-item {
+  border-top-width: 1px;
+  border-left-width: 0;
+}
+
+/* line 118, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item + .list-group-item.active {
+  margin-left: -1px;
+  border-left-width: 1px;
+}
+
+@media (min-width: 576px) {
+  /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-sm {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-sm > .list-group-item:first-child {
+    border-bottom-left-radius: 0.25rem;
+    border-top-right-radius: 0;
+  }
+  /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-sm > .list-group-item:last-child {
+    border-top-right-radius: 0.25rem;
+    border-bottom-left-radius: 0;
+  }
+  /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-sm > .list-group-item.active {
+    margin-top: 0;
+  }
+  /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-sm > .list-group-item + .list-group-item {
+    border-top-width: 1px;
+    border-left-width: 0;
+  }
+  /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-sm > .list-group-item + .list-group-item.active {
+    margin-left: -1px;
+    border-left-width: 1px;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-md {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-md > .list-group-item:first-child {
+    border-bottom-left-radius: 0.25rem;
+    border-top-right-radius: 0;
+  }
+  /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-md > .list-group-item:last-child {
+    border-top-right-radius: 0.25rem;
+    border-bottom-left-radius: 0;
+  }
+  /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-md > .list-group-item.active {
+    margin-top: 0;
+  }
+  /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-md > .list-group-item + .list-group-item {
+    border-top-width: 1px;
+    border-left-width: 0;
+  }
+  /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-md > .list-group-item + .list-group-item.active {
+    margin-left: -1px;
+    border-left-width: 1px;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-lg {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-lg > .list-group-item:first-child {
+    border-bottom-left-radius: 0.25rem;
+    border-top-right-radius: 0;
+  }
+  /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-lg > .list-group-item:last-child {
+    border-top-right-radius: 0.25rem;
+    border-bottom-left-radius: 0;
+  }
+  /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-lg > .list-group-item.active {
+    margin-top: 0;
+  }
+  /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-lg > .list-group-item + .list-group-item {
+    border-top-width: 1px;
+    border-left-width: 0;
+  }
+  /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-lg > .list-group-item + .list-group-item.active {
+    margin-left: -1px;
+    border-left-width: 1px;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xl {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xl > .list-group-item:first-child {
+    border-bottom-left-radius: 0.25rem;
+    border-top-right-radius: 0;
+  }
+  /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xl > .list-group-item:last-child {
+    border-top-right-radius: 0.25rem;
+    border-bottom-left-radius: 0;
+  }
+  /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xl > .list-group-item.active {
+    margin-top: 0;
+  }
+  /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xl > .list-group-item + .list-group-item {
+    border-top-width: 1px;
+    border-left-width: 0;
+  }
+  /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xl > .list-group-item + .list-group-item.active {
+    margin-left: -1px;
+    border-left-width: 1px;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xxl {
+    -webkit-box-orient: horizontal;
+    -webkit-box-direction: normal;
+        -ms-flex-direction: row;
+            flex-direction: row;
+  }
+  /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xxl > .list-group-item:first-child {
+    border-bottom-left-radius: 0.25rem;
+    border-top-right-radius: 0;
+  }
+  /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xxl > .list-group-item:last-child {
+    border-top-right-radius: 0.25rem;
+    border-bottom-left-radius: 0;
+  }
+  /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xxl > .list-group-item.active {
+    margin-top: 0;
+  }
+  /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xxl > .list-group-item + .list-group-item {
+    border-top-width: 1px;
+    border-left-width: 0;
+  }
+  /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+  .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {
+    margin-left: -1px;
+    border-left-width: 1px;
+  }
+}
+
+/* line 134, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-flush {
+  border-radius: 0;
+}
+
+/* line 137, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-flush > .list-group-item {
+  border-width: 0 0 1px;
+}
+
+/* line 140, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-flush > .list-group-item:last-child {
+  border-bottom-width: 0;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-primary {
+  color: #242524;
+  background-color: #cbcbcb;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {
+  color: #242524;
+  background-color: #bebebe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-primary.list-group-item-action.active {
+  color: #ffffff;
+  background-color: #242524;
+  border-color: #242524;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-secondary {
+  color: #7d7a76;
+  background-color: #fbf9f7;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {
+  color: #7d7a76;
+  background-color: #f3ece6;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-secondary.list-group-item-action.active {
+  color: #ffffff;
+  background-color: #7d7a76;
+  border-color: #7d7a76;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-success {
+  color: #7d7a76;
+  background-color: #fbf9f7;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {
+  color: #7d7a76;
+  background-color: #f3ece6;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-success.list-group-item-action.active {
+  color: #ffffff;
+  background-color: #7d7a76;
+  border-color: #7d7a76;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-info {
+  color: #242524;
+  background-color: #cbcbcb;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {
+  color: #242524;
+  background-color: #bebebe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-info.list-group-item-action.active {
+  color: #ffffff;
+  background-color: #242524;
+  border-color: #242524;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-warning {
+  color: #242524;
+  background-color: #cbcbcb;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {
+  color: #242524;
+  background-color: #bebebe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-warning.list-group-item-action.active {
+  color: #ffffff;
+  background-color: #242524;
+  border-color: #242524;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-danger {
+  color: #77260d;
+  background-color: #f8ccbf;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {
+  color: #77260d;
+  background-color: #f5baa8;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-danger.list-group-item-action.active {
+  color: #ffffff;
+  background-color: #77260d;
+  border-color: #77260d;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-light {
+  color: gray;
+  background-color: #fdfdfd;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {
+  color: gray;
+  background-color: #f0f0f0;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-light.list-group-item-action.active {
+  color: #ffffff;
+  background-color: gray;
+  border-color: gray;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-dark {
+  color: #1b1e21;
+  background-color: #c6c8ca;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {
+  color: #1b1e21;
+  background-color: #b9bbbe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-dark.list-group-item-action.active {
+  color: #ffffff;
+  background-color: #1b1e21;
+  border-color: #1b1e21;
+}
+
+/* line 1, node_modules/bootstrap/scss/_close.scss */
+.close {
+  float: right;
+  font-size: 1.5rem;
+  font-weight: 700;
+  line-height: 1;
+  color: #000000;
+  text-shadow: 0 1px 0 #ffffff;
+  opacity: .5;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.close:hover {
+  color: #000000;
+  text-decoration: none;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {
+  opacity: .75;
+}
+
+/* line 29, node_modules/bootstrap/scss/_close.scss */
+button.close {
+  padding: 0;
+  background-color: transparent;
+  border: 0;
+}
+
+/* line 38, node_modules/bootstrap/scss/_close.scss */
+a.close.disabled {
+  pointer-events: none;
+}
+
+/* line 1, node_modules/bootstrap/scss/_toasts.scss */
+.toast {
+  -ms-flex-preferred-size: 350px;
+      flex-basis: 350px;
+  max-width: 350px;
+  font-size: 0.875rem;
+  background-color: rgba(255, 255, 255, 0.85);
+  background-clip: padding-box;
+  border: 1px solid rgba(0, 0, 0, 0.1);
+  -webkit-box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);
+          box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);
+  opacity: 0;
+  border-radius: 0.25rem;
+}
+
+/* line 15, node_modules/bootstrap/scss/_toasts.scss */
+.toast:not(:last-child) {
+  margin-bottom: 0.75rem;
+}
+
+/* line 19, node_modules/bootstrap/scss/_toasts.scss */
+.toast.showing {
+  opacity: 1;
+}
+
+/* line 23, node_modules/bootstrap/scss/_toasts.scss */
+.toast.show {
+  display: block;
+  opacity: 1;
+}
+
+/* line 28, node_modules/bootstrap/scss/_toasts.scss */
+.toast.hide {
+  display: none;
+}
+
+/* line 33, node_modules/bootstrap/scss/_toasts.scss */
+.toast-header {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  padding: 0.25rem 0.75rem;
+  color: #6c757d;
+  background-color: rgba(255, 255, 255, 0.85);
+  background-clip: padding-box;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+  border-top-left-radius: calc(0.25rem - 1px);
+  border-top-right-radius: calc(0.25rem - 1px);
+}
+
+/* line 44, node_modules/bootstrap/scss/_toasts.scss */
+.toast-body {
+  padding: 0.75rem;
+}
+
+/* line 7, node_modules/bootstrap/scss/_modal.scss */
+.modal-open {
+  overflow: hidden;
+}
+
+/* line 11, node_modules/bootstrap/scss/_modal.scss */
+.modal-open .modal {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+/* line 18, node_modules/bootstrap/scss/_modal.scss */
+.modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 1050;
+  display: none;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  outline: 0;
+}
+
+/* line 36, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog {
+  position: relative;
+  width: auto;
+  margin: 0.5rem;
+  pointer-events: none;
+}
+
+/* line 44, node_modules/bootstrap/scss/_modal.scss */
+.modal.fade .modal-dialog {
+  -webkit-transition: -webkit-transform 0.3s ease-out;
+  transition: -webkit-transform 0.3s ease-out;
+  transition: transform 0.3s ease-out;
+  transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;
+  -webkit-transform: translate(0, -50px);
+          transform: translate(0, -50px);
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 44, node_modules/bootstrap/scss/_modal.scss */
+  .modal.fade .modal-dialog {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 48, node_modules/bootstrap/scss/_modal.scss */
+.modal.show .modal-dialog {
+  -webkit-transform: none;
+          transform: none;
+}
+
+/* line 53, node_modules/bootstrap/scss/_modal.scss */
+.modal.modal-static .modal-dialog {
+  -webkit-transform: scale(1.02);
+          transform: scale(1.02);
+}
+
+/* line 58, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  max-height: calc(100% - 1rem);
+}
+
+/* line 62, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable .modal-content {
+  max-height: calc(100vh - 1rem);
+  overflow: hidden;
+}
+
+/* line 67, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable .modal-header,
+.modal-dialog-scrollable .modal-footer {
+  -ms-flex-negative: 0;
+      flex-shrink: 0;
+}
+
+/* line 72, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable .modal-body {
+  overflow-y: auto;
+}
+
+/* line 77, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  min-height: calc(100% - 1rem);
+}
+
+/* line 83, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered::before {
+  display: block;
+  height: calc(100vh - 1rem);
+  height: -webkit-min-content;
+  height: -moz-min-content;
+  height: min-content;
+  content: "";
+}
+
+/* line 91, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered.modal-dialog-scrollable {
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: column;
+          flex-direction: column;
+  -webkit-box-pack: center;
+      -ms-flex-pack: center;
+          justify-content: center;
+  height: 100%;
+}
+
+/* line 96, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered.modal-dialog-scrollable .modal-content {
+  max-height: none;
+}
+
+/* line 100, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered.modal-dialog-scrollable::before {
+  content: none;
+}
+
+/* line 107, node_modules/bootstrap/scss/_modal.scss */
+.modal-content {
+  position: relative;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: column;
+          flex-direction: column;
+  width: 100%;
+  pointer-events: auto;
+  background-color: #ffffff;
+  background-clip: padding-box;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 0.3rem;
+  outline: 0;
+}
+
+/* line 125, node_modules/bootstrap/scss/_modal.scss */
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 1040;
+  width: 100vw;
+  height: 100vh;
+  background-color: #000000;
+}
+
+/* line 135, node_modules/bootstrap/scss/_modal.scss */
+.modal-backdrop.fade {
+  opacity: 0;
+}
+
+/* line 136, node_modules/bootstrap/scss/_modal.scss */
+.modal-backdrop.show {
+  opacity: 0.5;
+}
+
+/* line 141, node_modules/bootstrap/scss/_modal.scss */
+.modal-header {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-align: start;
+      -ms-flex-align: start;
+          align-items: flex-start;
+  -webkit-box-pack: justify;
+      -ms-flex-pack: justify;
+          justify-content: space-between;
+  padding: 1rem 1rem;
+  border-bottom: 1px solid #dee2e6;
+  border-top-left-radius: calc(0.3rem - 1px);
+  border-top-right-radius: calc(0.3rem - 1px);
+}
+
+/* line 149, node_modules/bootstrap/scss/_modal.scss */
+.modal-header .close {
+  padding: 1rem 1rem;
+  margin: -1rem -1rem -1rem auto;
+}
+
+/* line 157, node_modules/bootstrap/scss/_modal.scss */
+.modal-title {
+  margin-bottom: 0;
+  line-height: 1.5;
+}
+
+/* line 164, node_modules/bootstrap/scss/_modal.scss */
+.modal-body {
+  position: relative;
+  -webkit-box-flex: 1;
+      -ms-flex: 1 1 auto;
+          flex: 1 1 auto;
+  padding: 1rem;
+}
+
+/* line 173, node_modules/bootstrap/scss/_modal.scss */
+.modal-footer {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-wrap: wrap;
+      flex-wrap: wrap;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  -webkit-box-pack: end;
+      -ms-flex-pack: end;
+          justify-content: flex-end;
+  padding: 0.75rem;
+  border-top: 1px solid #dee2e6;
+  border-bottom-right-radius: calc(0.3rem - 1px);
+  border-bottom-left-radius: calc(0.3rem - 1px);
+}
+
+/* line 185, node_modules/bootstrap/scss/_modal.scss */
+.modal-footer > * {
+  margin: 0.25rem;
+}
+
+/* line 191, node_modules/bootstrap/scss/_modal.scss */
+.modal-scrollbar-measure {
+  position: absolute;
+  top: -9999px;
+  width: 50px;
+  height: 50px;
+  overflow: scroll;
+}
+
+@media (min-width: 576px) {
+  /* line 202, node_modules/bootstrap/scss/_modal.scss */
+  .modal-dialog {
+    max-width: 500px;
+    margin: 1.75rem auto;
+  }
+  /* line 207, node_modules/bootstrap/scss/_modal.scss */
+  .modal-dialog-scrollable {
+    max-height: calc(100% - 3.5rem);
+  }
+  /* line 210, node_modules/bootstrap/scss/_modal.scss */
+  .modal-dialog-scrollable .modal-content {
+    max-height: calc(100vh - 3.5rem);
+  }
+  /* line 215, node_modules/bootstrap/scss/_modal.scss */
+  .modal-dialog-centered {
+    min-height: calc(100% - 3.5rem);
+  }
+  /* line 218, node_modules/bootstrap/scss/_modal.scss */
+  .modal-dialog-centered::before {
+    height: calc(100vh - 3.5rem);
+    height: -webkit-min-content;
+    height: -moz-min-content;
+    height: min-content;
+  }
+  /* line 228, node_modules/bootstrap/scss/_modal.scss */
+  .modal-sm {
+    max-width: 300px;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 232, node_modules/bootstrap/scss/_modal.scss */
+  .modal-lg,
+  .modal-xl {
+    max-width: 800px;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 239, node_modules/bootstrap/scss/_modal.scss */
+  .modal-xl {
+    max-width: 1140px;
+  }
+}
+
+/* line 2, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip {
+  position: absolute;
+  z-index: 1070;
+  display: block;
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  font-style: normal;
+  font-weight: 400;
+  line-height: 1.5;
+  text-align: left;
+  text-align: start;
+  text-decoration: none;
+  text-shadow: none;
+  text-transform: none;
+  letter-spacing: normal;
+  word-break: normal;
+  word-spacing: normal;
+  white-space: normal;
+  line-break: auto;
+  font-size: 0.875rem;
+  word-wrap: break-word;
+  opacity: 0;
+}
+
+/* line 15, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip.show {
+  opacity: 0.9;
+}
+
+/* line 17, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip .arrow {
+  position: absolute;
+  display: block;
+  width: 0.8rem;
+  height: 0.4rem;
+}
+
+/* line 23, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip .arrow::before {
+  position: absolute;
+  content: "";
+  border-color: transparent;
+  border-style: solid;
+}
+
+/* line 32, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] {
+  padding: 0.4rem 0;
+}
+
+/* line 35, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow {
+  bottom: 0;
+}
+
+/* line 38, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before {
+  top: 0;
+  border-width: 0.4rem 0.4rem 0;
+  border-top-color: #000000;
+}
+
+/* line 46, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] {
+  padding: 0 0.4rem;
+}
+
+/* line 49, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow {
+  left: 0;
+  width: 0.4rem;
+  height: 0.8rem;
+}
+
+/* line 54, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before {
+  right: 0;
+  border-width: 0.4rem 0.4rem 0.4rem 0;
+  border-right-color: #000000;
+}
+
+/* line 62, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] {
+  padding: 0.4rem 0;
+}
+
+/* line 65, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow {
+  top: 0;
+}
+
+/* line 68, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before {
+  bottom: 0;
+  border-width: 0 0.4rem 0.4rem;
+  border-bottom-color: #000000;
+}
+
+/* line 76, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] {
+  padding: 0 0.4rem;
+}
+
+/* line 79, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow {
+  right: 0;
+  width: 0.4rem;
+  height: 0.8rem;
+}
+
+/* line 84, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before {
+  left: 0;
+  border-width: 0.4rem 0 0.4rem 0.4rem;
+  border-left-color: #000000;
+}
+
+/* line 108, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip-inner {
+  max-width: 200px;
+  padding: 0.25rem 0.5rem;
+  color: #ffffff;
+  text-align: center;
+  background-color: #000000;
+  border-radius: 0.25rem;
+}
+
+/* line 1, node_modules/bootstrap/scss/_popover.scss */
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1060;
+  display: block;
+  max-width: 276px;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+  font-style: normal;
+  font-weight: 400;
+  line-height: 1.5;
+  text-align: left;
+  text-align: start;
+  text-decoration: none;
+  text-shadow: none;
+  text-transform: none;
+  letter-spacing: normal;
+  word-break: normal;
+  word-spacing: normal;
+  white-space: normal;
+  line-break: auto;
+  font-size: 0.875rem;
+  word-wrap: break-word;
+  background-color: #ffffff;
+  background-clip: padding-box;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 0.3rem;
+}
+
+/* line 20, node_modules/bootstrap/scss/_popover.scss */
+.popover .arrow {
+  position: absolute;
+  display: block;
+  width: 1rem;
+  height: 0.5rem;
+  margin: 0 0.3rem;
+}
+
+/* line 27, node_modules/bootstrap/scss/_popover.scss */
+.popover .arrow::before, .popover .arrow::after {
+  position: absolute;
+  display: block;
+  content: "";
+  border-color: transparent;
+  border-style: solid;
+}
+
+/* line 38, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top, .bs-popover-auto[x-placement^="top"] {
+  margin-bottom: 0.5rem;
+}
+
+/* line 41, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow {
+  bottom: calc(-0.5rem - 1px);
+}
+
+/* line 44, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before {
+  bottom: 0;
+  border-width: 0.5rem 0.5rem 0;
+  border-top-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 50, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after {
+  bottom: 1px;
+  border-width: 0.5rem 0.5rem 0;
+  border-top-color: #ffffff;
+}
+
+/* line 58, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right, .bs-popover-auto[x-placement^="right"] {
+  margin-left: 0.5rem;
+}
+
+/* line 61, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow {
+  left: calc(-0.5rem - 1px);
+  width: 0.5rem;
+  height: 1rem;
+  margin: 0.3rem 0;
+}
+
+/* line 67, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before {
+  left: 0;
+  border-width: 0.5rem 0.5rem 0.5rem 0;
+  border-right-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 73, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after {
+  left: 1px;
+  border-width: 0.5rem 0.5rem 0.5rem 0;
+  border-right-color: #ffffff;
+}
+
+/* line 81, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] {
+  margin-top: 0.5rem;
+}
+
+/* line 84, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow {
+  top: calc(-0.5rem - 1px);
+}
+
+/* line 87, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before {
+  top: 0;
+  border-width: 0 0.5rem 0.5rem 0.5rem;
+  border-bottom-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 93, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after {
+  top: 1px;
+  border-width: 0 0.5rem 0.5rem 0.5rem;
+  border-bottom-color: #ffffff;
+}
+
+/* line 101, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before {
+  position: absolute;
+  top: 0;
+  left: 50%;
+  display: block;
+  width: 1rem;
+  margin-left: -0.5rem;
+  content: "";
+  border-bottom: 1px solid #f7f7f7;
+}
+
+/* line 113, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left, .bs-popover-auto[x-placement^="left"] {
+  margin-right: 0.5rem;
+}
+
+/* line 116, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow {
+  right: calc(-0.5rem - 1px);
+  width: 0.5rem;
+  height: 1rem;
+  margin: 0.3rem 0;
+}
+
+/* line 122, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before {
+  right: 0;
+  border-width: 0.5rem 0 0.5rem 0.5rem;
+  border-left-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 128, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after {
+  right: 1px;
+  border-width: 0.5rem 0 0.5rem 0.5rem;
+  border-left-color: #ffffff;
+}
+
+/* line 153, node_modules/bootstrap/scss/_popover.scss */
+.popover-header {
+  padding: 0.5rem 0.75rem;
+  margin-bottom: 0;
+  font-size: 1rem;
+  background-color: #f7f7f7;
+  border-bottom: 1px solid #ebebeb;
+  border-top-left-radius: calc(0.3rem - 1px);
+  border-top-right-radius: calc(0.3rem - 1px);
+}
+
+/* line 162, node_modules/bootstrap/scss/_popover.scss */
+.popover-header:empty {
+  display: none;
+}
+
+/* line 167, node_modules/bootstrap/scss/_popover.scss */
+.popover-body {
+  padding: 0.5rem 0.75rem;
+  color: #464746;
+}
+
+/* line 14, node_modules/bootstrap/scss/_carousel.scss */
+.carousel {
+  position: relative;
+}
+
+/* line 18, node_modules/bootstrap/scss/_carousel.scss */
+.carousel.pointer-event {
+  -ms-touch-action: pan-y;
+      touch-action: pan-y;
+}
+
+/* line 22, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-inner {
+  position: relative;
+  width: 100%;
+  overflow: hidden;
+}
+
+/* line 2, node_modules/bootstrap/scss/mixins/_clearfix.scss */
+.carousel-inner::after {
+  display: block;
+  clear: both;
+  content: "";
+}
+
+/* line 29, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item {
+  position: relative;
+  display: none;
+  float: left;
+  width: 100%;
+  margin-right: -100%;
+  -webkit-backface-visibility: hidden;
+          backface-visibility: hidden;
+  -webkit-transition: -webkit-transform 0.6s ease-in-out;
+  transition: -webkit-transform 0.6s ease-in-out;
+  transition: transform 0.6s ease-in-out;
+  transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 29, node_modules/bootstrap/scss/_carousel.scss */
+  .carousel-item {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 39, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item.active,
+.carousel-item-next,
+.carousel-item-prev {
+  display: block;
+}
+
+/* line 45, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item-next:not(.carousel-item-left),
+.active.carousel-item-right {
+  -webkit-transform: translateX(100%);
+          transform: translateX(100%);
+}
+
+/* line 50, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item-prev:not(.carousel-item-right),
+.active.carousel-item-left {
+  -webkit-transform: translateX(-100%);
+          transform: translateX(-100%);
+}
+
+/* line 61, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-fade .carousel-item {
+  opacity: 0;
+  -webkit-transition-property: opacity;
+  transition-property: opacity;
+  -webkit-transform: none;
+          transform: none;
+}
+
+/* line 67, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-fade .carousel-item.active,
+.carousel-fade .carousel-item-next.carousel-item-left,
+.carousel-fade .carousel-item-prev.carousel-item-right {
+  z-index: 1;
+  opacity: 1;
+}
+
+/* line 74, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-fade .active.carousel-item-left,
+.carousel-fade .active.carousel-item-right {
+  z-index: 0;
+  opacity: 0;
+  -webkit-transition: opacity 0s 0.6s;
+  transition: opacity 0s 0.6s;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 74, node_modules/bootstrap/scss/_carousel.scss */
+  .carousel-fade .active.carousel-item-left,
+  .carousel-fade .active.carousel-item-right {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 87, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev,
+.carousel-control-next {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  z-index: 1;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-align: center;
+      -ms-flex-align: center;
+          align-items: center;
+  -webkit-box-pack: center;
+      -ms-flex-pack: center;
+          justify-content: center;
+  width: 15%;
+  color: #ffffff;
+  text-align: center;
+  opacity: 0.5;
+  -webkit-transition: opacity 0.15s ease;
+  transition: opacity 0.15s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 87, node_modules/bootstrap/scss/_carousel.scss */
+  .carousel-control-prev,
+  .carousel-control-next {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.carousel-control-prev:hover, .carousel-control-prev:focus,
+.carousel-control-next:hover,
+.carousel-control-next:focus {
+  color: #ffffff;
+  text-decoration: none;
+  outline: 0;
+  opacity: 0.9;
+}
+
+/* line 111, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev {
+  left: 0;
+}
+
+/* line 117, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-next {
+  right: 0;
+}
+
+/* line 125, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev-icon,
+.carousel-control-next-icon {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  background: no-repeat 50% / 100% 100%;
+}
+
+/* line 132, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev-icon {
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e");
+}
+
+/* line 135, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-next-icon {
+  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e");
+}
+
+/* line 145, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-indicators {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 15;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-pack: center;
+      -ms-flex-pack: center;
+          justify-content: center;
+  padding-left: 0;
+  margin-right: 15%;
+  margin-left: 15%;
+  list-style: none;
+}
+
+/* line 159, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-indicators li {
+  -webkit-box-sizing: content-box;
+          box-sizing: content-box;
+  -webkit-box-flex: 0;
+      -ms-flex: 0 1 auto;
+          flex: 0 1 auto;
+  width: 30px;
+  height: 3px;
+  margin-right: 3px;
+  margin-left: 3px;
+  text-indent: -999px;
+  cursor: pointer;
+  background-color: #ffffff;
+  background-clip: padding-box;
+  border-top: 10px solid transparent;
+  border-bottom: 10px solid transparent;
+  opacity: .5;
+  -webkit-transition: opacity 0.6s ease;
+  transition: opacity 0.6s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  /* line 159, node_modules/bootstrap/scss/_carousel.scss */
+  .carousel-indicators li {
+    -webkit-transition: none;
+    transition: none;
+  }
+}
+
+/* line 177, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-indicators .active {
+  opacity: 1;
+}
+
+/* line 187, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-caption {
+  position: absolute;
+  right: 15%;
+  bottom: 20px;
+  left: 15%;
+  z-index: 10;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: #ffffff;
+  text-align: center;
+}
+
+@-webkit-keyframes spinner-border {
+  to {
+    -webkit-transform: rotate(360deg);
+            transform: rotate(360deg);
+  }
+}
+
+@keyframes spinner-border {
+  to {
+    -webkit-transform: rotate(360deg);
+            transform: rotate(360deg);
+  }
+}
+
+/* line 9, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-border {
+  display: inline-block;
+  width: 2rem;
+  height: 2rem;
+  vertical-align: text-bottom;
+  border: 0.25em solid currentColor;
+  border-right-color: transparent;
+  border-radius: 50%;
+  -webkit-animation: spinner-border .75s linear infinite;
+          animation: spinner-border .75s linear infinite;
+}
+
+/* line 21, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-border-sm {
+  width: 1rem;
+  height: 1rem;
+  border-width: 0.2em;
+}
+
+@-webkit-keyframes spinner-grow {
+  0% {
+    -webkit-transform: scale(0);
+            transform: scale(0);
+  }
+  50% {
+    opacity: 1;
+    -webkit-transform: none;
+            transform: none;
+  }
+}
+
+@keyframes spinner-grow {
+  0% {
+    -webkit-transform: scale(0);
+            transform: scale(0);
+  }
+  50% {
+    opacity: 1;
+    -webkit-transform: none;
+            transform: none;
+  }
+}
+
+/* line 41, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-grow {
+  display: inline-block;
+  width: 2rem;
+  height: 2rem;
+  vertical-align: text-bottom;
+  background-color: currentColor;
+  border-radius: 50%;
+  opacity: 0;
+  -webkit-animation: spinner-grow .75s linear infinite;
+          animation: spinner-grow .75s linear infinite;
+}
+
+/* line 53, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-grow-sm {
+  width: 1rem;
+  height: 1rem;
+}
+
+/* line 3, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-baseline {
+  vertical-align: baseline !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-top {
+  vertical-align: top !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-middle {
+  vertical-align: middle !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-bottom {
+  vertical-align: bottom !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-text-bottom {
+  vertical-align: text-bottom !important;
+}
+
+/* line 8, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-text-top {
+  vertical-align: text-top !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-primary {
+  background-color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-primary:hover, a.bg-primary:focus,
+button.bg-primary:hover,
+button.bg-primary:focus {
+  background-color: #2d2d2d !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-secondary {
+  background-color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-secondary:hover, a.bg-secondary:focus,
+button.bg-secondary:hover,
+button.bg-secondary:focus {
+  background-color: #ded3c2 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-success {
+  background-color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-success:hover, a.bg-success:focus,
+button.bg-success:hover,
+button.bg-success:focus {
+  background-color: #ded3c2 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-info {
+  background-color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-info:hover, a.bg-info:focus,
+button.bg-info:hover,
+button.bg-info:focus {
+  background-color: #2d2d2d !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-warning {
+  background-color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-warning:hover, a.bg-warning:focus,
+button.bg-warning:hover,
+button.bg-warning:focus {
+  background-color: #2d2d2d !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-danger {
+  background-color: #e54a19 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-danger:hover, a.bg-danger:focus,
+button.bg-danger:hover,
+button.bg-danger:focus {
+  background-color: #b73b14 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-light {
+  background-color: #f7f7f7 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-light:hover, a.bg-light:focus,
+button.bg-light:hover,
+button.bg-light:focus {
+  background-color: #dedede !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-dark {
+  background-color: #343a40 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-dark:hover, a.bg-dark:focus,
+button.bg-dark:hover,
+button.bg-dark:focus {
+  background-color: #1d2124 !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_background.scss */
+.bg-white {
+  background-color: #ffffff !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_background.scss */
+.bg-transparent {
+  background-color: transparent !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border {
+  border: 1px solid #dee2e6 !important;
+}
+
+/* line 8, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-top {
+  border-top: 1px solid #dee2e6 !important;
+}
+
+/* line 9, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-right {
+  border-right: 1px solid #dee2e6 !important;
+}
+
+/* line 10, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-bottom {
+  border-bottom: 1px solid #dee2e6 !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-left {
+  border-left: 1px solid #dee2e6 !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-0 {
+  border: 0 !important;
+}
+
+/* line 14, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-top-0 {
+  border-top: 0 !important;
+}
+
+/* line 15, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-right-0 {
+  border-right: 0 !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-bottom-0 {
+  border-bottom: 0 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-left-0 {
+  border-left: 0 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-primary {
+  border-color: #464746 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-secondary {
+  border-color: #f0ebe3 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-success {
+  border-color: #f0ebe3 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-info {
+  border-color: #464746 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-warning {
+  border-color: #464746 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-danger {
+  border-color: #e54a19 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-light {
+  border-color: #f7f7f7 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-dark {
+  border-color: #343a40 !important;
+}
+
+/* line 25, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-white {
+  border-color: #ffffff !important;
+}
+
+/* line 33, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-sm {
+  border-radius: 0.2rem !important;
+}
+
+/* line 37, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded {
+  border-radius: 0.25rem !important;
+}
+
+/* line 41, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-top {
+  border-top-left-radius: 0.25rem !important;
+  border-top-right-radius: 0.25rem !important;
+}
+
+/* line 46, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-right {
+  border-top-right-radius: 0.25rem !important;
+  border-bottom-right-radius: 0.25rem !important;
+}
+
+/* line 51, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-bottom {
+  border-bottom-right-radius: 0.25rem !important;
+  border-bottom-left-radius: 0.25rem !important;
+}
+
+/* line 56, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-left {
+  border-top-left-radius: 0.25rem !important;
+  border-bottom-left-radius: 0.25rem !important;
+}
+
+/* line 61, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-lg {
+  border-radius: 0.3rem !important;
+}
+
+/* line 65, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-circle {
+  border-radius: 50% !important;
+}
+
+/* line 69, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-pill {
+  border-radius: 50rem !important;
+}
+
+/* line 73, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-0 {
+  border-radius: 0 !important;
+}
+
+/* line 2, node_modules/bootstrap/scss/mixins/_clearfix.scss */
+.clearfix::after {
+  display: block;
+  clear: both;
+  content: "";
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-none {
+  display: none !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-inline {
+  display: inline !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-inline-block {
+  display: inline-block !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-block {
+  display: block !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-table {
+  display: table !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-table-row {
+  display: table-row !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-table-cell {
+  display: table-cell !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-flex {
+  display: -webkit-box !important;
+  display: -ms-flexbox !important;
+  display: flex !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-inline-flex {
+  display: -webkit-inline-box !important;
+  display: -ms-inline-flexbox !important;
+  display: inline-flex !important;
+}
+
+@media (min-width: 576px) {
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-none {
+    display: none !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-inline {
+    display: inline !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-block {
+    display: block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-table {
+    display: table !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-table-row {
+    display: table-row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-table-cell {
+    display: table-cell !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-flex {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-sm-inline-flex {
+    display: -webkit-inline-box !important;
+    display: -ms-inline-flexbox !important;
+    display: inline-flex !important;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-none {
+    display: none !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-inline {
+    display: inline !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-block {
+    display: block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-table {
+    display: table !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-table-row {
+    display: table-row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-table-cell {
+    display: table-cell !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-flex {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-md-inline-flex {
+    display: -webkit-inline-box !important;
+    display: -ms-inline-flexbox !important;
+    display: inline-flex !important;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-none {
+    display: none !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-inline {
+    display: inline !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-block {
+    display: block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-table {
+    display: table !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-table-row {
+    display: table-row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-table-cell {
+    display: table-cell !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-flex {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-lg-inline-flex {
+    display: -webkit-inline-box !important;
+    display: -ms-inline-flexbox !important;
+    display: inline-flex !important;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-none {
+    display: none !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-inline {
+    display: inline !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-block {
+    display: block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-table {
+    display: table !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-table-row {
+    display: table-row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-table-cell {
+    display: table-cell !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-flex {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xl-inline-flex {
+    display: -webkit-inline-box !important;
+    display: -ms-inline-flexbox !important;
+    display: inline-flex !important;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-none {
+    display: none !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-inline {
+    display: inline !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-inline-block {
+    display: inline-block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-block {
+    display: block !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-table {
+    display: table !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-table-row {
+    display: table-row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-table-cell {
+    display: table-cell !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-flex {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-xxl-inline-flex {
+    display: -webkit-inline-box !important;
+    display: -ms-inline-flexbox !important;
+    display: inline-flex !important;
+  }
+}
+
+@media print {
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-none {
+    display: none !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-inline {
+    display: inline !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-inline-block {
+    display: inline-block !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-block {
+    display: block !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-table {
+    display: table !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-table-row {
+    display: table-row !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-table-cell {
+    display: table-cell !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-flex {
+    display: -webkit-box !important;
+    display: -ms-flexbox !important;
+    display: flex !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+  .d-print-inline-flex {
+    display: -webkit-inline-box !important;
+    display: -ms-inline-flexbox !important;
+    display: inline-flex !important;
+  }
+}
+
+/* line 3, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive {
+  position: relative;
+  display: block;
+  width: 100%;
+  padding: 0;
+  overflow: hidden;
+}
+
+/* line 10, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive::before {
+  display: block;
+  content: "";
+}
+
+/* line 15, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive .embed-responsive-item,
+.embed-responsive iframe,
+.embed-responsive embed,
+.embed-responsive object,
+.embed-responsive video {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  border: 0;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-21by9::before {
+  padding-top: 42.85714%;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-16by9::before {
+  padding-top: 56.25%;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-4by3::before {
+  padding-top: 75%;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-1by1::before {
+  padding-top: 100%;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-row {
+  -webkit-box-orient: horizontal !important;
+  -webkit-box-direction: normal !important;
+      -ms-flex-direction: row !important;
+          flex-direction: row !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-column {
+  -webkit-box-orient: vertical !important;
+  -webkit-box-direction: normal !important;
+      -ms-flex-direction: column !important;
+          flex-direction: column !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-row-reverse {
+  -webkit-box-orient: horizontal !important;
+  -webkit-box-direction: reverse !important;
+      -ms-flex-direction: row-reverse !important;
+          flex-direction: row-reverse !important;
+}
+
+/* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-column-reverse {
+  -webkit-box-orient: vertical !important;
+  -webkit-box-direction: reverse !important;
+      -ms-flex-direction: column-reverse !important;
+          flex-direction: column-reverse !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-wrap {
+  -ms-flex-wrap: wrap !important;
+      flex-wrap: wrap !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-nowrap {
+  -ms-flex-wrap: nowrap !important;
+      flex-wrap: nowrap !important;
+}
+
+/* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-wrap-reverse {
+  -ms-flex-wrap: wrap-reverse !important;
+      flex-wrap: wrap-reverse !important;
+}
+
+/* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-fill {
+  -webkit-box-flex: 1 !important;
+      -ms-flex: 1 1 auto !important;
+          flex: 1 1 auto !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-grow-0 {
+  -webkit-box-flex: 0 !important;
+      -ms-flex-positive: 0 !important;
+          flex-grow: 0 !important;
+}
+
+/* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-grow-1 {
+  -webkit-box-flex: 1 !important;
+      -ms-flex-positive: 1 !important;
+          flex-grow: 1 !important;
+}
+
+/* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-shrink-0 {
+  -ms-flex-negative: 0 !important;
+      flex-shrink: 0 !important;
+}
+
+/* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-shrink-1 {
+  -ms-flex-negative: 1 !important;
+      flex-shrink: 1 !important;
+}
+
+/* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-start {
+  -webkit-box-pack: start !important;
+      -ms-flex-pack: start !important;
+          justify-content: flex-start !important;
+}
+
+/* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-end {
+  -webkit-box-pack: end !important;
+      -ms-flex-pack: end !important;
+          justify-content: flex-end !important;
+}
+
+/* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-center {
+  -webkit-box-pack: center !important;
+      -ms-flex-pack: center !important;
+          justify-content: center !important;
+}
+
+/* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-between {
+  -webkit-box-pack: justify !important;
+      -ms-flex-pack: justify !important;
+          justify-content: space-between !important;
+}
+
+/* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-around {
+  -ms-flex-pack: distribute !important;
+      justify-content: space-around !important;
+}
+
+/* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-start {
+  -webkit-box-align: start !important;
+      -ms-flex-align: start !important;
+          align-items: flex-start !important;
+}
+
+/* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-end {
+  -webkit-box-align: end !important;
+      -ms-flex-align: end !important;
+          align-items: flex-end !important;
+}
+
+/* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-center {
+  -webkit-box-align: center !important;
+      -ms-flex-align: center !important;
+          align-items: center !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-baseline {
+  -webkit-box-align: baseline !important;
+      -ms-flex-align: baseline !important;
+          align-items: baseline !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-stretch {
+  -webkit-box-align: stretch !important;
+      -ms-flex-align: stretch !important;
+          align-items: stretch !important;
+}
+
+/* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-start {
+  -ms-flex-line-pack: start !important;
+      align-content: flex-start !important;
+}
+
+/* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-end {
+  -ms-flex-line-pack: end !important;
+      align-content: flex-end !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-center {
+  -ms-flex-line-pack: center !important;
+      align-content: center !important;
+}
+
+/* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-between {
+  -ms-flex-line-pack: justify !important;
+      align-content: space-between !important;
+}
+
+/* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-around {
+  -ms-flex-line-pack: distribute !important;
+      align-content: space-around !important;
+}
+
+/* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-stretch {
+  -ms-flex-line-pack: stretch !important;
+      align-content: stretch !important;
+}
+
+/* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-auto {
+  -ms-flex-item-align: auto !important;
+      align-self: auto !important;
+}
+
+/* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-start {
+  -ms-flex-item-align: start !important;
+      align-self: flex-start !important;
+}
+
+/* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-end {
+  -ms-flex-item-align: end !important;
+      align-self: flex-end !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-center {
+  -ms-flex-item-align: center !important;
+      align-self: center !important;
+}
+
+/* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-baseline {
+  -ms-flex-item-align: baseline !important;
+      align-self: baseline !important;
+}
+
+/* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-stretch {
+  -ms-flex-item-align: stretch !important;
+      align-self: stretch !important;
+}
+
+@media (min-width: 576px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-row {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: row !important;
+            flex-direction: row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-column {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: column !important;
+            flex-direction: column !important;
+  }
+  /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-row-reverse {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: row-reverse !important;
+            flex-direction: row-reverse !important;
+  }
+  /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-column-reverse {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: column-reverse !important;
+            flex-direction: column-reverse !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-wrap {
+    -ms-flex-wrap: wrap !important;
+        flex-wrap: wrap !important;
+  }
+  /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-nowrap {
+    -ms-flex-wrap: nowrap !important;
+        flex-wrap: nowrap !important;
+  }
+  /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-wrap-reverse {
+    -ms-flex-wrap: wrap-reverse !important;
+        flex-wrap: wrap-reverse !important;
+  }
+  /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-fill {
+    -webkit-box-flex: 1 !important;
+        -ms-flex: 1 1 auto !important;
+            flex: 1 1 auto !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-grow-0 {
+    -webkit-box-flex: 0 !important;
+        -ms-flex-positive: 0 !important;
+            flex-grow: 0 !important;
+  }
+  /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-grow-1 {
+    -webkit-box-flex: 1 !important;
+        -ms-flex-positive: 1 !important;
+            flex-grow: 1 !important;
+  }
+  /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-shrink-0 {
+    -ms-flex-negative: 0 !important;
+        flex-shrink: 0 !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-sm-shrink-1 {
+    -ms-flex-negative: 1 !important;
+        flex-shrink: 1 !important;
+  }
+  /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-sm-start {
+    -webkit-box-pack: start !important;
+        -ms-flex-pack: start !important;
+            justify-content: flex-start !important;
+  }
+  /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-sm-end {
+    -webkit-box-pack: end !important;
+        -ms-flex-pack: end !important;
+            justify-content: flex-end !important;
+  }
+  /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-sm-center {
+    -webkit-box-pack: center !important;
+        -ms-flex-pack: center !important;
+            justify-content: center !important;
+  }
+  /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-sm-between {
+    -webkit-box-pack: justify !important;
+        -ms-flex-pack: justify !important;
+            justify-content: space-between !important;
+  }
+  /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-sm-around {
+    -ms-flex-pack: distribute !important;
+        justify-content: space-around !important;
+  }
+  /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-sm-start {
+    -webkit-box-align: start !important;
+        -ms-flex-align: start !important;
+            align-items: flex-start !important;
+  }
+  /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-sm-end {
+    -webkit-box-align: end !important;
+        -ms-flex-align: end !important;
+            align-items: flex-end !important;
+  }
+  /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-sm-center {
+    -webkit-box-align: center !important;
+        -ms-flex-align: center !important;
+            align-items: center !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-sm-baseline {
+    -webkit-box-align: baseline !important;
+        -ms-flex-align: baseline !important;
+            align-items: baseline !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-sm-stretch {
+    -webkit-box-align: stretch !important;
+        -ms-flex-align: stretch !important;
+            align-items: stretch !important;
+  }
+  /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-sm-start {
+    -ms-flex-line-pack: start !important;
+        align-content: flex-start !important;
+  }
+  /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-sm-end {
+    -ms-flex-line-pack: end !important;
+        align-content: flex-end !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-sm-center {
+    -ms-flex-line-pack: center !important;
+        align-content: center !important;
+  }
+  /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-sm-between {
+    -ms-flex-line-pack: justify !important;
+        align-content: space-between !important;
+  }
+  /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-sm-around {
+    -ms-flex-line-pack: distribute !important;
+        align-content: space-around !important;
+  }
+  /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-sm-stretch {
+    -ms-flex-line-pack: stretch !important;
+        align-content: stretch !important;
+  }
+  /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-sm-auto {
+    -ms-flex-item-align: auto !important;
+        align-self: auto !important;
+  }
+  /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-sm-start {
+    -ms-flex-item-align: start !important;
+        align-self: flex-start !important;
+  }
+  /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-sm-end {
+    -ms-flex-item-align: end !important;
+        align-self: flex-end !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-sm-center {
+    -ms-flex-item-align: center !important;
+        align-self: center !important;
+  }
+  /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-sm-baseline {
+    -ms-flex-item-align: baseline !important;
+        align-self: baseline !important;
+  }
+  /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-sm-stretch {
+    -ms-flex-item-align: stretch !important;
+        align-self: stretch !important;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-row {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: row !important;
+            flex-direction: row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-column {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: column !important;
+            flex-direction: column !important;
+  }
+  /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-row-reverse {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: row-reverse !important;
+            flex-direction: row-reverse !important;
+  }
+  /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-column-reverse {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: column-reverse !important;
+            flex-direction: column-reverse !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-wrap {
+    -ms-flex-wrap: wrap !important;
+        flex-wrap: wrap !important;
+  }
+  /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-nowrap {
+    -ms-flex-wrap: nowrap !important;
+        flex-wrap: nowrap !important;
+  }
+  /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-wrap-reverse {
+    -ms-flex-wrap: wrap-reverse !important;
+        flex-wrap: wrap-reverse !important;
+  }
+  /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-fill {
+    -webkit-box-flex: 1 !important;
+        -ms-flex: 1 1 auto !important;
+            flex: 1 1 auto !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-grow-0 {
+    -webkit-box-flex: 0 !important;
+        -ms-flex-positive: 0 !important;
+            flex-grow: 0 !important;
+  }
+  /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-grow-1 {
+    -webkit-box-flex: 1 !important;
+        -ms-flex-positive: 1 !important;
+            flex-grow: 1 !important;
+  }
+  /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-shrink-0 {
+    -ms-flex-negative: 0 !important;
+        flex-shrink: 0 !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-md-shrink-1 {
+    -ms-flex-negative: 1 !important;
+        flex-shrink: 1 !important;
+  }
+  /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-md-start {
+    -webkit-box-pack: start !important;
+        -ms-flex-pack: start !important;
+            justify-content: flex-start !important;
+  }
+  /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-md-end {
+    -webkit-box-pack: end !important;
+        -ms-flex-pack: end !important;
+            justify-content: flex-end !important;
+  }
+  /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-md-center {
+    -webkit-box-pack: center !important;
+        -ms-flex-pack: center !important;
+            justify-content: center !important;
+  }
+  /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-md-between {
+    -webkit-box-pack: justify !important;
+        -ms-flex-pack: justify !important;
+            justify-content: space-between !important;
+  }
+  /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-md-around {
+    -ms-flex-pack: distribute !important;
+        justify-content: space-around !important;
+  }
+  /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-md-start {
+    -webkit-box-align: start !important;
+        -ms-flex-align: start !important;
+            align-items: flex-start !important;
+  }
+  /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-md-end {
+    -webkit-box-align: end !important;
+        -ms-flex-align: end !important;
+            align-items: flex-end !important;
+  }
+  /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-md-center {
+    -webkit-box-align: center !important;
+        -ms-flex-align: center !important;
+            align-items: center !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-md-baseline {
+    -webkit-box-align: baseline !important;
+        -ms-flex-align: baseline !important;
+            align-items: baseline !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-md-stretch {
+    -webkit-box-align: stretch !important;
+        -ms-flex-align: stretch !important;
+            align-items: stretch !important;
+  }
+  /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-md-start {
+    -ms-flex-line-pack: start !important;
+        align-content: flex-start !important;
+  }
+  /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-md-end {
+    -ms-flex-line-pack: end !important;
+        align-content: flex-end !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-md-center {
+    -ms-flex-line-pack: center !important;
+        align-content: center !important;
+  }
+  /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-md-between {
+    -ms-flex-line-pack: justify !important;
+        align-content: space-between !important;
+  }
+  /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-md-around {
+    -ms-flex-line-pack: distribute !important;
+        align-content: space-around !important;
+  }
+  /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-md-stretch {
+    -ms-flex-line-pack: stretch !important;
+        align-content: stretch !important;
+  }
+  /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-md-auto {
+    -ms-flex-item-align: auto !important;
+        align-self: auto !important;
+  }
+  /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-md-start {
+    -ms-flex-item-align: start !important;
+        align-self: flex-start !important;
+  }
+  /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-md-end {
+    -ms-flex-item-align: end !important;
+        align-self: flex-end !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-md-center {
+    -ms-flex-item-align: center !important;
+        align-self: center !important;
+  }
+  /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-md-baseline {
+    -ms-flex-item-align: baseline !important;
+        align-self: baseline !important;
+  }
+  /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-md-stretch {
+    -ms-flex-item-align: stretch !important;
+        align-self: stretch !important;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-row {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: row !important;
+            flex-direction: row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-column {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: column !important;
+            flex-direction: column !important;
+  }
+  /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-row-reverse {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: row-reverse !important;
+            flex-direction: row-reverse !important;
+  }
+  /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-column-reverse {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: column-reverse !important;
+            flex-direction: column-reverse !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-wrap {
+    -ms-flex-wrap: wrap !important;
+        flex-wrap: wrap !important;
+  }
+  /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-nowrap {
+    -ms-flex-wrap: nowrap !important;
+        flex-wrap: nowrap !important;
+  }
+  /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-wrap-reverse {
+    -ms-flex-wrap: wrap-reverse !important;
+        flex-wrap: wrap-reverse !important;
+  }
+  /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-fill {
+    -webkit-box-flex: 1 !important;
+        -ms-flex: 1 1 auto !important;
+            flex: 1 1 auto !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-grow-0 {
+    -webkit-box-flex: 0 !important;
+        -ms-flex-positive: 0 !important;
+            flex-grow: 0 !important;
+  }
+  /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-grow-1 {
+    -webkit-box-flex: 1 !important;
+        -ms-flex-positive: 1 !important;
+            flex-grow: 1 !important;
+  }
+  /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-shrink-0 {
+    -ms-flex-negative: 0 !important;
+        flex-shrink: 0 !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-lg-shrink-1 {
+    -ms-flex-negative: 1 !important;
+        flex-shrink: 1 !important;
+  }
+  /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-lg-start {
+    -webkit-box-pack: start !important;
+        -ms-flex-pack: start !important;
+            justify-content: flex-start !important;
+  }
+  /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-lg-end {
+    -webkit-box-pack: end !important;
+        -ms-flex-pack: end !important;
+            justify-content: flex-end !important;
+  }
+  /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-lg-center {
+    -webkit-box-pack: center !important;
+        -ms-flex-pack: center !important;
+            justify-content: center !important;
+  }
+  /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-lg-between {
+    -webkit-box-pack: justify !important;
+        -ms-flex-pack: justify !important;
+            justify-content: space-between !important;
+  }
+  /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-lg-around {
+    -ms-flex-pack: distribute !important;
+        justify-content: space-around !important;
+  }
+  /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-lg-start {
+    -webkit-box-align: start !important;
+        -ms-flex-align: start !important;
+            align-items: flex-start !important;
+  }
+  /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-lg-end {
+    -webkit-box-align: end !important;
+        -ms-flex-align: end !important;
+            align-items: flex-end !important;
+  }
+  /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-lg-center {
+    -webkit-box-align: center !important;
+        -ms-flex-align: center !important;
+            align-items: center !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-lg-baseline {
+    -webkit-box-align: baseline !important;
+        -ms-flex-align: baseline !important;
+            align-items: baseline !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-lg-stretch {
+    -webkit-box-align: stretch !important;
+        -ms-flex-align: stretch !important;
+            align-items: stretch !important;
+  }
+  /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-lg-start {
+    -ms-flex-line-pack: start !important;
+        align-content: flex-start !important;
+  }
+  /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-lg-end {
+    -ms-flex-line-pack: end !important;
+        align-content: flex-end !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-lg-center {
+    -ms-flex-line-pack: center !important;
+        align-content: center !important;
+  }
+  /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-lg-between {
+    -ms-flex-line-pack: justify !important;
+        align-content: space-between !important;
+  }
+  /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-lg-around {
+    -ms-flex-line-pack: distribute !important;
+        align-content: space-around !important;
+  }
+  /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-lg-stretch {
+    -ms-flex-line-pack: stretch !important;
+        align-content: stretch !important;
+  }
+  /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-lg-auto {
+    -ms-flex-item-align: auto !important;
+        align-self: auto !important;
+  }
+  /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-lg-start {
+    -ms-flex-item-align: start !important;
+        align-self: flex-start !important;
+  }
+  /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-lg-end {
+    -ms-flex-item-align: end !important;
+        align-self: flex-end !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-lg-center {
+    -ms-flex-item-align: center !important;
+        align-self: center !important;
+  }
+  /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-lg-baseline {
+    -ms-flex-item-align: baseline !important;
+        align-self: baseline !important;
+  }
+  /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-lg-stretch {
+    -ms-flex-item-align: stretch !important;
+        align-self: stretch !important;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-row {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: row !important;
+            flex-direction: row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-column {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: column !important;
+            flex-direction: column !important;
+  }
+  /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-row-reverse {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: row-reverse !important;
+            flex-direction: row-reverse !important;
+  }
+  /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-column-reverse {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: column-reverse !important;
+            flex-direction: column-reverse !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-wrap {
+    -ms-flex-wrap: wrap !important;
+        flex-wrap: wrap !important;
+  }
+  /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-nowrap {
+    -ms-flex-wrap: nowrap !important;
+        flex-wrap: nowrap !important;
+  }
+  /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-wrap-reverse {
+    -ms-flex-wrap: wrap-reverse !important;
+        flex-wrap: wrap-reverse !important;
+  }
+  /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-fill {
+    -webkit-box-flex: 1 !important;
+        -ms-flex: 1 1 auto !important;
+            flex: 1 1 auto !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-grow-0 {
+    -webkit-box-flex: 0 !important;
+        -ms-flex-positive: 0 !important;
+            flex-grow: 0 !important;
+  }
+  /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-grow-1 {
+    -webkit-box-flex: 1 !important;
+        -ms-flex-positive: 1 !important;
+            flex-grow: 1 !important;
+  }
+  /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-shrink-0 {
+    -ms-flex-negative: 0 !important;
+        flex-shrink: 0 !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xl-shrink-1 {
+    -ms-flex-negative: 1 !important;
+        flex-shrink: 1 !important;
+  }
+  /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xl-start {
+    -webkit-box-pack: start !important;
+        -ms-flex-pack: start !important;
+            justify-content: flex-start !important;
+  }
+  /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xl-end {
+    -webkit-box-pack: end !important;
+        -ms-flex-pack: end !important;
+            justify-content: flex-end !important;
+  }
+  /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xl-center {
+    -webkit-box-pack: center !important;
+        -ms-flex-pack: center !important;
+            justify-content: center !important;
+  }
+  /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xl-between {
+    -webkit-box-pack: justify !important;
+        -ms-flex-pack: justify !important;
+            justify-content: space-between !important;
+  }
+  /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xl-around {
+    -ms-flex-pack: distribute !important;
+        justify-content: space-around !important;
+  }
+  /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xl-start {
+    -webkit-box-align: start !important;
+        -ms-flex-align: start !important;
+            align-items: flex-start !important;
+  }
+  /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xl-end {
+    -webkit-box-align: end !important;
+        -ms-flex-align: end !important;
+            align-items: flex-end !important;
+  }
+  /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xl-center {
+    -webkit-box-align: center !important;
+        -ms-flex-align: center !important;
+            align-items: center !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xl-baseline {
+    -webkit-box-align: baseline !important;
+        -ms-flex-align: baseline !important;
+            align-items: baseline !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xl-stretch {
+    -webkit-box-align: stretch !important;
+        -ms-flex-align: stretch !important;
+            align-items: stretch !important;
+  }
+  /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xl-start {
+    -ms-flex-line-pack: start !important;
+        align-content: flex-start !important;
+  }
+  /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xl-end {
+    -ms-flex-line-pack: end !important;
+        align-content: flex-end !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xl-center {
+    -ms-flex-line-pack: center !important;
+        align-content: center !important;
+  }
+  /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xl-between {
+    -ms-flex-line-pack: justify !important;
+        align-content: space-between !important;
+  }
+  /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xl-around {
+    -ms-flex-line-pack: distribute !important;
+        align-content: space-around !important;
+  }
+  /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xl-stretch {
+    -ms-flex-line-pack: stretch !important;
+        align-content: stretch !important;
+  }
+  /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xl-auto {
+    -ms-flex-item-align: auto !important;
+        align-self: auto !important;
+  }
+  /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xl-start {
+    -ms-flex-item-align: start !important;
+        align-self: flex-start !important;
+  }
+  /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xl-end {
+    -ms-flex-item-align: end !important;
+        align-self: flex-end !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xl-center {
+    -ms-flex-item-align: center !important;
+        align-self: center !important;
+  }
+  /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xl-baseline {
+    -ms-flex-item-align: baseline !important;
+        align-self: baseline !important;
+  }
+  /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xl-stretch {
+    -ms-flex-item-align: stretch !important;
+        align-self: stretch !important;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-row {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: row !important;
+            flex-direction: row !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-column {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: normal !important;
+        -ms-flex-direction: column !important;
+            flex-direction: column !important;
+  }
+  /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-row-reverse {
+    -webkit-box-orient: horizontal !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: row-reverse !important;
+            flex-direction: row-reverse !important;
+  }
+  /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-column-reverse {
+    -webkit-box-orient: vertical !important;
+    -webkit-box-direction: reverse !important;
+        -ms-flex-direction: column-reverse !important;
+            flex-direction: column-reverse !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-wrap {
+    -ms-flex-wrap: wrap !important;
+        flex-wrap: wrap !important;
+  }
+  /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-nowrap {
+    -ms-flex-wrap: nowrap !important;
+        flex-wrap: nowrap !important;
+  }
+  /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-wrap-reverse {
+    -ms-flex-wrap: wrap-reverse !important;
+        flex-wrap: wrap-reverse !important;
+  }
+  /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-fill {
+    -webkit-box-flex: 1 !important;
+        -ms-flex: 1 1 auto !important;
+            flex: 1 1 auto !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-grow-0 {
+    -webkit-box-flex: 0 !important;
+        -ms-flex-positive: 0 !important;
+            flex-grow: 0 !important;
+  }
+  /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-grow-1 {
+    -webkit-box-flex: 1 !important;
+        -ms-flex-positive: 1 !important;
+            flex-grow: 1 !important;
+  }
+  /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-shrink-0 {
+    -ms-flex-negative: 0 !important;
+        flex-shrink: 0 !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .flex-xxl-shrink-1 {
+    -ms-flex-negative: 1 !important;
+        flex-shrink: 1 !important;
+  }
+  /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xxl-start {
+    -webkit-box-pack: start !important;
+        -ms-flex-pack: start !important;
+            justify-content: flex-start !important;
+  }
+  /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xxl-end {
+    -webkit-box-pack: end !important;
+        -ms-flex-pack: end !important;
+            justify-content: flex-end !important;
+  }
+  /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xxl-center {
+    -webkit-box-pack: center !important;
+        -ms-flex-pack: center !important;
+            justify-content: center !important;
+  }
+  /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xxl-between {
+    -webkit-box-pack: justify !important;
+        -ms-flex-pack: justify !important;
+            justify-content: space-between !important;
+  }
+  /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .justify-content-xxl-around {
+    -ms-flex-pack: distribute !important;
+        justify-content: space-around !important;
+  }
+  /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xxl-start {
+    -webkit-box-align: start !important;
+        -ms-flex-align: start !important;
+            align-items: flex-start !important;
+  }
+  /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xxl-end {
+    -webkit-box-align: end !important;
+        -ms-flex-align: end !important;
+            align-items: flex-end !important;
+  }
+  /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xxl-center {
+    -webkit-box-align: center !important;
+        -ms-flex-align: center !important;
+            align-items: center !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xxl-baseline {
+    -webkit-box-align: baseline !important;
+        -ms-flex-align: baseline !important;
+            align-items: baseline !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-items-xxl-stretch {
+    -webkit-box-align: stretch !important;
+        -ms-flex-align: stretch !important;
+            align-items: stretch !important;
+  }
+  /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xxl-start {
+    -ms-flex-line-pack: start !important;
+        align-content: flex-start !important;
+  }
+  /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xxl-end {
+    -ms-flex-line-pack: end !important;
+        align-content: flex-end !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xxl-center {
+    -ms-flex-line-pack: center !important;
+        align-content: center !important;
+  }
+  /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xxl-between {
+    -ms-flex-line-pack: justify !important;
+        align-content: space-between !important;
+  }
+  /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xxl-around {
+    -ms-flex-line-pack: distribute !important;
+        align-content: space-around !important;
+  }
+  /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-content-xxl-stretch {
+    -ms-flex-line-pack: stretch !important;
+        align-content: stretch !important;
+  }
+  /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xxl-auto {
+    -ms-flex-item-align: auto !important;
+        align-self: auto !important;
+  }
+  /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xxl-start {
+    -ms-flex-item-align: start !important;
+        align-self: flex-start !important;
+  }
+  /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xxl-end {
+    -ms-flex-item-align: end !important;
+        align-self: flex-end !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xxl-center {
+    -ms-flex-item-align: center !important;
+        align-self: center !important;
+  }
+  /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xxl-baseline {
+    -ms-flex-item-align: baseline !important;
+        align-self: baseline !important;
+  }
+  /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+  .align-self-xxl-stretch {
+    -ms-flex-item-align: stretch !important;
+        align-self: stretch !important;
+  }
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+.float-left {
+  float: left !important;
+}
+
+/* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+.float-right {
+  float: right !important;
+}
+
+/* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+.float-none {
+  float: none !important;
+}
+
+@media (min-width: 576px) {
+  /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-sm-left {
+    float: left !important;
+  }
+  /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-sm-right {
+    float: right !important;
+  }
+  /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-sm-none {
+    float: none !important;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-md-left {
+    float: left !important;
+  }
+  /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-md-right {
+    float: right !important;
+  }
+  /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-md-none {
+    float: none !important;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-lg-left {
+    float: left !important;
+  }
+  /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-lg-right {
+    float: right !important;
+  }
+  /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-lg-none {
+    float: none !important;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-xl-left {
+    float: left !important;
+  }
+  /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-xl-right {
+    float: right !important;
+  }
+  /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-xl-none {
+    float: none !important;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-xxl-left {
+    float: left !important;
+  }
+  /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-xxl-right {
+    float: right !important;
+  }
+  /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+  .float-xxl-none {
+    float: none !important;
+  }
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_interactions.scss */
+.user-select-all {
+  -webkit-user-select: all !important;
+     -moz-user-select: all !important;
+      -ms-user-select: all !important;
+          user-select: all !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_interactions.scss */
+.user-select-auto {
+  -webkit-user-select: auto !important;
+     -moz-user-select: auto !important;
+      -ms-user-select: auto !important;
+          user-select: auto !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_interactions.scss */
+.user-select-none {
+  -webkit-user-select: none !important;
+     -moz-user-select: none !important;
+      -ms-user-select: none !important;
+          user-select: none !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_overflow.scss */
+.overflow-auto {
+  overflow: auto !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_overflow.scss */
+.overflow-hidden {
+  overflow: hidden !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-static {
+  position: static !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-relative {
+  position: relative !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-absolute {
+  position: absolute !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-fixed {
+  position: fixed !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-sticky {
+  position: sticky !important;
+}
+
+/* line 10, node_modules/bootstrap/scss/utilities/_position.scss */
+.fixed-top {
+  position: fixed;
+  top: 0;
+  right: 0;
+  left: 0;
+  z-index: 1030;
+}
+
+/* line 18, node_modules/bootstrap/scss/utilities/_position.scss */
+.fixed-bottom {
+  position: fixed;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1030;
+}
+
+@supports (position: sticky) {
+  /* line 26, node_modules/bootstrap/scss/utilities/_position.scss */
+  .sticky-top {
+    position: sticky;
+    top: 0;
+    z-index: 1020;
+  }
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_screenreaders.scss */
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border: 0;
+}
+
+/* line 25, node_modules/bootstrap/scss/mixins/_screen-reader.scss */
+.sr-only-focusable:active, .sr-only-focusable:focus {
+  position: static;
+  width: auto;
+  height: auto;
+  overflow: visible;
+  clip: auto;
+  white-space: normal;
+}
+
+/* line 3, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow-sm {
+  -webkit-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+          box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow {
+  -webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+          box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow-lg {
+  -webkit-box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
+          box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow-none {
+  -webkit-box-shadow: none !important;
+          box-shadow: none !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-25 {
+  width: 25% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-50 {
+  width: 50% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-75 {
+  width: 75% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-100 {
+  width: 100% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-auto {
+  width: auto !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-25 {
+  height: 25% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-50 {
+  height: 50% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-75 {
+  height: 75% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-100 {
+  height: 100% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-auto {
+  height: auto !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.mw-100 {
+  max-width: 100% !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.mh-100 {
+  max-height: 100% !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.min-vw-100 {
+  min-width: 100vw !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.min-vh-100 {
+  min-height: 100vh !important;
+}
+
+/* line 19, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.vw-100 {
+  width: 100vw !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.vh-100 {
+  height: 100vh !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-0 {
+  margin: 0 !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-0,
+.my-0 {
+  margin-top: 0 !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-0,
+.mx-0 {
+  margin-right: 0 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-0,
+.my-0 {
+  margin-bottom: 0 !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-0,
+.mx-0 {
+  margin-left: 0 !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-1 {
+  margin: 0.25rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-1,
+.my-1 {
+  margin-top: 0.25rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-1,
+.mx-1 {
+  margin-right: 0.25rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-1,
+.my-1 {
+  margin-bottom: 0.25rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-1,
+.mx-1 {
+  margin-left: 0.25rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-2 {
+  margin: 0.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-2,
+.my-2 {
+  margin-top: 0.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-2,
+.mx-2 {
+  margin-right: 0.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-2,
+.my-2 {
+  margin-bottom: 0.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-2,
+.mx-2 {
+  margin-left: 0.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-3 {
+  margin: 1rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-3,
+.my-3 {
+  margin-top: 1rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-3,
+.mx-3 {
+  margin-right: 1rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-3,
+.my-3 {
+  margin-bottom: 1rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-3,
+.mx-3 {
+  margin-left: 1rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-4 {
+  margin: 1.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-4,
+.my-4 {
+  margin-top: 1.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-4,
+.mx-4 {
+  margin-right: 1.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-4,
+.my-4 {
+  margin-bottom: 1.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-4,
+.mx-4 {
+  margin-left: 1.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-5 {
+  margin: 3rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-5,
+.my-5 {
+  margin-top: 3rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-5,
+.mx-5 {
+  margin-right: 3rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-5,
+.my-5 {
+  margin-bottom: 3rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-5,
+.mx-5 {
+  margin-left: 3rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-0 {
+  padding: 0 !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-0,
+.py-0 {
+  padding-top: 0 !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-0,
+.px-0 {
+  padding-right: 0 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-0,
+.py-0 {
+  padding-bottom: 0 !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-0,
+.px-0 {
+  padding-left: 0 !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-1 {
+  padding: 0.25rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-1,
+.py-1 {
+  padding-top: 0.25rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-1,
+.px-1 {
+  padding-right: 0.25rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-1,
+.py-1 {
+  padding-bottom: 0.25rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-1,
+.px-1 {
+  padding-left: 0.25rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-2 {
+  padding: 0.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-2,
+.py-2 {
+  padding-top: 0.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-2,
+.px-2 {
+  padding-right: 0.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-2,
+.py-2 {
+  padding-bottom: 0.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-2,
+.px-2 {
+  padding-left: 0.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-3 {
+  padding: 1rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-3,
+.py-3 {
+  padding-top: 1rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-3,
+.px-3 {
+  padding-right: 1rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-3,
+.py-3 {
+  padding-bottom: 1rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-3,
+.px-3 {
+  padding-left: 1rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-4 {
+  padding: 1.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-4,
+.py-4 {
+  padding-top: 1.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-4,
+.px-4 {
+  padding-right: 1.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-4,
+.py-4 {
+  padding-bottom: 1.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-4,
+.px-4 {
+  padding-left: 1.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-5 {
+  padding: 3rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-5,
+.py-5 {
+  padding-top: 3rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-5,
+.px-5 {
+  padding-right: 3rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-5,
+.py-5 {
+  padding-bottom: 3rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-5,
+.px-5 {
+  padding-left: 3rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n1 {
+  margin: -0.25rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n1,
+.my-n1 {
+  margin-top: -0.25rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n1,
+.mx-n1 {
+  margin-right: -0.25rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n1,
+.my-n1 {
+  margin-bottom: -0.25rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n1,
+.mx-n1 {
+  margin-left: -0.25rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n2 {
+  margin: -0.5rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n2,
+.my-n2 {
+  margin-top: -0.5rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n2,
+.mx-n2 {
+  margin-right: -0.5rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n2,
+.my-n2 {
+  margin-bottom: -0.5rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n2,
+.mx-n2 {
+  margin-left: -0.5rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n3 {
+  margin: -1rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n3,
+.my-n3 {
+  margin-top: -1rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n3,
+.mx-n3 {
+  margin-right: -1rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n3,
+.my-n3 {
+  margin-bottom: -1rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n3,
+.mx-n3 {
+  margin-left: -1rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n4 {
+  margin: -1.5rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n4,
+.my-n4 {
+  margin-top: -1.5rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n4,
+.mx-n4 {
+  margin-right: -1.5rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n4,
+.my-n4 {
+  margin-bottom: -1.5rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n4,
+.mx-n4 {
+  margin-left: -1.5rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n5 {
+  margin: -3rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n5,
+.my-n5 {
+  margin-top: -3rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n5,
+.mx-n5 {
+  margin-right: -3rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n5,
+.my-n5 {
+  margin-bottom: -3rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n5,
+.mx-n5 {
+  margin-left: -3rem !important;
+}
+
+/* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-auto {
+  margin: auto !important;
+}
+
+/* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-auto,
+.my-auto {
+  margin-top: auto !important;
+}
+
+/* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-auto,
+.mx-auto {
+  margin-right: auto !important;
+}
+
+/* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-auto,
+.my-auto {
+  margin-bottom: auto !important;
+}
+
+/* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-auto,
+.mx-auto {
+  margin-left: auto !important;
+}
+
+@media (min-width: 576px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-0 {
+    margin: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-0,
+  .my-sm-0 {
+    margin-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-0,
+  .mx-sm-0 {
+    margin-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-0,
+  .my-sm-0 {
+    margin-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-0,
+  .mx-sm-0 {
+    margin-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-1 {
+    margin: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-1,
+  .my-sm-1 {
+    margin-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-1,
+  .mx-sm-1 {
+    margin-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-1,
+  .my-sm-1 {
+    margin-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-1,
+  .mx-sm-1 {
+    margin-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-2 {
+    margin: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-2,
+  .my-sm-2 {
+    margin-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-2,
+  .mx-sm-2 {
+    margin-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-2,
+  .my-sm-2 {
+    margin-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-2,
+  .mx-sm-2 {
+    margin-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-3 {
+    margin: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-3,
+  .my-sm-3 {
+    margin-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-3,
+  .mx-sm-3 {
+    margin-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-3,
+  .my-sm-3 {
+    margin-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-3,
+  .mx-sm-3 {
+    margin-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-4 {
+    margin: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-4,
+  .my-sm-4 {
+    margin-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-4,
+  .mx-sm-4 {
+    margin-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-4,
+  .my-sm-4 {
+    margin-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-4,
+  .mx-sm-4 {
+    margin-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-5 {
+    margin: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-5,
+  .my-sm-5 {
+    margin-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-5,
+  .mx-sm-5 {
+    margin-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-5,
+  .my-sm-5 {
+    margin-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-5,
+  .mx-sm-5 {
+    margin-left: 3rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-sm-0 {
+    padding: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-sm-0,
+  .py-sm-0 {
+    padding-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-sm-0,
+  .px-sm-0 {
+    padding-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-sm-0,
+  .py-sm-0 {
+    padding-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-sm-0,
+  .px-sm-0 {
+    padding-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-sm-1 {
+    padding: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-sm-1,
+  .py-sm-1 {
+    padding-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-sm-1,
+  .px-sm-1 {
+    padding-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-sm-1,
+  .py-sm-1 {
+    padding-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-sm-1,
+  .px-sm-1 {
+    padding-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-sm-2 {
+    padding: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-sm-2,
+  .py-sm-2 {
+    padding-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-sm-2,
+  .px-sm-2 {
+    padding-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-sm-2,
+  .py-sm-2 {
+    padding-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-sm-2,
+  .px-sm-2 {
+    padding-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-sm-3 {
+    padding: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-sm-3,
+  .py-sm-3 {
+    padding-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-sm-3,
+  .px-sm-3 {
+    padding-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-sm-3,
+  .py-sm-3 {
+    padding-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-sm-3,
+  .px-sm-3 {
+    padding-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-sm-4 {
+    padding: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-sm-4,
+  .py-sm-4 {
+    padding-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-sm-4,
+  .px-sm-4 {
+    padding-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-sm-4,
+  .py-sm-4 {
+    padding-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-sm-4,
+  .px-sm-4 {
+    padding-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-sm-5 {
+    padding: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-sm-5,
+  .py-sm-5 {
+    padding-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-sm-5,
+  .px-sm-5 {
+    padding-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-sm-5,
+  .py-sm-5 {
+    padding-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-sm-5,
+  .px-sm-5 {
+    padding-left: 3rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-n1 {
+    margin: -0.25rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-n1,
+  .my-sm-n1 {
+    margin-top: -0.25rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-n1,
+  .mx-sm-n1 {
+    margin-right: -0.25rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-n1,
+  .my-sm-n1 {
+    margin-bottom: -0.25rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-n1,
+  .mx-sm-n1 {
+    margin-left: -0.25rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-n2 {
+    margin: -0.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-n2,
+  .my-sm-n2 {
+    margin-top: -0.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-n2,
+  .mx-sm-n2 {
+    margin-right: -0.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-n2,
+  .my-sm-n2 {
+    margin-bottom: -0.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-n2,
+  .mx-sm-n2 {
+    margin-left: -0.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-n3 {
+    margin: -1rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-n3,
+  .my-sm-n3 {
+    margin-top: -1rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-n3,
+  .mx-sm-n3 {
+    margin-right: -1rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-n3,
+  .my-sm-n3 {
+    margin-bottom: -1rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-n3,
+  .mx-sm-n3 {
+    margin-left: -1rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-n4 {
+    margin: -1.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-n4,
+  .my-sm-n4 {
+    margin-top: -1.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-n4,
+  .mx-sm-n4 {
+    margin-right: -1.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-n4,
+  .my-sm-n4 {
+    margin-bottom: -1.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-n4,
+  .mx-sm-n4 {
+    margin-left: -1.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-n5 {
+    margin: -3rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-n5,
+  .my-sm-n5 {
+    margin-top: -3rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-n5,
+  .mx-sm-n5 {
+    margin-right: -3rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-n5,
+  .my-sm-n5 {
+    margin-bottom: -3rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-n5,
+  .mx-sm-n5 {
+    margin-left: -3rem !important;
+  }
+  /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-sm-auto {
+    margin: auto !important;
+  }
+  /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-sm-auto,
+  .my-sm-auto {
+    margin-top: auto !important;
+  }
+  /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-sm-auto,
+  .mx-sm-auto {
+    margin-right: auto !important;
+  }
+  /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-sm-auto,
+  .my-sm-auto {
+    margin-bottom: auto !important;
+  }
+  /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-sm-auto,
+  .mx-sm-auto {
+    margin-left: auto !important;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-0 {
+    margin: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-0,
+  .my-md-0 {
+    margin-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-0,
+  .mx-md-0 {
+    margin-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-0,
+  .my-md-0 {
+    margin-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-0,
+  .mx-md-0 {
+    margin-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-1 {
+    margin: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-1,
+  .my-md-1 {
+    margin-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-1,
+  .mx-md-1 {
+    margin-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-1,
+  .my-md-1 {
+    margin-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-1,
+  .mx-md-1 {
+    margin-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-2 {
+    margin: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-2,
+  .my-md-2 {
+    margin-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-2,
+  .mx-md-2 {
+    margin-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-2,
+  .my-md-2 {
+    margin-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-2,
+  .mx-md-2 {
+    margin-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-3 {
+    margin: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-3,
+  .my-md-3 {
+    margin-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-3,
+  .mx-md-3 {
+    margin-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-3,
+  .my-md-3 {
+    margin-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-3,
+  .mx-md-3 {
+    margin-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-4 {
+    margin: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-4,
+  .my-md-4 {
+    margin-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-4,
+  .mx-md-4 {
+    margin-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-4,
+  .my-md-4 {
+    margin-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-4,
+  .mx-md-4 {
+    margin-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-5 {
+    margin: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-5,
+  .my-md-5 {
+    margin-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-5,
+  .mx-md-5 {
+    margin-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-5,
+  .my-md-5 {
+    margin-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-5,
+  .mx-md-5 {
+    margin-left: 3rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-md-0 {
+    padding: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-md-0,
+  .py-md-0 {
+    padding-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-md-0,
+  .px-md-0 {
+    padding-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-md-0,
+  .py-md-0 {
+    padding-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-md-0,
+  .px-md-0 {
+    padding-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-md-1 {
+    padding: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-md-1,
+  .py-md-1 {
+    padding-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-md-1,
+  .px-md-1 {
+    padding-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-md-1,
+  .py-md-1 {
+    padding-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-md-1,
+  .px-md-1 {
+    padding-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-md-2 {
+    padding: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-md-2,
+  .py-md-2 {
+    padding-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-md-2,
+  .px-md-2 {
+    padding-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-md-2,
+  .py-md-2 {
+    padding-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-md-2,
+  .px-md-2 {
+    padding-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-md-3 {
+    padding: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-md-3,
+  .py-md-3 {
+    padding-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-md-3,
+  .px-md-3 {
+    padding-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-md-3,
+  .py-md-3 {
+    padding-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-md-3,
+  .px-md-3 {
+    padding-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-md-4 {
+    padding: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-md-4,
+  .py-md-4 {
+    padding-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-md-4,
+  .px-md-4 {
+    padding-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-md-4,
+  .py-md-4 {
+    padding-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-md-4,
+  .px-md-4 {
+    padding-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-md-5 {
+    padding: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-md-5,
+  .py-md-5 {
+    padding-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-md-5,
+  .px-md-5 {
+    padding-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-md-5,
+  .py-md-5 {
+    padding-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-md-5,
+  .px-md-5 {
+    padding-left: 3rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-n1 {
+    margin: -0.25rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-n1,
+  .my-md-n1 {
+    margin-top: -0.25rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-n1,
+  .mx-md-n1 {
+    margin-right: -0.25rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-n1,
+  .my-md-n1 {
+    margin-bottom: -0.25rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-n1,
+  .mx-md-n1 {
+    margin-left: -0.25rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-n2 {
+    margin: -0.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-n2,
+  .my-md-n2 {
+    margin-top: -0.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-n2,
+  .mx-md-n2 {
+    margin-right: -0.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-n2,
+  .my-md-n2 {
+    margin-bottom: -0.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-n2,
+  .mx-md-n2 {
+    margin-left: -0.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-n3 {
+    margin: -1rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-n3,
+  .my-md-n3 {
+    margin-top: -1rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-n3,
+  .mx-md-n3 {
+    margin-right: -1rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-n3,
+  .my-md-n3 {
+    margin-bottom: -1rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-n3,
+  .mx-md-n3 {
+    margin-left: -1rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-n4 {
+    margin: -1.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-n4,
+  .my-md-n4 {
+    margin-top: -1.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-n4,
+  .mx-md-n4 {
+    margin-right: -1.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-n4,
+  .my-md-n4 {
+    margin-bottom: -1.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-n4,
+  .mx-md-n4 {
+    margin-left: -1.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-n5 {
+    margin: -3rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-n5,
+  .my-md-n5 {
+    margin-top: -3rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-n5,
+  .mx-md-n5 {
+    margin-right: -3rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-n5,
+  .my-md-n5 {
+    margin-bottom: -3rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-n5,
+  .mx-md-n5 {
+    margin-left: -3rem !important;
+  }
+  /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-md-auto {
+    margin: auto !important;
+  }
+  /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-md-auto,
+  .my-md-auto {
+    margin-top: auto !important;
+  }
+  /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-md-auto,
+  .mx-md-auto {
+    margin-right: auto !important;
+  }
+  /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-md-auto,
+  .my-md-auto {
+    margin-bottom: auto !important;
+  }
+  /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-md-auto,
+  .mx-md-auto {
+    margin-left: auto !important;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-0 {
+    margin: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-0,
+  .my-lg-0 {
+    margin-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-0,
+  .mx-lg-0 {
+    margin-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-0,
+  .my-lg-0 {
+    margin-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-0,
+  .mx-lg-0 {
+    margin-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-1 {
+    margin: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-1,
+  .my-lg-1 {
+    margin-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-1,
+  .mx-lg-1 {
+    margin-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-1,
+  .my-lg-1 {
+    margin-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-1,
+  .mx-lg-1 {
+    margin-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-2 {
+    margin: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-2,
+  .my-lg-2 {
+    margin-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-2,
+  .mx-lg-2 {
+    margin-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-2,
+  .my-lg-2 {
+    margin-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-2,
+  .mx-lg-2 {
+    margin-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-3 {
+    margin: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-3,
+  .my-lg-3 {
+    margin-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-3,
+  .mx-lg-3 {
+    margin-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-3,
+  .my-lg-3 {
+    margin-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-3,
+  .mx-lg-3 {
+    margin-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-4 {
+    margin: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-4,
+  .my-lg-4 {
+    margin-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-4,
+  .mx-lg-4 {
+    margin-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-4,
+  .my-lg-4 {
+    margin-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-4,
+  .mx-lg-4 {
+    margin-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-5 {
+    margin: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-5,
+  .my-lg-5 {
+    margin-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-5,
+  .mx-lg-5 {
+    margin-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-5,
+  .my-lg-5 {
+    margin-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-5,
+  .mx-lg-5 {
+    margin-left: 3rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-lg-0 {
+    padding: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-lg-0,
+  .py-lg-0 {
+    padding-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-lg-0,
+  .px-lg-0 {
+    padding-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-lg-0,
+  .py-lg-0 {
+    padding-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-lg-0,
+  .px-lg-0 {
+    padding-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-lg-1 {
+    padding: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-lg-1,
+  .py-lg-1 {
+    padding-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-lg-1,
+  .px-lg-1 {
+    padding-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-lg-1,
+  .py-lg-1 {
+    padding-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-lg-1,
+  .px-lg-1 {
+    padding-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-lg-2 {
+    padding: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-lg-2,
+  .py-lg-2 {
+    padding-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-lg-2,
+  .px-lg-2 {
+    padding-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-lg-2,
+  .py-lg-2 {
+    padding-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-lg-2,
+  .px-lg-2 {
+    padding-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-lg-3 {
+    padding: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-lg-3,
+  .py-lg-3 {
+    padding-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-lg-3,
+  .px-lg-3 {
+    padding-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-lg-3,
+  .py-lg-3 {
+    padding-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-lg-3,
+  .px-lg-3 {
+    padding-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-lg-4 {
+    padding: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-lg-4,
+  .py-lg-4 {
+    padding-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-lg-4,
+  .px-lg-4 {
+    padding-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-lg-4,
+  .py-lg-4 {
+    padding-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-lg-4,
+  .px-lg-4 {
+    padding-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-lg-5 {
+    padding: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-lg-5,
+  .py-lg-5 {
+    padding-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-lg-5,
+  .px-lg-5 {
+    padding-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-lg-5,
+  .py-lg-5 {
+    padding-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-lg-5,
+  .px-lg-5 {
+    padding-left: 3rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-n1 {
+    margin: -0.25rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-n1,
+  .my-lg-n1 {
+    margin-top: -0.25rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-n1,
+  .mx-lg-n1 {
+    margin-right: -0.25rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-n1,
+  .my-lg-n1 {
+    margin-bottom: -0.25rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-n1,
+  .mx-lg-n1 {
+    margin-left: -0.25rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-n2 {
+    margin: -0.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-n2,
+  .my-lg-n2 {
+    margin-top: -0.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-n2,
+  .mx-lg-n2 {
+    margin-right: -0.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-n2,
+  .my-lg-n2 {
+    margin-bottom: -0.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-n2,
+  .mx-lg-n2 {
+    margin-left: -0.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-n3 {
+    margin: -1rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-n3,
+  .my-lg-n3 {
+    margin-top: -1rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-n3,
+  .mx-lg-n3 {
+    margin-right: -1rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-n3,
+  .my-lg-n3 {
+    margin-bottom: -1rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-n3,
+  .mx-lg-n3 {
+    margin-left: -1rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-n4 {
+    margin: -1.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-n4,
+  .my-lg-n4 {
+    margin-top: -1.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-n4,
+  .mx-lg-n4 {
+    margin-right: -1.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-n4,
+  .my-lg-n4 {
+    margin-bottom: -1.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-n4,
+  .mx-lg-n4 {
+    margin-left: -1.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-n5 {
+    margin: -3rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-n5,
+  .my-lg-n5 {
+    margin-top: -3rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-n5,
+  .mx-lg-n5 {
+    margin-right: -3rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-n5,
+  .my-lg-n5 {
+    margin-bottom: -3rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-n5,
+  .mx-lg-n5 {
+    margin-left: -3rem !important;
+  }
+  /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-lg-auto {
+    margin: auto !important;
+  }
+  /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-lg-auto,
+  .my-lg-auto {
+    margin-top: auto !important;
+  }
+  /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-lg-auto,
+  .mx-lg-auto {
+    margin-right: auto !important;
+  }
+  /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-lg-auto,
+  .my-lg-auto {
+    margin-bottom: auto !important;
+  }
+  /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-lg-auto,
+  .mx-lg-auto {
+    margin-left: auto !important;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-0 {
+    margin: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-0,
+  .my-xl-0 {
+    margin-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-0,
+  .mx-xl-0 {
+    margin-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-0,
+  .my-xl-0 {
+    margin-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-0,
+  .mx-xl-0 {
+    margin-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-1 {
+    margin: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-1,
+  .my-xl-1 {
+    margin-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-1,
+  .mx-xl-1 {
+    margin-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-1,
+  .my-xl-1 {
+    margin-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-1,
+  .mx-xl-1 {
+    margin-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-2 {
+    margin: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-2,
+  .my-xl-2 {
+    margin-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-2,
+  .mx-xl-2 {
+    margin-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-2,
+  .my-xl-2 {
+    margin-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-2,
+  .mx-xl-2 {
+    margin-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-3 {
+    margin: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-3,
+  .my-xl-3 {
+    margin-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-3,
+  .mx-xl-3 {
+    margin-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-3,
+  .my-xl-3 {
+    margin-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-3,
+  .mx-xl-3 {
+    margin-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-4 {
+    margin: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-4,
+  .my-xl-4 {
+    margin-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-4,
+  .mx-xl-4 {
+    margin-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-4,
+  .my-xl-4 {
+    margin-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-4,
+  .mx-xl-4 {
+    margin-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-5 {
+    margin: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-5,
+  .my-xl-5 {
+    margin-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-5,
+  .mx-xl-5 {
+    margin-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-5,
+  .my-xl-5 {
+    margin-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-5,
+  .mx-xl-5 {
+    margin-left: 3rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xl-0 {
+    padding: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xl-0,
+  .py-xl-0 {
+    padding-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xl-0,
+  .px-xl-0 {
+    padding-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xl-0,
+  .py-xl-0 {
+    padding-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xl-0,
+  .px-xl-0 {
+    padding-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xl-1 {
+    padding: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xl-1,
+  .py-xl-1 {
+    padding-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xl-1,
+  .px-xl-1 {
+    padding-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xl-1,
+  .py-xl-1 {
+    padding-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xl-1,
+  .px-xl-1 {
+    padding-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xl-2 {
+    padding: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xl-2,
+  .py-xl-2 {
+    padding-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xl-2,
+  .px-xl-2 {
+    padding-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xl-2,
+  .py-xl-2 {
+    padding-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xl-2,
+  .px-xl-2 {
+    padding-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xl-3 {
+    padding: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xl-3,
+  .py-xl-3 {
+    padding-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xl-3,
+  .px-xl-3 {
+    padding-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xl-3,
+  .py-xl-3 {
+    padding-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xl-3,
+  .px-xl-3 {
+    padding-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xl-4 {
+    padding: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xl-4,
+  .py-xl-4 {
+    padding-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xl-4,
+  .px-xl-4 {
+    padding-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xl-4,
+  .py-xl-4 {
+    padding-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xl-4,
+  .px-xl-4 {
+    padding-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xl-5 {
+    padding: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xl-5,
+  .py-xl-5 {
+    padding-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xl-5,
+  .px-xl-5 {
+    padding-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xl-5,
+  .py-xl-5 {
+    padding-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xl-5,
+  .px-xl-5 {
+    padding-left: 3rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-n1 {
+    margin: -0.25rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-n1,
+  .my-xl-n1 {
+    margin-top: -0.25rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-n1,
+  .mx-xl-n1 {
+    margin-right: -0.25rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-n1,
+  .my-xl-n1 {
+    margin-bottom: -0.25rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-n1,
+  .mx-xl-n1 {
+    margin-left: -0.25rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-n2 {
+    margin: -0.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-n2,
+  .my-xl-n2 {
+    margin-top: -0.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-n2,
+  .mx-xl-n2 {
+    margin-right: -0.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-n2,
+  .my-xl-n2 {
+    margin-bottom: -0.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-n2,
+  .mx-xl-n2 {
+    margin-left: -0.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-n3 {
+    margin: -1rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-n3,
+  .my-xl-n3 {
+    margin-top: -1rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-n3,
+  .mx-xl-n3 {
+    margin-right: -1rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-n3,
+  .my-xl-n3 {
+    margin-bottom: -1rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-n3,
+  .mx-xl-n3 {
+    margin-left: -1rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-n4 {
+    margin: -1.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-n4,
+  .my-xl-n4 {
+    margin-top: -1.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-n4,
+  .mx-xl-n4 {
+    margin-right: -1.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-n4,
+  .my-xl-n4 {
+    margin-bottom: -1.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-n4,
+  .mx-xl-n4 {
+    margin-left: -1.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-n5 {
+    margin: -3rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-n5,
+  .my-xl-n5 {
+    margin-top: -3rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-n5,
+  .mx-xl-n5 {
+    margin-right: -3rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-n5,
+  .my-xl-n5 {
+    margin-bottom: -3rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-n5,
+  .mx-xl-n5 {
+    margin-left: -3rem !important;
+  }
+  /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xl-auto {
+    margin: auto !important;
+  }
+  /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xl-auto,
+  .my-xl-auto {
+    margin-top: auto !important;
+  }
+  /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xl-auto,
+  .mx-xl-auto {
+    margin-right: auto !important;
+  }
+  /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xl-auto,
+  .my-xl-auto {
+    margin-bottom: auto !important;
+  }
+  /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xl-auto,
+  .mx-xl-auto {
+    margin-left: auto !important;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-0 {
+    margin: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-0,
+  .my-xxl-0 {
+    margin-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-0,
+  .mx-xxl-0 {
+    margin-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-0,
+  .my-xxl-0 {
+    margin-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-0,
+  .mx-xxl-0 {
+    margin-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-1 {
+    margin: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-1,
+  .my-xxl-1 {
+    margin-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-1,
+  .mx-xxl-1 {
+    margin-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-1,
+  .my-xxl-1 {
+    margin-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-1,
+  .mx-xxl-1 {
+    margin-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-2 {
+    margin: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-2,
+  .my-xxl-2 {
+    margin-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-2,
+  .mx-xxl-2 {
+    margin-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-2,
+  .my-xxl-2 {
+    margin-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-2,
+  .mx-xxl-2 {
+    margin-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-3 {
+    margin: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-3,
+  .my-xxl-3 {
+    margin-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-3,
+  .mx-xxl-3 {
+    margin-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-3,
+  .my-xxl-3 {
+    margin-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-3,
+  .mx-xxl-3 {
+    margin-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-4 {
+    margin: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-4,
+  .my-xxl-4 {
+    margin-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-4,
+  .mx-xxl-4 {
+    margin-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-4,
+  .my-xxl-4 {
+    margin-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-4,
+  .mx-xxl-4 {
+    margin-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-5 {
+    margin: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-5,
+  .my-xxl-5 {
+    margin-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-5,
+  .mx-xxl-5 {
+    margin-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-5,
+  .my-xxl-5 {
+    margin-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-5,
+  .mx-xxl-5 {
+    margin-left: 3rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xxl-0 {
+    padding: 0 !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xxl-0,
+  .py-xxl-0 {
+    padding-top: 0 !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xxl-0,
+  .px-xxl-0 {
+    padding-right: 0 !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xxl-0,
+  .py-xxl-0 {
+    padding-bottom: 0 !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xxl-0,
+  .px-xxl-0 {
+    padding-left: 0 !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xxl-1 {
+    padding: 0.25rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xxl-1,
+  .py-xxl-1 {
+    padding-top: 0.25rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xxl-1,
+  .px-xxl-1 {
+    padding-right: 0.25rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xxl-1,
+  .py-xxl-1 {
+    padding-bottom: 0.25rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xxl-1,
+  .px-xxl-1 {
+    padding-left: 0.25rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xxl-2 {
+    padding: 0.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xxl-2,
+  .py-xxl-2 {
+    padding-top: 0.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xxl-2,
+  .px-xxl-2 {
+    padding-right: 0.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xxl-2,
+  .py-xxl-2 {
+    padding-bottom: 0.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xxl-2,
+  .px-xxl-2 {
+    padding-left: 0.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xxl-3 {
+    padding: 1rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xxl-3,
+  .py-xxl-3 {
+    padding-top: 1rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xxl-3,
+  .px-xxl-3 {
+    padding-right: 1rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xxl-3,
+  .py-xxl-3 {
+    padding-bottom: 1rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xxl-3,
+  .px-xxl-3 {
+    padding-left: 1rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xxl-4 {
+    padding: 1.5rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xxl-4,
+  .py-xxl-4 {
+    padding-top: 1.5rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xxl-4,
+  .px-xxl-4 {
+    padding-right: 1.5rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xxl-4,
+  .py-xxl-4 {
+    padding-bottom: 1.5rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xxl-4,
+  .px-xxl-4 {
+    padding-left: 1.5rem !important;
+  }
+  /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .p-xxl-5 {
+    padding: 3rem !important;
+  }
+  /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pt-xxl-5,
+  .py-xxl-5 {
+    padding-top: 3rem !important;
+  }
+  /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pr-xxl-5,
+  .px-xxl-5 {
+    padding-right: 3rem !important;
+  }
+  /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pb-xxl-5,
+  .py-xxl-5 {
+    padding-bottom: 3rem !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .pl-xxl-5,
+  .px-xxl-5 {
+    padding-left: 3rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-n1 {
+    margin: -0.25rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-n1,
+  .my-xxl-n1 {
+    margin-top: -0.25rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-n1,
+  .mx-xxl-n1 {
+    margin-right: -0.25rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-n1,
+  .my-xxl-n1 {
+    margin-bottom: -0.25rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-n1,
+  .mx-xxl-n1 {
+    margin-left: -0.25rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-n2 {
+    margin: -0.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-n2,
+  .my-xxl-n2 {
+    margin-top: -0.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-n2,
+  .mx-xxl-n2 {
+    margin-right: -0.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-n2,
+  .my-xxl-n2 {
+    margin-bottom: -0.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-n2,
+  .mx-xxl-n2 {
+    margin-left: -0.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-n3 {
+    margin: -1rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-n3,
+  .my-xxl-n3 {
+    margin-top: -1rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-n3,
+  .mx-xxl-n3 {
+    margin-right: -1rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-n3,
+  .my-xxl-n3 {
+    margin-bottom: -1rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-n3,
+  .mx-xxl-n3 {
+    margin-left: -1rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-n4 {
+    margin: -1.5rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-n4,
+  .my-xxl-n4 {
+    margin-top: -1.5rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-n4,
+  .mx-xxl-n4 {
+    margin-right: -1.5rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-n4,
+  .my-xxl-n4 {
+    margin-bottom: -1.5rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-n4,
+  .mx-xxl-n4 {
+    margin-left: -1.5rem !important;
+  }
+  /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-n5 {
+    margin: -3rem !important;
+  }
+  /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-n5,
+  .my-xxl-n5 {
+    margin-top: -3rem !important;
+  }
+  /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-n5,
+  .mx-xxl-n5 {
+    margin-right: -3rem !important;
+  }
+  /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-n5,
+  .my-xxl-n5 {
+    margin-bottom: -3rem !important;
+  }
+  /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-n5,
+  .mx-xxl-n5 {
+    margin-left: -3rem !important;
+  }
+  /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .m-xxl-auto {
+    margin: auto !important;
+  }
+  /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mt-xxl-auto,
+  .my-xxl-auto {
+    margin-top: auto !important;
+  }
+  /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mr-xxl-auto,
+  .mx-xxl-auto {
+    margin-right: auto !important;
+  }
+  /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .mb-xxl-auto,
+  .my-xxl-auto {
+    margin-bottom: auto !important;
+  }
+  /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+  .ml-xxl-auto,
+  .mx-xxl-auto {
+    margin-left: auto !important;
+  }
+}
+
+/* line 6, node_modules/bootstrap/scss/utilities/_stretched-link.scss */
+.stretched-link::after {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1;
+  pointer-events: auto;
+  content: "";
+  background-color: rgba(0, 0, 0, 0);
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-monospace {
+  font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-justify {
+  text-align: justify !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-wrap {
+  white-space: normal !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-nowrap {
+  white-space: nowrap !important;
+}
+
+/* line 14, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-truncate {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-left {
+  text-align: left !important;
+}
+
+/* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-right {
+  text-align: right !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-center {
+  text-align: center !important;
+}
+
+@media (min-width: 576px) {
+  /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-sm-left {
+    text-align: left !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-sm-right {
+    text-align: right !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-sm-center {
+    text-align: center !important;
+  }
+}
+
+@media (min-width: 768px) {
+  /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-md-left {
+    text-align: left !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-md-right {
+    text-align: right !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-md-center {
+    text-align: center !important;
+  }
+}
+
+@media (min-width: 1024px) {
+  /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-lg-left {
+    text-align: left !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-lg-right {
+    text-align: right !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-lg-center {
+    text-align: center !important;
+  }
+}
+
+@media (min-width: 1280px) {
+  /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-xl-left {
+    text-align: left !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-xl-right {
+    text-align: right !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-xl-center {
+    text-align: center !important;
+  }
+}
+
+@media (min-width: 1440px) {
+  /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-xxl-left {
+    text-align: left !important;
+  }
+  /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-xxl-right {
+    text-align: right !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+  .text-xxl-center {
+    text-align: center !important;
+  }
+}
+
+/* line 30, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-lowercase {
+  text-transform: lowercase !important;
+}
+
+/* line 31, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-uppercase {
+  text-transform: uppercase !important;
+}
+
+/* line 32, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-capitalize {
+  text-transform: capitalize !important;
+}
+
+/* line 36, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-light {
+  font-weight: 300 !important;
+}
+
+/* line 37, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-lighter {
+  font-weight: lighter !important;
+}
+
+/* line 38, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-normal {
+  font-weight: 400 !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-bold {
+  font-weight: 700 !important;
+}
+
+/* line 40, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-bolder {
+  font-weight: 800 !important;
+}
+
+/* line 41, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-italic {
+  font-style: italic !important;
+}
+
+/* line 45, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-white {
+  color: #ffffff !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-primary {
+  color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-primary:hover, a.text-primary:focus {
+  color: #202020 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-secondary {
+  color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-secondary:hover, a.text-secondary:focus {
+  color: #d5c7b1 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-success {
+  color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-success:hover, a.text-success:focus {
+  color: #d5c7b1 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-info {
+  color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-info:hover, a.text-info:focus {
+  color: #202020 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-warning {
+  color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-warning:hover, a.text-warning:focus {
+  color: #202020 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-danger {
+  color: #e54a19 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-danger:hover, a.text-danger:focus {
+  color: #a03411 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-light {
+  color: #f7f7f7 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-light:hover, a.text-light:focus {
+  color: #d1d1d1 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-dark {
+  color: #343a40 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-dark:hover, a.text-dark:focus {
+  color: #121416 !important;
+}
+
+/* line 51, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-body {
+  color: #464746 !important;
+}
+
+/* line 52, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-muted {
+  color: #6c757d !important;
+}
+
+/* line 54, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-black-50 {
+  color: rgba(0, 0, 0, 0.5) !important;
+}
+
+/* line 55, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-white-50 {
+  color: rgba(255, 255, 255, 0.5) !important;
+}
+
+/* line 59, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-hide {
+  font: 0/0 a;
+  color: transparent;
+  text-shadow: none;
+  background-color: transparent;
+  border: 0;
+}
+
+/* line 63, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-decoration-none {
+  text-decoration: none !important;
+}
+
+/* line 65, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-break {
+  word-break: break-word !important;
+  overflow-wrap: break-word !important;
+}
+
+/* line 72, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-reset {
+  color: inherit !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_visibility.scss */
+.visible {
+  visibility: visible !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_visibility.scss */
+.invisible {
+  visibility: hidden !important;
+}
+
+@media print {
+  /* line 13, node_modules/bootstrap/scss/_print.scss */
+  *,
+  *::before,
+  *::after {
+    text-shadow: none !important;
+    -webkit-box-shadow: none !important;
+            box-shadow: none !important;
+  }
+  /* line 24, node_modules/bootstrap/scss/_print.scss */
+  a:not(.btn) {
+    text-decoration: underline;
+  }
+  /* line 34, node_modules/bootstrap/scss/_print.scss */
+  abbr[title]::after {
+    content: " (" attr(title) ")";
+  }
+  /* line 49, node_modules/bootstrap/scss/_print.scss */
+  pre {
+    white-space: pre-wrap !important;
+  }
+  /* line 52, node_modules/bootstrap/scss/_print.scss */
+  pre,
+  blockquote {
+    border: 1px solid #adb5bd;
+    page-break-inside: avoid;
+  }
+  /* line 63, node_modules/bootstrap/scss/_print.scss */
+  thead {
+    display: table-header-group;
+  }
+  /* line 67, node_modules/bootstrap/scss/_print.scss */
+  tr,
+  img {
+    page-break-inside: avoid;
+  }
+  /* line 72, node_modules/bootstrap/scss/_print.scss */
+  p,
+  h2,
+  h3 {
+    orphans: 3;
+    widows: 3;
+  }
+  /* line 79, node_modules/bootstrap/scss/_print.scss */
+  h2,
+  h3 {
+    page-break-after: avoid;
+  }
+  @page {
+    size: a3;
+  }
+  /* line 92, node_modules/bootstrap/scss/_print.scss */
+  body {
+    min-width: 1024px !important;
+  }
+  /* line 95, node_modules/bootstrap/scss/_print.scss */
+  .container {
+    min-width: 1024px !important;
+  }
+  /* line 100, node_modules/bootstrap/scss/_print.scss */
+  .navbar {
+    display: none;
+  }
+  /* line 103, node_modules/bootstrap/scss/_print.scss */
+  .badge {
+    border: 1px solid #000000;
+  }
+  /* line 107, node_modules/bootstrap/scss/_print.scss */
+  .table {
+    border-collapse: collapse !important;
+  }
+  /* line 110, node_modules/bootstrap/scss/_print.scss */
+  .table td,
+  .table th {
+    background-color: #ffffff !important;
+  }
+  /* line 117, node_modules/bootstrap/scss/_print.scss */
+  .table-bordered th,
+  .table-bordered td {
+    border: 1px solid #dee2e6 !important;
+  }
+  /* line 123, node_modules/bootstrap/scss/_print.scss */
+  .table-dark {
+    color: inherit;
+  }
+  /* line 126, node_modules/bootstrap/scss/_print.scss */
+  .table-dark th,
+  .table-dark td,
+  .table-dark thead th,
+  .table-dark tbody + tbody {
+    border-color: #dee2e6;
+  }
+  /* line 134, node_modules/bootstrap/scss/_print.scss */
+  .table .thead-dark th {
+    color: inherit;
+    border-color: #dee2e6;
+  }
+}
+
+/**
+ * Set up a decent box model on the root element
+ */
+/* line 8, src/assets/scss/base/_base.scss */
+html {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+}
+
+/**
+ * Make all elements from the DOM inherit from the parent box-sizing
+ * Since `*` has a specificity of 0, it does not override the `html` value
+ * making all elements inheriting from the root box-sizing value
+ * See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/
+ */
+/* line 18, src/assets/scss/base/_base.scss */
+*,
+*::before,
+*::after {
+  -webkit-box-sizing: inherit;
+          box-sizing: inherit;
+}
+
+/**
+ * Basic styles for links
+ */
+/* line 27, src/assets/scss/base/_base.scss */
+a {
+  color: #e54a19;
+  text-decoration: none;
+}
+
+/**
+ * Basic typography style for copy text
+ A solution for this problem is percentage. Usually default font-size of the browser is 16px.
+ Setting font-size: 100% will make 1rem = 16px. But it will make calculations a little difficult.
+ A better way is to set font-size: 62.5%. Because 62.5% of 16px is 10px. Which makes 1rem = 10px.
+ CALCULATION: Element font size in rem x 16px;
+ */
+/* line 16, src/assets/scss/base/_typography.scss */
+body {
+  font-size: 0.875rem;
+  font-weight: 300;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+  letter-spacing: 0.4px;
+  line-height: 1.5rem;
+  color: #464746;
+}
+
+@media (min-width: 1024px) {
+  /* line 16, src/assets/scss/base/_typography.scss */
+  body {
+    font-size: 1rem;
+  }
+}
+
+/* line 30, src/assets/scss/base/_typography.scss */
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+  margin-bottom: 28px;
+}
+
+@media (min-width: 1024px) {
+  /* line 30, src/assets/scss/base/_typography.scss */
+  h1, h2, h3, h4, h5, h6,
+  .h1, .h2, .h3, .h4, .h5, .h6 {
+    margin-bottom: 36px;
+  }
+}
+
+/* line 49, src/assets/scss/base/_typography.scss */
+ol,
+ul,
+p,
+blockquote,
+.preamble {
+  margin-bottom: 28px;
+}
+
+@media (min-width: 1024px) {
+  /* line 49, src/assets/scss/base/_typography.scss */
+  ol,
+  ul,
+  p,
+  blockquote,
+  .preamble {
+    margin-bottom: 36px;
+  }
+}
+
+/* line 63, src/assets/scss/base/_typography.scss */
+h1,
+h2,
+h3,
+h4,
+.h1,
+.h2,
+.h3,
+.h4 {
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+}
+
+/* line 74, src/assets/scss/base/_typography.scss */
+h5,
+h6,
+.h5,
+.h6 {
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+}
+
+/* line 81, src/assets/scss/base/_typography.scss */
+h1, .h1 {
+  color: #111111;
+  font-size: 2.25rem;
+  font-weight: 400;
+  line-height: 2.5rem;
+  letter-spacing: 0.13px;
+  text-transform: uppercase;
+}
+
+@media (min-width: 1024px) {
+  /* line 81, src/assets/scss/base/_typography.scss */
+  h1, .h1 {
+    font-size: 4rem;
+    letter-spacing: 0.22px;
+    line-height: 4.5rem;
+  }
+}
+
+/* line 97, src/assets/scss/base/_typography.scss */
+h2, .h2 {
+  color: #111111;
+  font-size: 1.5rem;
+  font-weight: 400;
+  letter-spacing: 0.08px;
+  line-height: 2.25rem;
+  text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+  /* line 97, src/assets/scss/base/_typography.scss */
+  h2, .h2 {
+    font-size: 2.4375rem;
+    letter-spacing: 0.14px;
+    line-height: 3rem;
+  }
+}
+
+/* line 113, src/assets/scss/base/_typography.scss */
+h3, .h3 {
+  color: #111111;
+  font-size: 1.0625rem;
+  font-weight: 400;
+  letter-spacing: 0.06px;
+  line-height: 1.5rem;
+  text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+  /* line 113, src/assets/scss/base/_typography.scss */
+  h3, .h3 {
+    font-size: 1.5rem;
+    letter-spacing: 0.08px;
+    line-height: 2.25rem;
+  }
+}
+
+/* line 129, src/assets/scss/base/_typography.scss */
+h4, .h4 {
+  color: #111111;
+  font-size: 0.875rem;
+  font-weight: 400;
+  letter-spacing: 0.05px;
+  line-height: 1.5rem;
+  text-transform: uppercase;
+}
+
+@media (min-width: 1024px) {
+  /* line 129, src/assets/scss/base/_typography.scss */
+  h4, .h4 {
+    font-size: 0.9375rem;
+    line-height: 1.5rem;
+  }
+}
+
+/* line 144, src/assets/scss/base/_typography.scss */
+h5, .h5 {
+  color: #111111;
+  font-size: 0.75rem;
+  font-weight: 700;
+  letter-spacing: normal;
+  line-height: 1.5rem;
+  text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+  /* line 144, src/assets/scss/base/_typography.scss */
+  h5, .h5 {
+    font-size: 0.75rem;
+    line-height: 1.5rem;
+  }
+}
+
+/* line 159, src/assets/scss/base/_typography.scss */
+h6, .h6 {
+  color: #111111;
+  font-size: 0.6875rem;
+  font-weight: 700;
+  letter-spacing: normal;
+  line-height: 1.5rem;
+  text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+  /* line 159, src/assets/scss/base/_typography.scss */
+  h6, .h6 {
+    font-size: 0.6875rem;
+    line-height: 1.5rem;
+  }
+}
+
+/**
+ * Clear inner floats
+ */
+/* line 8, src/assets/scss/base/_helpers.scss */
+.clearfix::after {
+  clear: both;
+  content: '';
+  display: table;
+}
+
+/**
+ * Hide text while making it readable for screen readers
+ * 1. Needed in WebKit-based browsers because of an implementation bug;
+ *    See: https://code.google.com/p/chromium/issues/detail?id=457146
+ */
+/* line 19, src/assets/scss/base/_helpers.scss */
+.hide-text {
+  overflow: hidden;
+  padding: 0;
+  /* 1 */
+  text-indent: 101%;
+  white-space: nowrap;
+}
+
+/**
+ * Hide element while making it readable for screen readers
+ * Shamelessly borrowed from HTML5Boilerplate:
+ * https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css#L119-L133
+ */
+/* line 31, src/assets/scss/base/_helpers.scss */
+.visually-hidden {
+  border: 0;
+  clip: rect(0 0 0 0);
+  height: 1px;
+  margin: -1px;
+  overflow: hidden;
+  padding: 0;
+  position: absolute;
+  width: 1px;
+}
+
+/* line 4, src/assets/scss/layout/_header.scss */
+.logo-container {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: horizontal;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: row;
+          flex-direction: row;
+  -webkit-box-pack: justify;
+      -ms-flex-pack: justify;
+          justify-content: space-between;
+  padding: 20px 0;
+  margin: 10px 0 40px;
+  border-bottom: 1px solid #eaebea;
+}
+
+/* line 12, src/assets/scss/layout/_header.scss */
+.logo-container img {
+  display: block;
+  max-width: 50%;
+  height: 100%;
+}
+
+/* line 17, src/assets/scss/layout/_header.scss */
+.logo-container img:last-child {
+  max-width: 35%;
+}
+
+/* line 4, src/assets/scss/layout/_footer.scss */
+.footer {
+  border-top: 1px solid #f7f7f7;
+}
+
+/* line 1, src/assets/scss/components/_alert.scss */
+.alert {
+  border-radius: 0;
+  margin-top: 10px;
+  background: none;
+  border-width: 2px;
+  padding: 15px;
+  line-height: 1.4;
+}
+
+/* line 9, src/assets/scss/components/_alert.scss */
+.alert-danger {
+  color: #e54a19;
+}
+
+/* line 5, src/assets/scss/components/_button.scss */
+.btn {
+  border-radius: 0;
+}
+
+/* line 8, src/assets/scss/components/_button.scss */
+.btn.btn-lg, .btn-group-lg > .btn {
+  font-size: 21px;
+}
+
+/* line 2, src/assets/scss/components/_form.scss */
+form .form-group {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  margin-bottom: 8px;
+}
+
+/* line 6, src/assets/scss/components/_form.scss */
+form .form-group label {
+  width: 180px;
+  height: calc(1.5em + 0.75rem + 2px);
+  line-height: 38px;
+}
+
+/* line 12, src/assets/scss/components/_form.scss */
+form .form-group input,
+form .form-group textarea,
+form .form-group select {
+  border-radius: 0;
+}
+
+/* line 19, src/assets/scss/components/_form.scss */
+form button {
+  margin-top: 30px;
+}
+
+/* line 1, src/assets/scss/components/_statusbar.scss */
+.statusbar {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: horizontal;
+  -webkit-box-direction: normal;
+      -ms-flex-direction: row;
+          flex-direction: row;
+  list-style-type: none;
+  margin: 0 0 50px;
+  padding: 0;
+}
+
+/* line 8, src/assets/scss/components/_statusbar.scss */
+.statusbar li {
+  position: relative;
+  width: 33.33%;
+  padding: 5px 10px;
+  background: #eaebea;
+  color: #989998;
+  text-align: center;
+  text-transform: uppercase;
+  font-size: 18px;
+}
+
+/* line 18, src/assets/scss/components/_statusbar.scss */
+.statusbar li:not(:first-of-type)::before {
+  content: '';
+  border: 17px solid transparent;
+  width: 0;
+  height: 0;
+  border-top-color: #eaebea;
+  border-bottom-color: #eaebea;
+  border-right-width: 0;
+  line-height: 0;
+  position: absolute;
+  left: -17px;
+  top: 0;
+}
+
+/* line 32, src/assets/scss/components/_statusbar.scss */
+.statusbar li:not(:last-of-type) {
+  margin-right: 23px;
+}
+
+/* line 35, src/assets/scss/components/_statusbar.scss */
+.statusbar li:not(:last-of-type)::after {
+  content: '';
+  border: 17px solid transparent;
+  width: 0;
+  height: 0;
+  border-left-color: #eaebea;
+  border-right-width: 0;
+  line-height: 0;
+  position: absolute;
+  right: -17px;
+  top: 0;
+}
+
+/* line 49, src/assets/scss/components/_statusbar.scss */
+.statusbar li.active {
+  background: #e54a19;
+  color: #ffffff;
+}
+
+/* line 53, src/assets/scss/components/_statusbar.scss */
+.statusbar li.active::before {
+  border-top-color: #e54a19;
+  border-bottom-color: #e54a19;
+}
+
+/* line 58, src/assets/scss/components/_statusbar.scss */
+.statusbar li.active::after {
+  border-left-color: #e54a19;
+}
+
+/* line 5, src/assets/scss/pages/_home.scss */
+.container {
+  max-width: 800px;
+}
+
+/* line 9, src/assets/scss/pages/_home.scss */
+h2 {
+  font-size: 40px;
+  line-height: 1.2;
+}
+
+/* line 13, src/assets/scss/pages/_home.scss */
+h3 {
+  font-size: 26px;
+}
+
+/* line 16, src/assets/scss/pages/_home.scss */
+h2,
+h3 {
+  color: #464746;
+}
+
+/* line 21, src/assets/scss/pages/_home.scss */
+a {
+  color: #6fa7fd;
+}
+
+/* line 25, src/assets/scss/pages/_home.scss */
+p {
+  margin-bottom: 23px;
+}
+
+/* line 29, src/assets/scss/pages/_home.scss */
+.accounts {
+  list-style-type: none;
+  margin: 0 0 60px;
+  padding: 0;
+}
+
+/* line 34, src/assets/scss/pages/_home.scss */
+.accounts li {
+  margin-bottom: 30px;
+  color: #e54a19;
+  font-size: 21px;
+}
+
+/* line 39, src/assets/scss/pages/_home.scss */
+.accounts li .status {
+  background: #eaebea;
+  color: #111111;
+  padding: 3px 5px;
+  margin-left: 10px;
+  font-size: 16px;
+}
+
+/* line 46, src/assets/scss/pages/_home.scss */
+.accounts li .status.online {
+  background: #e54a19;
+  color: #ffffff;
+}
+
+/* line 51, src/assets/scss/pages/_home.scss */
+.accounts li label {
+  position: relative;
+  background: #eaebea;
+  color: #eaebea;
+  border-color: #464746;
+  font-size: 16px;
+  padding: 4px 5px;
+  margin: 0;
+  line-height: 1;
+  cursor: pointer;
+}
+
+/* line 62, src/assets/scss/pages/_home.scss */
+.accounts li label::before, .accounts li label::after {
+  content: '';
+  position: absolute;
+  border: 2px solid transparent;
+  border-top-color: inherit;
+  border-left-color: inherit;
+  height: 10px;
+  width: 10px;
+  top: 50%;
+  margin-top: -5px;
+}
+
+/* line 74, src/assets/scss/pages/_home.scss */
+.accounts li label::before {
+  -webkit-transform: rotate(-45deg);
+          transform: rotate(-45deg);
+  left: 8px;
+}
+
+/* line 78, src/assets/scss/pages/_home.scss */
+.accounts li label::after {
+  -webkit-transform: rotate(135deg);
+          transform: rotate(135deg);
+  right: 8px;
+}
+
+/* line 83, src/assets/scss/pages/_home.scss */
+.accounts li .trigger {
+  display: none;
+}
+
+/* line 86, src/assets/scss/pages/_home.scss */
+.accounts li .trigger:checked + label {
+  background: #464746;
+  color: #464746;
+  border-color: #ffffff;
+}
+
+/* line 91, src/assets/scss/pages/_home.scss */
+.accounts li .trigger:checked + label + .things {
+  display: block;
+}
+
+/* line 96, src/assets/scss/pages/_home.scss */
+.accounts li .things {
+  display: none;
+}
+
+/* line 102, src/assets/scss/pages/_home.scss */
+.things {
+  margin-top: 25px;
+}
+
+/* line 105, src/assets/scss/pages/_home.scss */
+.things .legend {
+  display: block;
+  color: #111111;
+  margin: 5px 0 10px;
+  font-size: 16px;
+}
+
+/* line 111, src/assets/scss/pages/_home.scss */
+.things .code-container {
+  position: relative;
+}
+
+/* line 114, src/assets/scss/pages/_home.scss */
+.things .code-container textarea {
+  background: #eaebea;
+  color: #464746;
+  border: none;
+  padding: 25px 20px;
+  width: 100%;
+  height: 200px;
+  font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
+  font-size: .8rem;
+  white-space: pre;
+}
+
+/* line 126, src/assets/scss/pages/_home.scss */
+.things .code-container .copy {
+  position: absolute;
+  top: 5px;
+  right: 5px;
+  border: 0;
+  color: #989998;
+}
+
+/* line 133, src/assets/scss/pages/_home.scss */
+.things .code-container .copy:hover, .things .code-container .copy:focus, .things .code-container .copy:active {
+  color: #fff;
+}
+
+/* line 143, src/assets/scss/pages/_home.scss */
+.accounts + .alert-danger::after {
+  content: '';
+  position: absolute;
+  left: 65px;
+  bottom: -8px;
+  height: 16px;
+  width: 16px;
+  background: white;
+  border: inherit;
+  border-top-color: transparent;
+  border-left-color: transparent;
+  -webkit-transform: rotate(45deg);
+          transform: rotate(45deg);
+}
+
+/* line 158, src/assets/scss/pages/_home.scss */
+.controls {
+  margin-top: 25px;
+}
+
+/*# sourceMappingURL=../../../scss */
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/css/rtl.css b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/css/rtl.css
new file mode 100644 (file)
index 0000000..f1e048e
--- /dev/null
@@ -0,0 +1,10 @@
+/* line 5, src/assets/scss/rtl.scss */
+.jumbotron {
+  direction: ltr;
+  text-align: left;
+  margin: 0 2em 0 1em;
+  padding-right: 1em;
+  margin: 0 !important;
+}
+
+/*# sourceMappingURL=../../../scss */
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/OpenHAB_logo.svg b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/OpenHAB_logo.svg
new file mode 100644 (file)
index 0000000..0c8c6eb
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 169.18 36.997" height="139.83" width="639.422"><path d="M70.658 31.467h-2.855c.01.426.142.751.394.978.253.226.579.34.98.34.437 0 .82-.068 1.147-.203l.13.529c-.387.173-.846.26-1.374.26-.613 0-1.1-.182-1.465-.545-.363-.363-.544-.847-.544-1.448 0-.603.174-1.107.523-1.513.349-.407.816-.61 1.404-.61.543 0 .959.18 1.249.542.29.362.435.806.435 1.329 0 .149-.008.263-.024.34zm-2.847-.545h2.14c0-.339-.087-.612-.263-.82-.174-.207-.424-.31-.746-.31-.307 0-.56.103-.761.31-.201.208-.324.481-.37.82zm3.782 2.36v-2.856c0-.436-.01-.797-.032-1.082h.658l.058.643c.292-.488.72-.732 1.285-.732.26 0 .492.07.694.211.202.141.355.336.46.586.323-.531.773-.797 1.351-.797.39 0 .712.148.964.443.252.296.378.728.378 1.298v2.285h-.74v-2.195c0-.392-.075-.691-.225-.9-.15-.209-.366-.313-.645-.313a.89.89 0 00-.644.278.957.957 0 00-.284.714v2.416h-.74v-2.326c0-.334-.075-.598-.224-.792a.73.73 0 00-.614-.29c-.252 0-.474.106-.668.317a1.06 1.06 0 00-.292.74v2.351zm7.004 1.61v-4.254c0-.529-.01-.96-.033-1.294h.676l.04.684h.016c.328-.516.817-.773 1.464-.773.494 0 .905.185 1.232.558.329.373.493.852.493 1.435 0 .648-.176 1.163-.53 1.546a1.73 1.73 0 01-1.325.577 1.66 1.66 0 01-.737-.163 1.23 1.23 0 01-.523-.464h-.017v2.147h-.756zm.756-3.872v.634c0 .318.112.584.332.799.222.215.487.324.8.324.379 0 .68-.136.902-.406.222-.269.333-.633.333-1.09 0-.413-.11-.753-.33-1.022a1.074 1.074 0 00-.873-.402 1.11 1.11 0 00-.643.2 1.067 1.067 0 00-.407.492 1.37 1.37 0 00-.114.471zm7.769.252c0 .423-.089.793-.265 1.11a1.811 1.811 0 01-.74.732c-.317.171-.66.257-1.029.257-.575 0-1.049-.189-1.424-.564-.374-.376-.56-.863-.56-1.462 0-.637.193-1.145.58-1.523.386-.38.873-.567 1.461-.567.586 0 1.062.187 1.428.562.366.376.549.861.549 1.455zm-3.246.049c0 .43.115.786.346 1.067.23.28.52.421.866.421.355 0 .652-.14.89-.423.237-.281.355-.645.355-1.09 0-.41-.109-.759-.327-1.047-.218-.29-.516-.433-.894-.433-.382 0-.683.142-.904.428-.22.286-.332.645-.332 1.077zm3.644-1.976h.773c.515 1.908.792 2.987.83 3.237h.016c.03-.212.366-1.291 1.008-3.237h.643c.583 1.824.909 2.903.976 3.237h.017c.075-.407.17-.806.284-1.196l.594-2.041h.748l-1.301 3.936h-.7c-.447-1.377-.699-2.16-.756-2.35a10.208 10.208 0 01-.195-.797h-.017c-.09.409-.207.826-.353 1.252l-.656 1.895h-.699l-1.212-3.936zm9.843 2.122h-2.856c.011.426.143.751.395.978.252.226.579.34.98.34.437 0 .82-.068 1.147-.203l.13.529c-.388.173-.846.26-1.374.26-.613 0-1.101-.181-1.465-.545-.363-.363-.544-.846-.544-1.448 0-.603.174-1.107.523-1.513.348-.407.816-.61 1.404-.61.543 0 .959.18 1.249.542.29.362.435.806.435 1.329 0 .149-.008.263-.024.34zm-2.847-.545h2.14c0-.339-.088-.612-.263-.82-.174-.207-.424-.31-.747-.31-.306 0-.56.103-.76.31-.201.208-.324.481-.37.82zm3.782 2.36v-2.685c0-.51-.011-.927-.032-1.252h.667l.048.789c.1-.277.254-.493.46-.648a1.097 1.097 0 01.874-.215v.716a1.235 1.235 0 00-.26-.025.924.924 0 00-.705.322c-.197.214-.295.519-.295.915v2.082h-.757zm3.522 0h-.756v-3.937h.756zm.106-5.134c0 .136-.045.25-.134.342a.47.47 0 01-.354.139.46.46 0 01-.346-.139.472.472 0 01-.134-.342c0-.132.046-.245.137-.336a.473.473 0 01.351-.136.465.465 0 01.48.472zm1.107 5.133v-2.855c0-.436-.012-.797-.033-1.081h.667l.065.667c.124-.229.308-.412.549-.55a1.57 1.57 0 01.793-.207c.412 0 .755.145 1.03.436.273.292.41.719.41 1.28v2.31h-.757v-2.229c0-.371-.08-.66-.24-.865-.16-.204-.392-.306-.695-.306-.271 0-.511.102-.72.305a.997.997 0 00-.313.744v2.351zm8.167-2.871v2.285c0 .858-.187 1.454-.558 1.79-.371.336-.877.504-1.517.504-.57 0-1.028-.114-1.375-.34l.188-.579c.35.217.75.325 1.203.325.42 0 .744-.116.97-.35.227-.232.34-.574.34-1.023l-.016-.432c-.286.45-.719.675-1.302.675-.496 0-.908-.184-1.238-.552-.33-.367-.494-.823-.494-1.368 0-.631.181-1.139.544-1.52.361-.38.792-.57 1.294-.57.596 0 1.027.227 1.294.683l.032-.593h.667c-.022.235-.032.59-.032 1.065zm-.757 1.155v-.643c0-.293-.099-.546-.297-.76-.198-.214-.457-.322-.777-.322a1.08 1.08 0 00-.866.403c-.225.269-.338.622-.338 1.061 0 .404.109.735.324.995.215.258.506.388.872.388.29 0 .543-.104.759-.312.215-.209.323-.48.323-.81zm3.904-2.953l.74-.22v.953h1.05v.577h-1.05v2.05c0 .273.045.47.134.59.088.121.227.182.42.182.176 0 .32-.016.43-.048l.033.569c-.187.07-.412.106-.675.106-.72 0-1.082-.45-1.082-1.35v-2.1h-.618v-.576h.618zm2.628 4.67V27.49h.756v2.48h.017c.132-.222.318-.397.555-.524.237-.127.48-.191.73-.191.406 0 .745.145 1.016.436.272.292.408.719.408 1.28v2.31h-.757v-2.229c0-.371-.08-.66-.24-.865-.16-.204-.392-.306-.696-.306-.257 0-.493.096-.709.29a.964.964 0 00-.324.751v2.36zm7.955-1.815h-2.855c.01.426.143.751.395.978.252.226.578.34.98.34.436 0 .819-.068 1.146-.203l.13.529c-.387.173-.845.26-1.374.26-.613 0-1.1-.181-1.464-.545-.364-.363-.545-.846-.545-1.448 0-.603.174-1.107.523-1.513.349-.407.816-.61 1.405-.61.542 0 .958.18 1.248.542.29.362.435.806.435 1.329 0 .149-.008.263-.024.34zm-2.847-.545h2.14c0-.339-.087-.612-.262-.82-.175-.207-.424-.31-.747-.31-.307 0-.56.103-.76.31-.202.208-.324.481-.371.82zm5.28 2.172l.178-.578c.32.196.65.293.992.293.244 0 .433-.051.566-.154a.498.498 0 00.199-.415.535.535 0 00-.164-.405c-.11-.105-.302-.202-.576-.295-.722-.246-1.082-.618-1.082-1.114 0-.33.128-.609.385-.834.256-.225.596-.337 1.022-.337.397 0 .732.081 1.009.244l-.187.545a1.68 1.68 0 00-.846-.236c-.201 0-.36.049-.476.147a.48.48 0 00-.175.381c0 .14.05.255.149.35.099.095.304.198.615.31.38.135.653.295.818.48.165.186.248.42.248.7 0 .357-.135.646-.408.865-.272.22-.644.33-1.113.33-.43 0-.816-.092-1.155-.277zm3.618.187v-2.855c0-.436-.01-.797-.032-1.081h.659l.057.642c.292-.488.721-.732 1.285-.732.26 0 .493.07.694.211.202.141.356.336.461.586.323-.531.772-.797 1.35-.797.39 0 .713.148.964.443.253.296.378.728.378 1.298v2.285h-.739v-2.195c0-.392-.075-.691-.225-.9-.15-.209-.367-.313-.646-.313a.888.888 0 00-.643.278.957.957 0 00-.284.714v2.416h-.74v-2.326c0-.334-.075-.598-.224-.792a.73.73 0 00-.614-.29c-.253 0-.475.106-.668.317a1.058 1.058 0 00-.292.74v2.351zm9.883-2.383v1.44c0 .39.023.705.066.943h-.692l-.089-.504c-.296.396-.71.594-1.244.594-.375 0-.674-.111-.9-.332a1.078 1.078 0 01-.337-.799c0-.477.207-.842.622-1.094.416-.252 1.022-.378 1.818-.378v-.074c0-.279-.076-.497-.229-.653-.153-.155-.38-.233-.682-.233-.4 0-.759.1-1.073.301l-.171-.496c.38-.239.832-.358 1.359-.358.511 0 .9.14 1.161.423.261.281.391.688.391 1.22zm-.74 1.04v-.65c-.634 0-1.074.077-1.322.228-.246.152-.37.36-.37.627 0 .209.063.37.186.488a.676.676 0 00.49.178c.287 0 .528-.09.724-.266.195-.179.293-.379.293-.604zm1.92 1.343v-2.684c0-.51-.01-.927-.032-1.252h.667l.048.789c.1-.277.254-.493.46-.648a1.098 1.098 0 01.875-.215v.716a1.247 1.247 0 00-.26-.025.925.925 0 00-.706.322c-.196.214-.295.519-.295.915v2.082h-.757zm3.116-4.669l.74-.22v.953h1.05v.577h-1.05v2.05c0 .273.045.47.133.59.088.121.228.182.42.182.176 0 .32-.016.43-.048l.034.569c-.188.07-.412.106-.675.106-.722 0-1.082-.45-1.082-1.35v-2.1h-.619v-.576h.619zm4.4 4.67V27.49h.758v2.48h.015a1.41 1.41 0 01.556-.524 1.53 1.53 0 01.73-.191c.406 0 .745.145 1.016.436.272.292.407.719.407 1.28v2.31h-.756v-2.229c0-.371-.08-.66-.24-.865-.16-.204-.392-.306-.696-.306a1.04 1.04 0 00-.71.29.966.966 0 00-.322.751v2.36zm8.387-2.01c0 .423-.088.793-.265 1.11a1.811 1.811 0 01-.74.732c-.318.171-.66.257-1.028.257-.576 0-1.05-.189-1.425-.564-.374-.376-.56-.863-.56-1.462 0-.637.193-1.145.58-1.523.386-.379.873-.567 1.461-.567.586 0 1.062.187 1.427.562.367.376.55.861.55 1.455zm-3.246.049c0 .43.115.786.346 1.067.23.28.52.421.867.421.355 0 .651-.14.889-.423.237-.281.355-.645.355-1.09 0-.41-.11-.759-.327-1.047-.218-.29-.516-.433-.893-.433-.383 0-.683.142-.905.428-.22.286-.332.645-.332 1.077zm4.165 1.96v-2.855c0-.436-.011-.797-.032-1.081h.658l.058.642c.292-.488.72-.732 1.284-.732.261 0 .493.07.694.211.203.141.356.336.462.586.322-.531.772-.797 1.35-.797.39 0 .712.148.964.443.252.296.378.728.378 1.298v2.285h-.74v-2.195c0-.392-.075-.691-.225-.9-.151-.209-.366-.313-.646-.313a.891.891 0 00-.644.278.958.958 0 00-.282.714v2.416h-.741v-2.326c0-.334-.075-.598-.224-.792a.731.731 0 00-.614-.29c-.252 0-.474.106-.668.317a1.06 1.06 0 00-.292.74v2.351zm10.29-1.814H166.3c.01.426.142.751.394.978.253.226.58.34.98.34.436 0 .82-.068 1.147-.203l.13.529c-.387.173-.845.26-1.374.26-.613 0-1.1-.181-1.464-.545-.363-.363-.545-.846-.545-1.448 0-.603.175-1.107.523-1.513.349-.407.817-.61 1.404-.61.543 0 .959.18 1.249.542.29.362.435.806.435 1.329 0 .149-.008.263-.025.34zm-2.847-.545h2.139c0-.339-.087-.612-.262-.82-.175-.207-.424-.31-.747-.31-.306 0-.56.103-.76.31-.2.208-.324.481-.37.82z" clip-rule="evenodd" fill="#989898" fill-rule="evenodd"/><g clip-rule="evenodd" fill-rule="evenodd"><path d="M1.914 26.411L16.39 11.925l2.108-2.108 2.108 2.108L31.292 22.61l-.016.054-.21.621-.24.606-.269.592-.296.577-.269.467-11.494-11.494L3.452 29.09c-.579-.853-1.117-1.732-1.538-2.679z" fill="#e64a19"/><path d="M18.498 0c10.19 0 18.498 8.31 18.498 18.498 0 10.189-8.309 18.499-18.498 18.499-5.565 0-10.569-2.482-13.964-6.393l.653-.656.475-.475.475-.475.476-.476.02-.02c2.852 3.379 7.114 5.531 11.865 5.531 8.557 0 15.535-6.978 15.535-15.535S27.055 2.963 18.498 2.963 2.963 9.941 2.963 18.498c0 1.153.128 2.276.368 3.358l-1.03 1.033-1.376 1.377A18.378 18.378 0 010 18.498C0 8.31 8.31 0 18.498 0z" fill="#474747"/></g><path d="M156.694 16.156v6.789h4.443l.264-.002.255-.005.244-.008.234-.012.225-.015.215-.018.206-.021.195-.025.186-.027.175-.03.166-.034.155-.036.145-.038.133-.04.125-.044.11-.043.223-.104.21-.116.197-.127.187-.14.173-.152.163-.166.152-.179.142-.193.13-.204.11-.205.093-.21.076-.213.06-.217.042-.223.024-.228.01-.233-.013-.288-.035-.276-.06-.265-.082-.255-.106-.248-.13-.24-.156-.232-.18-.225-.2-.211-.22-.195-.24-.178-.258-.161-.28-.145-.3-.129-.322-.111-.346-.094-.117-.026-.134-.025-.146-.024-.16-.022-.174-.021-.185-.019-.198-.017-.21-.016-.225-.013-.235-.012-.247-.01-.26-.008-.272-.006-.283-.005-.295-.003h-3.399zm0-8.45v5.518h3.103l.252-.001.241-.004.233-.008.223-.01.214-.014.205-.016.197-.019.185-.022.178-.024.168-.028.158-.03.147-.032.14-.035.128-.036.118-.04.107-.04.213-.093.198-.104.185-.114.173-.126.161-.138.152-.149.14-.162.13-.175.117-.183.101-.186.084-.192.07-.196.054-.2.04-.209.023-.214.008-.22-.014-.295-.038-.271-.064-.25-.086-.23-.111-.215-.135-.197-.16-.186-.19-.175-.096-.073-.11-.073-.12-.069-.128-.065-.14-.06-.148-.058-.157-.051-.168-.047-.18-.042-.187-.037-.198-.031-.209-.026-.217-.02-.227-.015-.238-.009-.249-.003h-4.276zm5.377-2.74l.26.026.253.03.245.035.236.04.229.044.22.05.214.054.204.06.198.065.19.071.185.077.34.164.325.188.307.21.29.229.269.252.25.273.231.292.209.309.1.164.09.164.086.166.08.169.072.17.067.173.06.175.054.177.047.18.041.18.035.183.028.185.022.187.016.187.009.19.004.196-.012.357-.033.354-.057.345-.079.34-.102.33-.124.323-.146.314-.168.307-.193.296-.213.28-.234.266-.255.25-.275.235-.143.107.266.117.332.163.31.17.289.177.266.184.246.193.224.2.21.21.197.22.185.231.172.24.16.25.148.261.137.272.12.28.105.285.088.292.073.297.055.303.04.308.024.313.008.319-.008.321-.021.316-.038.31-.053.307-.067.3-.083.293-.097.288-.113.282-.128.275-.143.269-.157.261-.173.256-.186.248-.2.241-.216.234-.23.229-.244.219-.255.203-.265.19-.275.177-.284.161-.294.147-.304.134-.312.119-.323.104-.33.09-.34.076-.348.063-.357.047-.365.034-.374.02-.379.008h-8.378V4.91h6.833l.298.002.293.007.285.01.276.017.268.02zm-47.325-.053h2.926v8.422h9.308V4.913h2.917v20.986h-2.917v-9.678h-9.308v9.678h-2.926zm23.606 13.236h7.125l-3.551-7.538zm8.435 2.86h-9.753l-2.316 4.883h-3.24l9.917-20.982h1.07l9.776 20.982h-3.143l-.837-1.755z" clip-rule="evenodd" fill="#474747" fill-rule="evenodd"/><path d="M50.546 9.667l.437.01.43.03.42.05.413.07.404.091.396.112.386.132.377.152.367.172.358.193.348.213.339.232.327.252.318.273.308.29.299.313.257.299.24.304.224.312.208.32.191.327.175.333.158.34.142.349.125.354.108.36.091.367.075.373.058.38.041.385.025.39.008.398-.009.398-.026.393-.044.387-.061.382-.08.375-.097.37-.115.364-.132.357-.15.351-.168.344-.184.337-.203.331-.22.324-.236.316-.253.31-.272.303-.286.292-.298.274-.309.256-.319.238-.33.219-.339.2-.35.18-.36.163-.37.143-.378.124-.388.104-.397.086-.407.065-.415.047-.425.03-.432.009-.436-.01-.426-.028-.417-.048-.408-.065-.399-.086-.39-.104-.379-.124-.37-.143-.36-.162-.35-.181-.34-.2-.328-.22-.32-.237-.307-.256-.298-.275-.285-.291-.27-.304-.255-.309-.236-.316-.22-.324-.202-.33-.184-.338-.167-.345-.15-.35-.132-.357-.114-.364-.097-.37-.079-.376-.06-.382-.045-.387-.026-.392-.008-.398.008-.396.025-.389.04-.384.06-.378.073-.372.092-.366.108-.36.125-.352.141-.347.159-.34.174-.334.192-.325.208-.32.224-.311.24-.305.257-.298.298-.313.307-.293.318-.274.328-.253.338-.235.349-.213.358-.194.368-.173.377-.154.387-.132.395-.112.405-.091.413-.072.422-.05.43-.03.439-.01zm0 2.736l-.269.006-.262.019-.256.03-.251.044-.246.056-.24.068-.237.08-.23.093-.228.105-.221.117-.219.13-.213.144-.21.156-.206.17-.203.182-.197.196-.19.204-.177.21-.164.213-.151.218-.14.224-.126.228-.115.232-.102.239-.09.243-.08.248-.065.254-.055.26-.043.265-.03.271-.018.278-.006.286.002.182.009.182.013.18.02.178.025.177.03.175.036.173.042.173.046.17.053.17.058.167.063.166.07.165.074.164.08.162.085.159.186.312.2.29.214.27.23.252.245.233.26.215.276.197.297.181.147.08.152.078.154.07.157.067.157.061.16.055.16.05.164.046.164.04.168.034.168.03.17.023.173.019.176.013.177.008.179.003.18-.003.18-.008.178-.013.174-.019.172-.024.17-.03.17-.034.166-.04.163-.044.162-.05.16-.057.158-.06.156-.066.156-.071.153-.077.146-.08.291-.18.274-.198.258-.215.243-.233.228-.252.214-.27.2-.291.186-.312.083-.157.08-.163.074-.163.069-.165.062-.167.058-.168.053-.17.046-.17.041-.172.036-.174.03-.176.026-.176.019-.179.014-.18.008-.182.003-.181-.006-.285-.019-.278-.03-.272-.043-.265-.055-.26-.067-.254-.079-.248-.09-.243-.104-.238-.114-.233-.128-.228-.139-.223-.152-.22-.164-.213-.177-.209L54.23 14l-.198-.196-.203-.182-.206-.17-.21-.156-.214-.143-.218-.13-.222-.118-.226-.105-.231-.093-.236-.08-.24-.068-.245-.056-.251-.043-.256-.031-.261-.019-.267-.006zm43.86 9.28l-.212.371-.221.353-.23.334-.241.315-.25.298-.26.278-.27.259-.276.24-.284.227-.294.21-.302.196-.311.181-.32.167-.327.15-.334.136-.174.063-.174.059-.177.054-.18.05-.181.047-.184.042-.187.038-.19.035-.192.03-.194.026-.197.022-.199.019-.202.013-.203.01-.206.007-.211.002-.458-.01-.448-.03-.437-.047-.426-.067-.414-.087-.403-.106-.39-.127-.379-.145-.366-.166-.354-.186-.341-.206-.328-.224-.316-.245-.301-.264-.288-.283-.273-.3-.257-.31-.239-.314-.223-.32-.206-.325-.191-.331-.174-.337-.158-.342-.141-.346-.125-.353-.108-.357-.092-.363-.074-.367-.058-.372-.042-.376-.024-.381-.008-.384.006-.364.022-.359.034-.354.05-.35.063-.345.077-.34.092-.336.106-.33.12-.326.134-.32.148-.316.163-.309.175-.305.19-.3.203-.293.218-.29.288-.351.304-.332.317-.31.33-.288.342-.265.356-.242.368-.22.381-.196.393-.174.405-.15.416-.127.429-.103.438-.08.45-.057.46-.034.47-.011.481.011.47.035.46.058.45.083.437.105.426.13.416.153.403.178.39.2.379.225.365.248.353.271.34.294.327.317.314.339.3.361.203.27.191.278.179.286.165.295.15.302.14.31.125.32.113.326.098.333.086.341.073.348.06.357.047.362.034.37.02.378.019.837H82.036l.01.132.033.282.045.276.055.267.067.26.077.252.088.245.099.238.11.23.12.223.132.218.143.21.153.204.165.198.177.192.184.183.189.17.193.158.198.146.203.133.208.123.213.11.218.099.223.087.228.075.234.064.24.053.246.04.252.03.258.018.264.006.26-.006.258-.017.255-.029.253-.039.25-.051.25-.062.247-.074.244-.085.24-.096.233-.103.221-.11.212-.116.202-.123.192-.13.182-.135.172-.141.167-.155.174-.18.181-.206.188-.233.193-.26.2-.287.203-.312.435-.702 2.337 1.25-.404.776zm-2.914-7.046l-.147-.23-.155-.208-.169-.196-.183-.189-.196-.18-.21-.17-.225-.161-.239-.152-.253-.143-.271-.134-.137-.061-.138-.059-.14-.054-.14-.05-.141-.046-.142-.042-.142-.038-.144-.034-.144-.03-.145-.026-.145-.022-.148-.018-.147-.014-.148-.01-.15-.006-.148-.002-.25.005-.24.015-.238.025-.231.035-.227.044-.222.056-.217.064-.213.074-.21.084-.202.094-.2.104-.196.114-.192.124-.188.134-.184.145-.18.155-.123.116-.12.124-.118.132-.113.14-.11.147-.107.155-.104.163-.1.171-.097.18-.092.187-.09.195-.085.204-.082.212-.041.115h9.663l-.038-.122-.113-.31-.122-.288-.131-.269-.14-.25zm-22.555-4.97l.438.01.429.03.42.05.413.07.404.091.396.112.386.132.377.152.367.172.358.193.349.213.338.232.328.252.317.273.308.29.299.313.257.299.24.304.225.312.207.32.191.327.175.333.159.34.141.349.125.354.108.36.091.367.075.373.058.38.042.385.024.39.009.398-.01.398-.026.393-.044.387-.061.382-.08.375-.097.37-.114.364-.133.357-.15.351-.168.344-.184.337-.203.331-.22.324-.236.316-.253.31-.271.303-.287.292-.298.274-.309.256-.318.238-.33.219-.34.2-.35.18-.359.163-.37.143-.379.124-.388.104-.397.086-.407.065-.415.047-.424.03-.433.009-.436-.01-.426-.028-.417-.048-.408-.065-.399-.086-.39-.104-.379-.124-.37-.143-.36-.162-.35-.181-.34-.2-.328-.22-.32-.237-.307-.256-.007-.007v7.453h-2.82V17.718h.001l.006-.27.025-.39.041-.384.058-.378.075-.372.09-.366.11-.36.124-.352.141-.347.159-.34.175-.334.191-.325.208-.32.224-.311.24-.305.257-.298.298-.313.308-.293.317-.274.328-.253.338-.235.349-.213.358-.194.368-.173.377-.154.387-.132.395-.112.405-.091.413-.072.422-.05.43-.03.44-.01zM63.7 18.055v-.304l.003-.165.018-.278.03-.27.043-.266.055-.26.066-.254.078-.248.091-.243.102-.239.115-.232.127-.228.14-.224.15-.218.164-.214.177-.209.19-.204.198-.196.202-.182.206-.17.21-.156.214-.143.218-.13.221-.118.228-.105.23-.093.237-.08.24-.068.246-.056.251-.043.257-.031.262-.019.268-.006.267.006.261.019.256.03.25.044.246.056.24.068.236.08.231.093.226.105.222.117.218.131.215.143.21.156.206.17.202.182.198.196.19.204.176.21.165.213.151.219.14.223.127.228.115.233.103.238.09.243.08.248.066.255.055.26.043.264.03.272.019.278.006.285-.003.181-.008.182-.014.18-.02.179-.024.176-.03.176-.037.174-.04.172-.047.17-.053.17-.057.168-.063.167-.069.165-.074.163-.08.163-.083.157-.186.312-.2.29-.213.271-.229.252-.243.233-.258.215-.274.197-.29.181-.147.08-.153.077-.155.07-.156.067-.159.06-.16.056-.162.05-.163.046-.166.04-.17.033-.17.03-.172.024-.174.019-.178.013-.179.008-.18.003-.18-.003-.177-.008-.176-.013-.172-.019-.17-.024-.17-.03-.167-.034-.164-.04-.163-.044-.162-.05-.159-.056-.157-.061-.156-.066-.155-.071-.152-.077-.147-.08-.296-.182-.277-.197-.26-.215-.245-.233-.23-.252-.214-.27-.2-.29-.186-.312-.084-.159-.08-.162-.075-.164-.07-.165-.063-.166-.058-.167-.053-.17-.046-.17-.042-.173-.036-.173-.03-.175-.025-.177-.02-.179-.013-.18-.008-.18zm40.82-8.365c-1.223 0-2.852.294-3.9.899-1.06.612-1.828 1.45-2.283 2.487-.447 1.025-.665 2.558-.665 4.69v8.13h2.765v-7.567c0-1.7.072-2.84.214-3.394.214-.908.611-1.522 1.217-1.885.628-.376 1.371-.688 2.651-.688 1.28 0 2.009.314 2.637.69.606.363 1.003.978 1.217 1.886.142.552.213 1.694.213 3.393v7.567h2.766v-8.13c0-2.131-.218-3.665-.666-4.69-.455-1.037-1.222-1.874-2.282-2.487-1.048-.605-2.661-.901-3.884-.901z" clip-rule="evenodd" fill="#e64a19" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/favicon.ico b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/favicon.ico
new file mode 100644 (file)
index 0000000..070fbb1
Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/favicon.ico differ
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/miele.png b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/miele.png
new file mode 100644 (file)
index 0000000..e533379
Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/img/miele.png differ
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/js/main.js b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/js/main.js
new file mode 100644 (file)
index 0000000..df9dac3
--- /dev/null
@@ -0,0 +1,17768 @@
+/*!
+ * jQuery JavaScript Library v3.4.1
+ * https://jquery.com/
+ *
+ * Includes Sizzle.js
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2019-05-01T21:04Z
+ */
+( function( global, factory ) {
+
+       "use strict";
+
+       if ( typeof module === "object" && typeof module.exports === "object" ) {
+
+               // For CommonJS and CommonJS-like environments where a proper `window`
+               // is present, execute the factory and get jQuery.
+               // For environments that do not have a `window` with a `document`
+               // (such as Node.js), expose a factory as module.exports.
+               // This accentuates the need for the creation of a real `window`.
+               // e.g. var jQuery = require("jquery")(window);
+               // See ticket #14549 for more info.
+               module.exports = global.document ?
+                       factory( global, true ) :
+                       function( w ) {
+                               if ( !w.document ) {
+                                       throw new Error( "jQuery requires a window with a document" );
+                               }
+                               return factory( w );
+                       };
+       } else {
+               factory( global );
+       }
+
+// Pass this if window is not defined yet
+} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
+
+// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
+// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
+// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
+// enough that all such attempts are guarded in a try block.
+"use strict";
+
+var arr = [];
+
+var document = window.document;
+
+var getProto = Object.getPrototypeOf;
+
+var slice = arr.slice;
+
+var concat = arr.concat;
+
+var push = arr.push;
+
+var indexOf = arr.indexOf;
+
+var class2type = {};
+
+var toString = class2type.toString;
+
+var hasOwn = class2type.hasOwnProperty;
+
+var fnToString = hasOwn.toString;
+
+var ObjectFunctionString = fnToString.call( Object );
+
+var support = {};
+
+var isFunction = function isFunction( obj ) {
+
+      // Support: Chrome <=57, Firefox <=52
+      // In some browsers, typeof returns "function" for HTML <object> elements
+      // (i.e., `typeof document.createElement( "object" ) === "function"`).
+      // We don't want to classify *any* DOM node as a function.
+      return typeof obj === "function" && typeof obj.nodeType !== "number";
+  };
+
+
+var isWindow = function isWindow( obj ) {
+               return obj != null && obj === obj.window;
+       };
+
+
+
+
+       var preservedScriptAttributes = {
+               type: true,
+               src: true,
+               nonce: true,
+               noModule: true
+       };
+
+       function DOMEval( code, node, doc ) {
+               doc = doc || document;
+
+               var i, val,
+                       script = doc.createElement( "script" );
+
+               script.text = code;
+               if ( node ) {
+                       for ( i in preservedScriptAttributes ) {
+
+                               // Support: Firefox 64+, Edge 18+
+                               // Some browsers don't support the "nonce" property on scripts.
+                               // On the other hand, just using `getAttribute` is not enough as
+                               // the `nonce` attribute is reset to an empty string whenever it
+                               // becomes browsing-context connected.
+                               // See https://github.com/whatwg/html/issues/2369
+                               // See https://html.spec.whatwg.org/#nonce-attributes
+                               // The `node.getAttribute` check was added for the sake of
+                               // `jQuery.globalEval` so that it can fake a nonce-containing node
+                               // via an object.
+                               val = node[ i ] || node.getAttribute && node.getAttribute( i );
+                               if ( val ) {
+                                       script.setAttribute( i, val );
+                               }
+                       }
+               }
+               doc.head.appendChild( script ).parentNode.removeChild( script );
+       }
+
+
+function toType( obj ) {
+       if ( obj == null ) {
+               return obj + "";
+       }
+
+       // Support: Android <=2.3 only (functionish RegExp)
+       return typeof obj === "object" || typeof obj === "function" ?
+               class2type[ toString.call( obj ) ] || "object" :
+               typeof obj;
+}
+/* global Symbol */
+// Defining this global in .eslintrc.json would create a danger of using the global
+// unguarded in another place, it seems safer to define global only for this module
+
+
+
+var
+       version = "3.4.1",
+
+       // Define a local copy of jQuery
+       jQuery = function( selector, context ) {
+
+               // The jQuery object is actually just the init constructor 'enhanced'
+               // Need init if jQuery is called (just allow error to be thrown if not included)
+               return new jQuery.fn.init( selector, context );
+       },
+
+       // Support: Android <=4.0 only
+       // Make sure we trim BOM and NBSP
+       rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
+
+jQuery.fn = jQuery.prototype = {
+
+       // The current version of jQuery being used
+       jquery: version,
+
+       constructor: jQuery,
+
+       // The default length of a jQuery object is 0
+       length: 0,
+
+       toArray: function() {
+               return slice.call( this );
+       },
+
+       // Get the Nth element in the matched element set OR
+       // Get the whole matched element set as a clean array
+       get: function( num ) {
+
+               // Return all the elements in a clean array
+               if ( num == null ) {
+                       return slice.call( this );
+               }
+
+               // Return just the one element from the set
+               return num < 0 ? this[ num + this.length ] : this[ num ];
+       },
+
+       // Take an array of elements and push it onto the stack
+       // (returning the new matched element set)
+       pushStack: function( elems ) {
+
+               // Build a new jQuery matched element set
+               var ret = jQuery.merge( this.constructor(), elems );
+
+               // Add the old object onto the stack (as a reference)
+               ret.prevObject = this;
+
+               // Return the newly-formed element set
+               return ret;
+       },
+
+       // Execute a callback for every element in the matched set.
+       each: function( callback ) {
+               return jQuery.each( this, callback );
+       },
+
+       map: function( callback ) {
+               return this.pushStack( jQuery.map( this, function( elem, i ) {
+                       return callback.call( elem, i, elem );
+               } ) );
+       },
+
+       slice: function() {
+               return this.pushStack( slice.apply( this, arguments ) );
+       },
+
+       first: function() {
+               return this.eq( 0 );
+       },
+
+       last: function() {
+               return this.eq( -1 );
+       },
+
+       eq: function( i ) {
+               var len = this.length,
+                       j = +i + ( i < 0 ? len : 0 );
+               return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
+       },
+
+       end: function() {
+               return this.prevObject || this.constructor();
+       },
+
+       // For internal use only.
+       // Behaves like an Array's method, not like a jQuery method.
+       push: push,
+       sort: arr.sort,
+       splice: arr.splice
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+       var options, name, src, copy, copyIsArray, clone,
+               target = arguments[ 0 ] || {},
+               i = 1,
+               length = arguments.length,
+               deep = false;
+
+       // Handle a deep copy situation
+       if ( typeof target === "boolean" ) {
+               deep = target;
+
+               // Skip the boolean and the target
+               target = arguments[ i ] || {};
+               i++;
+       }
+
+       // Handle case when target is a string or something (possible in deep copy)
+       if ( typeof target !== "object" && !isFunction( target ) ) {
+               target = {};
+       }
+
+       // Extend jQuery itself if only one argument is passed
+       if ( i === length ) {
+               target = this;
+               i--;
+       }
+
+       for ( ; i < length; i++ ) {
+
+               // Only deal with non-null/undefined values
+               if ( ( options = arguments[ i ] ) != null ) {
+
+                       // Extend the base object
+                       for ( name in options ) {
+                               copy = options[ name ];
+
+                               // Prevent Object.prototype pollution
+                               // Prevent never-ending loop
+                               if ( name === "__proto__" || target === copy ) {
+                                       continue;
+                               }
+
+                               // Recurse if we're merging plain objects or arrays
+                               if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
+                                       ( copyIsArray = Array.isArray( copy ) ) ) ) {
+                                       src = target[ name ];
+
+                                       // Ensure proper type for the source value
+                                       if ( copyIsArray && !Array.isArray( src ) ) {
+                                               clone = [];
+                                       } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
+                                               clone = {};
+                                       } else {
+                                               clone = src;
+                                       }
+                                       copyIsArray = false;
+
+                                       // Never move original objects, clone them
+                                       target[ name ] = jQuery.extend( deep, clone, copy );
+
+                               // Don't bring in undefined values
+                               } else if ( copy !== undefined ) {
+                                       target[ name ] = copy;
+                               }
+                       }
+               }
+       }
+
+       // Return the modified object
+       return target;
+};
+
+jQuery.extend( {
+
+       // Unique for each copy of jQuery on the page
+       expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
+
+       // Assume jQuery is ready without the ready module
+       isReady: true,
+
+       error: function( msg ) {
+               throw new Error( msg );
+       },
+
+       noop: function() {},
+
+       isPlainObject: function( obj ) {
+               var proto, Ctor;
+
+               // Detect obvious negatives
+               // Use toString instead of jQuery.type to catch host objects
+               if ( !obj || toString.call( obj ) !== "[object Object]" ) {
+                       return false;
+               }
+
+               proto = getProto( obj );
+
+               // Objects with no prototype (e.g., `Object.create( null )`) are plain
+               if ( !proto ) {
+                       return true;
+               }
+
+               // Objects with prototype are plain iff they were constructed by a global Object function
+               Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
+               return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
+       },
+
+       isEmptyObject: function( obj ) {
+               var name;
+
+               for ( name in obj ) {
+                       return false;
+               }
+               return true;
+       },
+
+       // Evaluates a script in a global context
+       globalEval: function( code, options ) {
+               DOMEval( code, { nonce: options && options.nonce } );
+       },
+
+       each: function( obj, callback ) {
+               var length, i = 0;
+
+               if ( isArrayLike( obj ) ) {
+                       length = obj.length;
+                       for ( ; i < length; i++ ) {
+                               if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+                                       break;
+                               }
+                       }
+               } else {
+                       for ( i in obj ) {
+                               if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+                                       break;
+                               }
+                       }
+               }
+
+               return obj;
+       },
+
+       // Support: Android <=4.0 only
+       trim: function( text ) {
+               return text == null ?
+                       "" :
+                       ( text + "" ).replace( rtrim, "" );
+       },
+
+       // results is for internal usage only
+       makeArray: function( arr, results ) {
+               var ret = results || [];
+
+               if ( arr != null ) {
+                       if ( isArrayLike( Object( arr ) ) ) {
+                               jQuery.merge( ret,
+                                       typeof arr === "string" ?
+                                       [ arr ] : arr
+                               );
+                       } else {
+                               push.call( ret, arr );
+                       }
+               }
+
+               return ret;
+       },
+
+       inArray: function( elem, arr, i ) {
+               return arr == null ? -1 : indexOf.call( arr, elem, i );
+       },
+
+       // Support: Android <=4.0 only, PhantomJS 1 only
+       // push.apply(_, arraylike) throws on ancient WebKit
+       merge: function( first, second ) {
+               var len = +second.length,
+                       j = 0,
+                       i = first.length;
+
+               for ( ; j < len; j++ ) {
+                       first[ i++ ] = second[ j ];
+               }
+
+               first.length = i;
+
+               return first;
+       },
+
+       grep: function( elems, callback, invert ) {
+               var callbackInverse,
+                       matches = [],
+                       i = 0,
+                       length = elems.length,
+                       callbackExpect = !invert;
+
+               // Go through the array, only saving the items
+               // that pass the validator function
+               for ( ; i < length; i++ ) {
+                       callbackInverse = !callback( elems[ i ], i );
+                       if ( callbackInverse !== callbackExpect ) {
+                               matches.push( elems[ i ] );
+                       }
+               }
+
+               return matches;
+       },
+
+       // arg is for internal usage only
+       map: function( elems, callback, arg ) {
+               var length, value,
+                       i = 0,
+                       ret = [];
+
+               // Go through the array, translating each of the items to their new values
+               if ( isArrayLike( elems ) ) {
+                       length = elems.length;
+                       for ( ; i < length; i++ ) {
+                               value = callback( elems[ i ], i, arg );
+
+                               if ( value != null ) {
+                                       ret.push( value );
+                               }
+                       }
+
+               // Go through every key on the object,
+               } else {
+                       for ( i in elems ) {
+                               value = callback( elems[ i ], i, arg );
+
+                               if ( value != null ) {
+                                       ret.push( value );
+                               }
+                       }
+               }
+
+               // Flatten any nested arrays
+               return concat.apply( [], ret );
+       },
+
+       // A global GUID counter for objects
+       guid: 1,
+
+       // jQuery.support is not used in Core but other projects attach their
+       // properties to it so it needs to exist.
+       support: support
+} );
+
+if ( typeof Symbol === "function" ) {
+       jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];
+}
+
+// Populate the class2type map
+jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
+function( i, name ) {
+       class2type[ "[object " + name + "]" ] = name.toLowerCase();
+} );
+
+function isArrayLike( obj ) {
+
+       // Support: real iOS 8.2 only (not reproducible in simulator)
+       // `in` check used to prevent JIT error (gh-2145)
+       // hasOwn isn't used here due to false negatives
+       // regarding Nodelist length in IE
+       var length = !!obj && "length" in obj && obj.length,
+               type = toType( obj );
+
+       if ( isFunction( obj ) || isWindow( obj ) ) {
+               return false;
+       }
+
+       return type === "array" || length === 0 ||
+               typeof length === "number" && length > 0 && ( length - 1 ) in obj;
+}
+var Sizzle =
+/*!
+ * Sizzle CSS Selector Engine v2.3.4
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://js.foundation/
+ *
+ * Date: 2019-04-08
+ */
+(function( window ) {
+
+var i,
+       support,
+       Expr,
+       getText,
+       isXML,
+       tokenize,
+       compile,
+       select,
+       outermostContext,
+       sortInput,
+       hasDuplicate,
+
+       // Local document vars
+       setDocument,
+       document,
+       docElem,
+       documentIsHTML,
+       rbuggyQSA,
+       rbuggyMatches,
+       matches,
+       contains,
+
+       // Instance-specific data
+       expando = "sizzle" + 1 * new Date(),
+       preferredDoc = window.document,
+       dirruns = 0,
+       done = 0,
+       classCache = createCache(),
+       tokenCache = createCache(),
+       compilerCache = createCache(),
+       nonnativeSelectorCache = createCache(),
+       sortOrder = function( a, b ) {
+               if ( a === b ) {
+                       hasDuplicate = true;
+               }
+               return 0;
+       },
+
+       // Instance methods
+       hasOwn = ({}).hasOwnProperty,
+       arr = [],
+       pop = arr.pop,
+       push_native = arr.push,
+       push = arr.push,
+       slice = arr.slice,
+       // Use a stripped-down indexOf as it's faster than native
+       // https://jsperf.com/thor-indexof-vs-for/5
+       indexOf = function( list, elem ) {
+               var i = 0,
+                       len = list.length;
+               for ( ; i < len; i++ ) {
+                       if ( list[i] === elem ) {
+                               return i;
+                       }
+               }
+               return -1;
+       },
+
+       booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+       // Regular expressions
+
+       // http://www.w3.org/TR/css3-selectors/#whitespace
+       whitespace = "[\\x20\\t\\r\\n\\f]",
+
+       // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+       identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",
+
+       // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+       attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+               // Operator (capture 2)
+               "*([*^$|!~]?=)" + whitespace +
+               // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
+               "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
+               "*\\]",
+
+       pseudos = ":(" + identifier + ")(?:\\((" +
+               // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+               // 1. quoted (capture 3; capture 4 or capture 5)
+               "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+               // 2. simple (capture 6)
+               "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+               // 3. anything else (capture 2)
+               ".*" +
+               ")\\)|)",
+
+       // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+       rwhitespace = new RegExp( whitespace + "+", "g" ),
+       rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+       rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+       rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+       rdescend = new RegExp( whitespace + "|>" ),
+
+       rpseudo = new RegExp( pseudos ),
+       ridentifier = new RegExp( "^" + identifier + "$" ),
+
+       matchExpr = {
+               "ID": new RegExp( "^#(" + identifier + ")" ),
+               "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+               "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+               "ATTR": new RegExp( "^" + attributes ),
+               "PSEUDO": new RegExp( "^" + pseudos ),
+               "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+                       "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+                       "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+               "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+               // For use in libraries implementing .is()
+               // We use this for POS matching in `select`
+               "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+                       whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+       },
+
+       rhtml = /HTML$/i,
+       rinputs = /^(?:input|select|textarea|button)$/i,
+       rheader = /^h\d$/i,
+
+       rnative = /^[^{]+\{\s*\[native \w/,
+
+       // Easily-parseable/retrievable ID or TAG or CLASS selectors
+       rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+       rsibling = /[+~]/,
+
+       // CSS escapes
+       // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+       runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+       funescape = function( _, escaped, escapedWhitespace ) {
+               var high = "0x" + escaped - 0x10000;
+               // NaN means non-codepoint
+               // Support: Firefox<24
+               // Workaround erroneous numeric interpretation of +"0x"
+               return high !== high || escapedWhitespace ?
+                       escaped :
+                       high < 0 ?
+                               // BMP codepoint
+                               String.fromCharCode( high + 0x10000 ) :
+                               // Supplemental Plane codepoint (surrogate pair)
+                               String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+       },
+
+       // CSS string/identifier serialization
+       // https://drafts.csswg.org/cssom/#common-serializing-idioms
+       rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
+       fcssescape = function( ch, asCodePoint ) {
+               if ( asCodePoint ) {
+
+                       // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
+                       if ( ch === "\0" ) {
+                               return "\uFFFD";
+                       }
+
+                       // Control characters and (dependent upon position) numbers get escaped as code points
+                       return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
+               }
+
+               // Other potentially-special ASCII characters get backslash-escaped
+               return "\\" + ch;
+       },
+
+       // Used for iframes
+       // See setDocument()
+       // Removing the function wrapper causes a "Permission Denied"
+       // error in IE
+       unloadHandler = function() {
+               setDocument();
+       },
+
+       inDisabledFieldset = addCombinator(
+               function( elem ) {
+                       return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset";
+               },
+               { dir: "parentNode", next: "legend" }
+       );
+
+// Optimize for push.apply( _, NodeList )
+try {
+       push.apply(
+               (arr = slice.call( preferredDoc.childNodes )),
+               preferredDoc.childNodes
+       );
+       // Support: Android<4.0
+       // Detect silently failing push.apply
+       arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+       push = { apply: arr.length ?
+
+               // Leverage slice if possible
+               function( target, els ) {
+                       push_native.apply( target, slice.call(els) );
+               } :
+
+               // Support: IE<9
+               // Otherwise append directly
+               function( target, els ) {
+                       var j = target.length,
+                               i = 0;
+                       // Can't trust NodeList.length
+                       while ( (target[j++] = els[i++]) ) {}
+                       target.length = j - 1;
+               }
+       };
+}
+
+function Sizzle( selector, context, results, seed ) {
+       var m, i, elem, nid, match, groups, newSelector,
+               newContext = context && context.ownerDocument,
+
+               // nodeType defaults to 9, since context defaults to document
+               nodeType = context ? context.nodeType : 9;
+
+       results = results || [];
+
+       // Return early from calls with invalid selector or context
+       if ( typeof selector !== "string" || !selector ||
+               nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+               return results;
+       }
+
+       // Try to shortcut find operations (as opposed to filters) in HTML documents
+       if ( !seed ) {
+
+               if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+                       setDocument( context );
+               }
+               context = context || document;
+
+               if ( documentIsHTML ) {
+
+                       // If the selector is sufficiently simple, try using a "get*By*" DOM method
+                       // (excepting DocumentFragment context, where the methods don't exist)
+                       if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
+
+                               // ID selector
+                               if ( (m = match[1]) ) {
+
+                                       // Document context
+                                       if ( nodeType === 9 ) {
+                                               if ( (elem = context.getElementById( m )) ) {
+
+                                                       // Support: IE, Opera, Webkit
+                                                       // TODO: identify versions
+                                                       // getElementById can match elements by name instead of ID
+                                                       if ( elem.id === m ) {
+                                                               results.push( elem );
+                                                               return results;
+                                                       }
+                                               } else {
+                                                       return results;
+                                               }
+
+                                       // Element context
+                                       } else {
+
+                                               // Support: IE, Opera, Webkit
+                                               // TODO: identify versions
+                                               // getElementById can match elements by name instead of ID
+                                               if ( newContext && (elem = newContext.getElementById( m )) &&
+                                                       contains( context, elem ) &&
+                                                       elem.id === m ) {
+
+                                                       results.push( elem );
+                                                       return results;
+                                               }
+                                       }
+
+                               // Type selector
+                               } else if ( match[2] ) {
+                                       push.apply( results, context.getElementsByTagName( selector ) );
+                                       return results;
+
+                               // Class selector
+                               } else if ( (m = match[3]) && support.getElementsByClassName &&
+                                       context.getElementsByClassName ) {
+
+                                       push.apply( results, context.getElementsByClassName( m ) );
+                                       return results;
+                               }
+                       }
+
+                       // Take advantage of querySelectorAll
+                       if ( support.qsa &&
+                               !nonnativeSelectorCache[ selector + " " ] &&
+                               (!rbuggyQSA || !rbuggyQSA.test( selector )) &&
+
+                               // Support: IE 8 only
+                               // Exclude object elements
+                               (nodeType !== 1 || context.nodeName.toLowerCase() !== "object") ) {
+
+                               newSelector = selector;
+                               newContext = context;
+
+                               // qSA considers elements outside a scoping root when evaluating child or
+                               // descendant combinators, which is not what we want.
+                               // In such cases, we work around the behavior by prefixing every selector in the
+                               // list with an ID selector referencing the scope context.
+                               // Thanks to Andrew Dupont for this technique.
+                               if ( nodeType === 1 && rdescend.test( selector ) ) {
+
+                                       // Capture the context ID, setting it first if necessary
+                                       if ( (nid = context.getAttribute( "id" )) ) {
+                                               nid = nid.replace( rcssescape, fcssescape );
+                                       } else {
+                                               context.setAttribute( "id", (nid = expando) );
+                                       }
+
+                                       // Prefix every selector in the list
+                                       groups = tokenize( selector );
+                                       i = groups.length;
+                                       while ( i-- ) {
+                                               groups[i] = "#" + nid + " " + toSelector( groups[i] );
+                                       }
+                                       newSelector = groups.join( "," );
+
+                                       // Expand context for sibling selectors
+                                       newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+                                               context;
+                               }
+
+                               try {
+                                       push.apply( results,
+                                               newContext.querySelectorAll( newSelector )
+                                       );
+                                       return results;
+                               } catch ( qsaError ) {
+                                       nonnativeSelectorCache( selector, true );
+                               } finally {
+                                       if ( nid === expando ) {
+                                               context.removeAttribute( "id" );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       // All others
+       return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ *     property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ *     deleting the oldest entry
+ */
+function createCache() {
+       var keys = [];
+
+       function cache( key, value ) {
+               // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+               if ( keys.push( key + " " ) > Expr.cacheLength ) {
+                       // Only keep the most recent entries
+                       delete cache[ keys.shift() ];
+               }
+               return (cache[ key + " " ] = value);
+       }
+       return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+       fn[ expando ] = true;
+       return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created element and returns a boolean result
+ */
+function assert( fn ) {
+       var el = document.createElement("fieldset");
+
+       try {
+               return !!fn( el );
+       } catch (e) {
+               return false;
+       } finally {
+               // Remove from its parent by default
+               if ( el.parentNode ) {
+                       el.parentNode.removeChild( el );
+               }
+               // release memory in IE
+               el = null;
+       }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+       var arr = attrs.split("|"),
+               i = arr.length;
+
+       while ( i-- ) {
+               Expr.attrHandle[ arr[i] ] = handler;
+       }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+       var cur = b && a,
+               diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+                       a.sourceIndex - b.sourceIndex;
+
+       // Use IE sourceIndex if available on both nodes
+       if ( diff ) {
+               return diff;
+       }
+
+       // Check if b follows a
+       if ( cur ) {
+               while ( (cur = cur.nextSibling) ) {
+                       if ( cur === b ) {
+                               return -1;
+                       }
+               }
+       }
+
+       return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+       return function( elem ) {
+               var name = elem.nodeName.toLowerCase();
+               return name === "input" && elem.type === type;
+       };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+       return function( elem ) {
+               var name = elem.nodeName.toLowerCase();
+               return (name === "input" || name === "button") && elem.type === type;
+       };
+}
+
+/**
+ * Returns a function to use in pseudos for :enabled/:disabled
+ * @param {Boolean} disabled true for :disabled; false for :enabled
+ */
+function createDisabledPseudo( disabled ) {
+
+       // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
+       return function( elem ) {
+
+               // Only certain elements can match :enabled or :disabled
+               // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+               // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+               if ( "form" in elem ) {
+
+                       // Check for inherited disabledness on relevant non-disabled elements:
+                       // * listed form-associated elements in a disabled fieldset
+                       //   https://html.spec.whatwg.org/multipage/forms.html#category-listed
+                       //   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+                       // * option elements in a disabled optgroup
+                       //   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+                       // All such elements have a "form" property.
+                       if ( elem.parentNode && elem.disabled === false ) {
+
+                               // Option elements defer to a parent optgroup if present
+                               if ( "label" in elem ) {
+                                       if ( "label" in elem.parentNode ) {
+                                               return elem.parentNode.disabled === disabled;
+                                       } else {
+                                               return elem.disabled === disabled;
+                                       }
+                               }
+
+                               // Support: IE 6 - 11
+                               // Use the isDisabled shortcut property to check for disabled fieldset ancestors
+                               return elem.isDisabled === disabled ||
+
+                                       // Where there is no isDisabled, check manually
+                                       /* jshint -W018 */
+                                       elem.isDisabled !== !disabled &&
+                                               inDisabledFieldset( elem ) === disabled;
+                       }
+
+                       return elem.disabled === disabled;
+
+               // Try to winnow out elements that can't be disabled before trusting the disabled property.
+               // Some victims get caught in our net (label, legend, menu, track), but it shouldn't
+               // even exist on them, let alone have a boolean value.
+               } else if ( "label" in elem ) {
+                       return elem.disabled === disabled;
+               }
+
+               // Remaining elements are neither :enabled nor :disabled
+               return false;
+       };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+       return markFunction(function( argument ) {
+               argument = +argument;
+               return markFunction(function( seed, matches ) {
+                       var j,
+                               matchIndexes = fn( [], seed.length, argument ),
+                               i = matchIndexes.length;
+
+                       // Match elements found at the specified indexes
+                       while ( i-- ) {
+                               if ( seed[ (j = matchIndexes[i]) ] ) {
+                                       seed[j] = !(matches[j] = seed[j]);
+                               }
+                       }
+               });
+       });
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+       return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+       var namespace = elem.namespaceURI,
+               docElem = (elem.ownerDocument || elem).documentElement;
+
+       // Support: IE <=8
+       // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
+       // https://bugs.jquery.com/ticket/4833
+       return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" );
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+       var hasCompare, subWindow,
+               doc = node ? node.ownerDocument || node : preferredDoc;
+
+       // Return early if doc is invalid or already selected
+       if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+               return document;
+       }
+
+       // Update global variables
+       document = doc;
+       docElem = document.documentElement;
+       documentIsHTML = !isXML( document );
+
+       // Support: IE 9-11, Edge
+       // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+       if ( preferredDoc !== document &&
+               (subWindow = document.defaultView) && subWindow.top !== subWindow ) {
+
+               // Support: IE 11, Edge
+               if ( subWindow.addEventListener ) {
+                       subWindow.addEventListener( "unload", unloadHandler, false );
+
+               // Support: IE 9 - 10 only
+               } else if ( subWindow.attachEvent ) {
+                       subWindow.attachEvent( "onunload", unloadHandler );
+               }
+       }
+
+       /* Attributes
+       ---------------------------------------------------------------------- */
+
+       // Support: IE<8
+       // Verify that getAttribute really returns attributes and not properties
+       // (excepting IE8 booleans)
+       support.attributes = assert(function( el ) {
+               el.className = "i";
+               return !el.getAttribute("className");
+       });
+
+       /* getElement(s)By*
+       ---------------------------------------------------------------------- */
+
+       // Check if getElementsByTagName("*") returns only elements
+       support.getElementsByTagName = assert(function( el ) {
+               el.appendChild( document.createComment("") );
+               return !el.getElementsByTagName("*").length;
+       });
+
+       // Support: IE<9
+       support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+       // Support: IE<10
+       // Check if getElementById returns elements by name
+       // The broken getElementById methods don't pick up programmatically-set names,
+       // so use a roundabout getElementsByName test
+       support.getById = assert(function( el ) {
+               docElem.appendChild( el ).id = expando;
+               return !document.getElementsByName || !document.getElementsByName( expando ).length;
+       });
+
+       // ID filter and find
+       if ( support.getById ) {
+               Expr.filter["ID"] = function( id ) {
+                       var attrId = id.replace( runescape, funescape );
+                       return function( elem ) {
+                               return elem.getAttribute("id") === attrId;
+                       };
+               };
+               Expr.find["ID"] = function( id, context ) {
+                       if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+                               var elem = context.getElementById( id );
+                               return elem ? [ elem ] : [];
+                       }
+               };
+       } else {
+               Expr.filter["ID"] =  function( id ) {
+                       var attrId = id.replace( runescape, funescape );
+                       return function( elem ) {
+                               var node = typeof elem.getAttributeNode !== "undefined" &&
+                                       elem.getAttributeNode("id");
+                               return node && node.value === attrId;
+                       };
+               };
+
+               // Support: IE 6 - 7 only
+               // getElementById is not reliable as a find shortcut
+               Expr.find["ID"] = function( id, context ) {
+                       if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+                               var node, i, elems,
+                                       elem = context.getElementById( id );
+
+                               if ( elem ) {
+
+                                       // Verify the id attribute
+                                       node = elem.getAttributeNode("id");
+                                       if ( node && node.value === id ) {
+                                               return [ elem ];
+                                       }
+
+                                       // Fall back on getElementsByName
+                                       elems = context.getElementsByName( id );
+                                       i = 0;
+                                       while ( (elem = elems[i++]) ) {
+                                               node = elem.getAttributeNode("id");
+                                               if ( node && node.value === id ) {
+                                                       return [ elem ];
+                                               }
+                                       }
+                               }
+
+                               return [];
+                       }
+               };
+       }
+
+       // Tag
+       Expr.find["TAG"] = support.getElementsByTagName ?
+               function( tag, context ) {
+                       if ( typeof context.getElementsByTagName !== "undefined" ) {
+                               return context.getElementsByTagName( tag );
+
+                       // DocumentFragment nodes don't have gEBTN
+                       } else if ( support.qsa ) {
+                               return context.querySelectorAll( tag );
+                       }
+               } :
+
+               function( tag, context ) {
+                       var elem,
+                               tmp = [],
+                               i = 0,
+                               // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+                               results = context.getElementsByTagName( tag );
+
+                       // Filter out possible comments
+                       if ( tag === "*" ) {
+                               while ( (elem = results[i++]) ) {
+                                       if ( elem.nodeType === 1 ) {
+                                               tmp.push( elem );
+                                       }
+                               }
+
+                               return tmp;
+                       }
+                       return results;
+               };
+
+       // Class
+       Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+               if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+                       return context.getElementsByClassName( className );
+               }
+       };
+
+       /* QSA/matchesSelector
+       ---------------------------------------------------------------------- */
+
+       // QSA and matchesSelector support
+
+       // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+       rbuggyMatches = [];
+
+       // qSa(:focus) reports false when true (Chrome 21)
+       // We allow this because of a bug in IE8/9 that throws an error
+       // whenever `document.activeElement` is accessed on an iframe
+       // So, we allow :focus to pass through QSA all the time to avoid the IE error
+       // See https://bugs.jquery.com/ticket/13378
+       rbuggyQSA = [];
+
+       if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
+               // Build QSA regex
+               // Regex strategy adopted from Diego Perini
+               assert(function( el ) {
+                       // Select is set to empty string on purpose
+                       // This is to test IE's treatment of not explicitly
+                       // setting a boolean content attribute,
+                       // since its presence should be enough
+                       // https://bugs.jquery.com/ticket/12359
+                       docElem.appendChild( el ).innerHTML = "<a id='" + expando + "'></a>" +
+                               "<select id='" + expando + "-\r\\' msallowcapture=''>" +
+                               "<option selected=''></option></select>";
+
+                       // Support: IE8, Opera 11-12.16
+                       // Nothing should be selected when empty strings follow ^= or $= or *=
+                       // The test attribute must be unknown in Opera but "safe" for WinRT
+                       // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+                       if ( el.querySelectorAll("[msallowcapture^='']").length ) {
+                               rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+                       }
+
+                       // Support: IE8
+                       // Boolean attributes and "value" are not treated correctly
+                       if ( !el.querySelectorAll("[selected]").length ) {
+                               rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+                       }
+
+                       // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+                       if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+                               rbuggyQSA.push("~=");
+                       }
+
+                       // Webkit/Opera - :checked should return selected option elements
+                       // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+                       // IE8 throws error here and will not see later tests
+                       if ( !el.querySelectorAll(":checked").length ) {
+                               rbuggyQSA.push(":checked");
+                       }
+
+                       // Support: Safari 8+, iOS 8+
+                       // https://bugs.webkit.org/show_bug.cgi?id=136851
+                       // In-page `selector#id sibling-combinator selector` fails
+                       if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
+                               rbuggyQSA.push(".#.+[+~]");
+                       }
+               });
+
+               assert(function( el ) {
+                       el.innerHTML = "<a href='' disabled='disabled'></a>" +
+                               "<select disabled='disabled'><option/></select>";
+
+                       // Support: Windows 8 Native Apps
+                       // The type and name attributes are restricted during .innerHTML assignment
+                       var input = document.createElement("input");
+                       input.setAttribute( "type", "hidden" );
+                       el.appendChild( input ).setAttribute( "name", "D" );
+
+                       // Support: IE8
+                       // Enforce case-sensitivity of name attribute
+                       if ( el.querySelectorAll("[name=d]").length ) {
+                               rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+                       }
+
+                       // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+                       // IE8 throws error here and will not see later tests
+                       if ( el.querySelectorAll(":enabled").length !== 2 ) {
+                               rbuggyQSA.push( ":enabled", ":disabled" );
+                       }
+
+                       // Support: IE9-11+
+                       // IE's :disabled selector does not pick up the children of disabled fieldsets
+                       docElem.appendChild( el ).disabled = true;
+                       if ( el.querySelectorAll(":disabled").length !== 2 ) {
+                               rbuggyQSA.push( ":enabled", ":disabled" );
+                       }
+
+                       // Opera 10-11 does not throw on post-comma invalid pseudos
+                       el.querySelectorAll("*,:x");
+                       rbuggyQSA.push(",.*:");
+               });
+       }
+
+       if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+               docElem.webkitMatchesSelector ||
+               docElem.mozMatchesSelector ||
+               docElem.oMatchesSelector ||
+               docElem.msMatchesSelector) )) ) {
+
+               assert(function( el ) {
+                       // Check to see if it's possible to do matchesSelector
+                       // on a disconnected node (IE 9)
+                       support.disconnectedMatch = matches.call( el, "*" );
+
+                       // This should fail with an exception
+                       // Gecko does not error, returns false instead
+                       matches.call( el, "[s!='']:x" );
+                       rbuggyMatches.push( "!=", pseudos );
+               });
+       }
+
+       rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+       rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+       /* Contains
+       ---------------------------------------------------------------------- */
+       hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+       // Element contains another
+       // Purposefully self-exclusive
+       // As in, an element does not contain itself
+       contains = hasCompare || rnative.test( docElem.contains ) ?
+               function( a, b ) {
+                       var adown = a.nodeType === 9 ? a.documentElement : a,
+                               bup = b && b.parentNode;
+                       return a === bup || !!( bup && bup.nodeType === 1 && (
+                               adown.contains ?
+                                       adown.contains( bup ) :
+                                       a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+                       ));
+               } :
+               function( a, b ) {
+                       if ( b ) {
+                               while ( (b = b.parentNode) ) {
+                                       if ( b === a ) {
+                                               return true;
+                                       }
+                               }
+                       }
+                       return false;
+               };
+
+       /* Sorting
+       ---------------------------------------------------------------------- */
+
+       // Document order sorting
+       sortOrder = hasCompare ?
+       function( a, b ) {
+
+               // Flag for duplicate removal
+               if ( a === b ) {
+                       hasDuplicate = true;
+                       return 0;
+               }
+
+               // Sort on method existence if only one input has compareDocumentPosition
+               var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+               if ( compare ) {
+                       return compare;
+               }
+
+               // Calculate position if both inputs belong to the same document
+               compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+                       a.compareDocumentPosition( b ) :
+
+                       // Otherwise we know they are disconnected
+                       1;
+
+               // Disconnected nodes
+               if ( compare & 1 ||
+                       (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+                       // Choose the first element that is related to our preferred document
+                       if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+                               return -1;
+                       }
+                       if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+                               return 1;
+                       }
+
+                       // Maintain original order
+                       return sortInput ?
+                               ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+                               0;
+               }
+
+               return compare & 4 ? -1 : 1;
+       } :
+       function( a, b ) {
+               // Exit early if the nodes are identical
+               if ( a === b ) {
+                       hasDuplicate = true;
+                       return 0;
+               }
+
+               var cur,
+                       i = 0,
+                       aup = a.parentNode,
+                       bup = b.parentNode,
+                       ap = [ a ],
+                       bp = [ b ];
+
+               // Parentless nodes are either documents or disconnected
+               if ( !aup || !bup ) {
+                       return a === document ? -1 :
+                               b === document ? 1 :
+                               aup ? -1 :
+                               bup ? 1 :
+                               sortInput ?
+                               ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+                               0;
+
+               // If the nodes are siblings, we can do a quick check
+               } else if ( aup === bup ) {
+                       return siblingCheck( a, b );
+               }
+
+               // Otherwise we need full lists of their ancestors for comparison
+               cur = a;
+               while ( (cur = cur.parentNode) ) {
+                       ap.unshift( cur );
+               }
+               cur = b;
+               while ( (cur = cur.parentNode) ) {
+                       bp.unshift( cur );
+               }
+
+               // Walk down the tree looking for a discrepancy
+               while ( ap[i] === bp[i] ) {
+                       i++;
+               }
+
+               return i ?
+                       // Do a sibling check if the nodes have a common ancestor
+                       siblingCheck( ap[i], bp[i] ) :
+
+                       // Otherwise nodes in our document sort first
+                       ap[i] === preferredDoc ? -1 :
+                       bp[i] === preferredDoc ? 1 :
+                       0;
+       };
+
+       return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+       return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+       // Set document vars if needed
+       if ( ( elem.ownerDocument || elem ) !== document ) {
+               setDocument( elem );
+       }
+
+       if ( support.matchesSelector && documentIsHTML &&
+               !nonnativeSelectorCache[ expr + " " ] &&
+               ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+               ( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {
+
+               try {
+                       var ret = matches.call( elem, expr );
+
+                       // IE 9's matchesSelector returns false on disconnected nodes
+                       if ( ret || support.disconnectedMatch ||
+                                       // As well, disconnected nodes are said to be in a document
+                                       // fragment in IE 9
+                                       elem.document && elem.document.nodeType !== 11 ) {
+                               return ret;
+                       }
+               } catch (e) {
+                       nonnativeSelectorCache( expr, true );
+               }
+       }
+
+       return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+       // Set document vars if needed
+       if ( ( context.ownerDocument || context ) !== document ) {
+               setDocument( context );
+       }
+       return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+       // Set document vars if needed
+       if ( ( elem.ownerDocument || elem ) !== document ) {
+               setDocument( elem );
+       }
+
+       var fn = Expr.attrHandle[ name.toLowerCase() ],
+               // Don't get fooled by Object.prototype properties (jQuery #13807)
+               val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+                       fn( elem, name, !documentIsHTML ) :
+                       undefined;
+
+       return val !== undefined ?
+               val :
+               support.attributes || !documentIsHTML ?
+                       elem.getAttribute( name ) :
+                       (val = elem.getAttributeNode(name)) && val.specified ?
+                               val.value :
+                               null;
+};
+
+Sizzle.escape = function( sel ) {
+       return (sel + "").replace( rcssescape, fcssescape );
+};
+
+Sizzle.error = function( msg ) {
+       throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+       var elem,
+               duplicates = [],
+               j = 0,
+               i = 0;
+
+       // Unless we *know* we can detect duplicates, assume their presence
+       hasDuplicate = !support.detectDuplicates;
+       sortInput = !support.sortStable && results.slice( 0 );
+       results.sort( sortOrder );
+
+       if ( hasDuplicate ) {
+               while ( (elem = results[i++]) ) {
+                       if ( elem === results[ i ] ) {
+                               j = duplicates.push( i );
+                       }
+               }
+               while ( j-- ) {
+                       results.splice( duplicates[ j ], 1 );
+               }
+       }
+
+       // Clear input after sorting to release objects
+       // See https://github.com/jquery/sizzle/pull/225
+       sortInput = null;
+
+       return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+       var node,
+               ret = "",
+               i = 0,
+               nodeType = elem.nodeType;
+
+       if ( !nodeType ) {
+               // If no nodeType, this is expected to be an array
+               while ( (node = elem[i++]) ) {
+                       // Do not traverse comment nodes
+                       ret += getText( node );
+               }
+       } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+               // Use textContent for elements
+               // innerText usage removed for consistency of new lines (jQuery #11153)
+               if ( typeof elem.textContent === "string" ) {
+                       return elem.textContent;
+               } else {
+                       // Traverse its children
+                       for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+                               ret += getText( elem );
+                       }
+               }
+       } else if ( nodeType === 3 || nodeType === 4 ) {
+               return elem.nodeValue;
+       }
+       // Do not include comment or processing instruction nodes
+
+       return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+       // Can be adjusted by the user
+       cacheLength: 50,
+
+       createPseudo: markFunction,
+
+       match: matchExpr,
+
+       attrHandle: {},
+
+       find: {},
+
+       relative: {
+               ">": { dir: "parentNode", first: true },
+               " ": { dir: "parentNode" },
+               "+": { dir: "previousSibling", first: true },
+               "~": { dir: "previousSibling" }
+       },
+
+       preFilter: {
+               "ATTR": function( match ) {
+                       match[1] = match[1].replace( runescape, funescape );
+
+                       // Move the given value to match[3] whether quoted or unquoted
+                       match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+
+                       if ( match[2] === "~=" ) {
+                               match[3] = " " + match[3] + " ";
+                       }
+
+                       return match.slice( 0, 4 );
+               },
+
+               "CHILD": function( match ) {
+                       /* matches from matchExpr["CHILD"]
+                               1 type (only|nth|...)
+                               2 what (child|of-type)
+                               3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+                               4 xn-component of xn+y argument ([+-]?\d*n|)
+                               5 sign of xn-component
+                               6 x of xn-component
+                               7 sign of y-component
+                               8 y of y-component
+                       */
+                       match[1] = match[1].toLowerCase();
+
+                       if ( match[1].slice( 0, 3 ) === "nth" ) {
+                               // nth-* requires argument
+                               if ( !match[3] ) {
+                                       Sizzle.error( match[0] );
+                               }
+
+                               // numeric x and y parameters for Expr.filter.CHILD
+                               // remember that false/true cast respectively to 0/1
+                               match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+                               match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+                       // other types prohibit arguments
+                       } else if ( match[3] ) {
+                               Sizzle.error( match[0] );
+                       }
+
+                       return match;
+               },
+
+               "PSEUDO": function( match ) {
+                       var excess,
+                               unquoted = !match[6] && match[2];
+
+                       if ( matchExpr["CHILD"].test( match[0] ) ) {
+                               return null;
+                       }
+
+                       // Accept quoted arguments as-is
+                       if ( match[3] ) {
+                               match[2] = match[4] || match[5] || "";
+
+                       // Strip excess characters from unquoted arguments
+                       } else if ( unquoted && rpseudo.test( unquoted ) &&
+                               // Get excess from tokenize (recursively)
+                               (excess = tokenize( unquoted, true )) &&
+                               // advance to the next closing parenthesis
+                               (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+                               // excess is a negative index
+                               match[0] = match[0].slice( 0, excess );
+                               match[2] = unquoted.slice( 0, excess );
+                       }
+
+                       // Return only captures needed by the pseudo filter method (type and argument)
+                       return match.slice( 0, 3 );
+               }
+       },
+
+       filter: {
+
+               "TAG": function( nodeNameSelector ) {
+                       var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+                       return nodeNameSelector === "*" ?
+                               function() { return true; } :
+                               function( elem ) {
+                                       return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+                               };
+               },
+
+               "CLASS": function( className ) {
+                       var pattern = classCache[ className + " " ];
+
+                       return pattern ||
+                               (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+                               classCache( className, function( elem ) {
+                                       return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
+                               });
+               },
+
+               "ATTR": function( name, operator, check ) {
+                       return function( elem ) {
+                               var result = Sizzle.attr( elem, name );
+
+                               if ( result == null ) {
+                                       return operator === "!=";
+                               }
+                               if ( !operator ) {
+                                       return true;
+                               }
+
+                               result += "";
+
+                               return operator === "=" ? result === check :
+                                       operator === "!=" ? result !== check :
+                                       operator === "^=" ? check && result.indexOf( check ) === 0 :
+                                       operator === "*=" ? check && result.indexOf( check ) > -1 :
+                                       operator === "$=" ? check && result.slice( -check.length ) === check :
+                                       operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+                                       operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+                                       false;
+                       };
+               },
+
+               "CHILD": function( type, what, argument, first, last ) {
+                       var simple = type.slice( 0, 3 ) !== "nth",
+                               forward = type.slice( -4 ) !== "last",
+                               ofType = what === "of-type";
+
+                       return first === 1 && last === 0 ?
+
+                               // Shortcut for :nth-*(n)
+                               function( elem ) {
+                                       return !!elem.parentNode;
+                               } :
+
+                               function( elem, context, xml ) {
+                                       var cache, uniqueCache, outerCache, node, nodeIndex, start,
+                                               dir = simple !== forward ? "nextSibling" : "previousSibling",
+                                               parent = elem.parentNode,
+                                               name = ofType && elem.nodeName.toLowerCase(),
+                                               useCache = !xml && !ofType,
+                                               diff = false;
+
+                                       if ( parent ) {
+
+                                               // :(first|last|only)-(child|of-type)
+                                               if ( simple ) {
+                                                       while ( dir ) {
+                                                               node = elem;
+                                                               while ( (node = node[ dir ]) ) {
+                                                                       if ( ofType ?
+                                                                               node.nodeName.toLowerCase() === name :
+                                                                               node.nodeType === 1 ) {
+
+                                                                               return false;
+                                                                       }
+                                                               }
+                                                               // Reverse direction for :only-* (if we haven't yet done so)
+                                                               start = dir = type === "only" && !start && "nextSibling";
+                                                       }
+                                                       return true;
+                                               }
+
+                                               start = [ forward ? parent.firstChild : parent.lastChild ];
+
+                                               // non-xml :nth-child(...) stores cache data on `parent`
+                                               if ( forward && useCache ) {
+
+                                                       // Seek `elem` from a previously-cached index
+
+                                                       // ...in a gzip-friendly way
+                                                       node = parent;
+                                                       outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                                       // Support: IE <9 only
+                                                       // Defend against cloned attroperties (jQuery gh-1709)
+                                                       uniqueCache = outerCache[ node.uniqueID ] ||
+                                                               (outerCache[ node.uniqueID ] = {});
+
+                                                       cache = uniqueCache[ type ] || [];
+                                                       nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+                                                       diff = nodeIndex && cache[ 2 ];
+                                                       node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+                                                       while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+                                                               // Fallback to seeking `elem` from the start
+                                                               (diff = nodeIndex = 0) || start.pop()) ) {
+
+                                                               // When found, cache indexes on `parent` and break
+                                                               if ( node.nodeType === 1 && ++diff && node === elem ) {
+                                                                       uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+                                                                       break;
+                                                               }
+                                                       }
+
+                                               } else {
+                                                       // Use previously-cached element index if available
+                                                       if ( useCache ) {
+                                                               // ...in a gzip-friendly way
+                                                               node = elem;
+                                                               outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                                               // Support: IE <9 only
+                                                               // Defend against cloned attroperties (jQuery gh-1709)
+                                                               uniqueCache = outerCache[ node.uniqueID ] ||
+                                                                       (outerCache[ node.uniqueID ] = {});
+
+                                                               cache = uniqueCache[ type ] || [];
+                                                               nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+                                                               diff = nodeIndex;
+                                                       }
+
+                                                       // xml :nth-child(...)
+                                                       // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+                                                       if ( diff === false ) {
+                                                               // Use the same loop as above to seek `elem` from the start
+                                                               while ( (node = ++nodeIndex && node && node[ dir ] ||
+                                                                       (diff = nodeIndex = 0) || start.pop()) ) {
+
+                                                                       if ( ( ofType ?
+                                                                               node.nodeName.toLowerCase() === name :
+                                                                               node.nodeType === 1 ) &&
+                                                                               ++diff ) {
+
+                                                                               // Cache the index of each encountered element
+                                                                               if ( useCache ) {
+                                                                                       outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                                                                       // Support: IE <9 only
+                                                                                       // Defend against cloned attroperties (jQuery gh-1709)
+                                                                                       uniqueCache = outerCache[ node.uniqueID ] ||
+                                                                                               (outerCache[ node.uniqueID ] = {});
+
+                                                                                       uniqueCache[ type ] = [ dirruns, diff ];
+                                                                               }
+
+                                                                               if ( node === elem ) {
+                                                                                       break;
+                                                                               }
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+
+                                               // Incorporate the offset, then check against cycle size
+                                               diff -= last;
+                                               return diff === first || ( diff % first === 0 && diff / first >= 0 );
+                                       }
+                               };
+               },
+
+               "PSEUDO": function( pseudo, argument ) {
+                       // pseudo-class names are case-insensitive
+                       // http://www.w3.org/TR/selectors/#pseudo-classes
+                       // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+                       // Remember that setFilters inherits from pseudos
+                       var args,
+                               fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+                                       Sizzle.error( "unsupported pseudo: " + pseudo );
+
+                       // The user may use createPseudo to indicate that
+                       // arguments are needed to create the filter function
+                       // just as Sizzle does
+                       if ( fn[ expando ] ) {
+                               return fn( argument );
+                       }
+
+                       // But maintain support for old signatures
+                       if ( fn.length > 1 ) {
+                               args = [ pseudo, pseudo, "", argument ];
+                               return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+                                       markFunction(function( seed, matches ) {
+                                               var idx,
+                                                       matched = fn( seed, argument ),
+                                                       i = matched.length;
+                                               while ( i-- ) {
+                                                       idx = indexOf( seed, matched[i] );
+                                                       seed[ idx ] = !( matches[ idx ] = matched[i] );
+                                               }
+                                       }) :
+                                       function( elem ) {
+                                               return fn( elem, 0, args );
+                                       };
+                       }
+
+                       return fn;
+               }
+       },
+
+       pseudos: {
+               // Potentially complex pseudos
+               "not": markFunction(function( selector ) {
+                       // Trim the selector passed to compile
+                       // to avoid treating leading and trailing
+                       // spaces as combinators
+                       var input = [],
+                               results = [],
+                               matcher = compile( selector.replace( rtrim, "$1" ) );
+
+                       return matcher[ expando ] ?
+                               markFunction(function( seed, matches, context, xml ) {
+                                       var elem,
+                                               unmatched = matcher( seed, null, xml, [] ),
+                                               i = seed.length;
+
+                                       // Match elements unmatched by `matcher`
+                                       while ( i-- ) {
+                                               if ( (elem = unmatched[i]) ) {
+                                                       seed[i] = !(matches[i] = elem);
+                                               }
+                                       }
+                               }) :
+                               function( elem, context, xml ) {
+                                       input[0] = elem;
+                                       matcher( input, null, xml, results );
+                                       // Don't keep the element (issue #299)
+                                       input[0] = null;
+                                       return !results.pop();
+                               };
+               }),
+
+               "has": markFunction(function( selector ) {
+                       return function( elem ) {
+                               return Sizzle( selector, elem ).length > 0;
+                       };
+               }),
+
+               "contains": markFunction(function( text ) {
+                       text = text.replace( runescape, funescape );
+                       return function( elem ) {
+                               return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;
+                       };
+               }),
+
+               // "Whether an element is represented by a :lang() selector
+               // is based solely on the element's language value
+               // being equal to the identifier C,
+               // or beginning with the identifier C immediately followed by "-".
+               // The matching of C against the element's language value is performed case-insensitively.
+               // The identifier C does not have to be a valid language name."
+               // http://www.w3.org/TR/selectors/#lang-pseudo
+               "lang": markFunction( function( lang ) {
+                       // lang value must be a valid identifier
+                       if ( !ridentifier.test(lang || "") ) {
+                               Sizzle.error( "unsupported lang: " + lang );
+                       }
+                       lang = lang.replace( runescape, funescape ).toLowerCase();
+                       return function( elem ) {
+                               var elemLang;
+                               do {
+                                       if ( (elemLang = documentIsHTML ?
+                                               elem.lang :
+                                               elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+                                               elemLang = elemLang.toLowerCase();
+                                               return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+                                       }
+                               } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+                               return false;
+                       };
+               }),
+
+               // Miscellaneous
+               "target": function( elem ) {
+                       var hash = window.location && window.location.hash;
+                       return hash && hash.slice( 1 ) === elem.id;
+               },
+
+               "root": function( elem ) {
+                       return elem === docElem;
+               },
+
+               "focus": function( elem ) {
+                       return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+               },
+
+               // Boolean properties
+               "enabled": createDisabledPseudo( false ),
+               "disabled": createDisabledPseudo( true ),
+
+               "checked": function( elem ) {
+                       // In CSS3, :checked should return both checked and selected elements
+                       // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+                       var nodeName = elem.nodeName.toLowerCase();
+                       return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+               },
+
+               "selected": function( elem ) {
+                       // Accessing this property makes selected-by-default
+                       // options in Safari work properly
+                       if ( elem.parentNode ) {
+                               elem.parentNode.selectedIndex;
+                       }
+
+                       return elem.selected === true;
+               },
+
+               // Contents
+               "empty": function( elem ) {
+                       // http://www.w3.org/TR/selectors/#empty-pseudo
+                       // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+                       //   but not by others (comment: 8; processing instruction: 7; etc.)
+                       // nodeType < 6 works because attributes (2) do not appear as children
+                       for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+                               if ( elem.nodeType < 6 ) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               },
+
+               "parent": function( elem ) {
+                       return !Expr.pseudos["empty"]( elem );
+               },
+
+               // Element/input types
+               "header": function( elem ) {
+                       return rheader.test( elem.nodeName );
+               },
+
+               "input": function( elem ) {
+                       return rinputs.test( elem.nodeName );
+               },
+
+               "button": function( elem ) {
+                       var name = elem.nodeName.toLowerCase();
+                       return name === "input" && elem.type === "button" || name === "button";
+               },
+
+               "text": function( elem ) {
+                       var attr;
+                       return elem.nodeName.toLowerCase() === "input" &&
+                               elem.type === "text" &&
+
+                               // Support: IE<8
+                               // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+                               ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+               },
+
+               // Position-in-collection
+               "first": createPositionalPseudo(function() {
+                       return [ 0 ];
+               }),
+
+               "last": createPositionalPseudo(function( matchIndexes, length ) {
+                       return [ length - 1 ];
+               }),
+
+               "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+                       return [ argument < 0 ? argument + length : argument ];
+               }),
+
+               "even": createPositionalPseudo(function( matchIndexes, length ) {
+                       var i = 0;
+                       for ( ; i < length; i += 2 ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               }),
+
+               "odd": createPositionalPseudo(function( matchIndexes, length ) {
+                       var i = 1;
+                       for ( ; i < length; i += 2 ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               }),
+
+               "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+                       var i = argument < 0 ?
+                               argument + length :
+                               argument > length ?
+                                       length :
+                                       argument;
+                       for ( ; --i >= 0; ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               }),
+
+               "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+                       var i = argument < 0 ? argument + length : argument;
+                       for ( ; ++i < length; ) {
+                               matchIndexes.push( i );
+                       }
+                       return matchIndexes;
+               })
+       }
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+       Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+       Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+       var matched, match, tokens, type,
+               soFar, groups, preFilters,
+               cached = tokenCache[ selector + " " ];
+
+       if ( cached ) {
+               return parseOnly ? 0 : cached.slice( 0 );
+       }
+
+       soFar = selector;
+       groups = [];
+       preFilters = Expr.preFilter;
+
+       while ( soFar ) {
+
+               // Comma and first run
+               if ( !matched || (match = rcomma.exec( soFar )) ) {
+                       if ( match ) {
+                               // Don't consume trailing commas as valid
+                               soFar = soFar.slice( match[0].length ) || soFar;
+                       }
+                       groups.push( (tokens = []) );
+               }
+
+               matched = false;
+
+               // Combinators
+               if ( (match = rcombinators.exec( soFar )) ) {
+                       matched = match.shift();
+                       tokens.push({
+                               value: matched,
+                               // Cast descendant combinators to space
+                               type: match[0].replace( rtrim, " " )
+                       });
+                       soFar = soFar.slice( matched.length );
+               }
+
+               // Filters
+               for ( type in Expr.filter ) {
+                       if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+                               (match = preFilters[ type ]( match ))) ) {
+                               matched = match.shift();
+                               tokens.push({
+                                       value: matched,
+                                       type: type,
+                                       matches: match
+                               });
+                               soFar = soFar.slice( matched.length );
+                       }
+               }
+
+               if ( !matched ) {
+                       break;
+               }
+       }
+
+       // Return the length of the invalid excess
+       // if we're just parsing
+       // Otherwise, throw an error or return tokens
+       return parseOnly ?
+               soFar.length :
+               soFar ?
+                       Sizzle.error( selector ) :
+                       // Cache the tokens
+                       tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+       var i = 0,
+               len = tokens.length,
+               selector = "";
+       for ( ; i < len; i++ ) {
+               selector += tokens[i].value;
+       }
+       return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+       var dir = combinator.dir,
+               skip = combinator.next,
+               key = skip || dir,
+               checkNonElements = base && key === "parentNode",
+               doneName = done++;
+
+       return combinator.first ?
+               // Check against closest ancestor/preceding element
+               function( elem, context, xml ) {
+                       while ( (elem = elem[ dir ]) ) {
+                               if ( elem.nodeType === 1 || checkNonElements ) {
+                                       return matcher( elem, context, xml );
+                               }
+                       }
+                       return false;
+               } :
+
+               // Check against all ancestor/preceding elements
+               function( elem, context, xml ) {
+                       var oldCache, uniqueCache, outerCache,
+                               newCache = [ dirruns, doneName ];
+
+                       // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+                       if ( xml ) {
+                               while ( (elem = elem[ dir ]) ) {
+                                       if ( elem.nodeType === 1 || checkNonElements ) {
+                                               if ( matcher( elem, context, xml ) ) {
+                                                       return true;
+                                               }
+                                       }
+                               }
+                       } else {
+                               while ( (elem = elem[ dir ]) ) {
+                                       if ( elem.nodeType === 1 || checkNonElements ) {
+                                               outerCache = elem[ expando ] || (elem[ expando ] = {});
+
+                                               // Support: IE <9 only
+                                               // Defend against cloned attroperties (jQuery gh-1709)
+                                               uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
+
+                                               if ( skip && skip === elem.nodeName.toLowerCase() ) {
+                                                       elem = elem[ dir ] || elem;
+                                               } else if ( (oldCache = uniqueCache[ key ]) &&
+                                                       oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+                                                       // Assign to newCache so results back-propagate to previous elements
+                                                       return (newCache[ 2 ] = oldCache[ 2 ]);
+                                               } else {
+                                                       // Reuse newcache so results back-propagate to previous elements
+                                                       uniqueCache[ key ] = newCache;
+
+                                                       // A match means we're done; a fail means we have to keep checking
+                                                       if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+                                                               return true;
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return false;
+               };
+}
+
+function elementMatcher( matchers ) {
+       return matchers.length > 1 ?
+               function( elem, context, xml ) {
+                       var i = matchers.length;
+                       while ( i-- ) {
+                               if ( !matchers[i]( elem, context, xml ) ) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               } :
+               matchers[0];
+}
+
+function multipleContexts( selector, contexts, results ) {
+       var i = 0,
+               len = contexts.length;
+       for ( ; i < len; i++ ) {
+               Sizzle( selector, contexts[i], results );
+       }
+       return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+       var elem,
+               newUnmatched = [],
+               i = 0,
+               len = unmatched.length,
+               mapped = map != null;
+
+       for ( ; i < len; i++ ) {
+               if ( (elem = unmatched[i]) ) {
+                       if ( !filter || filter( elem, context, xml ) ) {
+                               newUnmatched.push( elem );
+                               if ( mapped ) {
+                                       map.push( i );
+                               }
+                       }
+               }
+       }
+
+       return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+       if ( postFilter && !postFilter[ expando ] ) {
+               postFilter = setMatcher( postFilter );
+       }
+       if ( postFinder && !postFinder[ expando ] ) {
+               postFinder = setMatcher( postFinder, postSelector );
+       }
+       return markFunction(function( seed, results, context, xml ) {
+               var temp, i, elem,
+                       preMap = [],
+                       postMap = [],
+                       preexisting = results.length,
+
+                       // Get initial elements from seed or context
+                       elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+                       // Prefilter to get matcher input, preserving a map for seed-results synchronization
+                       matcherIn = preFilter && ( seed || !selector ) ?
+                               condense( elems, preMap, preFilter, context, xml ) :
+                               elems,
+
+                       matcherOut = matcher ?
+                               // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+                               postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+                                       // ...intermediate processing is necessary
+                                       [] :
+
+                                       // ...otherwise use results directly
+                                       results :
+                               matcherIn;
+
+               // Find primary matches
+               if ( matcher ) {
+                       matcher( matcherIn, matcherOut, context, xml );
+               }
+
+               // Apply postFilter
+               if ( postFilter ) {
+                       temp = condense( matcherOut, postMap );
+                       postFilter( temp, [], context, xml );
+
+                       // Un-match failing elements by moving them back to matcherIn
+                       i = temp.length;
+                       while ( i-- ) {
+                               if ( (elem = temp[i]) ) {
+                                       matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+                               }
+                       }
+               }
+
+               if ( seed ) {
+                       if ( postFinder || preFilter ) {
+                               if ( postFinder ) {
+                                       // Get the final matcherOut by condensing this intermediate into postFinder contexts
+                                       temp = [];
+                                       i = matcherOut.length;
+                                       while ( i-- ) {
+                                               if ( (elem = matcherOut[i]) ) {
+                                                       // Restore matcherIn since elem is not yet a final match
+                                                       temp.push( (matcherIn[i] = elem) );
+                                               }
+                                       }
+                                       postFinder( null, (matcherOut = []), temp, xml );
+                               }
+
+                               // Move matched elements from seed to results to keep them synchronized
+                               i = matcherOut.length;
+                               while ( i-- ) {
+                                       if ( (elem = matcherOut[i]) &&
+                                               (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
+
+                                               seed[temp] = !(results[temp] = elem);
+                                       }
+                               }
+                       }
+
+               // Add elements to results, through postFinder if defined
+               } else {
+                       matcherOut = condense(
+                               matcherOut === results ?
+                                       matcherOut.splice( preexisting, matcherOut.length ) :
+                                       matcherOut
+                       );
+                       if ( postFinder ) {
+                               postFinder( null, results, matcherOut, xml );
+                       } else {
+                               push.apply( results, matcherOut );
+                       }
+               }
+       });
+}
+
+function matcherFromTokens( tokens ) {
+       var checkContext, matcher, j,
+               len = tokens.length,
+               leadingRelative = Expr.relative[ tokens[0].type ],
+               implicitRelative = leadingRelative || Expr.relative[" "],
+               i = leadingRelative ? 1 : 0,
+
+               // The foundational matcher ensures that elements are reachable from top-level context(s)
+               matchContext = addCombinator( function( elem ) {
+                       return elem === checkContext;
+               }, implicitRelative, true ),
+               matchAnyContext = addCombinator( function( elem ) {
+                       return indexOf( checkContext, elem ) > -1;
+               }, implicitRelative, true ),
+               matchers = [ function( elem, context, xml ) {
+                       var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+                               (checkContext = context).nodeType ?
+                                       matchContext( elem, context, xml ) :
+                                       matchAnyContext( elem, context, xml ) );
+                       // Avoid hanging onto element (issue #299)
+                       checkContext = null;
+                       return ret;
+               } ];
+
+       for ( ; i < len; i++ ) {
+               if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+                       matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+               } else {
+                       matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+                       // Return special upon seeing a positional matcher
+                       if ( matcher[ expando ] ) {
+                               // Find the next relative operator (if any) for proper handling
+                               j = ++i;
+                               for ( ; j < len; j++ ) {
+                                       if ( Expr.relative[ tokens[j].type ] ) {
+                                               break;
+                                       }
+                               }
+                               return setMatcher(
+                                       i > 1 && elementMatcher( matchers ),
+                                       i > 1 && toSelector(
+                                               // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+                                               tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+                                       ).replace( rtrim, "$1" ),
+                                       matcher,
+                                       i < j && matcherFromTokens( tokens.slice( i, j ) ),
+                                       j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+                                       j < len && toSelector( tokens )
+                               );
+                       }
+                       matchers.push( matcher );
+               }
+       }
+
+       return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+       var bySet = setMatchers.length > 0,
+               byElement = elementMatchers.length > 0,
+               superMatcher = function( seed, context, xml, results, outermost ) {
+                       var elem, j, matcher,
+                               matchedCount = 0,
+                               i = "0",
+                               unmatched = seed && [],
+                               setMatched = [],
+                               contextBackup = outermostContext,
+                               // We must always have either seed elements or outermost context
+                               elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+                               // Use integer dirruns iff this is the outermost matcher
+                               dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+                               len = elems.length;
+
+                       if ( outermost ) {
+                               outermostContext = context === document || context || outermost;
+                       }
+
+                       // Add elements passing elementMatchers directly to results
+                       // Support: IE<9, Safari
+                       // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+                       for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+                               if ( byElement && elem ) {
+                                       j = 0;
+                                       if ( !context && elem.ownerDocument !== document ) {
+                                               setDocument( elem );
+                                               xml = !documentIsHTML;
+                                       }
+                                       while ( (matcher = elementMatchers[j++]) ) {
+                                               if ( matcher( elem, context || document, xml) ) {
+                                                       results.push( elem );
+                                                       break;
+                                               }
+                                       }
+                                       if ( outermost ) {
+                                               dirruns = dirrunsUnique;
+                                       }
+                               }
+
+                               // Track unmatched elements for set filters
+                               if ( bySet ) {
+                                       // They will have gone through all possible matchers
+                                       if ( (elem = !matcher && elem) ) {
+                                               matchedCount--;
+                                       }
+
+                                       // Lengthen the array for every element, matched or not
+                                       if ( seed ) {
+                                               unmatched.push( elem );
+                                       }
+                               }
+                       }
+
+                       // `i` is now the count of elements visited above, and adding it to `matchedCount`
+                       // makes the latter nonnegative.
+                       matchedCount += i;
+
+                       // Apply set filters to unmatched elements
+                       // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+                       // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+                       // no element matchers and no seed.
+                       // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+                       // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+                       // numerically zero.
+                       if ( bySet && i !== matchedCount ) {
+                               j = 0;
+                               while ( (matcher = setMatchers[j++]) ) {
+                                       matcher( unmatched, setMatched, context, xml );
+                               }
+
+                               if ( seed ) {
+                                       // Reintegrate element matches to eliminate the need for sorting
+                                       if ( matchedCount > 0 ) {
+                                               while ( i-- ) {
+                                                       if ( !(unmatched[i] || setMatched[i]) ) {
+                                                               setMatched[i] = pop.call( results );
+                                                       }
+                                               }
+                                       }
+
+                                       // Discard index placeholder values to get only actual matches
+                                       setMatched = condense( setMatched );
+                               }
+
+                               // Add matches to results
+                               push.apply( results, setMatched );
+
+                               // Seedless set matches succeeding multiple successful matchers stipulate sorting
+                               if ( outermost && !seed && setMatched.length > 0 &&
+                                       ( matchedCount + setMatchers.length ) > 1 ) {
+
+                                       Sizzle.uniqueSort( results );
+                               }
+                       }
+
+                       // Override manipulation of globals by nested matchers
+                       if ( outermost ) {
+                               dirruns = dirrunsUnique;
+                               outermostContext = contextBackup;
+                       }
+
+                       return unmatched;
+               };
+
+       return bySet ?
+               markFunction( superMatcher ) :
+               superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+       var i,
+               setMatchers = [],
+               elementMatchers = [],
+               cached = compilerCache[ selector + " " ];
+
+       if ( !cached ) {
+               // Generate a function of recursive functions that can be used to check each element
+               if ( !match ) {
+                       match = tokenize( selector );
+               }
+               i = match.length;
+               while ( i-- ) {
+                       cached = matcherFromTokens( match[i] );
+                       if ( cached[ expando ] ) {
+                               setMatchers.push( cached );
+                       } else {
+                               elementMatchers.push( cached );
+                       }
+               }
+
+               // Cache the compiled function
+               cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+
+               // Save selector and tokenization
+               cached.selector = selector;
+       }
+       return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ *  selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ *  selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+       var i, tokens, token, type, find,
+               compiled = typeof selector === "function" && selector,
+               match = !seed && tokenize( (selector = compiled.selector || selector) );
+
+       results = results || [];
+
+       // Try to minimize operations if there is only one selector in the list and no seed
+       // (the latter of which guarantees us context)
+       if ( match.length === 1 ) {
+
+               // Reduce context if the leading compound selector is an ID
+               tokens = match[0] = match[0].slice( 0 );
+               if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+                               context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {
+
+                       context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+                       if ( !context ) {
+                               return results;
+
+                       // Precompiled matchers will still verify ancestry, so step up a level
+                       } else if ( compiled ) {
+                               context = context.parentNode;
+                       }
+
+                       selector = selector.slice( tokens.shift().value.length );
+               }
+
+               // Fetch a seed set for right-to-left matching
+               i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+               while ( i-- ) {
+                       token = tokens[i];
+
+                       // Abort if we hit a combinator
+                       if ( Expr.relative[ (type = token.type) ] ) {
+                               break;
+                       }
+                       if ( (find = Expr.find[ type ]) ) {
+                               // Search, expanding context for leading sibling combinators
+                               if ( (seed = find(
+                                       token.matches[0].replace( runescape, funescape ),
+                                       rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
+                               )) ) {
+
+                                       // If seed is empty or no tokens remain, we can return early
+                                       tokens.splice( i, 1 );
+                                       selector = seed.length && toSelector( tokens );
+                                       if ( !selector ) {
+                                               push.apply( results, seed );
+                                               return results;
+                                       }
+
+                                       break;
+                               }
+                       }
+               }
+       }
+
+       // Compile and execute a filtering function if one is not provided
+       // Provide `match` to avoid retokenization if we modified the selector above
+       ( compiled || compile( selector, match ) )(
+               seed,
+               context,
+               !documentIsHTML,
+               results,
+               !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+       );
+       return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( el ) {
+       // Should return 1, but returns 4 (following)
+       return el.compareDocumentPosition( document.createElement("fieldset") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( el ) {
+       el.innerHTML = "<a href='#'></a>";
+       return el.firstChild.getAttribute("href") === "#" ;
+}) ) {
+       addHandle( "type|href|height|width", function( elem, name, isXML ) {
+               if ( !isXML ) {
+                       return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+               }
+       });
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( el ) {
+       el.innerHTML = "<input/>";
+       el.firstChild.setAttribute( "value", "" );
+       return el.firstChild.getAttribute( "value" ) === "";
+}) ) {
+       addHandle( "value", function( elem, name, isXML ) {
+               if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+                       return elem.defaultValue;
+               }
+       });
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( el ) {
+       return el.getAttribute("disabled") == null;
+}) ) {
+       addHandle( booleans, function( elem, name, isXML ) {
+               var val;
+               if ( !isXML ) {
+                       return elem[ name ] === true ? name.toLowerCase() :
+                                       (val = elem.getAttributeNode( name )) && val.specified ?
+                                       val.value :
+                               null;
+               }
+       });
+}
+
+return Sizzle;
+
+})( window );
+
+
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+
+// Deprecated
+jQuery.expr[ ":" ] = jQuery.expr.pseudos;
+jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+jQuery.escapeSelector = Sizzle.escape;
+
+
+
+
+var dir = function( elem, dir, until ) {
+       var matched = [],
+               truncate = until !== undefined;
+
+       while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
+               if ( elem.nodeType === 1 ) {
+                       if ( truncate && jQuery( elem ).is( until ) ) {
+                               break;
+                       }
+                       matched.push( elem );
+               }
+       }
+       return matched;
+};
+
+
+var siblings = function( n, elem ) {
+       var matched = [];
+
+       for ( ; n; n = n.nextSibling ) {
+               if ( n.nodeType === 1 && n !== elem ) {
+                       matched.push( n );
+               }
+       }
+
+       return matched;
+};
+
+
+var rneedsContext = jQuery.expr.match.needsContext;
+
+
+
+function nodeName( elem, name ) {
+
+  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+
+};
+var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
+
+
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+       if ( isFunction( qualifier ) ) {
+               return jQuery.grep( elements, function( elem, i ) {
+                       return !!qualifier.call( elem, i, elem ) !== not;
+               } );
+       }
+
+       // Single element
+       if ( qualifier.nodeType ) {
+               return jQuery.grep( elements, function( elem ) {
+                       return ( elem === qualifier ) !== not;
+               } );
+       }
+
+       // Arraylike of elements (jQuery, arguments, Array)
+       if ( typeof qualifier !== "string" ) {
+               return jQuery.grep( elements, function( elem ) {
+                       return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
+               } );
+       }
+
+       // Filtered directly for both simple and complex selectors
+       return jQuery.filter( qualifier, elements, not );
+}
+
+jQuery.filter = function( expr, elems, not ) {
+       var elem = elems[ 0 ];
+
+       if ( not ) {
+               expr = ":not(" + expr + ")";
+       }
+
+       if ( elems.length === 1 && elem.nodeType === 1 ) {
+               return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
+       }
+
+       return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+               return elem.nodeType === 1;
+       } ) );
+};
+
+jQuery.fn.extend( {
+       find: function( selector ) {
+               var i, ret,
+                       len = this.length,
+                       self = this;
+
+               if ( typeof selector !== "string" ) {
+                       return this.pushStack( jQuery( selector ).filter( function() {
+                               for ( i = 0; i < len; i++ ) {
+                                       if ( jQuery.contains( self[ i ], this ) ) {
+                                               return true;
+                                       }
+                               }
+                       } ) );
+               }
+
+               ret = this.pushStack( [] );
+
+               for ( i = 0; i < len; i++ ) {
+                       jQuery.find( selector, self[ i ], ret );
+               }
+
+               return len > 1 ? jQuery.uniqueSort( ret ) : ret;
+       },
+       filter: function( selector ) {
+               return this.pushStack( winnow( this, selector || [], false ) );
+       },
+       not: function( selector ) {
+               return this.pushStack( winnow( this, selector || [], true ) );
+       },
+       is: function( selector ) {
+               return !!winnow(
+                       this,
+
+                       // If this is a positional/relative selector, check membership in the returned set
+                       // so $("p:first").is("p:last") won't return true for a doc with two "p".
+                       typeof selector === "string" && rneedsContext.test( selector ) ?
+                               jQuery( selector ) :
+                               selector || [],
+                       false
+               ).length;
+       }
+} );
+
+
+// Initialize a jQuery object
+
+
+// A central reference to the root jQuery(document)
+var rootjQuery,
+
+       // A simple way to check for HTML strings
+       // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+       // Strict HTML recognition (#11290: must start with <)
+       // Shortcut simple #id case for speed
+       rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
+
+       init = jQuery.fn.init = function( selector, context, root ) {
+               var match, elem;
+
+               // HANDLE: $(""), $(null), $(undefined), $(false)
+               if ( !selector ) {
+                       return this;
+               }
+
+               // Method init() accepts an alternate rootjQuery
+               // so migrate can support jQuery.sub (gh-2101)
+               root = root || rootjQuery;
+
+               // Handle HTML strings
+               if ( typeof selector === "string" ) {
+                       if ( selector[ 0 ] === "<" &&
+                               selector[ selector.length - 1 ] === ">" &&
+                               selector.length >= 3 ) {
+
+                               // Assume that strings that start and end with <> are HTML and skip the regex check
+                               match = [ null, selector, null ];
+
+                       } else {
+                               match = rquickExpr.exec( selector );
+                       }
+
+                       // Match html or make sure no context is specified for #id
+                       if ( match && ( match[ 1 ] || !context ) ) {
+
+                               // HANDLE: $(html) -> $(array)
+                               if ( match[ 1 ] ) {
+                                       context = context instanceof jQuery ? context[ 0 ] : context;
+
+                                       // Option to run scripts is true for back-compat
+                                       // Intentionally let the error be thrown if parseHTML is not present
+                                       jQuery.merge( this, jQuery.parseHTML(
+                                               match[ 1 ],
+                                               context && context.nodeType ? context.ownerDocument || context : document,
+                                               true
+                                       ) );
+
+                                       // HANDLE: $(html, props)
+                                       if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
+                                               for ( match in context ) {
+
+                                                       // Properties of context are called as methods if possible
+                                                       if ( isFunction( this[ match ] ) ) {
+                                                               this[ match ]( context[ match ] );
+
+                                                       // ...and otherwise set as attributes
+                                                       } else {
+                                                               this.attr( match, context[ match ] );
+                                                       }
+                                               }
+                                       }
+
+                                       return this;
+
+                               // HANDLE: $(#id)
+                               } else {
+                                       elem = document.getElementById( match[ 2 ] );
+
+                                       if ( elem ) {
+
+                                               // Inject the element directly into the jQuery object
+                                               this[ 0 ] = elem;
+                                               this.length = 1;
+                                       }
+                                       return this;
+                               }
+
+                       // HANDLE: $(expr, $(...))
+                       } else if ( !context || context.jquery ) {
+                               return ( context || root ).find( selector );
+
+                       // HANDLE: $(expr, context)
+                       // (which is just equivalent to: $(context).find(expr)
+                       } else {
+                               return this.constructor( context ).find( selector );
+                       }
+
+               // HANDLE: $(DOMElement)
+               } else if ( selector.nodeType ) {
+                       this[ 0 ] = selector;
+                       this.length = 1;
+                       return this;
+
+               // HANDLE: $(function)
+               // Shortcut for document ready
+               } else if ( isFunction( selector ) ) {
+                       return root.ready !== undefined ?
+                               root.ready( selector ) :
+
+                               // Execute immediately if ready is not present
+                               selector( jQuery );
+               }
+
+               return jQuery.makeArray( selector, this );
+       };
+
+// Give the init function the jQuery prototype for later instantiation
+init.prototype = jQuery.fn;
+
+// Initialize central reference
+rootjQuery = jQuery( document );
+
+
+var rparentsprev = /^(?:parents|prev(?:Until|All))/,
+
+       // Methods guaranteed to produce a unique set when starting from a unique set
+       guaranteedUnique = {
+               children: true,
+               contents: true,
+               next: true,
+               prev: true
+       };
+
+jQuery.fn.extend( {
+       has: function( target ) {
+               var targets = jQuery( target, this ),
+                       l = targets.length;
+
+               return this.filter( function() {
+                       var i = 0;
+                       for ( ; i < l; i++ ) {
+                               if ( jQuery.contains( this, targets[ i ] ) ) {
+                                       return true;
+                               }
+                       }
+               } );
+       },
+
+       closest: function( selectors, context ) {
+               var cur,
+                       i = 0,
+                       l = this.length,
+                       matched = [],
+                       targets = typeof selectors !== "string" && jQuery( selectors );
+
+               // Positional selectors never match, since there's no _selection_ context
+               if ( !rneedsContext.test( selectors ) ) {
+                       for ( ; i < l; i++ ) {
+                               for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {
+
+                                       // Always skip document fragments
+                                       if ( cur.nodeType < 11 && ( targets ?
+                                               targets.index( cur ) > -1 :
+
+                                               // Don't pass non-elements to Sizzle
+                                               cur.nodeType === 1 &&
+                                                       jQuery.find.matchesSelector( cur, selectors ) ) ) {
+
+                                               matched.push( cur );
+                                               break;
+                                       }
+                               }
+                       }
+               }
+
+               return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
+       },
+
+       // Determine the position of an element within the set
+       index: function( elem ) {
+
+               // No argument, return index in parent
+               if ( !elem ) {
+                       return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
+               }
+
+               // Index in selector
+               if ( typeof elem === "string" ) {
+                       return indexOf.call( jQuery( elem ), this[ 0 ] );
+               }
+
+               // Locate the position of the desired element
+               return indexOf.call( this,
+
+                       // If it receives a jQuery object, the first element is used
+                       elem.jquery ? elem[ 0 ] : elem
+               );
+       },
+
+       add: function( selector, context ) {
+               return this.pushStack(
+                       jQuery.uniqueSort(
+                               jQuery.merge( this.get(), jQuery( selector, context ) )
+                       )
+               );
+       },
+
+       addBack: function( selector ) {
+               return this.add( selector == null ?
+                       this.prevObject : this.prevObject.filter( selector )
+               );
+       }
+} );
+
+function sibling( cur, dir ) {
+       while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
+       return cur;
+}
+
+jQuery.each( {
+       parent: function( elem ) {
+               var parent = elem.parentNode;
+               return parent && parent.nodeType !== 11 ? parent : null;
+       },
+       parents: function( elem ) {
+               return dir( elem, "parentNode" );
+       },
+       parentsUntil: function( elem, i, until ) {
+               return dir( elem, "parentNode", until );
+       },
+       next: function( elem ) {
+               return sibling( elem, "nextSibling" );
+       },
+       prev: function( elem ) {
+               return sibling( elem, "previousSibling" );
+       },
+       nextAll: function( elem ) {
+               return dir( elem, "nextSibling" );
+       },
+       prevAll: function( elem ) {
+               return dir( elem, "previousSibling" );
+       },
+       nextUntil: function( elem, i, until ) {
+               return dir( elem, "nextSibling", until );
+       },
+       prevUntil: function( elem, i, until ) {
+               return dir( elem, "previousSibling", until );
+       },
+       siblings: function( elem ) {
+               return siblings( ( elem.parentNode || {} ).firstChild, elem );
+       },
+       children: function( elem ) {
+               return siblings( elem.firstChild );
+       },
+       contents: function( elem ) {
+               if ( typeof elem.contentDocument !== "undefined" ) {
+                       return elem.contentDocument;
+               }
+
+               // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
+               // Treat the template element as a regular one in browsers that
+               // don't support it.
+               if ( nodeName( elem, "template" ) ) {
+                       elem = elem.content || elem;
+               }
+
+               return jQuery.merge( [], elem.childNodes );
+       }
+}, function( name, fn ) {
+       jQuery.fn[ name ] = function( until, selector ) {
+               var matched = jQuery.map( this, fn, until );
+
+               if ( name.slice( -5 ) !== "Until" ) {
+                       selector = until;
+               }
+
+               if ( selector && typeof selector === "string" ) {
+                       matched = jQuery.filter( selector, matched );
+               }
+
+               if ( this.length > 1 ) {
+
+                       // Remove duplicates
+                       if ( !guaranteedUnique[ name ] ) {
+                               jQuery.uniqueSort( matched );
+                       }
+
+                       // Reverse order for parents* and prev-derivatives
+                       if ( rparentsprev.test( name ) ) {
+                               matched.reverse();
+                       }
+               }
+
+               return this.pushStack( matched );
+       };
+} );
+var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g );
+
+
+
+// Convert String-formatted options into Object-formatted ones
+function createOptions( options ) {
+       var object = {};
+       jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
+               object[ flag ] = true;
+       } );
+       return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *     options: an optional list of space-separated options that will change how
+ *                     the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *     once:                   will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *     memory:                 will keep track of previous values and will call any callback added
+ *                                     after the list has been fired right away with the latest "memorized"
+ *                                     values (like a Deferred)
+ *
+ *     unique:                 will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *     stopOnFalse:    interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+       // Convert options from String-formatted to Object-formatted if needed
+       // (we check in cache first)
+       options = typeof options === "string" ?
+               createOptions( options ) :
+               jQuery.extend( {}, options );
+
+       var // Flag to know if list is currently firing
+               firing,
+
+               // Last fire value for non-forgettable lists
+               memory,
+
+               // Flag to know if list was already fired
+               fired,
+
+               // Flag to prevent firing
+               locked,
+
+               // Actual callback list
+               list = [],
+
+               // Queue of execution data for repeatable lists
+               queue = [],
+
+               // Index of currently firing callback (modified by add/remove as needed)
+               firingIndex = -1,
+
+               // Fire callbacks
+               fire = function() {
+
+                       // Enforce single-firing
+                       locked = locked || options.once;
+
+                       // Execute callbacks for all pending executions,
+                       // respecting firingIndex overrides and runtime changes
+                       fired = firing = true;
+                       for ( ; queue.length; firingIndex = -1 ) {
+                               memory = queue.shift();
+                               while ( ++firingIndex < list.length ) {
+
+                                       // Run callback and check for early termination
+                                       if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
+                                               options.stopOnFalse ) {
+
+                                               // Jump to end and forget the data so .add doesn't re-fire
+                                               firingIndex = list.length;
+                                               memory = false;
+                                       }
+                               }
+                       }
+
+                       // Forget the data if we're done with it
+                       if ( !options.memory ) {
+                               memory = false;
+                       }
+
+                       firing = false;
+
+                       // Clean up if we're done firing for good
+                       if ( locked ) {
+
+                               // Keep an empty list if we have data for future add calls
+                               if ( memory ) {
+                                       list = [];
+
+                               // Otherwise, this object is spent
+                               } else {
+                                       list = "";
+                               }
+                       }
+               },
+
+               // Actual Callbacks object
+               self = {
+
+                       // Add a callback or a collection of callbacks to the list
+                       add: function() {
+                               if ( list ) {
+
+                                       // If we have memory from a past run, we should fire after adding
+                                       if ( memory && !firing ) {
+                                               firingIndex = list.length - 1;
+                                               queue.push( memory );
+                                       }
+
+                                       ( function add( args ) {
+                                               jQuery.each( args, function( _, arg ) {
+                                                       if ( isFunction( arg ) ) {
+                                                               if ( !options.unique || !self.has( arg ) ) {
+                                                                       list.push( arg );
+                                                               }
+                                                       } else if ( arg && arg.length && toType( arg ) !== "string" ) {
+
+                                                               // Inspect recursively
+                                                               add( arg );
+                                                       }
+                                               } );
+                                       } )( arguments );
+
+                                       if ( memory && !firing ) {
+                                               fire();
+                                       }
+                               }
+                               return this;
+                       },
+
+                       // Remove a callback from the list
+                       remove: function() {
+                               jQuery.each( arguments, function( _, arg ) {
+                                       var index;
+                                       while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+                                               list.splice( index, 1 );
+
+                                               // Handle firing indexes
+                                               if ( index <= firingIndex ) {
+                                                       firingIndex--;
+                                               }
+                                       }
+                               } );
+                               return this;
+                       },
+
+                       // Check if a given callback is in the list.
+                       // If no argument is given, return whether or not list has callbacks attached.
+                       has: function( fn ) {
+                               return fn ?
+                                       jQuery.inArray( fn, list ) > -1 :
+                                       list.length > 0;
+                       },
+
+                       // Remove all callbacks from the list
+                       empty: function() {
+                               if ( list ) {
+                                       list = [];
+                               }
+                               return this;
+                       },
+
+                       // Disable .fire and .add
+                       // Abort any current/pending executions
+                       // Clear all callbacks and values
+                       disable: function() {
+                               locked = queue = [];
+                               list = memory = "";
+                               return this;
+                       },
+                       disabled: function() {
+                               return !list;
+                       },
+
+                       // Disable .fire
+                       // Also disable .add unless we have memory (since it would have no effect)
+                       // Abort any pending executions
+                       lock: function() {
+                               locked = queue = [];
+                               if ( !memory && !firing ) {
+                                       list = memory = "";
+                               }
+                               return this;
+                       },
+                       locked: function() {
+                               return !!locked;
+                       },
+
+                       // Call all callbacks with the given context and arguments
+                       fireWith: function( context, args ) {
+                               if ( !locked ) {
+                                       args = args || [];
+                                       args = [ context, args.slice ? args.slice() : args ];
+                                       queue.push( args );
+                                       if ( !firing ) {
+                                               fire();
+                                       }
+                               }
+                               return this;
+                       },
+
+                       // Call all the callbacks with the given arguments
+                       fire: function() {
+                               self.fireWith( this, arguments );
+                               return this;
+                       },
+
+                       // To know if the callbacks have already been called at least once
+                       fired: function() {
+                               return !!fired;
+                       }
+               };
+
+       return self;
+};
+
+
+function Identity( v ) {
+       return v;
+}
+function Thrower( ex ) {
+       throw ex;
+}
+
+function adoptValue( value, resolve, reject, noValue ) {
+       var method;
+
+       try {
+
+               // Check for promise aspect first to privilege synchronous behavior
+               if ( value && isFunction( ( method = value.promise ) ) ) {
+                       method.call( value ).done( resolve ).fail( reject );
+
+               // Other thenables
+               } else if ( value && isFunction( ( method = value.then ) ) ) {
+                       method.call( value, resolve, reject );
+
+               // Other non-thenables
+               } else {
+
+                       // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
+                       // * false: [ value ].slice( 0 ) => resolve( value )
+                       // * true: [ value ].slice( 1 ) => resolve()
+                       resolve.apply( undefined, [ value ].slice( noValue ) );
+               }
+
+       // For Promises/A+, convert exceptions into rejections
+       // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
+       // Deferred#then to conditionally suppress rejection.
+       } catch ( value ) {
+
+               // Support: Android 4.0 only
+               // Strict mode functions invoked without .call/.apply get global-object context
+               reject.apply( undefined, [ value ] );
+       }
+}
+
+jQuery.extend( {
+
+       Deferred: function( func ) {
+               var tuples = [
+
+                               // action, add listener, callbacks,
+                               // ... .then handlers, argument index, [final state]
+                               [ "notify", "progress", jQuery.Callbacks( "memory" ),
+                                       jQuery.Callbacks( "memory" ), 2 ],
+                               [ "resolve", "done", jQuery.Callbacks( "once memory" ),
+                                       jQuery.Callbacks( "once memory" ), 0, "resolved" ],
+                               [ "reject", "fail", jQuery.Callbacks( "once memory" ),
+                                       jQuery.Callbacks( "once memory" ), 1, "rejected" ]
+                       ],
+                       state = "pending",
+                       promise = {
+                               state: function() {
+                                       return state;
+                               },
+                               always: function() {
+                                       deferred.done( arguments ).fail( arguments );
+                                       return this;
+                               },
+                               "catch": function( fn ) {
+                                       return promise.then( null, fn );
+                               },
+
+                               // Keep pipe for back-compat
+                               pipe: function( /* fnDone, fnFail, fnProgress */ ) {
+                                       var fns = arguments;
+
+                                       return jQuery.Deferred( function( newDefer ) {
+                                               jQuery.each( tuples, function( i, tuple ) {
+
+                                                       // Map tuples (progress, done, fail) to arguments (done, fail, progress)
+                                                       var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];
+
+                                                       // deferred.progress(function() { bind to newDefer or newDefer.notify })
+                                                       // deferred.done(function() { bind to newDefer or newDefer.resolve })
+                                                       // deferred.fail(function() { bind to newDefer or newDefer.reject })
+                                                       deferred[ tuple[ 1 ] ]( function() {
+                                                               var returned = fn && fn.apply( this, arguments );
+                                                               if ( returned && isFunction( returned.promise ) ) {
+                                                                       returned.promise()
+                                                                               .progress( newDefer.notify )
+                                                                               .done( newDefer.resolve )
+                                                                               .fail( newDefer.reject );
+                                                               } else {
+                                                                       newDefer[ tuple[ 0 ] + "With" ](
+                                                                               this,
+                                                                               fn ? [ returned ] : arguments
+                                                                       );
+                                                               }
+                                                       } );
+                                               } );
+                                               fns = null;
+                                       } ).promise();
+                               },
+                               then: function( onFulfilled, onRejected, onProgress ) {
+                                       var maxDepth = 0;
+                                       function resolve( depth, deferred, handler, special ) {
+                                               return function() {
+                                                       var that = this,
+                                                               args = arguments,
+                                                               mightThrow = function() {
+                                                                       var returned, then;
+
+                                                                       // Support: Promises/A+ section 2.3.3.3.3
+                                                                       // https://promisesaplus.com/#point-59
+                                                                       // Ignore double-resolution attempts
+                                                                       if ( depth < maxDepth ) {
+                                                                               return;
+                                                                       }
+
+                                                                       returned = handler.apply( that, args );
+
+                                                                       // Support: Promises/A+ section 2.3.1
+                                                                       // https://promisesaplus.com/#point-48
+                                                                       if ( returned === deferred.promise() ) {
+                                                                               throw new TypeError( "Thenable self-resolution" );
+                                                                       }
+
+                                                                       // Support: Promises/A+ sections 2.3.3.1, 3.5
+                                                                       // https://promisesaplus.com/#point-54
+                                                                       // https://promisesaplus.com/#point-75
+                                                                       // Retrieve `then` only once
+                                                                       then = returned &&
+
+                                                                               // Support: Promises/A+ section 2.3.4
+                                                                               // https://promisesaplus.com/#point-64
+                                                                               // Only check objects and functions for thenability
+                                                                               ( typeof returned === "object" ||
+                                                                                       typeof returned === "function" ) &&
+                                                                               returned.then;
+
+                                                                       // Handle a returned thenable
+                                                                       if ( isFunction( then ) ) {
+
+                                                                               // Special processors (notify) just wait for resolution
+                                                                               if ( special ) {
+                                                                                       then.call(
+                                                                                               returned,
+                                                                                               resolve( maxDepth, deferred, Identity, special ),
+                                                                                               resolve( maxDepth, deferred, Thrower, special )
+                                                                                       );
+
+                                                                               // Normal processors (resolve) also hook into progress
+                                                                               } else {
+
+                                                                                       // ...and disregard older resolution values
+                                                                                       maxDepth++;
+
+                                                                                       then.call(
+                                                                                               returned,
+                                                                                               resolve( maxDepth, deferred, Identity, special ),
+                                                                                               resolve( maxDepth, deferred, Thrower, special ),
+                                                                                               resolve( maxDepth, deferred, Identity,
+                                                                                                       deferred.notifyWith )
+                                                                                       );
+                                                                               }
+
+                                                                       // Handle all other returned values
+                                                                       } else {
+
+                                                                               // Only substitute handlers pass on context
+                                                                               // and multiple values (non-spec behavior)
+                                                                               if ( handler !== Identity ) {
+                                                                                       that = undefined;
+                                                                                       args = [ returned ];
+                                                                               }
+
+                                                                               // Process the value(s)
+                                                                               // Default process is resolve
+                                                                               ( special || deferred.resolveWith )( that, args );
+                                                                       }
+                                                               },
+
+                                                               // Only normal processors (resolve) catch and reject exceptions
+                                                               process = special ?
+                                                                       mightThrow :
+                                                                       function() {
+                                                                               try {
+                                                                                       mightThrow();
+                                                                               } catch ( e ) {
+
+                                                                                       if ( jQuery.Deferred.exceptionHook ) {
+                                                                                               jQuery.Deferred.exceptionHook( e,
+                                                                                                       process.stackTrace );
+                                                                                       }
+
+                                                                                       // Support: Promises/A+ section 2.3.3.3.4.1
+                                                                                       // https://promisesaplus.com/#point-61
+                                                                                       // Ignore post-resolution exceptions
+                                                                                       if ( depth + 1 >= maxDepth ) {
+
+                                                                                               // Only substitute handlers pass on context
+                                                                                               // and multiple values (non-spec behavior)
+                                                                                               if ( handler !== Thrower ) {
+                                                                                                       that = undefined;
+                                                                                                       args = [ e ];
+                                                                                               }
+
+                                                                                               deferred.rejectWith( that, args );
+                                                                                       }
+                                                                               }
+                                                                       };
+
+                                                       // Support: Promises/A+ section 2.3.3.3.1
+                                                       // https://promisesaplus.com/#point-57
+                                                       // Re-resolve promises immediately to dodge false rejection from
+                                                       // subsequent errors
+                                                       if ( depth ) {
+                                                               process();
+                                                       } else {
+
+                                                               // Call an optional hook to record the stack, in case of exception
+                                                               // since it's otherwise lost when execution goes async
+                                                               if ( jQuery.Deferred.getStackHook ) {
+                                                                       process.stackTrace = jQuery.Deferred.getStackHook();
+                                                               }
+                                                               window.setTimeout( process );
+                                                       }
+                                               };
+                                       }
+
+                                       return jQuery.Deferred( function( newDefer ) {
+
+                                               // progress_handlers.add( ... )
+                                               tuples[ 0 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               isFunction( onProgress ) ?
+                                                                       onProgress :
+                                                                       Identity,
+                                                               newDefer.notifyWith
+                                                       )
+                                               );
+
+                                               // fulfilled_handlers.add( ... )
+                                               tuples[ 1 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               isFunction( onFulfilled ) ?
+                                                                       onFulfilled :
+                                                                       Identity
+                                                       )
+                                               );
+
+                                               // rejected_handlers.add( ... )
+                                               tuples[ 2 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               isFunction( onRejected ) ?
+                                                                       onRejected :
+                                                                       Thrower
+                                                       )
+                                               );
+                                       } ).promise();
+                               },
+
+                               // Get a promise for this deferred
+                               // If obj is provided, the promise aspect is added to the object
+                               promise: function( obj ) {
+                                       return obj != null ? jQuery.extend( obj, promise ) : promise;
+                               }
+                       },
+                       deferred = {};
+
+               // Add list-specific methods
+               jQuery.each( tuples, function( i, tuple ) {
+                       var list = tuple[ 2 ],
+                               stateString = tuple[ 5 ];
+
+                       // promise.progress = list.add
+                       // promise.done = list.add
+                       // promise.fail = list.add
+                       promise[ tuple[ 1 ] ] = list.add;
+
+                       // Handle state
+                       if ( stateString ) {
+                               list.add(
+                                       function() {
+
+                                               // state = "resolved" (i.e., fulfilled)
+                                               // state = "rejected"
+                                               state = stateString;
+                                       },
+
+                                       // rejected_callbacks.disable
+                                       // fulfilled_callbacks.disable
+                                       tuples[ 3 - i ][ 2 ].disable,
+
+                                       // rejected_handlers.disable
+                                       // fulfilled_handlers.disable
+                                       tuples[ 3 - i ][ 3 ].disable,
+
+                                       // progress_callbacks.lock
+                                       tuples[ 0 ][ 2 ].lock,
+
+                                       // progress_handlers.lock
+                                       tuples[ 0 ][ 3 ].lock
+                               );
+                       }
+
+                       // progress_handlers.fire
+                       // fulfilled_handlers.fire
+                       // rejected_handlers.fire
+                       list.add( tuple[ 3 ].fire );
+
+                       // deferred.notify = function() { deferred.notifyWith(...) }
+                       // deferred.resolve = function() { deferred.resolveWith(...) }
+                       // deferred.reject = function() { deferred.rejectWith(...) }
+                       deferred[ tuple[ 0 ] ] = function() {
+                               deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
+                               return this;
+                       };
+
+                       // deferred.notifyWith = list.fireWith
+                       // deferred.resolveWith = list.fireWith
+                       // deferred.rejectWith = list.fireWith
+                       deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
+               } );
+
+               // Make the deferred a promise
+               promise.promise( deferred );
+
+               // Call given func if any
+               if ( func ) {
+                       func.call( deferred, deferred );
+               }
+
+               // All done!
+               return deferred;
+       },
+
+       // Deferred helper
+       when: function( singleValue ) {
+               var
+
+                       // count of uncompleted subordinates
+                       remaining = arguments.length,
+
+                       // count of unprocessed arguments
+                       i = remaining,
+
+                       // subordinate fulfillment data
+                       resolveContexts = Array( i ),
+                       resolveValues = slice.call( arguments ),
+
+                       // the master Deferred
+                       master = jQuery.Deferred(),
+
+                       // subordinate callback factory
+                       updateFunc = function( i ) {
+                               return function( value ) {
+                                       resolveContexts[ i ] = this;
+                                       resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
+                                       if ( !( --remaining ) ) {
+                                               master.resolveWith( resolveContexts, resolveValues );
+                                       }
+                               };
+                       };
+
+               // Single- and empty arguments are adopted like Promise.resolve
+               if ( remaining <= 1 ) {
+                       adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
+                               !remaining );
+
+                       // Use .then() to unwrap secondary thenables (cf. gh-3000)
+                       if ( master.state() === "pending" ||
+                               isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
+
+                               return master.then();
+                       }
+               }
+
+               // Multiple arguments are aggregated like Promise.all array elements
+               while ( i-- ) {
+                       adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
+               }
+
+               return master.promise();
+       }
+} );
+
+
+// These usually indicate a programmer mistake during development,
+// warn about them ASAP rather than swallowing them by default.
+var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;
+
+jQuery.Deferred.exceptionHook = function( error, stack ) {
+
+       // Support: IE 8 - 9 only
+       // Console exists when dev tools are open, which can happen at any time
+       if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {
+               window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack );
+       }
+};
+
+
+
+
+jQuery.readyException = function( error ) {
+       window.setTimeout( function() {
+               throw error;
+       } );
+};
+
+
+
+
+// The deferred used on DOM ready
+var readyList = jQuery.Deferred();
+
+jQuery.fn.ready = function( fn ) {
+
+       readyList
+               .then( fn )
+
+               // Wrap jQuery.readyException in a function so that the lookup
+               // happens at the time of error handling instead of callback
+               // registration.
+               .catch( function( error ) {
+                       jQuery.readyException( error );
+               } );
+
+       return this;
+};
+
+jQuery.extend( {
+
+       // Is the DOM ready to be used? Set to true once it occurs.
+       isReady: false,
+
+       // A counter to track how many items to wait for before
+       // the ready event fires. See #6781
+       readyWait: 1,
+
+       // Handle when the DOM is ready
+       ready: function( wait ) {
+
+               // Abort if there are pending holds or we're already ready
+               if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+                       return;
+               }
+
+               // Remember that the DOM is ready
+               jQuery.isReady = true;
+
+               // If a normal DOM Ready event fired, decrement, and wait if need be
+               if ( wait !== true && --jQuery.readyWait > 0 ) {
+                       return;
+               }
+
+               // If there are functions bound, to execute
+               readyList.resolveWith( document, [ jQuery ] );
+       }
+} );
+
+jQuery.ready.then = readyList.then;
+
+// The ready event handler and self cleanup method
+function completed() {
+       document.removeEventListener( "DOMContentLoaded", completed );
+       window.removeEventListener( "load", completed );
+       jQuery.ready();
+}
+
+// Catch cases where $(document).ready() is called
+// after the browser event has already occurred.
+// Support: IE <=9 - 10 only
+// Older IE sometimes signals "interactive" too soon
+if ( document.readyState === "complete" ||
+       ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
+
+       // Handle it asynchronously to allow scripts the opportunity to delay ready
+       window.setTimeout( jQuery.ready );
+
+} else {
+
+       // Use the handy event callback
+       document.addEventListener( "DOMContentLoaded", completed );
+
+       // A fallback to window.onload, that will always work
+       window.addEventListener( "load", completed );
+}
+
+
+
+
+// Multifunctional method to get and set values of a collection
+// The value/s can optionally be executed if it's a function
+var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
+       var i = 0,
+               len = elems.length,
+               bulk = key == null;
+
+       // Sets many values
+       if ( toType( key ) === "object" ) {
+               chainable = true;
+               for ( i in key ) {
+                       access( elems, fn, i, key[ i ], true, emptyGet, raw );
+               }
+
+       // Sets one value
+       } else if ( value !== undefined ) {
+               chainable = true;
+
+               if ( !isFunction( value ) ) {
+                       raw = true;
+               }
+
+               if ( bulk ) {
+
+                       // Bulk operations run against the entire set
+                       if ( raw ) {
+                               fn.call( elems, value );
+                               fn = null;
+
+                       // ...except when executing function values
+                       } else {
+                               bulk = fn;
+                               fn = function( elem, key, value ) {
+                                       return bulk.call( jQuery( elem ), value );
+                               };
+                       }
+               }
+
+               if ( fn ) {
+                       for ( ; i < len; i++ ) {
+                               fn(
+                                       elems[ i ], key, raw ?
+                                       value :
+                                       value.call( elems[ i ], i, fn( elems[ i ], key ) )
+                               );
+                       }
+               }
+       }
+
+       if ( chainable ) {
+               return elems;
+       }
+
+       // Gets
+       if ( bulk ) {
+               return fn.call( elems );
+       }
+
+       return len ? fn( elems[ 0 ], key ) : emptyGet;
+};
+
+
+// Matches dashed string for camelizing
+var rmsPrefix = /^-ms-/,
+       rdashAlpha = /-([a-z])/g;
+
+// Used by camelCase as callback to replace()
+function fcamelCase( all, letter ) {
+       return letter.toUpperCase();
+}
+
+// Convert dashed to camelCase; used by the css and data modules
+// Support: IE <=9 - 11, Edge 12 - 15
+// Microsoft forgot to hump their vendor prefix (#9572)
+function camelCase( string ) {
+       return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+}
+var acceptData = function( owner ) {
+
+       // Accepts only:
+       //  - Node
+       //    - Node.ELEMENT_NODE
+       //    - Node.DOCUMENT_NODE
+       //  - Object
+       //    - Any
+       return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
+};
+
+
+
+
+function Data() {
+       this.expando = jQuery.expando + Data.uid++;
+}
+
+Data.uid = 1;
+
+Data.prototype = {
+
+       cache: function( owner ) {
+
+               // Check if the owner object already has a cache
+               var value = owner[ this.expando ];
+
+               // If not, create one
+               if ( !value ) {
+                       value = {};
+
+                       // We can accept data for non-element nodes in modern browsers,
+                       // but we should not, see #8335.
+                       // Always return an empty object.
+                       if ( acceptData( owner ) ) {
+
+                               // If it is a node unlikely to be stringify-ed or looped over
+                               // use plain assignment
+                               if ( owner.nodeType ) {
+                                       owner[ this.expando ] = value;
+
+                               // Otherwise secure it in a non-enumerable property
+                               // configurable must be true to allow the property to be
+                               // deleted when data is removed
+                               } else {
+                                       Object.defineProperty( owner, this.expando, {
+                                               value: value,
+                                               configurable: true
+                                       } );
+                               }
+                       }
+               }
+
+               return value;
+       },
+       set: function( owner, data, value ) {
+               var prop,
+                       cache = this.cache( owner );
+
+               // Handle: [ owner, key, value ] args
+               // Always use camelCase key (gh-2257)
+               if ( typeof data === "string" ) {
+                       cache[ camelCase( data ) ] = value;
+
+               // Handle: [ owner, { properties } ] args
+               } else {
+
+                       // Copy the properties one-by-one to the cache object
+                       for ( prop in data ) {
+                               cache[ camelCase( prop ) ] = data[ prop ];
+                       }
+               }
+               return cache;
+       },
+       get: function( owner, key ) {
+               return key === undefined ?
+                       this.cache( owner ) :
+
+                       // Always use camelCase key (gh-2257)
+                       owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];
+       },
+       access: function( owner, key, value ) {
+
+               // In cases where either:
+               //
+               //   1. No key was specified
+               //   2. A string key was specified, but no value provided
+               //
+               // Take the "read" path and allow the get method to determine
+               // which value to return, respectively either:
+               //
+               //   1. The entire cache object
+               //   2. The data stored at the key
+               //
+               if ( key === undefined ||
+                               ( ( key && typeof key === "string" ) && value === undefined ) ) {
+
+                       return this.get( owner, key );
+               }
+
+               // When the key is not a string, or both a key and value
+               // are specified, set or extend (existing objects) with either:
+               //
+               //   1. An object of properties
+               //   2. A key and value
+               //
+               this.set( owner, key, value );
+
+               // Since the "set" path can have two possible entry points
+               // return the expected data based on which path was taken[*]
+               return value !== undefined ? value : key;
+       },
+       remove: function( owner, key ) {
+               var i,
+                       cache = owner[ this.expando ];
+
+               if ( cache === undefined ) {
+                       return;
+               }
+
+               if ( key !== undefined ) {
+
+                       // Support array or space separated string of keys
+                       if ( Array.isArray( key ) ) {
+
+                               // If key is an array of keys...
+                               // We always set camelCase keys, so remove that.
+                               key = key.map( camelCase );
+                       } else {
+                               key = camelCase( key );
+
+                               // If a key with the spaces exists, use it.
+                               // Otherwise, create an array by matching non-whitespace
+                               key = key in cache ?
+                                       [ key ] :
+                                       ( key.match( rnothtmlwhite ) || [] );
+                       }
+
+                       i = key.length;
+
+                       while ( i-- ) {
+                               delete cache[ key[ i ] ];
+                       }
+               }
+
+               // Remove the expando if there's no more data
+               if ( key === undefined || jQuery.isEmptyObject( cache ) ) {
+
+                       // Support: Chrome <=35 - 45
+                       // Webkit & Blink performance suffers when deleting properties
+                       // from DOM nodes, so set to undefined instead
+                       // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)
+                       if ( owner.nodeType ) {
+                               owner[ this.expando ] = undefined;
+                       } else {
+                               delete owner[ this.expando ];
+                       }
+               }
+       },
+       hasData: function( owner ) {
+               var cache = owner[ this.expando ];
+               return cache !== undefined && !jQuery.isEmptyObject( cache );
+       }
+};
+var dataPriv = new Data();
+
+var dataUser = new Data();
+
+
+
+//     Implementation Summary
+//
+//     1. Enforce API surface and semantic compatibility with 1.9.x branch
+//     2. Improve the module's maintainability by reducing the storage
+//             paths to a single mechanism.
+//     3. Use the same single mechanism to support "private" and "user" data.
+//     4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
+//     5. Avoid exposing implementation details on user objects (eg. expando properties)
+//     6. Provide a clear path for implementation upgrade to WeakMap in 2014
+
+var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
+       rmultiDash = /[A-Z]/g;
+
+function getData( data ) {
+       if ( data === "true" ) {
+               return true;
+       }
+
+       if ( data === "false" ) {
+               return false;
+       }
+
+       if ( data === "null" ) {
+               return null;
+       }
+
+       // Only convert to a number if it doesn't change the string
+       if ( data === +data + "" ) {
+               return +data;
+       }
+
+       if ( rbrace.test( data ) ) {
+               return JSON.parse( data );
+       }
+
+       return data;
+}
+
+function dataAttr( elem, key, data ) {
+       var name;
+
+       // If nothing was found internally, try to fetch any
+       // data from the HTML5 data-* attribute
+       if ( data === undefined && elem.nodeType === 1 ) {
+               name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase();
+               data = elem.getAttribute( name );
+
+               if ( typeof data === "string" ) {
+                       try {
+                               data = getData( data );
+                       } catch ( e ) {}
+
+                       // Make sure we set the data so it isn't changed later
+                       dataUser.set( elem, key, data );
+               } else {
+                       data = undefined;
+               }
+       }
+       return data;
+}
+
+jQuery.extend( {
+       hasData: function( elem ) {
+               return dataUser.hasData( elem ) || dataPriv.hasData( elem );
+       },
+
+       data: function( elem, name, data ) {
+               return dataUser.access( elem, name, data );
+       },
+
+       removeData: function( elem, name ) {
+               dataUser.remove( elem, name );
+       },
+
+       // TODO: Now that all calls to _data and _removeData have been replaced
+       // with direct calls to dataPriv methods, these can be deprecated.
+       _data: function( elem, name, data ) {
+               return dataPriv.access( elem, name, data );
+       },
+
+       _removeData: function( elem, name ) {
+               dataPriv.remove( elem, name );
+       }
+} );
+
+jQuery.fn.extend( {
+       data: function( key, value ) {
+               var i, name, data,
+                       elem = this[ 0 ],
+                       attrs = elem && elem.attributes;
+
+               // Gets all values
+               if ( key === undefined ) {
+                       if ( this.length ) {
+                               data = dataUser.get( elem );
+
+                               if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
+                                       i = attrs.length;
+                                       while ( i-- ) {
+
+                                               // Support: IE 11 only
+                                               // The attrs elements can be null (#14894)
+                                               if ( attrs[ i ] ) {
+                                                       name = attrs[ i ].name;
+                                                       if ( name.indexOf( "data-" ) === 0 ) {
+                                                               name = camelCase( name.slice( 5 ) );
+                                                               dataAttr( elem, name, data[ name ] );
+                                                       }
+                                               }
+                                       }
+                                       dataPriv.set( elem, "hasDataAttrs", true );
+                               }
+                       }
+
+                       return data;
+               }
+
+               // Sets multiple values
+               if ( typeof key === "object" ) {
+                       return this.each( function() {
+                               dataUser.set( this, key );
+                       } );
+               }
+
+               return access( this, function( value ) {
+                       var data;
+
+                       // The calling jQuery object (element matches) is not empty
+                       // (and therefore has an element appears at this[ 0 ]) and the
+                       // `value` parameter was not undefined. An empty jQuery object
+                       // will result in `undefined` for elem = this[ 0 ] which will
+                       // throw an exception if an attempt to read a data cache is made.
+                       if ( elem && value === undefined ) {
+
+                               // Attempt to get data from the cache
+                               // The key will always be camelCased in Data
+                               data = dataUser.get( elem, key );
+                               if ( data !== undefined ) {
+                                       return data;
+                               }
+
+                               // Attempt to "discover" the data in
+                               // HTML5 custom data-* attrs
+                               data = dataAttr( elem, key );
+                               if ( data !== undefined ) {
+                                       return data;
+                               }
+
+                               // We tried really hard, but the data doesn't exist.
+                               return;
+                       }
+
+                       // Set the data...
+                       this.each( function() {
+
+                               // We always store the camelCased key
+                               dataUser.set( this, key, value );
+                       } );
+               }, null, value, arguments.length > 1, null, true );
+       },
+
+       removeData: function( key ) {
+               return this.each( function() {
+                       dataUser.remove( this, key );
+               } );
+       }
+} );
+
+
+jQuery.extend( {
+       queue: function( elem, type, data ) {
+               var queue;
+
+               if ( elem ) {
+                       type = ( type || "fx" ) + "queue";
+                       queue = dataPriv.get( elem, type );
+
+                       // Speed up dequeue by getting out quickly if this is just a lookup
+                       if ( data ) {
+                               if ( !queue || Array.isArray( data ) ) {
+                                       queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
+                               } else {
+                                       queue.push( data );
+                               }
+                       }
+                       return queue || [];
+               }
+       },
+
+       dequeue: function( elem, type ) {
+               type = type || "fx";
+
+               var queue = jQuery.queue( elem, type ),
+                       startLength = queue.length,
+                       fn = queue.shift(),
+                       hooks = jQuery._queueHooks( elem, type ),
+                       next = function() {
+                               jQuery.dequeue( elem, type );
+                       };
+
+               // If the fx queue is dequeued, always remove the progress sentinel
+               if ( fn === "inprogress" ) {
+                       fn = queue.shift();
+                       startLength--;
+               }
+
+               if ( fn ) {
+
+                       // Add a progress sentinel to prevent the fx queue from being
+                       // automatically dequeued
+                       if ( type === "fx" ) {
+                               queue.unshift( "inprogress" );
+                       }
+
+                       // Clear up the last queue stop function
+                       delete hooks.stop;
+                       fn.call( elem, next, hooks );
+               }
+
+               if ( !startLength && hooks ) {
+                       hooks.empty.fire();
+               }
+       },
+
+       // Not public - generate a queueHooks object, or return the current one
+       _queueHooks: function( elem, type ) {
+               var key = type + "queueHooks";
+               return dataPriv.get( elem, key ) || dataPriv.access( elem, key, {
+                       empty: jQuery.Callbacks( "once memory" ).add( function() {
+                               dataPriv.remove( elem, [ type + "queue", key ] );
+                       } )
+               } );
+       }
+} );
+
+jQuery.fn.extend( {
+       queue: function( type, data ) {
+               var setter = 2;
+
+               if ( typeof type !== "string" ) {
+                       data = type;
+                       type = "fx";
+                       setter--;
+               }
+
+               if ( arguments.length < setter ) {
+                       return jQuery.queue( this[ 0 ], type );
+               }
+
+               return data === undefined ?
+                       this :
+                       this.each( function() {
+                               var queue = jQuery.queue( this, type, data );
+
+                               // Ensure a hooks for this queue
+                               jQuery._queueHooks( this, type );
+
+                               if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
+                                       jQuery.dequeue( this, type );
+                               }
+                       } );
+       },
+       dequeue: function( type ) {
+               return this.each( function() {
+                       jQuery.dequeue( this, type );
+               } );
+       },
+       clearQueue: function( type ) {
+               return this.queue( type || "fx", [] );
+       },
+
+       // Get a promise resolved when queues of a certain type
+       // are emptied (fx is the type by default)
+       promise: function( type, obj ) {
+               var tmp,
+                       count = 1,
+                       defer = jQuery.Deferred(),
+                       elements = this,
+                       i = this.length,
+                       resolve = function() {
+                               if ( !( --count ) ) {
+                                       defer.resolveWith( elements, [ elements ] );
+                               }
+                       };
+
+               if ( typeof type !== "string" ) {
+                       obj = type;
+                       type = undefined;
+               }
+               type = type || "fx";
+
+               while ( i-- ) {
+                       tmp = dataPriv.get( elements[ i ], type + "queueHooks" );
+                       if ( tmp && tmp.empty ) {
+                               count++;
+                               tmp.empty.add( resolve );
+                       }
+               }
+               resolve();
+               return defer.promise( obj );
+       }
+} );
+var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
+
+var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
+
+
+var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
+
+var documentElement = document.documentElement;
+
+
+
+       var isAttached = function( elem ) {
+                       return jQuery.contains( elem.ownerDocument, elem );
+               },
+               composed = { composed: true };
+
+       // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only
+       // Check attachment across shadow DOM boundaries when possible (gh-3504)
+       // Support: iOS 10.0-10.2 only
+       // Early iOS 10 versions support `attachShadow` but not `getRootNode`,
+       // leading to errors. We need to check for `getRootNode`.
+       if ( documentElement.getRootNode ) {
+               isAttached = function( elem ) {
+                       return jQuery.contains( elem.ownerDocument, elem ) ||
+                               elem.getRootNode( composed ) === elem.ownerDocument;
+               };
+       }
+var isHiddenWithinTree = function( elem, el ) {
+
+               // isHiddenWithinTree might be called from jQuery#filter function;
+               // in that case, element will be second argument
+               elem = el || elem;
+
+               // Inline style trumps all
+               return elem.style.display === "none" ||
+                       elem.style.display === "" &&
+
+                       // Otherwise, check computed style
+                       // Support: Firefox <=43 - 45
+                       // Disconnected elements can have computed display: none, so first confirm that elem is
+                       // in the document.
+                       isAttached( elem ) &&
+
+                       jQuery.css( elem, "display" ) === "none";
+       };
+
+var swap = function( elem, options, callback, args ) {
+       var ret, name,
+               old = {};
+
+       // Remember the old values, and insert the new ones
+       for ( name in options ) {
+               old[ name ] = elem.style[ name ];
+               elem.style[ name ] = options[ name ];
+       }
+
+       ret = callback.apply( elem, args || [] );
+
+       // Revert the old values
+       for ( name in options ) {
+               elem.style[ name ] = old[ name ];
+       }
+
+       return ret;
+};
+
+
+
+
+function adjustCSS( elem, prop, valueParts, tween ) {
+       var adjusted, scale,
+               maxIterations = 20,
+               currentValue = tween ?
+                       function() {
+                               return tween.cur();
+                       } :
+                       function() {
+                               return jQuery.css( elem, prop, "" );
+                       },
+               initial = currentValue(),
+               unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+               // Starting value computation is required for potential unit mismatches
+               initialInUnit = elem.nodeType &&
+                       ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
+                       rcssNum.exec( jQuery.css( elem, prop ) );
+
+       if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
+
+               // Support: Firefox <=54
+               // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)
+               initial = initial / 2;
+
+               // Trust units reported by jQuery.css
+               unit = unit || initialInUnit[ 3 ];
+
+               // Iteratively approximate from a nonzero starting point
+               initialInUnit = +initial || 1;
+
+               while ( maxIterations-- ) {
+
+                       // Evaluate and update our best guess (doubling guesses that zero out).
+                       // Finish if the scale equals or crosses 1 (making the old*new product non-positive).
+                       jQuery.style( elem, prop, initialInUnit + unit );
+                       if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {
+                               maxIterations = 0;
+                       }
+                       initialInUnit = initialInUnit / scale;
+
+               }
+
+               initialInUnit = initialInUnit * 2;
+               jQuery.style( elem, prop, initialInUnit + unit );
+
+               // Make sure we update the tween properties later on
+               valueParts = valueParts || [];
+       }
+
+       if ( valueParts ) {
+               initialInUnit = +initialInUnit || +initial || 0;
+
+               // Apply relative offset (+=/-=) if specified
+               adjusted = valueParts[ 1 ] ?
+                       initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+                       +valueParts[ 2 ];
+               if ( tween ) {
+                       tween.unit = unit;
+                       tween.start = initialInUnit;
+                       tween.end = adjusted;
+               }
+       }
+       return adjusted;
+}
+
+
+var defaultDisplayMap = {};
+
+function getDefaultDisplay( elem ) {
+       var temp,
+               doc = elem.ownerDocument,
+               nodeName = elem.nodeName,
+               display = defaultDisplayMap[ nodeName ];
+
+       if ( display ) {
+               return display;
+       }
+
+       temp = doc.body.appendChild( doc.createElement( nodeName ) );
+       display = jQuery.css( temp, "display" );
+
+       temp.parentNode.removeChild( temp );
+
+       if ( display === "none" ) {
+               display = "block";
+       }
+       defaultDisplayMap[ nodeName ] = display;
+
+       return display;
+}
+
+function showHide( elements, show ) {
+       var display, elem,
+               values = [],
+               index = 0,
+               length = elements.length;
+
+       // Determine new display value for elements that need to change
+       for ( ; index < length; index++ ) {
+               elem = elements[ index ];
+               if ( !elem.style ) {
+                       continue;
+               }
+
+               display = elem.style.display;
+               if ( show ) {
+
+                       // Since we force visibility upon cascade-hidden elements, an immediate (and slow)
+                       // check is required in this first loop unless we have a nonempty display value (either
+                       // inline or about-to-be-restored)
+                       if ( display === "none" ) {
+                               values[ index ] = dataPriv.get( elem, "display" ) || null;
+                               if ( !values[ index ] ) {
+                                       elem.style.display = "";
+                               }
+                       }
+                       if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
+                               values[ index ] = getDefaultDisplay( elem );
+                       }
+               } else {
+                       if ( display !== "none" ) {
+                               values[ index ] = "none";
+
+                               // Remember what we're overwriting
+                               dataPriv.set( elem, "display", display );
+                       }
+               }
+       }
+
+       // Set the display of the elements in a second loop to avoid constant reflow
+       for ( index = 0; index < length; index++ ) {
+               if ( values[ index ] != null ) {
+                       elements[ index ].style.display = values[ index ];
+               }
+       }
+
+       return elements;
+}
+
+jQuery.fn.extend( {
+       show: function() {
+               return showHide( this, true );
+       },
+       hide: function() {
+               return showHide( this );
+       },
+       toggle: function( state ) {
+               if ( typeof state === "boolean" ) {
+                       return state ? this.show() : this.hide();
+               }
+
+               return this.each( function() {
+                       if ( isHiddenWithinTree( this ) ) {
+                               jQuery( this ).show();
+                       } else {
+                               jQuery( this ).hide();
+                       }
+               } );
+       }
+} );
+var rcheckableType = ( /^(?:checkbox|radio)$/i );
+
+var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i );
+
+var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i );
+
+
+
+// We have to close these tags to support XHTML (#13200)
+var wrapMap = {
+
+       // Support: IE <=9 only
+       option: [ 1, "<select multiple='multiple'>", "</select>" ],
+
+       // XHTML parsers do not magically insert elements in the
+       // same way that tag soup parsers do. So we cannot shorten
+       // this by omitting <tbody> or other required elements.
+       thead: [ 1, "<table>", "</table>" ],
+       col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
+       tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+       td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+       _default: [ 0, "", "" ]
+};
+
+// Support: IE <=9 only
+wrapMap.optgroup = wrapMap.option;
+
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+
+function getAll( context, tag ) {
+
+       // Support: IE <=9 - 11 only
+       // Use typeof to avoid zero-argument method invocation on host objects (#15151)
+       var ret;
+
+       if ( typeof context.getElementsByTagName !== "undefined" ) {
+               ret = context.getElementsByTagName( tag || "*" );
+
+       } else if ( typeof context.querySelectorAll !== "undefined" ) {
+               ret = context.querySelectorAll( tag || "*" );
+
+       } else {
+               ret = [];
+       }
+
+       if ( tag === undefined || tag && nodeName( context, tag ) ) {
+               return jQuery.merge( [ context ], ret );
+       }
+
+       return ret;
+}
+
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+       var i = 0,
+               l = elems.length;
+
+       for ( ; i < l; i++ ) {
+               dataPriv.set(
+                       elems[ i ],
+                       "globalEval",
+                       !refElements || dataPriv.get( refElements[ i ], "globalEval" )
+               );
+       }
+}
+
+
+var rhtml = /<|&#?\w+;/;
+
+function buildFragment( elems, context, scripts, selection, ignored ) {
+       var elem, tmp, tag, wrap, attached, j,
+               fragment = context.createDocumentFragment(),
+               nodes = [],
+               i = 0,
+               l = elems.length;
+
+       for ( ; i < l; i++ ) {
+               elem = elems[ i ];
+
+               if ( elem || elem === 0 ) {
+
+                       // Add nodes directly
+                       if ( toType( elem ) === "object" ) {
+
+                               // Support: Android <=4.0 only, PhantomJS 1 only
+                               // push.apply(_, arraylike) throws on ancient WebKit
+                               jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+                       // Convert non-html into a text node
+                       } else if ( !rhtml.test( elem ) ) {
+                               nodes.push( context.createTextNode( elem ) );
+
+                       // Convert html into DOM nodes
+                       } else {
+                               tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
+
+                               // Deserialize a standard representation
+                               tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
+                               wrap = wrapMap[ tag ] || wrapMap._default;
+                               tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
+
+                               // Descend through wrappers to the right content
+                               j = wrap[ 0 ];
+                               while ( j-- ) {
+                                       tmp = tmp.lastChild;
+                               }
+
+                               // Support: Android <=4.0 only, PhantomJS 1 only
+                               // push.apply(_, arraylike) throws on ancient WebKit
+                               jQuery.merge( nodes, tmp.childNodes );
+
+                               // Remember the top-level container
+                               tmp = fragment.firstChild;
+
+                               // Ensure the created nodes are orphaned (#12392)
+                               tmp.textContent = "";
+                       }
+               }
+       }
+
+       // Remove wrapper from fragment
+       fragment.textContent = "";
+
+       i = 0;
+       while ( ( elem = nodes[ i++ ] ) ) {
+
+               // Skip elements already in the context collection (trac-4087)
+               if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
+                       if ( ignored ) {
+                               ignored.push( elem );
+                       }
+                       continue;
+               }
+
+               attached = isAttached( elem );
+
+               // Append to fragment
+               tmp = getAll( fragment.appendChild( elem ), "script" );
+
+               // Preserve script evaluation history
+               if ( attached ) {
+                       setGlobalEval( tmp );
+               }
+
+               // Capture executables
+               if ( scripts ) {
+                       j = 0;
+                       while ( ( elem = tmp[ j++ ] ) ) {
+                               if ( rscriptType.test( elem.type || "" ) ) {
+                                       scripts.push( elem );
+                               }
+                       }
+               }
+       }
+
+       return fragment;
+}
+
+
+( function() {
+       var fragment = document.createDocumentFragment(),
+               div = fragment.appendChild( document.createElement( "div" ) ),
+               input = document.createElement( "input" );
+
+       // Support: Android 4.0 - 4.3 only
+       // Check state lost if the name is set (#11217)
+       // Support: Windows Web Apps (WWA)
+       // `name` and `type` must use .setAttribute for WWA (#14901)
+       input.setAttribute( "type", "radio" );
+       input.setAttribute( "checked", "checked" );
+       input.setAttribute( "name", "t" );
+
+       div.appendChild( input );
+
+       // Support: Android <=4.1 only
+       // Older WebKit doesn't clone checked state correctly in fragments
+       support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+       // Support: IE <=11 only
+       // Make sure textarea (and checkbox) defaultValue is properly cloned
+       div.innerHTML = "<textarea>x</textarea>";
+       support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
+} )();
+
+
+var
+       rkeyEvent = /^key/,
+       rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
+       rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+
+function returnTrue() {
+       return true;
+}
+
+function returnFalse() {
+       return false;
+}
+
+// Support: IE <=9 - 11+
+// focus() and blur() are asynchronous, except when they are no-op.
+// So expect focus to be synchronous when the element is already active,
+// and blur to be synchronous when the element is not already active.
+// (focus and blur are always synchronous in other supported browsers,
+// this just defines when we can count on it).
+function expectSync( elem, type ) {
+       return ( elem === safeActiveElement() ) === ( type === "focus" );
+}
+
+// Support: IE <=9 only
+// Accessing document.activeElement can throw unexpectedly
+// https://bugs.jquery.com/ticket/13393
+function safeActiveElement() {
+       try {
+               return document.activeElement;
+       } catch ( err ) { }
+}
+
+function on( elem, types, selector, data, fn, one ) {
+       var origFn, type;
+
+       // Types can be a map of types/handlers
+       if ( typeof types === "object" ) {
+
+               // ( types-Object, selector, data )
+               if ( typeof selector !== "string" ) {
+
+                       // ( types-Object, data )
+                       data = data || selector;
+                       selector = undefined;
+               }
+               for ( type in types ) {
+                       on( elem, type, selector, data, types[ type ], one );
+               }
+               return elem;
+       }
+
+       if ( data == null && fn == null ) {
+
+               // ( types, fn )
+               fn = selector;
+               data = selector = undefined;
+       } else if ( fn == null ) {
+               if ( typeof selector === "string" ) {
+
+                       // ( types, selector, fn )
+                       fn = data;
+                       data = undefined;
+               } else {
+
+                       // ( types, data, fn )
+                       fn = data;
+                       data = selector;
+                       selector = undefined;
+               }
+       }
+       if ( fn === false ) {
+               fn = returnFalse;
+       } else if ( !fn ) {
+               return elem;
+       }
+
+       if ( one === 1 ) {
+               origFn = fn;
+               fn = function( event ) {
+
+                       // Can use an empty set, since event contains the info
+                       jQuery().off( event );
+                       return origFn.apply( this, arguments );
+               };
+
+               // Use same guid so caller can remove using origFn
+               fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+       }
+       return elem.each( function() {
+               jQuery.event.add( this, types, fn, data, selector );
+       } );
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+       global: {},
+
+       add: function( elem, types, handler, data, selector ) {
+
+               var handleObjIn, eventHandle, tmp,
+                       events, t, handleObj,
+                       special, handlers, type, namespaces, origType,
+                       elemData = dataPriv.get( elem );
+
+               // Don't attach events to noData or text/comment nodes (but allow plain objects)
+               if ( !elemData ) {
+                       return;
+               }
+
+               // Caller can pass in an object of custom data in lieu of the handler
+               if ( handler.handler ) {
+                       handleObjIn = handler;
+                       handler = handleObjIn.handler;
+                       selector = handleObjIn.selector;
+               }
+
+               // Ensure that invalid selectors throw exceptions at attach time
+               // Evaluate against documentElement in case elem is a non-element node (e.g., document)
+               if ( selector ) {
+                       jQuery.find.matchesSelector( documentElement, selector );
+               }
+
+               // Make sure that the handler has a unique ID, used to find/remove it later
+               if ( !handler.guid ) {
+                       handler.guid = jQuery.guid++;
+               }
+
+               // Init the element's event structure and main handler, if this is the first
+               if ( !( events = elemData.events ) ) {
+                       events = elemData.events = {};
+               }
+               if ( !( eventHandle = elemData.handle ) ) {
+                       eventHandle = elemData.handle = function( e ) {
+
+                               // Discard the second event of a jQuery.event.trigger() and
+                               // when an event is called after a page has unloaded
+                               return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
+                                       jQuery.event.dispatch.apply( elem, arguments ) : undefined;
+                       };
+               }
+
+               // Handle multiple events separated by a space
+               types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+               t = types.length;
+               while ( t-- ) {
+                       tmp = rtypenamespace.exec( types[ t ] ) || [];
+                       type = origType = tmp[ 1 ];
+                       namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+                       // There *must* be a type, no attaching namespace-only handlers
+                       if ( !type ) {
+                               continue;
+                       }
+
+                       // If event changes its type, use the special event handlers for the changed type
+                       special = jQuery.event.special[ type ] || {};
+
+                       // If selector defined, determine special event api type, otherwise given type
+                       type = ( selector ? special.delegateType : special.bindType ) || type;
+
+                       // Update special based on newly reset type
+                       special = jQuery.event.special[ type ] || {};
+
+                       // handleObj is passed to all event handlers
+                       handleObj = jQuery.extend( {
+                               type: type,
+                               origType: origType,
+                               data: data,
+                               handler: handler,
+                               guid: handler.guid,
+                               selector: selector,
+                               needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+                               namespace: namespaces.join( "." )
+                       }, handleObjIn );
+
+                       // Init the event handler queue if we're the first
+                       if ( !( handlers = events[ type ] ) ) {
+                               handlers = events[ type ] = [];
+                               handlers.delegateCount = 0;
+
+                               // Only use addEventListener if the special events handler returns false
+                               if ( !special.setup ||
+                                       special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+
+                                       if ( elem.addEventListener ) {
+                                               elem.addEventListener( type, eventHandle );
+                                       }
+                               }
+                       }
+
+                       if ( special.add ) {
+                               special.add.call( elem, handleObj );
+
+                               if ( !handleObj.handler.guid ) {
+                                       handleObj.handler.guid = handler.guid;
+                               }
+                       }
+
+                       // Add to the element's handler list, delegates in front
+                       if ( selector ) {
+                               handlers.splice( handlers.delegateCount++, 0, handleObj );
+                       } else {
+                               handlers.push( handleObj );
+                       }
+
+                       // Keep track of which events have ever been used, for event optimization
+                       jQuery.event.global[ type ] = true;
+               }
+
+       },
+
+       // Detach an event or set of events from an element
+       remove: function( elem, types, handler, selector, mappedTypes ) {
+
+               var j, origCount, tmp,
+                       events, t, handleObj,
+                       special, handlers, type, namespaces, origType,
+                       elemData = dataPriv.hasData( elem ) && dataPriv.get( elem );
+
+               if ( !elemData || !( events = elemData.events ) ) {
+                       return;
+               }
+
+               // Once for each type.namespace in types; type may be omitted
+               types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+               t = types.length;
+               while ( t-- ) {
+                       tmp = rtypenamespace.exec( types[ t ] ) || [];
+                       type = origType = tmp[ 1 ];
+                       namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+                       // Unbind all events (on this namespace, if provided) for the element
+                       if ( !type ) {
+                               for ( type in events ) {
+                                       jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+                               }
+                               continue;
+                       }
+
+                       special = jQuery.event.special[ type ] || {};
+                       type = ( selector ? special.delegateType : special.bindType ) || type;
+                       handlers = events[ type ] || [];
+                       tmp = tmp[ 2 ] &&
+                               new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );
+
+                       // Remove matching events
+                       origCount = j = handlers.length;
+                       while ( j-- ) {
+                               handleObj = handlers[ j ];
+
+                               if ( ( mappedTypes || origType === handleObj.origType ) &&
+                                       ( !handler || handler.guid === handleObj.guid ) &&
+                                       ( !tmp || tmp.test( handleObj.namespace ) ) &&
+                                       ( !selector || selector === handleObj.selector ||
+                                               selector === "**" && handleObj.selector ) ) {
+                                       handlers.splice( j, 1 );
+
+                                       if ( handleObj.selector ) {
+                                               handlers.delegateCount--;
+                                       }
+                                       if ( special.remove ) {
+                                               special.remove.call( elem, handleObj );
+                                       }
+                               }
+                       }
+
+                       // Remove generic event handler if we removed something and no more handlers exist
+                       // (avoids potential for endless recursion during removal of special event handlers)
+                       if ( origCount && !handlers.length ) {
+                               if ( !special.teardown ||
+                                       special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+
+                                       jQuery.removeEvent( elem, type, elemData.handle );
+                               }
+
+                               delete events[ type ];
+                       }
+               }
+
+               // Remove data and the expando if it's no longer used
+               if ( jQuery.isEmptyObject( events ) ) {
+                       dataPriv.remove( elem, "handle events" );
+               }
+       },
+
+       dispatch: function( nativeEvent ) {
+
+               // Make a writable jQuery.Event from the native event object
+               var event = jQuery.event.fix( nativeEvent );
+
+               var i, j, ret, matched, handleObj, handlerQueue,
+                       args = new Array( arguments.length ),
+                       handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
+                       special = jQuery.event.special[ event.type ] || {};
+
+               // Use the fix-ed jQuery.Event rather than the (read-only) native event
+               args[ 0 ] = event;
+
+               for ( i = 1; i < arguments.length; i++ ) {
+                       args[ i ] = arguments[ i ];
+               }
+
+               event.delegateTarget = this;
+
+               // Call the preDispatch hook for the mapped type, and let it bail if desired
+               if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+                       return;
+               }
+
+               // Determine handlers
+               handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+               // Run delegates first; they may want to stop propagation beneath us
+               i = 0;
+               while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
+                       event.currentTarget = matched.elem;
+
+                       j = 0;
+                       while ( ( handleObj = matched.handlers[ j++ ] ) &&
+                               !event.isImmediatePropagationStopped() ) {
+
+                               // If the event is namespaced, then each handler is only invoked if it is
+                               // specially universal or its namespaces are a superset of the event's.
+                               if ( !event.rnamespace || handleObj.namespace === false ||
+                                       event.rnamespace.test( handleObj.namespace ) ) {
+
+                                       event.handleObj = handleObj;
+                                       event.data = handleObj.data;
+
+                                       ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
+                                               handleObj.handler ).apply( matched.elem, args );
+
+                                       if ( ret !== undefined ) {
+                                               if ( ( event.result = ret ) === false ) {
+                                                       event.preventDefault();
+                                                       event.stopPropagation();
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // Call the postDispatch hook for the mapped type
+               if ( special.postDispatch ) {
+                       special.postDispatch.call( this, event );
+               }
+
+               return event.result;
+       },
+
+       handlers: function( event, handlers ) {
+               var i, handleObj, sel, matchedHandlers, matchedSelectors,
+                       handlerQueue = [],
+                       delegateCount = handlers.delegateCount,
+                       cur = event.target;
+
+               // Find delegate handlers
+               if ( delegateCount &&
+
+                       // Support: IE <=9
+                       // Black-hole SVG <use> instance trees (trac-13180)
+                       cur.nodeType &&
+
+                       // Support: Firefox <=42
+                       // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)
+                       // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click
+                       // Support: IE 11 only
+                       // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343)
+                       !( event.type === "click" && event.button >= 1 ) ) {
+
+                       for ( ; cur !== this; cur = cur.parentNode || this ) {
+
+                               // Don't check non-elements (#13208)
+                               // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+                               if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
+                                       matchedHandlers = [];
+                                       matchedSelectors = {};
+                                       for ( i = 0; i < delegateCount; i++ ) {
+                                               handleObj = handlers[ i ];
+
+                                               // Don't conflict with Object.prototype properties (#13203)
+                                               sel = handleObj.selector + " ";
+
+                                               if ( matchedSelectors[ sel ] === undefined ) {
+                                                       matchedSelectors[ sel ] = handleObj.needsContext ?
+                                                               jQuery( sel, this ).index( cur ) > -1 :
+                                                               jQuery.find( sel, this, null, [ cur ] ).length;
+                                               }
+                                               if ( matchedSelectors[ sel ] ) {
+                                                       matchedHandlers.push( handleObj );
+                                               }
+                                       }
+                                       if ( matchedHandlers.length ) {
+                                               handlerQueue.push( { elem: cur, handlers: matchedHandlers } );
+                                       }
+                               }
+                       }
+               }
+
+               // Add the remaining (directly-bound) handlers
+               cur = this;
+               if ( delegateCount < handlers.length ) {
+                       handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );
+               }
+
+               return handlerQueue;
+       },
+
+       addProp: function( name, hook ) {
+               Object.defineProperty( jQuery.Event.prototype, name, {
+                       enumerable: true,
+                       configurable: true,
+
+                       get: isFunction( hook ) ?
+                               function() {
+                                       if ( this.originalEvent ) {
+                                                       return hook( this.originalEvent );
+                                       }
+                               } :
+                               function() {
+                                       if ( this.originalEvent ) {
+                                                       return this.originalEvent[ name ];
+                                       }
+                               },
+
+                       set: function( value ) {
+                               Object.defineProperty( this, name, {
+                                       enumerable: true,
+                                       configurable: true,
+                                       writable: true,
+                                       value: value
+                               } );
+                       }
+               } );
+       },
+
+       fix: function( originalEvent ) {
+               return originalEvent[ jQuery.expando ] ?
+                       originalEvent :
+                       new jQuery.Event( originalEvent );
+       },
+
+       special: {
+               load: {
+
+                       // Prevent triggered image.load events from bubbling to window.load
+                       noBubble: true
+               },
+               click: {
+
+                       // Utilize native event to ensure correct state for checkable inputs
+                       setup: function( data ) {
+
+                               // For mutual compressibility with _default, replace `this` access with a local var.
+                               // `|| data` is dead code meant only to preserve the variable through minification.
+                               var el = this || data;
+
+                               // Claim the first handler
+                               if ( rcheckableType.test( el.type ) &&
+                                       el.click && nodeName( el, "input" ) ) {
+
+                                       // dataPriv.set( el, "click", ... )
+                                       leverageNative( el, "click", returnTrue );
+                               }
+
+                               // Return false to allow normal processing in the caller
+                               return false;
+                       },
+                       trigger: function( data ) {
+
+                               // For mutual compressibility with _default, replace `this` access with a local var.
+                               // `|| data` is dead code meant only to preserve the variable through minification.
+                               var el = this || data;
+
+                               // Force setup before triggering a click
+                               if ( rcheckableType.test( el.type ) &&
+                                       el.click && nodeName( el, "input" ) ) {
+
+                                       leverageNative( el, "click" );
+                               }
+
+                               // Return non-false to allow normal event-path propagation
+                               return true;
+                       },
+
+                       // For cross-browser consistency, suppress native .click() on links
+                       // Also prevent it if we're currently inside a leveraged native-event stack
+                       _default: function( event ) {
+                               var target = event.target;
+                               return rcheckableType.test( target.type ) &&
+                                       target.click && nodeName( target, "input" ) &&
+                                       dataPriv.get( target, "click" ) ||
+                                       nodeName( target, "a" );
+                       }
+               },
+
+               beforeunload: {
+                       postDispatch: function( event ) {
+
+                               // Support: Firefox 20+
+                               // Firefox doesn't alert if the returnValue field is not set.
+                               if ( event.result !== undefined && event.originalEvent ) {
+                                       event.originalEvent.returnValue = event.result;
+                               }
+                       }
+               }
+       }
+};
+
+// Ensure the presence of an event listener that handles manually-triggered
+// synthetic events by interrupting progress until reinvoked in response to
+// *native* events that it fires directly, ensuring that state changes have
+// already occurred before other listeners are invoked.
+function leverageNative( el, type, expectSync ) {
+
+       // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add
+       if ( !expectSync ) {
+               if ( dataPriv.get( el, type ) === undefined ) {
+                       jQuery.event.add( el, type, returnTrue );
+               }
+               return;
+       }
+
+       // Register the controller as a special universal handler for all event namespaces
+       dataPriv.set( el, type, false );
+       jQuery.event.add( el, type, {
+               namespace: false,
+               handler: function( event ) {
+                       var notAsync, result,
+                               saved = dataPriv.get( this, type );
+
+                       if ( ( event.isTrigger & 1 ) && this[ type ] ) {
+
+                               // Interrupt processing of the outer synthetic .trigger()ed event
+                               // Saved data should be false in such cases, but might be a leftover capture object
+                               // from an async native handler (gh-4350)
+                               if ( !saved.length ) {
+
+                                       // Store arguments for use when handling the inner native event
+                                       // There will always be at least one argument (an event object), so this array
+                                       // will not be confused with a leftover capture object.
+                                       saved = slice.call( arguments );
+                                       dataPriv.set( this, type, saved );
+
+                                       // Trigger the native event and capture its result
+                                       // Support: IE <=9 - 11+
+                                       // focus() and blur() are asynchronous
+                                       notAsync = expectSync( this, type );
+                                       this[ type ]();
+                                       result = dataPriv.get( this, type );
+                                       if ( saved !== result || notAsync ) {
+                                               dataPriv.set( this, type, false );
+                                       } else {
+                                               result = {};
+                                       }
+                                       if ( saved !== result ) {
+
+                                               // Cancel the outer synthetic event
+                                               event.stopImmediatePropagation();
+                                               event.preventDefault();
+                                               return result.value;
+                                       }
+
+                               // If this is an inner synthetic event for an event with a bubbling surrogate
+                               // (focus or blur), assume that the surrogate already propagated from triggering the
+                               // native event and prevent that from happening again here.
+                               // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the
+                               // bubbling surrogate propagates *after* the non-bubbling base), but that seems
+                               // less bad than duplication.
+                               } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {
+                                       event.stopPropagation();
+                               }
+
+                       // If this is a native event triggered above, everything is now in order
+                       // Fire an inner synthetic event with the original arguments
+                       } else if ( saved.length ) {
+
+                               // ...and capture the result
+                               dataPriv.set( this, type, {
+                                       value: jQuery.event.trigger(
+
+                                               // Support: IE <=9 - 11+
+                                               // Extend with the prototype to reset the above stopImmediatePropagation()
+                                               jQuery.extend( saved[ 0 ], jQuery.Event.prototype ),
+                                               saved.slice( 1 ),
+                                               this
+                                       )
+                               } );
+
+                               // Abort handling of the native event
+                               event.stopImmediatePropagation();
+                       }
+               }
+       } );
+}
+
+jQuery.removeEvent = function( elem, type, handle ) {
+
+       // This "if" is needed for plain objects
+       if ( elem.removeEventListener ) {
+               elem.removeEventListener( type, handle );
+       }
+};
+
+jQuery.Event = function( src, props ) {
+
+       // Allow instantiation without the 'new' keyword
+       if ( !( this instanceof jQuery.Event ) ) {
+               return new jQuery.Event( src, props );
+       }
+
+       // Event object
+       if ( src && src.type ) {
+               this.originalEvent = src;
+               this.type = src.type;
+
+               // Events bubbling up the document may have been marked as prevented
+               // by a handler lower down the tree; reflect the correct value.
+               this.isDefaultPrevented = src.defaultPrevented ||
+                               src.defaultPrevented === undefined &&
+
+                               // Support: Android <=2.3 only
+                               src.returnValue === false ?
+                       returnTrue :
+                       returnFalse;
+
+               // Create target properties
+               // Support: Safari <=6 - 7 only
+               // Target should not be a text node (#504, #13143)
+               this.target = ( src.target && src.target.nodeType === 3 ) ?
+                       src.target.parentNode :
+                       src.target;
+
+               this.currentTarget = src.currentTarget;
+               this.relatedTarget = src.relatedTarget;
+
+       // Event type
+       } else {
+               this.type = src;
+       }
+
+       // Put explicitly provided properties onto the event object
+       if ( props ) {
+               jQuery.extend( this, props );
+       }
+
+       // Create a timestamp if incoming event doesn't have one
+       this.timeStamp = src && src.timeStamp || Date.now();
+
+       // Mark it as fixed
+       this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+       constructor: jQuery.Event,
+       isDefaultPrevented: returnFalse,
+       isPropagationStopped: returnFalse,
+       isImmediatePropagationStopped: returnFalse,
+       isSimulated: false,
+
+       preventDefault: function() {
+               var e = this.originalEvent;
+
+               this.isDefaultPrevented = returnTrue;
+
+               if ( e && !this.isSimulated ) {
+                       e.preventDefault();
+               }
+       },
+       stopPropagation: function() {
+               var e = this.originalEvent;
+
+               this.isPropagationStopped = returnTrue;
+
+               if ( e && !this.isSimulated ) {
+                       e.stopPropagation();
+               }
+       },
+       stopImmediatePropagation: function() {
+               var e = this.originalEvent;
+
+               this.isImmediatePropagationStopped = returnTrue;
+
+               if ( e && !this.isSimulated ) {
+                       e.stopImmediatePropagation();
+               }
+
+               this.stopPropagation();
+       }
+};
+
+// Includes all common event props including KeyEvent and MouseEvent specific props
+jQuery.each( {
+       altKey: true,
+       bubbles: true,
+       cancelable: true,
+       changedTouches: true,
+       ctrlKey: true,
+       detail: true,
+       eventPhase: true,
+       metaKey: true,
+       pageX: true,
+       pageY: true,
+       shiftKey: true,
+       view: true,
+       "char": true,
+       code: true,
+       charCode: true,
+       key: true,
+       keyCode: true,
+       button: true,
+       buttons: true,
+       clientX: true,
+       clientY: true,
+       offsetX: true,
+       offsetY: true,
+       pointerId: true,
+       pointerType: true,
+       screenX: true,
+       screenY: true,
+       targetTouches: true,
+       toElement: true,
+       touches: true,
+
+       which: function( event ) {
+               var button = event.button;
+
+               // Add which for key events
+               if ( event.which == null && rkeyEvent.test( event.type ) ) {
+                       return event.charCode != null ? event.charCode : event.keyCode;
+               }
+
+               // Add which for click: 1 === left; 2 === middle; 3 === right
+               if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
+                       if ( button & 1 ) {
+                               return 1;
+                       }
+
+                       if ( button & 2 ) {
+                               return 3;
+                       }
+
+                       if ( button & 4 ) {
+                               return 2;
+                       }
+
+                       return 0;
+               }
+
+               return event.which;
+       }
+}, jQuery.event.addProp );
+
+jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {
+       jQuery.event.special[ type ] = {
+
+               // Utilize native event if possible so blur/focus sequence is correct
+               setup: function() {
+
+                       // Claim the first handler
+                       // dataPriv.set( this, "focus", ... )
+                       // dataPriv.set( this, "blur", ... )
+                       leverageNative( this, type, expectSync );
+
+                       // Return false to allow normal processing in the caller
+                       return false;
+               },
+               trigger: function() {
+
+                       // Force setup before trigger
+                       leverageNative( this, type );
+
+                       // Return non-false to allow normal event-path propagation
+                       return true;
+               },
+
+               delegateType: delegateType
+       };
+} );
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+// so that event delegation works in jQuery.
+// Do the same for pointerenter/pointerleave and pointerover/pointerout
+//
+// Support: Safari 7 only
+// Safari sends mouseenter too often; see:
+// https://bugs.chromium.org/p/chromium/issues/detail?id=470258
+// for the description of the bug (it existed in older Chrome versions as well).
+jQuery.each( {
+       mouseenter: "mouseover",
+       mouseleave: "mouseout",
+       pointerenter: "pointerover",
+       pointerleave: "pointerout"
+}, function( orig, fix ) {
+       jQuery.event.special[ orig ] = {
+               delegateType: fix,
+               bindType: fix,
+
+               handle: function( event ) {
+                       var ret,
+                               target = this,
+                               related = event.relatedTarget,
+                               handleObj = event.handleObj;
+
+                       // For mouseenter/leave call the handler if related is outside the target.
+                       // NB: No relatedTarget if the mouse left/entered the browser window
+                       if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {
+                               event.type = handleObj.origType;
+                               ret = handleObj.handler.apply( this, arguments );
+                               event.type = fix;
+                       }
+                       return ret;
+               }
+       };
+} );
+
+jQuery.fn.extend( {
+
+       on: function( types, selector, data, fn ) {
+               return on( this, types, selector, data, fn );
+       },
+       one: function( types, selector, data, fn ) {
+               return on( this, types, selector, data, fn, 1 );
+       },
+       off: function( types, selector, fn ) {
+               var handleObj, type;
+               if ( types && types.preventDefault && types.handleObj ) {
+
+                       // ( event )  dispatched jQuery.Event
+                       handleObj = types.handleObj;
+                       jQuery( types.delegateTarget ).off(
+                               handleObj.namespace ?
+                                       handleObj.origType + "." + handleObj.namespace :
+                                       handleObj.origType,
+                               handleObj.selector,
+                               handleObj.handler
+                       );
+                       return this;
+               }
+               if ( typeof types === "object" ) {
+
+                       // ( types-object [, selector] )
+                       for ( type in types ) {
+                               this.off( type, selector, types[ type ] );
+                       }
+                       return this;
+               }
+               if ( selector === false || typeof selector === "function" ) {
+
+                       // ( types [, fn] )
+                       fn = selector;
+                       selector = undefined;
+               }
+               if ( fn === false ) {
+                       fn = returnFalse;
+               }
+               return this.each( function() {
+                       jQuery.event.remove( this, types, fn, selector );
+               } );
+       }
+} );
+
+
+var
+
+       /* eslint-disable max-len */
+
+       // See https://github.com/eslint/eslint/issues/3229
+       rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,
+
+       /* eslint-enable */
+
+       // Support: IE <=10 - 11, Edge 12 - 13 only
+       // In IE/Edge using regex groups here causes severe slowdowns.
+       // See https://connect.microsoft.com/IE/feedback/details/1736512/
+       rnoInnerhtml = /<script|<style|<link/i,
+
+       // checked="checked" or checked
+       rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+       rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;
+
+// Prefer a tbody over its parent table for containing new rows
+function manipulationTarget( elem, content ) {
+       if ( nodeName( elem, "table" ) &&
+               nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) {
+
+               return jQuery( elem ).children( "tbody" )[ 0 ] || elem;
+       }
+
+       return elem;
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+       elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
+       return elem;
+}
+function restoreScript( elem ) {
+       if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) {
+               elem.type = elem.type.slice( 5 );
+       } else {
+               elem.removeAttribute( "type" );
+       }
+
+       return elem;
+}
+
+function cloneCopyEvent( src, dest ) {
+       var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
+
+       if ( dest.nodeType !== 1 ) {
+               return;
+       }
+
+       // 1. Copy private data: events, handlers, etc.
+       if ( dataPriv.hasData( src ) ) {
+               pdataOld = dataPriv.access( src );
+               pdataCur = dataPriv.set( dest, pdataOld );
+               events = pdataOld.events;
+
+               if ( events ) {
+                       delete pdataCur.handle;
+                       pdataCur.events = {};
+
+                       for ( type in events ) {
+                               for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+                                       jQuery.event.add( dest, type, events[ type ][ i ] );
+                               }
+                       }
+               }
+       }
+
+       // 2. Copy user data
+       if ( dataUser.hasData( src ) ) {
+               udataOld = dataUser.access( src );
+               udataCur = jQuery.extend( {}, udataOld );
+
+               dataUser.set( dest, udataCur );
+       }
+}
+
+// Fix IE bugs, see support tests
+function fixInput( src, dest ) {
+       var nodeName = dest.nodeName.toLowerCase();
+
+       // Fails to persist the checked state of a cloned checkbox or radio button.
+       if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+               dest.checked = src.checked;
+
+       // Fails to return the selected option to the default selected state when cloning options
+       } else if ( nodeName === "input" || nodeName === "textarea" ) {
+               dest.defaultValue = src.defaultValue;
+       }
+}
+
+function domManip( collection, args, callback, ignored ) {
+
+       // Flatten any nested arrays
+       args = concat.apply( [], args );
+
+       var fragment, first, scripts, hasScripts, node, doc,
+               i = 0,
+               l = collection.length,
+               iNoClone = l - 1,
+               value = args[ 0 ],
+               valueIsFunction = isFunction( value );
+
+       // We can't cloneNode fragments that contain checked, in WebKit
+       if ( valueIsFunction ||
+                       ( l > 1 && typeof value === "string" &&
+                               !support.checkClone && rchecked.test( value ) ) ) {
+               return collection.each( function( index ) {
+                       var self = collection.eq( index );
+                       if ( valueIsFunction ) {
+                               args[ 0 ] = value.call( this, index, self.html() );
+                       }
+                       domManip( self, args, callback, ignored );
+               } );
+       }
+
+       if ( l ) {
+               fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
+               first = fragment.firstChild;
+
+               if ( fragment.childNodes.length === 1 ) {
+                       fragment = first;
+               }
+
+               // Require either new content or an interest in ignored elements to invoke the callback
+               if ( first || ignored ) {
+                       scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+                       hasScripts = scripts.length;
+
+                       // Use the original fragment for the last item
+                       // instead of the first because it can end up
+                       // being emptied incorrectly in certain situations (#8070).
+                       for ( ; i < l; i++ ) {
+                               node = fragment;
+
+                               if ( i !== iNoClone ) {
+                                       node = jQuery.clone( node, true, true );
+
+                                       // Keep references to cloned scripts for later restoration
+                                       if ( hasScripts ) {
+
+                                               // Support: Android <=4.0 only, PhantomJS 1 only
+                                               // push.apply(_, arraylike) throws on ancient WebKit
+                                               jQuery.merge( scripts, getAll( node, "script" ) );
+                                       }
+                               }
+
+                               callback.call( collection[ i ], node, i );
+                       }
+
+                       if ( hasScripts ) {
+                               doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+                               // Reenable scripts
+                               jQuery.map( scripts, restoreScript );
+
+                               // Evaluate executable scripts on first document insertion
+                               for ( i = 0; i < hasScripts; i++ ) {
+                                       node = scripts[ i ];
+                                       if ( rscriptType.test( node.type || "" ) &&
+                                               !dataPriv.access( node, "globalEval" ) &&
+                                               jQuery.contains( doc, node ) ) {
+
+                                               if ( node.src && ( node.type || "" ).toLowerCase()  !== "module" ) {
+
+                                                       // Optional AJAX dependency, but won't run scripts if not present
+                                                       if ( jQuery._evalUrl && !node.noModule ) {
+                                                               jQuery._evalUrl( node.src, {
+                                                                       nonce: node.nonce || node.getAttribute( "nonce" )
+                                                               } );
+                                                       }
+                                               } else {
+                                                       DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc );
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       return collection;
+}
+
+function remove( elem, selector, keepData ) {
+       var node,
+               nodes = selector ? jQuery.filter( selector, elem ) : elem,
+               i = 0;
+
+       for ( ; ( node = nodes[ i ] ) != null; i++ ) {
+               if ( !keepData && node.nodeType === 1 ) {
+                       jQuery.cleanData( getAll( node ) );
+               }
+
+               if ( node.parentNode ) {
+                       if ( keepData && isAttached( node ) ) {
+                               setGlobalEval( getAll( node, "script" ) );
+                       }
+                       node.parentNode.removeChild( node );
+               }
+       }
+
+       return elem;
+}
+
+jQuery.extend( {
+       htmlPrefilter: function( html ) {
+               return html.replace( rxhtmlTag, "<$1></$2>" );
+       },
+
+       clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+               var i, l, srcElements, destElements,
+                       clone = elem.cloneNode( true ),
+                       inPage = isAttached( elem );
+
+               // Fix IE cloning issues
+               if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
+                               !jQuery.isXMLDoc( elem ) ) {
+
+                       // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2
+                       destElements = getAll( clone );
+                       srcElements = getAll( elem );
+
+                       for ( i = 0, l = srcElements.length; i < l; i++ ) {
+                               fixInput( srcElements[ i ], destElements[ i ] );
+                       }
+               }
+
+               // Copy the events from the original to the clone
+               if ( dataAndEvents ) {
+                       if ( deepDataAndEvents ) {
+                               srcElements = srcElements || getAll( elem );
+                               destElements = destElements || getAll( clone );
+
+                               for ( i = 0, l = srcElements.length; i < l; i++ ) {
+                                       cloneCopyEvent( srcElements[ i ], destElements[ i ] );
+                               }
+                       } else {
+                               cloneCopyEvent( elem, clone );
+                       }
+               }
+
+               // Preserve script evaluation history
+               destElements = getAll( clone, "script" );
+               if ( destElements.length > 0 ) {
+                       setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+               }
+
+               // Return the cloned set
+               return clone;
+       },
+
+       cleanData: function( elems ) {
+               var data, elem, type,
+                       special = jQuery.event.special,
+                       i = 0;
+
+               for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
+                       if ( acceptData( elem ) ) {
+                               if ( ( data = elem[ dataPriv.expando ] ) ) {
+                                       if ( data.events ) {
+                                               for ( type in data.events ) {
+                                                       if ( special[ type ] ) {
+                                                               jQuery.event.remove( elem, type );
+
+                                                       // This is a shortcut to avoid jQuery.event.remove's overhead
+                                                       } else {
+                                                               jQuery.removeEvent( elem, type, data.handle );
+                                                       }
+                                               }
+                                       }
+
+                                       // Support: Chrome <=35 - 45+
+                                       // Assign undefined instead of using delete, see Data#remove
+                                       elem[ dataPriv.expando ] = undefined;
+                               }
+                               if ( elem[ dataUser.expando ] ) {
+
+                                       // Support: Chrome <=35 - 45+
+                                       // Assign undefined instead of using delete, see Data#remove
+                                       elem[ dataUser.expando ] = undefined;
+                               }
+                       }
+               }
+       }
+} );
+
+jQuery.fn.extend( {
+       detach: function( selector ) {
+               return remove( this, selector, true );
+       },
+
+       remove: function( selector ) {
+               return remove( this, selector );
+       },
+
+       text: function( value ) {
+               return access( this, function( value ) {
+                       return value === undefined ?
+                               jQuery.text( this ) :
+                               this.empty().each( function() {
+                                       if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                                               this.textContent = value;
+                                       }
+                               } );
+               }, null, value, arguments.length );
+       },
+
+       append: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                               var target = manipulationTarget( this, elem );
+                               target.appendChild( elem );
+                       }
+               } );
+       },
+
+       prepend: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                               var target = manipulationTarget( this, elem );
+                               target.insertBefore( elem, target.firstChild );
+                       }
+               } );
+       },
+
+       before: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.parentNode ) {
+                               this.parentNode.insertBefore( elem, this );
+                       }
+               } );
+       },
+
+       after: function() {
+               return domManip( this, arguments, function( elem ) {
+                       if ( this.parentNode ) {
+                               this.parentNode.insertBefore( elem, this.nextSibling );
+                       }
+               } );
+       },
+
+       empty: function() {
+               var elem,
+                       i = 0;
+
+               for ( ; ( elem = this[ i ] ) != null; i++ ) {
+                       if ( elem.nodeType === 1 ) {
+
+                               // Prevent memory leaks
+                               jQuery.cleanData( getAll( elem, false ) );
+
+                               // Remove any remaining nodes
+                               elem.textContent = "";
+                       }
+               }
+
+               return this;
+       },
+
+       clone: function( dataAndEvents, deepDataAndEvents ) {
+               dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+               deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+               return this.map( function() {
+                       return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+               } );
+       },
+
+       html: function( value ) {
+               return access( this, function( value ) {
+                       var elem = this[ 0 ] || {},
+                               i = 0,
+                               l = this.length;
+
+                       if ( value === undefined && elem.nodeType === 1 ) {
+                               return elem.innerHTML;
+                       }
+
+                       // See if we can take a shortcut and just use innerHTML
+                       if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+                               !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
+
+                               value = jQuery.htmlPrefilter( value );
+
+                               try {
+                                       for ( ; i < l; i++ ) {
+                                               elem = this[ i ] || {};
+
+                                               // Remove element nodes and prevent memory leaks
+                                               if ( elem.nodeType === 1 ) {
+                                                       jQuery.cleanData( getAll( elem, false ) );
+                                                       elem.innerHTML = value;
+                                               }
+                                       }
+
+                                       elem = 0;
+
+                               // If using innerHTML throws an exception, use the fallback method
+                               } catch ( e ) {}
+                       }
+
+                       if ( elem ) {
+                               this.empty().append( value );
+                       }
+               }, null, value, arguments.length );
+       },
+
+       replaceWith: function() {
+               var ignored = [];
+
+               // Make the changes, replacing each non-ignored context element with the new content
+               return domManip( this, arguments, function( elem ) {
+                       var parent = this.parentNode;
+
+                       if ( jQuery.inArray( this, ignored ) < 0 ) {
+                               jQuery.cleanData( getAll( this ) );
+                               if ( parent ) {
+                                       parent.replaceChild( elem, this );
+                               }
+                       }
+
+               // Force callback invocation
+               }, ignored );
+       }
+} );
+
+jQuery.each( {
+       appendTo: "append",
+       prependTo: "prepend",
+       insertBefore: "before",
+       insertAfter: "after",
+       replaceAll: "replaceWith"
+}, function( name, original ) {
+       jQuery.fn[ name ] = function( selector ) {
+               var elems,
+                       ret = [],
+                       insert = jQuery( selector ),
+                       last = insert.length - 1,
+                       i = 0;
+
+               for ( ; i <= last; i++ ) {
+                       elems = i === last ? this : this.clone( true );
+                       jQuery( insert[ i ] )[ original ]( elems );
+
+                       // Support: Android <=4.0 only, PhantomJS 1 only
+                       // .get() because push.apply(_, arraylike) throws on ancient WebKit
+                       push.apply( ret, elems.get() );
+               }
+
+               return this.pushStack( ret );
+       };
+} );
+var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
+
+var getStyles = function( elem ) {
+
+               // Support: IE <=11 only, Firefox <=30 (#15098, #14150)
+               // IE throws on elements created in popups
+               // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
+               var view = elem.ownerDocument.defaultView;
+
+               if ( !view || !view.opener ) {
+                       view = window;
+               }
+
+               return view.getComputedStyle( elem );
+       };
+
+var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
+
+
+
+( function() {
+
+       // Executing both pixelPosition & boxSizingReliable tests require only one layout
+       // so they're executed at the same time to save the second computation.
+       function computeStyleTests() {
+
+               // This is a singleton, we need to execute it only once
+               if ( !div ) {
+                       return;
+               }
+
+               container.style.cssText = "position:absolute;left:-11111px;width:60px;" +
+                       "margin-top:1px;padding:0;border:0";
+               div.style.cssText =
+                       "position:relative;display:block;box-sizing:border-box;overflow:scroll;" +
+                       "margin:auto;border:1px;padding:1px;" +
+                       "width:60%;top:1%";
+               documentElement.appendChild( container ).appendChild( div );
+
+               var divStyle = window.getComputedStyle( div );
+               pixelPositionVal = divStyle.top !== "1%";
+
+               // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44
+               reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;
+
+               // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3
+               // Some styles come back with percentage values, even though they shouldn't
+               div.style.right = "60%";
+               pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;
+
+               // Support: IE 9 - 11 only
+               // Detect misreporting of content dimensions for box-sizing:border-box elements
+               boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;
+
+               // Support: IE 9 only
+               // Detect overflow:scroll screwiness (gh-3699)
+               // Support: Chrome <=64
+               // Don't get tricked when zoom affects offsetWidth (gh-4029)
+               div.style.position = "absolute";
+               scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12;
+
+               documentElement.removeChild( container );
+
+               // Nullify the div so it wouldn't be stored in the memory and
+               // it will also be a sign that checks already performed
+               div = null;
+       }
+
+       function roundPixelMeasures( measure ) {
+               return Math.round( parseFloat( measure ) );
+       }
+
+       var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,
+               reliableMarginLeftVal,
+               container = document.createElement( "div" ),
+               div = document.createElement( "div" );
+
+       // Finish early in limited (non-browser) environments
+       if ( !div.style ) {
+               return;
+       }
+
+       // Support: IE <=9 - 11 only
+       // Style of cloned element affects source element cloned (#8908)
+       div.style.backgroundClip = "content-box";
+       div.cloneNode( true ).style.backgroundClip = "";
+       support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+       jQuery.extend( support, {
+               boxSizingReliable: function() {
+                       computeStyleTests();
+                       return boxSizingReliableVal;
+               },
+               pixelBoxStyles: function() {
+                       computeStyleTests();
+                       return pixelBoxStylesVal;
+               },
+               pixelPosition: function() {
+                       computeStyleTests();
+                       return pixelPositionVal;
+               },
+               reliableMarginLeft: function() {
+                       computeStyleTests();
+                       return reliableMarginLeftVal;
+               },
+               scrollboxSize: function() {
+                       computeStyleTests();
+                       return scrollboxSizeVal;
+               }
+       } );
+} )();
+
+
+function curCSS( elem, name, computed ) {
+       var width, minWidth, maxWidth, ret,
+
+               // Support: Firefox 51+
+               // Retrieving style before computed somehow
+               // fixes an issue with getting wrong values
+               // on detached elements
+               style = elem.style;
+
+       computed = computed || getStyles( elem );
+
+       // getPropertyValue is needed for:
+       //   .css('filter') (IE 9 only, #12537)
+       //   .css('--customProperty) (#3144)
+       if ( computed ) {
+               ret = computed.getPropertyValue( name ) || computed[ name ];
+
+               if ( ret === "" && !isAttached( elem ) ) {
+                       ret = jQuery.style( elem, name );
+               }
+
+               // A tribute to the "awesome hack by Dean Edwards"
+               // Android Browser returns percentage for some values,
+               // but width seems to be reliably pixels.
+               // This is against the CSSOM draft spec:
+               // https://drafts.csswg.org/cssom/#resolved-values
+               if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {
+
+                       // Remember the original values
+                       width = style.width;
+                       minWidth = style.minWidth;
+                       maxWidth = style.maxWidth;
+
+                       // Put in the new values to get a computed value out
+                       style.minWidth = style.maxWidth = style.width = ret;
+                       ret = computed.width;
+
+                       // Revert the changed values
+                       style.width = width;
+                       style.minWidth = minWidth;
+                       style.maxWidth = maxWidth;
+               }
+       }
+
+       return ret !== undefined ?
+
+               // Support: IE <=9 - 11 only
+               // IE returns zIndex value as an integer.
+               ret + "" :
+               ret;
+}
+
+
+function addGetHookIf( conditionFn, hookFn ) {
+
+       // Define the hook, we'll check on the first run if it's really needed.
+       return {
+               get: function() {
+                       if ( conditionFn() ) {
+
+                               // Hook not needed (or it's not possible to use it due
+                               // to missing dependency), remove it.
+                               delete this.get;
+                               return;
+                       }
+
+                       // Hook needed; redefine it so that the support test is not executed again.
+                       return ( this.get = hookFn ).apply( this, arguments );
+               }
+       };
+}
+
+
+var cssPrefixes = [ "Webkit", "Moz", "ms" ],
+       emptyStyle = document.createElement( "div" ).style,
+       vendorProps = {};
+
+// Return a vendor-prefixed property or undefined
+function vendorPropName( name ) {
+
+       // Check for vendor prefixed names
+       var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
+               i = cssPrefixes.length;
+
+       while ( i-- ) {
+               name = cssPrefixes[ i ] + capName;
+               if ( name in emptyStyle ) {
+                       return name;
+               }
+       }
+}
+
+// Return a potentially-mapped jQuery.cssProps or vendor prefixed property
+function finalPropName( name ) {
+       var final = jQuery.cssProps[ name ] || vendorProps[ name ];
+
+       if ( final ) {
+               return final;
+       }
+       if ( name in emptyStyle ) {
+               return name;
+       }
+       return vendorProps[ name ] = vendorPropName( name ) || name;
+}
+
+
+var
+
+       // Swappable if display is none or starts with table
+       // except "table", "table-cell", or "table-caption"
+       // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+       rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+       rcustomProp = /^--/,
+       cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+       cssNormalTransform = {
+               letterSpacing: "0",
+               fontWeight: "400"
+       };
+
+function setPositiveNumber( elem, value, subtract ) {
+
+       // Any relative (+/-) values have already been
+       // normalized at this point
+       var matches = rcssNum.exec( value );
+       return matches ?
+
+               // Guard against undefined "subtract", e.g., when used as in cssHooks
+               Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) :
+               value;
+}
+
+function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {
+       var i = dimension === "width" ? 1 : 0,
+               extra = 0,
+               delta = 0;
+
+       // Adjustment may not be necessary
+       if ( box === ( isBorderBox ? "border" : "content" ) ) {
+               return 0;
+       }
+
+       for ( ; i < 4; i += 2 ) {
+
+               // Both box models exclude margin
+               if ( box === "margin" ) {
+                       delta += jQuery.css( elem, box + cssExpand[ i ], true, styles );
+               }
+
+               // If we get here with a content-box, we're seeking "padding" or "border" or "margin"
+               if ( !isBorderBox ) {
+
+                       // Add padding
+                       delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+                       // For "border" or "margin", add border
+                       if ( box !== "padding" ) {
+                               delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+
+                       // But still keep track of it otherwise
+                       } else {
+                               extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+                       }
+
+               // If we get here with a border-box (content + padding + border), we're seeking "content" or
+               // "padding" or "margin"
+               } else {
+
+                       // For "content", subtract padding
+                       if ( box === "content" ) {
+                               delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+                       }
+
+                       // For "content" or "padding", subtract border
+                       if ( box !== "margin" ) {
+                               delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+                       }
+               }
+       }
+
+       // Account for positive content-box scroll gutter when requested by providing computedVal
+       if ( !isBorderBox && computedVal >= 0 ) {
+
+               // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border
+               // Assuming integer scroll gutter, subtract the rest and round down
+               delta += Math.max( 0, Math.ceil(
+                       elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+                       computedVal -
+                       delta -
+                       extra -
+                       0.5
+
+               // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter
+               // Use an explicit zero to avoid NaN (gh-3964)
+               ) ) || 0;
+       }
+
+       return delta;
+}
+
+function getWidthOrHeight( elem, dimension, extra ) {
+
+       // Start with computed style
+       var styles = getStyles( elem ),
+
+               // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).
+               // Fake content-box until we know it's needed to know the true value.
+               boxSizingNeeded = !support.boxSizingReliable() || extra,
+               isBorderBox = boxSizingNeeded &&
+                       jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+               valueIsBorderBox = isBorderBox,
+
+               val = curCSS( elem, dimension, styles ),
+               offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );
+
+       // Support: Firefox <=54
+       // Return a confounding non-pixel value or feign ignorance, as appropriate.
+       if ( rnumnonpx.test( val ) ) {
+               if ( !extra ) {
+                       return val;
+               }
+               val = "auto";
+       }
+
+
+       // Fall back to offsetWidth/offsetHeight when value is "auto"
+       // This happens for inline elements with no explicit setting (gh-3571)
+       // Support: Android <=4.1 - 4.3 only
+       // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)
+       // Support: IE 9-11 only
+       // Also use offsetWidth/offsetHeight for when box sizing is unreliable
+       // We use getClientRects() to check for hidden/disconnected.
+       // In those cases, the computed value can be trusted to be border-box
+       if ( ( !support.boxSizingReliable() && isBorderBox ||
+               val === "auto" ||
+               !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) &&
+               elem.getClientRects().length ) {
+
+               isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+               // Where available, offsetWidth/offsetHeight approximate border box dimensions.
+               // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the
+               // retrieved value as a content box dimension.
+               valueIsBorderBox = offsetProp in elem;
+               if ( valueIsBorderBox ) {
+                       val = elem[ offsetProp ];
+               }
+       }
+
+       // Normalize "" and auto
+       val = parseFloat( val ) || 0;
+
+       // Adjust for the element's box model
+       return ( val +
+               boxModelAdjustment(
+                       elem,
+                       dimension,
+                       extra || ( isBorderBox ? "border" : "content" ),
+                       valueIsBorderBox,
+                       styles,
+
+                       // Provide the current computed size to request scroll gutter calculation (gh-3589)
+                       val
+               )
+       ) + "px";
+}
+
+jQuery.extend( {
+
+       // Add in style property hooks for overriding the default
+       // behavior of getting and setting a style property
+       cssHooks: {
+               opacity: {
+                       get: function( elem, computed ) {
+                               if ( computed ) {
+
+                                       // We should always get a number back from opacity
+                                       var ret = curCSS( elem, "opacity" );
+                                       return ret === "" ? "1" : ret;
+                               }
+                       }
+               }
+       },
+
+       // Don't automatically add "px" to these possibly-unitless properties
+       cssNumber: {
+               "animationIterationCount": true,
+               "columnCount": true,
+               "fillOpacity": true,
+               "flexGrow": true,
+               "flexShrink": true,
+               "fontWeight": true,
+               "gridArea": true,
+               "gridColumn": true,
+               "gridColumnEnd": true,
+               "gridColumnStart": true,
+               "gridRow": true,
+               "gridRowEnd": true,
+               "gridRowStart": true,
+               "lineHeight": true,
+               "opacity": true,
+               "order": true,
+               "orphans": true,
+               "widows": true,
+               "zIndex": true,
+               "zoom": true
+       },
+
+       // Add in properties whose names you wish to fix before
+       // setting or getting the value
+       cssProps: {},
+
+       // Get and set the style property on a DOM Node
+       style: function( elem, name, value, extra ) {
+
+               // Don't set styles on text and comment nodes
+               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+                       return;
+               }
+
+               // Make sure that we're working with the right name
+               var ret, type, hooks,
+                       origName = camelCase( name ),
+                       isCustomProp = rcustomProp.test( name ),
+                       style = elem.style;
+
+               // Make sure that we're working with the right name. We don't
+               // want to query the value if it is a CSS custom property
+               // since they are user-defined.
+               if ( !isCustomProp ) {
+                       name = finalPropName( origName );
+               }
+
+               // Gets hook for the prefixed version, then unprefixed version
+               hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+               // Check if we're setting a value
+               if ( value !== undefined ) {
+                       type = typeof value;
+
+                       // Convert "+=" or "-=" to relative numbers (#7345)
+                       if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
+                               value = adjustCSS( elem, name, ret );
+
+                               // Fixes bug #9237
+                               type = "number";
+                       }
+
+                       // Make sure that null and NaN values aren't set (#7116)
+                       if ( value == null || value !== value ) {
+                               return;
+                       }
+
+                       // If a number was passed in, add the unit (except for certain CSS properties)
+                       // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append
+                       // "px" to a few hardcoded values.
+                       if ( type === "number" && !isCustomProp ) {
+                               value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
+                       }
+
+                       // background-* props affect original clone's values
+                       if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
+                               style[ name ] = "inherit";
+                       }
+
+                       // If a hook was provided, use that value, otherwise just set the specified value
+                       if ( !hooks || !( "set" in hooks ) ||
+                               ( value = hooks.set( elem, value, extra ) ) !== undefined ) {
+
+                               if ( isCustomProp ) {
+                                       style.setProperty( name, value );
+                               } else {
+                                       style[ name ] = value;
+                               }
+                       }
+
+               } else {
+
+                       // If a hook was provided get the non-computed value from there
+                       if ( hooks && "get" in hooks &&
+                               ( ret = hooks.get( elem, false, extra ) ) !== undefined ) {
+
+                               return ret;
+                       }
+
+                       // Otherwise just get the value from the style object
+                       return style[ name ];
+               }
+       },
+
+       css: function( elem, name, extra, styles ) {
+               var val, num, hooks,
+                       origName = camelCase( name ),
+                       isCustomProp = rcustomProp.test( name );
+
+               // Make sure that we're working with the right name. We don't
+               // want to modify the value if it is a CSS custom property
+               // since they are user-defined.
+               if ( !isCustomProp ) {
+                       name = finalPropName( origName );
+               }
+
+               // Try prefixed name followed by the unprefixed name
+               hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+               // If a hook was provided get the computed value from there
+               if ( hooks && "get" in hooks ) {
+                       val = hooks.get( elem, true, extra );
+               }
+
+               // Otherwise, if a way to get the computed value exists, use that
+               if ( val === undefined ) {
+                       val = curCSS( elem, name, styles );
+               }
+
+               // Convert "normal" to computed value
+               if ( val === "normal" && name in cssNormalTransform ) {
+                       val = cssNormalTransform[ name ];
+               }
+
+               // Make numeric if forced or a qualifier was provided and val looks numeric
+               if ( extra === "" || extra ) {
+                       num = parseFloat( val );
+                       return extra === true || isFinite( num ) ? num || 0 : val;
+               }
+
+               return val;
+       }
+} );
+
+jQuery.each( [ "height", "width" ], function( i, dimension ) {
+       jQuery.cssHooks[ dimension ] = {
+               get: function( elem, computed, extra ) {
+                       if ( computed ) {
+
+                               // Certain elements can have dimension info if we invisibly show them
+                               // but it must have a current display style that would benefit
+                               return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
+
+                                       // Support: Safari 8+
+                                       // Table columns in Safari have non-zero offsetWidth & zero
+                                       // getBoundingClientRect().width unless display is changed.
+                                       // Support: IE <=11 only
+                                       // Running getBoundingClientRect on a disconnected node
+                                       // in IE throws an error.
+                                       ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
+                                               swap( elem, cssShow, function() {
+                                                       return getWidthOrHeight( elem, dimension, extra );
+                                               } ) :
+                                               getWidthOrHeight( elem, dimension, extra );
+                       }
+               },
+
+               set: function( elem, value, extra ) {
+                       var matches,
+                               styles = getStyles( elem ),
+
+                               // Only read styles.position if the test has a chance to fail
+                               // to avoid forcing a reflow.
+                               scrollboxSizeBuggy = !support.scrollboxSize() &&
+                                       styles.position === "absolute",
+
+                               // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)
+                               boxSizingNeeded = scrollboxSizeBuggy || extra,
+                               isBorderBox = boxSizingNeeded &&
+                                       jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+                               subtract = extra ?
+                                       boxModelAdjustment(
+                                               elem,
+                                               dimension,
+                                               extra,
+                                               isBorderBox,
+                                               styles
+                                       ) :
+                                       0;
+
+                       // Account for unreliable border-box dimensions by comparing offset* to computed and
+                       // faking a content-box to get border and padding (gh-3699)
+                       if ( isBorderBox && scrollboxSizeBuggy ) {
+                               subtract -= Math.ceil(
+                                       elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+                                       parseFloat( styles[ dimension ] ) -
+                                       boxModelAdjustment( elem, dimension, "border", false, styles ) -
+                                       0.5
+                               );
+                       }
+
+                       // Convert to pixels if value adjustment is needed
+                       if ( subtract && ( matches = rcssNum.exec( value ) ) &&
+                               ( matches[ 3 ] || "px" ) !== "px" ) {
+
+                               elem.style[ dimension ] = value;
+                               value = jQuery.css( elem, dimension );
+                       }
+
+                       return setPositiveNumber( elem, value, subtract );
+               }
+       };
+} );
+
+jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
+       function( elem, computed ) {
+               if ( computed ) {
+                       return ( parseFloat( curCSS( elem, "marginLeft" ) ) ||
+                               elem.getBoundingClientRect().left -
+                                       swap( elem, { marginLeft: 0 }, function() {
+                                               return elem.getBoundingClientRect().left;
+                                       } )
+                               ) + "px";
+               }
+       }
+);
+
+// These hooks are used by animate to expand properties
+jQuery.each( {
+       margin: "",
+       padding: "",
+       border: "Width"
+}, function( prefix, suffix ) {
+       jQuery.cssHooks[ prefix + suffix ] = {
+               expand: function( value ) {
+                       var i = 0,
+                               expanded = {},
+
+                               // Assumes a single number if not a string
+                               parts = typeof value === "string" ? value.split( " " ) : [ value ];
+
+                       for ( ; i < 4; i++ ) {
+                               expanded[ prefix + cssExpand[ i ] + suffix ] =
+                                       parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+                       }
+
+                       return expanded;
+               }
+       };
+
+       if ( prefix !== "margin" ) {
+               jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+       }
+} );
+
+jQuery.fn.extend( {
+       css: function( name, value ) {
+               return access( this, function( elem, name, value ) {
+                       var styles, len,
+                               map = {},
+                               i = 0;
+
+                       if ( Array.isArray( name ) ) {
+                               styles = getStyles( elem );
+                               len = name.length;
+
+                               for ( ; i < len; i++ ) {
+                                       map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+                               }
+
+                               return map;
+                       }
+
+                       return value !== undefined ?
+                               jQuery.style( elem, name, value ) :
+                               jQuery.css( elem, name );
+               }, name, value, arguments.length > 1 );
+       }
+} );
+
+
+function Tween( elem, options, prop, end, easing ) {
+       return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+       constructor: Tween,
+       init: function( elem, options, prop, end, easing, unit ) {
+               this.elem = elem;
+               this.prop = prop;
+               this.easing = easing || jQuery.easing._default;
+               this.options = options;
+               this.start = this.now = this.cur();
+               this.end = end;
+               this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+       },
+       cur: function() {
+               var hooks = Tween.propHooks[ this.prop ];
+
+               return hooks && hooks.get ?
+                       hooks.get( this ) :
+                       Tween.propHooks._default.get( this );
+       },
+       run: function( percent ) {
+               var eased,
+                       hooks = Tween.propHooks[ this.prop ];
+
+               if ( this.options.duration ) {
+                       this.pos = eased = jQuery.easing[ this.easing ](
+                               percent, this.options.duration * percent, 0, 1, this.options.duration
+                       );
+               } else {
+                       this.pos = eased = percent;
+               }
+               this.now = ( this.end - this.start ) * eased + this.start;
+
+               if ( this.options.step ) {
+                       this.options.step.call( this.elem, this.now, this );
+               }
+
+               if ( hooks && hooks.set ) {
+                       hooks.set( this );
+               } else {
+                       Tween.propHooks._default.set( this );
+               }
+               return this;
+       }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+       _default: {
+               get: function( tween ) {
+                       var result;
+
+                       // Use a property on the element directly when it is not a DOM element,
+                       // or when there is no matching style property that exists.
+                       if ( tween.elem.nodeType !== 1 ||
+                               tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
+                               return tween.elem[ tween.prop ];
+                       }
+
+                       // Passing an empty string as a 3rd parameter to .css will automatically
+                       // attempt a parseFloat and fallback to a string if the parse fails.
+                       // Simple values such as "10px" are parsed to Float;
+                       // complex values such as "rotate(1rad)" are returned as-is.
+                       result = jQuery.css( tween.elem, tween.prop, "" );
+
+                       // Empty strings, null, undefined and "auto" are converted to 0.
+                       return !result || result === "auto" ? 0 : result;
+               },
+               set: function( tween ) {
+
+                       // Use step hook for back compat.
+                       // Use cssHook if its there.
+                       // Use .style if available and use plain properties where available.
+                       if ( jQuery.fx.step[ tween.prop ] ) {
+                               jQuery.fx.step[ tween.prop ]( tween );
+                       } else if ( tween.elem.nodeType === 1 && (
+                                       jQuery.cssHooks[ tween.prop ] ||
+                                       tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {
+                               jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+                       } else {
+                               tween.elem[ tween.prop ] = tween.now;
+                       }
+               }
+       }
+};
+
+// Support: IE <=9 only
+// Panic based approach to setting things on disconnected nodes
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+       set: function( tween ) {
+               if ( tween.elem.nodeType && tween.elem.parentNode ) {
+                       tween.elem[ tween.prop ] = tween.now;
+               }
+       }
+};
+
+jQuery.easing = {
+       linear: function( p ) {
+               return p;
+       },
+       swing: function( p ) {
+               return 0.5 - Math.cos( p * Math.PI ) / 2;
+       },
+       _default: "swing"
+};
+
+jQuery.fx = Tween.prototype.init;
+
+// Back compat <1.8 extension point
+jQuery.fx.step = {};
+
+
+
+
+var
+       fxNow, inProgress,
+       rfxtypes = /^(?:toggle|show|hide)$/,
+       rrun = /queueHooks$/;
+
+function schedule() {
+       if ( inProgress ) {
+               if ( document.hidden === false && window.requestAnimationFrame ) {
+                       window.requestAnimationFrame( schedule );
+               } else {
+                       window.setTimeout( schedule, jQuery.fx.interval );
+               }
+
+               jQuery.fx.tick();
+       }
+}
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+       window.setTimeout( function() {
+               fxNow = undefined;
+       } );
+       return ( fxNow = Date.now() );
+}
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+       var which,
+               i = 0,
+               attrs = { height: type };
+
+       // If we include width, step value is 1 to do all cssExpand values,
+       // otherwise step value is 2 to skip over Left and Right
+       includeWidth = includeWidth ? 1 : 0;
+       for ( ; i < 4; i += 2 - includeWidth ) {
+               which = cssExpand[ i ];
+               attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+       }
+
+       if ( includeWidth ) {
+               attrs.opacity = attrs.width = type;
+       }
+
+       return attrs;
+}
+
+function createTween( value, prop, animation ) {
+       var tween,
+               collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
+               index = 0,
+               length = collection.length;
+       for ( ; index < length; index++ ) {
+               if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {
+
+                       // We're done with this property
+                       return tween;
+               }
+       }
+}
+
+function defaultPrefilter( elem, props, opts ) {
+       var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,
+               isBox = "width" in props || "height" in props,
+               anim = this,
+               orig = {},
+               style = elem.style,
+               hidden = elem.nodeType && isHiddenWithinTree( elem ),
+               dataShow = dataPriv.get( elem, "fxshow" );
+
+       // Queue-skipping animations hijack the fx hooks
+       if ( !opts.queue ) {
+               hooks = jQuery._queueHooks( elem, "fx" );
+               if ( hooks.unqueued == null ) {
+                       hooks.unqueued = 0;
+                       oldfire = hooks.empty.fire;
+                       hooks.empty.fire = function() {
+                               if ( !hooks.unqueued ) {
+                                       oldfire();
+                               }
+                       };
+               }
+               hooks.unqueued++;
+
+               anim.always( function() {
+
+                       // Ensure the complete handler is called before this completes
+                       anim.always( function() {
+                               hooks.unqueued--;
+                               if ( !jQuery.queue( elem, "fx" ).length ) {
+                                       hooks.empty.fire();
+                               }
+                       } );
+               } );
+       }
+
+       // Detect show/hide animations
+       for ( prop in props ) {
+               value = props[ prop ];
+               if ( rfxtypes.test( value ) ) {
+                       delete props[ prop ];
+                       toggle = toggle || value === "toggle";
+                       if ( value === ( hidden ? "hide" : "show" ) ) {
+
+                               // Pretend to be hidden if this is a "show" and
+                               // there is still data from a stopped show/hide
+                               if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
+                                       hidden = true;
+
+                               // Ignore all other no-op show/hide data
+                               } else {
+                                       continue;
+                               }
+                       }
+                       orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
+               }
+       }
+
+       // Bail out if this is a no-op like .hide().hide()
+       propTween = !jQuery.isEmptyObject( props );
+       if ( !propTween && jQuery.isEmptyObject( orig ) ) {
+               return;
+       }
+
+       // Restrict "overflow" and "display" styles during box animations
+       if ( isBox && elem.nodeType === 1 ) {
+
+               // Support: IE <=9 - 11, Edge 12 - 15
+               // Record all 3 overflow attributes because IE does not infer the shorthand
+               // from identically-valued overflowX and overflowY and Edge just mirrors
+               // the overflowX value there.
+               opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+               // Identify a display type, preferring old show/hide data over the CSS cascade
+               restoreDisplay = dataShow && dataShow.display;
+               if ( restoreDisplay == null ) {
+                       restoreDisplay = dataPriv.get( elem, "display" );
+               }
+               display = jQuery.css( elem, "display" );
+               if ( display === "none" ) {
+                       if ( restoreDisplay ) {
+                               display = restoreDisplay;
+                       } else {
+
+                               // Get nonempty value(s) by temporarily forcing visibility
+                               showHide( [ elem ], true );
+                               restoreDisplay = elem.style.display || restoreDisplay;
+                               display = jQuery.css( elem, "display" );
+                               showHide( [ elem ] );
+                       }
+               }
+
+               // Animate inline elements as inline-block
+               if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) {
+                       if ( jQuery.css( elem, "float" ) === "none" ) {
+
+                               // Restore the original display value at the end of pure show/hide animations
+                               if ( !propTween ) {
+                                       anim.done( function() {
+                                               style.display = restoreDisplay;
+                                       } );
+                                       if ( restoreDisplay == null ) {
+                                               display = style.display;
+                                               restoreDisplay = display === "none" ? "" : display;
+                                       }
+                               }
+                               style.display = "inline-block";
+                       }
+               }
+       }
+
+       if ( opts.overflow ) {
+               style.overflow = "hidden";
+               anim.always( function() {
+                       style.overflow = opts.overflow[ 0 ];
+                       style.overflowX = opts.overflow[ 1 ];
+                       style.overflowY = opts.overflow[ 2 ];
+               } );
+       }
+
+       // Implement show/hide animations
+       propTween = false;
+       for ( prop in orig ) {
+
+               // General show/hide setup for this element animation
+               if ( !propTween ) {
+                       if ( dataShow ) {
+                               if ( "hidden" in dataShow ) {
+                                       hidden = dataShow.hidden;
+                               }
+                       } else {
+                               dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } );
+                       }
+
+                       // Store hidden/visible for toggle so `.stop().toggle()` "reverses"
+                       if ( toggle ) {
+                               dataShow.hidden = !hidden;
+                       }
+
+                       // Show elements before animating them
+                       if ( hidden ) {
+                               showHide( [ elem ], true );
+                       }
+
+                       /* eslint-disable no-loop-func */
+
+                       anim.done( function() {
+
+                       /* eslint-enable no-loop-func */
+
+                               // The final step of a "hide" animation is actually hiding the element
+                               if ( !hidden ) {
+                                       showHide( [ elem ] );
+                               }
+                               dataPriv.remove( elem, "fxshow" );
+                               for ( prop in orig ) {
+                                       jQuery.style( elem, prop, orig[ prop ] );
+                               }
+                       } );
+               }
+
+               // Per-property setup
+               propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
+               if ( !( prop in dataShow ) ) {
+                       dataShow[ prop ] = propTween.start;
+                       if ( hidden ) {
+                               propTween.end = propTween.start;
+                               propTween.start = 0;
+                       }
+               }
+       }
+}
+
+function propFilter( props, specialEasing ) {
+       var index, name, easing, value, hooks;
+
+       // camelCase, specialEasing and expand cssHook pass
+       for ( index in props ) {
+               name = camelCase( index );
+               easing = specialEasing[ name ];
+               value = props[ index ];
+               if ( Array.isArray( value ) ) {
+                       easing = value[ 1 ];
+                       value = props[ index ] = value[ 0 ];
+               }
+
+               if ( index !== name ) {
+                       props[ name ] = value;
+                       delete props[ index ];
+               }
+
+               hooks = jQuery.cssHooks[ name ];
+               if ( hooks && "expand" in hooks ) {
+                       value = hooks.expand( value );
+                       delete props[ name ];
+
+                       // Not quite $.extend, this won't overwrite existing keys.
+                       // Reusing 'index' because we have the correct "name"
+                       for ( index in value ) {
+                               if ( !( index in props ) ) {
+                                       props[ index ] = value[ index ];
+                                       specialEasing[ index ] = easing;
+                               }
+                       }
+               } else {
+                       specialEasing[ name ] = easing;
+               }
+       }
+}
+
+function Animation( elem, properties, options ) {
+       var result,
+               stopped,
+               index = 0,
+               length = Animation.prefilters.length,
+               deferred = jQuery.Deferred().always( function() {
+
+                       // Don't match elem in the :animated selector
+                       delete tick.elem;
+               } ),
+               tick = function() {
+                       if ( stopped ) {
+                               return false;
+                       }
+                       var currentTime = fxNow || createFxNow(),
+                               remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+
+                               // Support: Android 2.3 only
+                               // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
+                               temp = remaining / animation.duration || 0,
+                               percent = 1 - temp,
+                               index = 0,
+                               length = animation.tweens.length;
+
+                       for ( ; index < length; index++ ) {
+                               animation.tweens[ index ].run( percent );
+                       }
+
+                       deferred.notifyWith( elem, [ animation, percent, remaining ] );
+
+                       // If there's more to do, yield
+                       if ( percent < 1 && length ) {
+                               return remaining;
+                       }
+
+                       // If this was an empty animation, synthesize a final progress notification
+                       if ( !length ) {
+                               deferred.notifyWith( elem, [ animation, 1, 0 ] );
+                       }
+
+                       // Resolve the animation and report its conclusion
+                       deferred.resolveWith( elem, [ animation ] );
+                       return false;
+               },
+               animation = deferred.promise( {
+                       elem: elem,
+                       props: jQuery.extend( {}, properties ),
+                       opts: jQuery.extend( true, {
+                               specialEasing: {},
+                               easing: jQuery.easing._default
+                       }, options ),
+                       originalProperties: properties,
+                       originalOptions: options,
+                       startTime: fxNow || createFxNow(),
+                       duration: options.duration,
+                       tweens: [],
+                       createTween: function( prop, end ) {
+                               var tween = jQuery.Tween( elem, animation.opts, prop, end,
+                                               animation.opts.specialEasing[ prop ] || animation.opts.easing );
+                               animation.tweens.push( tween );
+                               return tween;
+                       },
+                       stop: function( gotoEnd ) {
+                               var index = 0,
+
+                                       // If we are going to the end, we want to run all the tweens
+                                       // otherwise we skip this part
+                                       length = gotoEnd ? animation.tweens.length : 0;
+                               if ( stopped ) {
+                                       return this;
+                               }
+                               stopped = true;
+                               for ( ; index < length; index++ ) {
+                                       animation.tweens[ index ].run( 1 );
+                               }
+
+                               // Resolve when we played the last frame; otherwise, reject
+                               if ( gotoEnd ) {
+                                       deferred.notifyWith( elem, [ animation, 1, 0 ] );
+                                       deferred.resolveWith( elem, [ animation, gotoEnd ] );
+                               } else {
+                                       deferred.rejectWith( elem, [ animation, gotoEnd ] );
+                               }
+                               return this;
+                       }
+               } ),
+               props = animation.props;
+
+       propFilter( props, animation.opts.specialEasing );
+
+       for ( ; index < length; index++ ) {
+               result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
+               if ( result ) {
+                       if ( isFunction( result.stop ) ) {
+                               jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
+                                       result.stop.bind( result );
+                       }
+                       return result;
+               }
+       }
+
+       jQuery.map( props, createTween, animation );
+
+       if ( isFunction( animation.opts.start ) ) {
+               animation.opts.start.call( elem, animation );
+       }
+
+       // Attach callbacks from options
+       animation
+               .progress( animation.opts.progress )
+               .done( animation.opts.done, animation.opts.complete )
+               .fail( animation.opts.fail )
+               .always( animation.opts.always );
+
+       jQuery.fx.timer(
+               jQuery.extend( tick, {
+                       elem: elem,
+                       anim: animation,
+                       queue: animation.opts.queue
+               } )
+       );
+
+       return animation;
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+       tweeners: {
+               "*": [ function( prop, value ) {
+                       var tween = this.createTween( prop, value );
+                       adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
+                       return tween;
+               } ]
+       },
+
+       tweener: function( props, callback ) {
+               if ( isFunction( props ) ) {
+                       callback = props;
+                       props = [ "*" ];
+               } else {
+                       props = props.match( rnothtmlwhite );
+               }
+
+               var prop,
+                       index = 0,
+                       length = props.length;
+
+               for ( ; index < length; index++ ) {
+                       prop = props[ index ];
+                       Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
+                       Animation.tweeners[ prop ].unshift( callback );
+               }
+       },
+
+       prefilters: [ defaultPrefilter ],
+
+       prefilter: function( callback, prepend ) {
+               if ( prepend ) {
+                       Animation.prefilters.unshift( callback );
+               } else {
+                       Animation.prefilters.push( callback );
+               }
+       }
+} );
+
+jQuery.speed = function( speed, easing, fn ) {
+       var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+               complete: fn || !fn && easing ||
+                       isFunction( speed ) && speed,
+               duration: speed,
+               easing: fn && easing || easing && !isFunction( easing ) && easing
+       };
+
+       // Go to the end state if fx are off
+       if ( jQuery.fx.off ) {
+               opt.duration = 0;
+
+       } else {
+               if ( typeof opt.duration !== "number" ) {
+                       if ( opt.duration in jQuery.fx.speeds ) {
+                               opt.duration = jQuery.fx.speeds[ opt.duration ];
+
+                       } else {
+                               opt.duration = jQuery.fx.speeds._default;
+                       }
+               }
+       }
+
+       // Normalize opt.queue - true/undefined/null -> "fx"
+       if ( opt.queue == null || opt.queue === true ) {
+               opt.queue = "fx";
+       }
+
+       // Queueing
+       opt.old = opt.complete;
+
+       opt.complete = function() {
+               if ( isFunction( opt.old ) ) {
+                       opt.old.call( this );
+               }
+
+               if ( opt.queue ) {
+                       jQuery.dequeue( this, opt.queue );
+               }
+       };
+
+       return opt;
+};
+
+jQuery.fn.extend( {
+       fadeTo: function( speed, to, easing, callback ) {
+
+               // Show any hidden elements after setting opacity to 0
+               return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show()
+
+                       // Animate to the value specified
+                       .end().animate( { opacity: to }, speed, easing, callback );
+       },
+       animate: function( prop, speed, easing, callback ) {
+               var empty = jQuery.isEmptyObject( prop ),
+                       optall = jQuery.speed( speed, easing, callback ),
+                       doAnimation = function() {
+
+                               // Operate on a copy of prop so per-property easing won't be lost
+                               var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+                               // Empty animations, or finishing resolves immediately
+                               if ( empty || dataPriv.get( this, "finish" ) ) {
+                                       anim.stop( true );
+                               }
+                       };
+                       doAnimation.finish = doAnimation;
+
+               return empty || optall.queue === false ?
+                       this.each( doAnimation ) :
+                       this.queue( optall.queue, doAnimation );
+       },
+       stop: function( type, clearQueue, gotoEnd ) {
+               var stopQueue = function( hooks ) {
+                       var stop = hooks.stop;
+                       delete hooks.stop;
+                       stop( gotoEnd );
+               };
+
+               if ( typeof type !== "string" ) {
+                       gotoEnd = clearQueue;
+                       clearQueue = type;
+                       type = undefined;
+               }
+               if ( clearQueue && type !== false ) {
+                       this.queue( type || "fx", [] );
+               }
+
+               return this.each( function() {
+                       var dequeue = true,
+                               index = type != null && type + "queueHooks",
+                               timers = jQuery.timers,
+                               data = dataPriv.get( this );
+
+                       if ( index ) {
+                               if ( data[ index ] && data[ index ].stop ) {
+                                       stopQueue( data[ index ] );
+                               }
+                       } else {
+                               for ( index in data ) {
+                                       if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+                                               stopQueue( data[ index ] );
+                                       }
+                               }
+                       }
+
+                       for ( index = timers.length; index--; ) {
+                               if ( timers[ index ].elem === this &&
+                                       ( type == null || timers[ index ].queue === type ) ) {
+
+                                       timers[ index ].anim.stop( gotoEnd );
+                                       dequeue = false;
+                                       timers.splice( index, 1 );
+                               }
+                       }
+
+                       // Start the next in the queue if the last step wasn't forced.
+                       // Timers currently will call their complete callbacks, which
+                       // will dequeue but only if they were gotoEnd.
+                       if ( dequeue || !gotoEnd ) {
+                               jQuery.dequeue( this, type );
+                       }
+               } );
+       },
+       finish: function( type ) {
+               if ( type !== false ) {
+                       type = type || "fx";
+               }
+               return this.each( function() {
+                       var index,
+                               data = dataPriv.get( this ),
+                               queue = data[ type + "queue" ],
+                               hooks = data[ type + "queueHooks" ],
+                               timers = jQuery.timers,
+                               length = queue ? queue.length : 0;
+
+                       // Enable finishing flag on private data
+                       data.finish = true;
+
+                       // Empty the queue first
+                       jQuery.queue( this, type, [] );
+
+                       if ( hooks && hooks.stop ) {
+                               hooks.stop.call( this, true );
+                       }
+
+                       // Look for any active animations, and finish them
+                       for ( index = timers.length; index--; ) {
+                               if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+                                       timers[ index ].anim.stop( true );
+                                       timers.splice( index, 1 );
+                               }
+                       }
+
+                       // Look for any animations in the old queue and finish them
+                       for ( index = 0; index < length; index++ ) {
+                               if ( queue[ index ] && queue[ index ].finish ) {
+                                       queue[ index ].finish.call( this );
+                               }
+                       }
+
+                       // Turn off finishing flag
+                       delete data.finish;
+               } );
+       }
+} );
+
+jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
+       var cssFn = jQuery.fn[ name ];
+       jQuery.fn[ name ] = function( speed, easing, callback ) {
+               return speed == null || typeof speed === "boolean" ?
+                       cssFn.apply( this, arguments ) :
+                       this.animate( genFx( name, true ), speed, easing, callback );
+       };
+} );
+
+// Generate shortcuts for custom animations
+jQuery.each( {
+       slideDown: genFx( "show" ),
+       slideUp: genFx( "hide" ),
+       slideToggle: genFx( "toggle" ),
+       fadeIn: { opacity: "show" },
+       fadeOut: { opacity: "hide" },
+       fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+       jQuery.fn[ name ] = function( speed, easing, callback ) {
+               return this.animate( props, speed, easing, callback );
+       };
+} );
+
+jQuery.timers = [];
+jQuery.fx.tick = function() {
+       var timer,
+               i = 0,
+               timers = jQuery.timers;
+
+       fxNow = Date.now();
+
+       for ( ; i < timers.length; i++ ) {
+               timer = timers[ i ];
+
+               // Run the timer and safely remove it when done (allowing for external removal)
+               if ( !timer() && timers[ i ] === timer ) {
+                       timers.splice( i--, 1 );
+               }
+       }
+
+       if ( !timers.length ) {
+               jQuery.fx.stop();
+       }
+       fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+       jQuery.timers.push( timer );
+       jQuery.fx.start();
+};
+
+jQuery.fx.interval = 13;
+jQuery.fx.start = function() {
+       if ( inProgress ) {
+               return;
+       }
+
+       inProgress = true;
+       schedule();
+};
+
+jQuery.fx.stop = function() {
+       inProgress = null;
+};
+
+jQuery.fx.speeds = {
+       slow: 600,
+       fast: 200,
+
+       // Default speed
+       _default: 400
+};
+
+
+// Based off of the plugin by Clint Helfers, with permission.
+// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
+jQuery.fn.delay = function( time, type ) {
+       time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+       type = type || "fx";
+
+       return this.queue( type, function( next, hooks ) {
+               var timeout = window.setTimeout( next, time );
+               hooks.stop = function() {
+                       window.clearTimeout( timeout );
+               };
+       } );
+};
+
+
+( function() {
+       var input = document.createElement( "input" ),
+               select = document.createElement( "select" ),
+               opt = select.appendChild( document.createElement( "option" ) );
+
+       input.type = "checkbox";
+
+       // Support: Android <=4.3 only
+       // Default value for a checkbox should be "on"
+       support.checkOn = input.value !== "";
+
+       // Support: IE <=11 only
+       // Must access selectedIndex to make default options select
+       support.optSelected = opt.selected;
+
+       // Support: IE <=11 only
+       // An input loses its value after becoming a radio
+       input = document.createElement( "input" );
+       input.value = "t";
+       input.type = "radio";
+       support.radioValue = input.value === "t";
+} )();
+
+
+var boolHook,
+       attrHandle = jQuery.expr.attrHandle;
+
+jQuery.fn.extend( {
+       attr: function( name, value ) {
+               return access( this, jQuery.attr, name, value, arguments.length > 1 );
+       },
+
+       removeAttr: function( name ) {
+               return this.each( function() {
+                       jQuery.removeAttr( this, name );
+               } );
+       }
+} );
+
+jQuery.extend( {
+       attr: function( elem, name, value ) {
+               var ret, hooks,
+                       nType = elem.nodeType;
+
+               // Don't get/set attributes on text, comment and attribute nodes
+               if ( nType === 3 || nType === 8 || nType === 2 ) {
+                       return;
+               }
+
+               // Fallback to prop when attributes are not supported
+               if ( typeof elem.getAttribute === "undefined" ) {
+                       return jQuery.prop( elem, name, value );
+               }
+
+               // Attribute hooks are determined by the lowercase version
+               // Grab necessary hook if one is defined
+               if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+                       hooks = jQuery.attrHooks[ name.toLowerCase() ] ||
+                               ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );
+               }
+
+               if ( value !== undefined ) {
+                       if ( value === null ) {
+                               jQuery.removeAttr( elem, name );
+                               return;
+                       }
+
+                       if ( hooks && "set" in hooks &&
+                               ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+                               return ret;
+                       }
+
+                       elem.setAttribute( name, value + "" );
+                       return value;
+               }
+
+               if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+                       return ret;
+               }
+
+               ret = jQuery.find.attr( elem, name );
+
+               // Non-existent attributes return null, we normalize to undefined
+               return ret == null ? undefined : ret;
+       },
+
+       attrHooks: {
+               type: {
+                       set: function( elem, value ) {
+                               if ( !support.radioValue && value === "radio" &&
+                                       nodeName( elem, "input" ) ) {
+                                       var val = elem.value;
+                                       elem.setAttribute( "type", value );
+                                       if ( val ) {
+                                               elem.value = val;
+                                       }
+                                       return value;
+                               }
+                       }
+               }
+       },
+
+       removeAttr: function( elem, value ) {
+               var name,
+                       i = 0,
+
+                       // Attribute names can contain non-HTML whitespace characters
+                       // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
+                       attrNames = value && value.match( rnothtmlwhite );
+
+               if ( attrNames && elem.nodeType === 1 ) {
+                       while ( ( name = attrNames[ i++ ] ) ) {
+                               elem.removeAttribute( name );
+                       }
+               }
+       }
+} );
+
+// Hooks for boolean attributes
+boolHook = {
+       set: function( elem, value, name ) {
+               if ( value === false ) {
+
+                       // Remove boolean attributes when set to false
+                       jQuery.removeAttr( elem, name );
+               } else {
+                       elem.setAttribute( name, name );
+               }
+               return name;
+       }
+};
+
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+       var getter = attrHandle[ name ] || jQuery.find.attr;
+
+       attrHandle[ name ] = function( elem, name, isXML ) {
+               var ret, handle,
+                       lowercaseName = name.toLowerCase();
+
+               if ( !isXML ) {
+
+                       // Avoid an infinite loop by temporarily removing this function from the getter
+                       handle = attrHandle[ lowercaseName ];
+                       attrHandle[ lowercaseName ] = ret;
+                       ret = getter( elem, name, isXML ) != null ?
+                               lowercaseName :
+                               null;
+                       attrHandle[ lowercaseName ] = handle;
+               }
+               return ret;
+       };
+} );
+
+
+
+
+var rfocusable = /^(?:input|select|textarea|button)$/i,
+       rclickable = /^(?:a|area)$/i;
+
+jQuery.fn.extend( {
+       prop: function( name, value ) {
+               return access( this, jQuery.prop, name, value, arguments.length > 1 );
+       },
+
+       removeProp: function( name ) {
+               return this.each( function() {
+                       delete this[ jQuery.propFix[ name ] || name ];
+               } );
+       }
+} );
+
+jQuery.extend( {
+       prop: function( elem, name, value ) {
+               var ret, hooks,
+                       nType = elem.nodeType;
+
+               // Don't get/set properties on text, comment and attribute nodes
+               if ( nType === 3 || nType === 8 || nType === 2 ) {
+                       return;
+               }
+
+               if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+
+                       // Fix name and attach hooks
+                       name = jQuery.propFix[ name ] || name;
+                       hooks = jQuery.propHooks[ name ];
+               }
+
+               if ( value !== undefined ) {
+                       if ( hooks && "set" in hooks &&
+                               ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+                               return ret;
+                       }
+
+                       return ( elem[ name ] = value );
+               }
+
+               if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+                       return ret;
+               }
+
+               return elem[ name ];
+       },
+
+       propHooks: {
+               tabIndex: {
+                       get: function( elem ) {
+
+                               // Support: IE <=9 - 11 only
+                               // elem.tabIndex doesn't always return the
+                               // correct value when it hasn't been explicitly set
+                               // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+                               // Use proper attribute retrieval(#12072)
+                               var tabindex = jQuery.find.attr( elem, "tabindex" );
+
+                               if ( tabindex ) {
+                                       return parseInt( tabindex, 10 );
+                               }
+
+                               if (
+                                       rfocusable.test( elem.nodeName ) ||
+                                       rclickable.test( elem.nodeName ) &&
+                                       elem.href
+                               ) {
+                                       return 0;
+                               }
+
+                               return -1;
+                       }
+               }
+       },
+
+       propFix: {
+               "for": "htmlFor",
+               "class": "className"
+       }
+} );
+
+// Support: IE <=11 only
+// Accessing the selectedIndex property
+// forces the browser to respect setting selected
+// on the option
+// The getter ensures a default option is selected
+// when in an optgroup
+// eslint rule "no-unused-expressions" is disabled for this code
+// since it considers such accessions noop
+if ( !support.optSelected ) {
+       jQuery.propHooks.selected = {
+               get: function( elem ) {
+
+                       /* eslint no-unused-expressions: "off" */
+
+                       var parent = elem.parentNode;
+                       if ( parent && parent.parentNode ) {
+                               parent.parentNode.selectedIndex;
+                       }
+                       return null;
+               },
+               set: function( elem ) {
+
+                       /* eslint no-unused-expressions: "off" */
+
+                       var parent = elem.parentNode;
+                       if ( parent ) {
+                               parent.selectedIndex;
+
+                               if ( parent.parentNode ) {
+                                       parent.parentNode.selectedIndex;
+                               }
+                       }
+               }
+       };
+}
+
+jQuery.each( [
+       "tabIndex",
+       "readOnly",
+       "maxLength",
+       "cellSpacing",
+       "cellPadding",
+       "rowSpan",
+       "colSpan",
+       "useMap",
+       "frameBorder",
+       "contentEditable"
+], function() {
+       jQuery.propFix[ this.toLowerCase() ] = this;
+} );
+
+
+
+
+       // Strip and collapse whitespace according to HTML spec
+       // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace
+       function stripAndCollapse( value ) {
+               var tokens = value.match( rnothtmlwhite ) || [];
+               return tokens.join( " " );
+       }
+
+
+function getClass( elem ) {
+       return elem.getAttribute && elem.getAttribute( "class" ) || "";
+}
+
+function classesToArray( value ) {
+       if ( Array.isArray( value ) ) {
+               return value;
+       }
+       if ( typeof value === "string" ) {
+               return value.match( rnothtmlwhite ) || [];
+       }
+       return [];
+}
+
+jQuery.fn.extend( {
+       addClass: function( value ) {
+               var classes, elem, cur, curValue, clazz, j, finalValue,
+                       i = 0;
+
+               if ( isFunction( value ) ) {
+                       return this.each( function( j ) {
+                               jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
+                       } );
+               }
+
+               classes = classesToArray( value );
+
+               if ( classes.length ) {
+                       while ( ( elem = this[ i++ ] ) ) {
+                               curValue = getClass( elem );
+                               cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
+
+                               if ( cur ) {
+                                       j = 0;
+                                       while ( ( clazz = classes[ j++ ] ) ) {
+                                               if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+                                                       cur += clazz + " ";
+                                               }
+                                       }
+
+                                       // Only assign if different to avoid unneeded rendering.
+                                       finalValue = stripAndCollapse( cur );
+                                       if ( curValue !== finalValue ) {
+                                               elem.setAttribute( "class", finalValue );
+                                       }
+                               }
+                       }
+               }
+
+               return this;
+       },
+
+       removeClass: function( value ) {
+               var classes, elem, cur, curValue, clazz, j, finalValue,
+                       i = 0;
+
+               if ( isFunction( value ) ) {
+                       return this.each( function( j ) {
+                               jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
+                       } );
+               }
+
+               if ( !arguments.length ) {
+                       return this.attr( "class", "" );
+               }
+
+               classes = classesToArray( value );
+
+               if ( classes.length ) {
+                       while ( ( elem = this[ i++ ] ) ) {
+                               curValue = getClass( elem );
+
+                               // This expression is here for better compressibility (see addClass)
+                               cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
+
+                               if ( cur ) {
+                                       j = 0;
+                                       while ( ( clazz = classes[ j++ ] ) ) {
+
+                                               // Remove *all* instances
+                                               while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
+                                                       cur = cur.replace( " " + clazz + " ", " " );
+                                               }
+                                       }
+
+                                       // Only assign if different to avoid unneeded rendering.
+                                       finalValue = stripAndCollapse( cur );
+                                       if ( curValue !== finalValue ) {
+                                               elem.setAttribute( "class", finalValue );
+                                       }
+                               }
+                       }
+               }
+
+               return this;
+       },
+
+       toggleClass: function( value, stateVal ) {
+               var type = typeof value,
+                       isValidValue = type === "string" || Array.isArray( value );
+
+               if ( typeof stateVal === "boolean" && isValidValue ) {
+                       return stateVal ? this.addClass( value ) : this.removeClass( value );
+               }
+
+               if ( isFunction( value ) ) {
+                       return this.each( function( i ) {
+                               jQuery( this ).toggleClass(
+                                       value.call( this, i, getClass( this ), stateVal ),
+                                       stateVal
+                               );
+                       } );
+               }
+
+               return this.each( function() {
+                       var className, i, self, classNames;
+
+                       if ( isValidValue ) {
+
+                               // Toggle individual class names
+                               i = 0;
+                               self = jQuery( this );
+                               classNames = classesToArray( value );
+
+                               while ( ( className = classNames[ i++ ] ) ) {
+
+                                       // Check each className given, space separated list
+                                       if ( self.hasClass( className ) ) {
+                                               self.removeClass( className );
+                                       } else {
+                                               self.addClass( className );
+                                       }
+                               }
+
+                       // Toggle whole class name
+                       } else if ( value === undefined || type === "boolean" ) {
+                               className = getClass( this );
+                               if ( className ) {
+
+                                       // Store className if set
+                                       dataPriv.set( this, "__className__", className );
+                               }
+
+                               // If the element has a class name or if we're passed `false`,
+                               // then remove the whole classname (if there was one, the above saved it).
+                               // Otherwise bring back whatever was previously saved (if anything),
+                               // falling back to the empty string if nothing was stored.
+                               if ( this.setAttribute ) {
+                                       this.setAttribute( "class",
+                                               className || value === false ?
+                                               "" :
+                                               dataPriv.get( this, "__className__" ) || ""
+                                       );
+                               }
+                       }
+               } );
+       },
+
+       hasClass: function( selector ) {
+               var className, elem,
+                       i = 0;
+
+               className = " " + selector + " ";
+               while ( ( elem = this[ i++ ] ) ) {
+                       if ( elem.nodeType === 1 &&
+                               ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
+                                       return true;
+                       }
+               }
+
+               return false;
+       }
+} );
+
+
+
+
+var rreturn = /\r/g;
+
+jQuery.fn.extend( {
+       val: function( value ) {
+               var hooks, ret, valueIsFunction,
+                       elem = this[ 0 ];
+
+               if ( !arguments.length ) {
+                       if ( elem ) {
+                               hooks = jQuery.valHooks[ elem.type ] ||
+                                       jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+                               if ( hooks &&
+                                       "get" in hooks &&
+                                       ( ret = hooks.get( elem, "value" ) ) !== undefined
+                               ) {
+                                       return ret;
+                               }
+
+                               ret = elem.value;
+
+                               // Handle most common string cases
+                               if ( typeof ret === "string" ) {
+                                       return ret.replace( rreturn, "" );
+                               }
+
+                               // Handle cases where value is null/undef or number
+                               return ret == null ? "" : ret;
+                       }
+
+                       return;
+               }
+
+               valueIsFunction = isFunction( value );
+
+               return this.each( function( i ) {
+                       var val;
+
+                       if ( this.nodeType !== 1 ) {
+                               return;
+                       }
+
+                       if ( valueIsFunction ) {
+                               val = value.call( this, i, jQuery( this ).val() );
+                       } else {
+                               val = value;
+                       }
+
+                       // Treat null/undefined as ""; convert numbers to string
+                       if ( val == null ) {
+                               val = "";
+
+                       } else if ( typeof val === "number" ) {
+                               val += "";
+
+                       } else if ( Array.isArray( val ) ) {
+                               val = jQuery.map( val, function( value ) {
+                                       return value == null ? "" : value + "";
+                               } );
+                       }
+
+                       hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+                       // If set returns undefined, fall back to normal setting
+                       if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
+                               this.value = val;
+                       }
+               } );
+       }
+} );
+
+jQuery.extend( {
+       valHooks: {
+               option: {
+                       get: function( elem ) {
+
+                               var val = jQuery.find.attr( elem, "value" );
+                               return val != null ?
+                                       val :
+
+                                       // Support: IE <=10 - 11 only
+                                       // option.text throws exceptions (#14686, #14858)
+                                       // Strip and collapse whitespace
+                                       // https://html.spec.whatwg.org/#strip-and-collapse-whitespace
+                                       stripAndCollapse( jQuery.text( elem ) );
+                       }
+               },
+               select: {
+                       get: function( elem ) {
+                               var value, option, i,
+                                       options = elem.options,
+                                       index = elem.selectedIndex,
+                                       one = elem.type === "select-one",
+                                       values = one ? null : [],
+                                       max = one ? index + 1 : options.length;
+
+                               if ( index < 0 ) {
+                                       i = max;
+
+                               } else {
+                                       i = one ? index : 0;
+                               }
+
+                               // Loop through all the selected options
+                               for ( ; i < max; i++ ) {
+                                       option = options[ i ];
+
+                                       // Support: IE <=9 only
+                                       // IE8-9 doesn't update selected after form reset (#2551)
+                                       if ( ( option.selected || i === index ) &&
+
+                                                       // Don't return options that are disabled or in a disabled optgroup
+                                                       !option.disabled &&
+                                                       ( !option.parentNode.disabled ||
+                                                               !nodeName( option.parentNode, "optgroup" ) ) ) {
+
+                                               // Get the specific value for the option
+                                               value = jQuery( option ).val();
+
+                                               // We don't need an array for one selects
+                                               if ( one ) {
+                                                       return value;
+                                               }
+
+                                               // Multi-Selects return an array
+                                               values.push( value );
+                                       }
+                               }
+
+                               return values;
+                       },
+
+                       set: function( elem, value ) {
+                               var optionSet, option,
+                                       options = elem.options,
+                                       values = jQuery.makeArray( value ),
+                                       i = options.length;
+
+                               while ( i-- ) {
+                                       option = options[ i ];
+
+                                       /* eslint-disable no-cond-assign */
+
+                                       if ( option.selected =
+                                               jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1
+                                       ) {
+                                               optionSet = true;
+                                       }
+
+                                       /* eslint-enable no-cond-assign */
+                               }
+
+                               // Force browsers to behave consistently when non-matching value is set
+                               if ( !optionSet ) {
+                                       elem.selectedIndex = -1;
+                               }
+                               return values;
+                       }
+               }
+       }
+} );
+
+// Radios and checkboxes getter/setter
+jQuery.each( [ "radio", "checkbox" ], function() {
+       jQuery.valHooks[ this ] = {
+               set: function( elem, value ) {
+                       if ( Array.isArray( value ) ) {
+                               return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
+                       }
+               }
+       };
+       if ( !support.checkOn ) {
+               jQuery.valHooks[ this ].get = function( elem ) {
+                       return elem.getAttribute( "value" ) === null ? "on" : elem.value;
+               };
+       }
+} );
+
+
+
+
+// Return jQuery for attributes-only inclusion
+
+
+support.focusin = "onfocusin" in window;
+
+
+var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+       stopPropagationCallback = function( e ) {
+               e.stopPropagation();
+       };
+
+jQuery.extend( jQuery.event, {
+
+       trigger: function( event, data, elem, onlyHandlers ) {
+
+               var i, cur, tmp, bubbleType, ontype, handle, special, lastElement,
+                       eventPath = [ elem || document ],
+                       type = hasOwn.call( event, "type" ) ? event.type : event,
+                       namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];
+
+               cur = lastElement = tmp = elem = elem || document;
+
+               // Don't do events on text and comment nodes
+               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+                       return;
+               }
+
+               // focus/blur morphs to focusin/out; ensure we're not firing them right now
+               if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+                       return;
+               }
+
+               if ( type.indexOf( "." ) > -1 ) {
+
+                       // Namespaced trigger; create a regexp to match event type in handle()
+                       namespaces = type.split( "." );
+                       type = namespaces.shift();
+                       namespaces.sort();
+               }
+               ontype = type.indexOf( ":" ) < 0 && "on" + type;
+
+               // Caller can pass in a jQuery.Event object, Object, or just an event type string
+               event = event[ jQuery.expando ] ?
+                       event :
+                       new jQuery.Event( type, typeof event === "object" && event );
+
+               // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
+               event.isTrigger = onlyHandlers ? 2 : 3;
+               event.namespace = namespaces.join( "." );
+               event.rnamespace = event.namespace ?
+                       new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
+                       null;
+
+               // Clean up the event in case it is being reused
+               event.result = undefined;
+               if ( !event.target ) {
+                       event.target = elem;
+               }
+
+               // Clone any incoming data and prepend the event, creating the handler arg list
+               data = data == null ?
+                       [ event ] :
+                       jQuery.makeArray( data, [ event ] );
+
+               // Allow special events to draw outside the lines
+               special = jQuery.event.special[ type ] || {};
+               if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+                       return;
+               }
+
+               // Determine event propagation path in advance, per W3C events spec (#9951)
+               // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+               if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {
+
+                       bubbleType = special.delegateType || type;
+                       if ( !rfocusMorph.test( bubbleType + type ) ) {
+                               cur = cur.parentNode;
+                       }
+                       for ( ; cur; cur = cur.parentNode ) {
+                               eventPath.push( cur );
+                               tmp = cur;
+                       }
+
+                       // Only add window if we got to document (e.g., not plain obj or detached DOM)
+                       if ( tmp === ( elem.ownerDocument || document ) ) {
+                               eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+                       }
+               }
+
+               // Fire handlers on the event path
+               i = 0;
+               while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
+                       lastElement = cur;
+                       event.type = i > 1 ?
+                               bubbleType :
+                               special.bindType || type;
+
+                       // jQuery handler
+                       handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
+                               dataPriv.get( cur, "handle" );
+                       if ( handle ) {
+                               handle.apply( cur, data );
+                       }
+
+                       // Native handler
+                       handle = ontype && cur[ ontype ];
+                       if ( handle && handle.apply && acceptData( cur ) ) {
+                               event.result = handle.apply( cur, data );
+                               if ( event.result === false ) {
+                                       event.preventDefault();
+                               }
+                       }
+               }
+               event.type = type;
+
+               // If nobody prevented the default action, do it now
+               if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+                       if ( ( !special._default ||
+                               special._default.apply( eventPath.pop(), data ) === false ) &&
+                               acceptData( elem ) ) {
+
+                               // Call a native DOM method on the target with the same name as the event.
+                               // Don't do default actions on window, that's where global variables be (#6170)
+                               if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {
+
+                                       // Don't re-trigger an onFOO event when we call its FOO() method
+                                       tmp = elem[ ontype ];
+
+                                       if ( tmp ) {
+                                               elem[ ontype ] = null;
+                                       }
+
+                                       // Prevent re-triggering of the same event, since we already bubbled it above
+                                       jQuery.event.triggered = type;
+
+                                       if ( event.isPropagationStopped() ) {
+                                               lastElement.addEventListener( type, stopPropagationCallback );
+                                       }
+
+                                       elem[ type ]();
+
+                                       if ( event.isPropagationStopped() ) {
+                                               lastElement.removeEventListener( type, stopPropagationCallback );
+                                       }
+
+                                       jQuery.event.triggered = undefined;
+
+                                       if ( tmp ) {
+                                               elem[ ontype ] = tmp;
+                                       }
+                               }
+                       }
+               }
+
+               return event.result;
+       },
+
+       // Piggyback on a donor event to simulate a different one
+       // Used only for `focus(in | out)` events
+       simulate: function( type, elem, event ) {
+               var e = jQuery.extend(
+                       new jQuery.Event(),
+                       event,
+                       {
+                               type: type,
+                               isSimulated: true
+                       }
+               );
+
+               jQuery.event.trigger( e, null, elem );
+       }
+
+} );
+
+jQuery.fn.extend( {
+
+       trigger: function( type, data ) {
+               return this.each( function() {
+                       jQuery.event.trigger( type, data, this );
+               } );
+       },
+       triggerHandler: function( type, data ) {
+               var elem = this[ 0 ];
+               if ( elem ) {
+                       return jQuery.event.trigger( type, data, elem, true );
+               }
+       }
+} );
+
+
+// Support: Firefox <=44
+// Firefox doesn't have focus(in | out) events
+// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
+//
+// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1
+// focus(in | out) events fire after focus & blur events,
+// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
+// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857
+if ( !support.focusin ) {
+       jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+               // Attach a single capturing handler on the document while someone wants focusin/focusout
+               var handler = function( event ) {
+                       jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );
+               };
+
+               jQuery.event.special[ fix ] = {
+                       setup: function() {
+                               var doc = this.ownerDocument || this,
+                                       attaches = dataPriv.access( doc, fix );
+
+                               if ( !attaches ) {
+                                       doc.addEventListener( orig, handler, true );
+                               }
+                               dataPriv.access( doc, fix, ( attaches || 0 ) + 1 );
+                       },
+                       teardown: function() {
+                               var doc = this.ownerDocument || this,
+                                       attaches = dataPriv.access( doc, fix ) - 1;
+
+                               if ( !attaches ) {
+                                       doc.removeEventListener( orig, handler, true );
+                                       dataPriv.remove( doc, fix );
+
+                               } else {
+                                       dataPriv.access( doc, fix, attaches );
+                               }
+                       }
+               };
+       } );
+}
+var location = window.location;
+
+var nonce = Date.now();
+
+var rquery = ( /\?/ );
+
+
+
+// Cross-browser xml parsing
+jQuery.parseXML = function( data ) {
+       var xml;
+       if ( !data || typeof data !== "string" ) {
+               return null;
+       }
+
+       // Support: IE 9 - 11 only
+       // IE throws on parseFromString with invalid input.
+       try {
+               xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
+       } catch ( e ) {
+               xml = undefined;
+       }
+
+       if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
+               jQuery.error( "Invalid XML: " + data );
+       }
+       return xml;
+};
+
+
+var
+       rbracket = /\[\]$/,
+       rCRLF = /\r?\n/g,
+       rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+       rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+function buildParams( prefix, obj, traditional, add ) {
+       var name;
+
+       if ( Array.isArray( obj ) ) {
+
+               // Serialize array item.
+               jQuery.each( obj, function( i, v ) {
+                       if ( traditional || rbracket.test( prefix ) ) {
+
+                               // Treat each array item as a scalar.
+                               add( prefix, v );
+
+                       } else {
+
+                               // Item is non-scalar (array or object), encode its numeric index.
+                               buildParams(
+                                       prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]",
+                                       v,
+                                       traditional,
+                                       add
+                               );
+                       }
+               } );
+
+       } else if ( !traditional && toType( obj ) === "object" ) {
+
+               // Serialize object item.
+               for ( name in obj ) {
+                       buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+               }
+
+       } else {
+
+               // Serialize scalar item.
+               add( prefix, obj );
+       }
+}
+
+// Serialize an array of form elements or a set of
+// key/values into a query string
+jQuery.param = function( a, traditional ) {
+       var prefix,
+               s = [],
+               add = function( key, valueOrFunction ) {
+
+                       // If value is a function, invoke it and use its return value
+                       var value = isFunction( valueOrFunction ) ?
+                               valueOrFunction() :
+                               valueOrFunction;
+
+                       s[ s.length ] = encodeURIComponent( key ) + "=" +
+                               encodeURIComponent( value == null ? "" : value );
+               };
+
+       if ( a == null ) {
+               return "";
+       }
+
+       // If an array was passed in, assume that it is an array of form elements.
+       if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+
+               // Serialize the form elements
+               jQuery.each( a, function() {
+                       add( this.name, this.value );
+               } );
+
+       } else {
+
+               // If traditional, encode the "old" way (the way 1.3.2 or older
+               // did it), otherwise encode params recursively.
+               for ( prefix in a ) {
+                       buildParams( prefix, a[ prefix ], traditional, add );
+               }
+       }
+
+       // Return the resulting serialization
+       return s.join( "&" );
+};
+
+jQuery.fn.extend( {
+       serialize: function() {
+               return jQuery.param( this.serializeArray() );
+       },
+       serializeArray: function() {
+               return this.map( function() {
+
+                       // Can add propHook for "elements" to filter or add form elements
+                       var elements = jQuery.prop( this, "elements" );
+                       return elements ? jQuery.makeArray( elements ) : this;
+               } )
+               .filter( function() {
+                       var type = this.type;
+
+                       // Use .is( ":disabled" ) so that fieldset[disabled] works
+                       return this.name && !jQuery( this ).is( ":disabled" ) &&
+                               rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+                               ( this.checked || !rcheckableType.test( type ) );
+               } )
+               .map( function( i, elem ) {
+                       var val = jQuery( this ).val();
+
+                       if ( val == null ) {
+                               return null;
+                       }
+
+                       if ( Array.isArray( val ) ) {
+                               return jQuery.map( val, function( val ) {
+                                       return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+                               } );
+                       }
+
+                       return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+               } ).get();
+       }
+} );
+
+
+var
+       r20 = /%20/g,
+       rhash = /#.*$/,
+       rantiCache = /([?&])_=[^&]*/,
+       rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
+
+       // #7653, #8125, #8152: local protocol detection
+       rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+       rnoContent = /^(?:GET|HEAD)$/,
+       rprotocol = /^\/\//,
+
+       /* Prefilters
+        * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+        * 2) These are called:
+        *    - BEFORE asking for a transport
+        *    - AFTER param serialization (s.data is a string if s.processData is true)
+        * 3) key is the dataType
+        * 4) the catchall symbol "*" can be used
+        * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+        */
+       prefilters = {},
+
+       /* Transports bindings
+        * 1) key is the dataType
+        * 2) the catchall symbol "*" can be used
+        * 3) selection will start with transport dataType and THEN go to "*" if needed
+        */
+       transports = {},
+
+       // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+       allTypes = "*/".concat( "*" ),
+
+       // Anchor tag for parsing the document origin
+       originAnchor = document.createElement( "a" );
+       originAnchor.href = location.href;
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+       // dataTypeExpression is optional and defaults to "*"
+       return function( dataTypeExpression, func ) {
+
+               if ( typeof dataTypeExpression !== "string" ) {
+                       func = dataTypeExpression;
+                       dataTypeExpression = "*";
+               }
+
+               var dataType,
+                       i = 0,
+                       dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];
+
+               if ( isFunction( func ) ) {
+
+                       // For each dataType in the dataTypeExpression
+                       while ( ( dataType = dataTypes[ i++ ] ) ) {
+
+                               // Prepend if requested
+                               if ( dataType[ 0 ] === "+" ) {
+                                       dataType = dataType.slice( 1 ) || "*";
+                                       ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );
+
+                               // Otherwise append
+                               } else {
+                                       ( structure[ dataType ] = structure[ dataType ] || [] ).push( func );
+                               }
+                       }
+               }
+       };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+       var inspected = {},
+               seekingTransport = ( structure === transports );
+
+       function inspect( dataType ) {
+               var selected;
+               inspected[ dataType ] = true;
+               jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+                       var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+                       if ( typeof dataTypeOrTransport === "string" &&
+                               !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+
+                               options.dataTypes.unshift( dataTypeOrTransport );
+                               inspect( dataTypeOrTransport );
+                               return false;
+                       } else if ( seekingTransport ) {
+                               return !( selected = dataTypeOrTransport );
+                       }
+               } );
+               return selected;
+       }
+
+       return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+       var key, deep,
+               flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+       for ( key in src ) {
+               if ( src[ key ] !== undefined ) {
+                       ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+               }
+       }
+       if ( deep ) {
+               jQuery.extend( true, target, deep );
+       }
+
+       return target;
+}
+
+/* Handles responses to an ajax request:
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+       var ct, type, finalDataType, firstDataType,
+               contents = s.contents,
+               dataTypes = s.dataTypes;
+
+       // Remove auto dataType and get content-type in the process
+       while ( dataTypes[ 0 ] === "*" ) {
+               dataTypes.shift();
+               if ( ct === undefined ) {
+                       ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" );
+               }
+       }
+
+       // Check if we're dealing with a known content-type
+       if ( ct ) {
+               for ( type in contents ) {
+                       if ( contents[ type ] && contents[ type ].test( ct ) ) {
+                               dataTypes.unshift( type );
+                               break;
+                       }
+               }
+       }
+
+       // Check to see if we have a response for the expected dataType
+       if ( dataTypes[ 0 ] in responses ) {
+               finalDataType = dataTypes[ 0 ];
+       } else {
+
+               // Try convertible dataTypes
+               for ( type in responses ) {
+                       if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) {
+                               finalDataType = type;
+                               break;
+                       }
+                       if ( !firstDataType ) {
+                               firstDataType = type;
+                       }
+               }
+
+               // Or just use first one
+               finalDataType = finalDataType || firstDataType;
+       }
+
+       // If we found a dataType
+       // We add the dataType to the list if needed
+       // and return the corresponding response
+       if ( finalDataType ) {
+               if ( finalDataType !== dataTypes[ 0 ] ) {
+                       dataTypes.unshift( finalDataType );
+               }
+               return responses[ finalDataType ];
+       }
+}
+
+/* Chain conversions given the request and the original response
+ * Also sets the responseXXX fields on the jqXHR instance
+ */
+function ajaxConvert( s, response, jqXHR, isSuccess ) {
+       var conv2, current, conv, tmp, prev,
+               converters = {},
+
+               // Work with a copy of dataTypes in case we need to modify it for conversion
+               dataTypes = s.dataTypes.slice();
+
+       // Create converters map with lowercased keys
+       if ( dataTypes[ 1 ] ) {
+               for ( conv in s.converters ) {
+                       converters[ conv.toLowerCase() ] = s.converters[ conv ];
+               }
+       }
+
+       current = dataTypes.shift();
+
+       // Convert to each sequential dataType
+       while ( current ) {
+
+               if ( s.responseFields[ current ] ) {
+                       jqXHR[ s.responseFields[ current ] ] = response;
+               }
+
+               // Apply the dataFilter if provided
+               if ( !prev && isSuccess && s.dataFilter ) {
+                       response = s.dataFilter( response, s.dataType );
+               }
+
+               prev = current;
+               current = dataTypes.shift();
+
+               if ( current ) {
+
+                       // There's only work to do if current dataType is non-auto
+                       if ( current === "*" ) {
+
+                               current = prev;
+
+                       // Convert response if prev dataType is non-auto and differs from current
+                       } else if ( prev !== "*" && prev !== current ) {
+
+                               // Seek a direct converter
+                               conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+                               // If none found, seek a pair
+                               if ( !conv ) {
+                                       for ( conv2 in converters ) {
+
+                                               // If conv2 outputs current
+                                               tmp = conv2.split( " " );
+                                               if ( tmp[ 1 ] === current ) {
+
+                                                       // If prev can be converted to accepted input
+                                                       conv = converters[ prev + " " + tmp[ 0 ] ] ||
+                                                               converters[ "* " + tmp[ 0 ] ];
+                                                       if ( conv ) {
+
+                                                               // Condense equivalence converters
+                                                               if ( conv === true ) {
+                                                                       conv = converters[ conv2 ];
+
+                                                               // Otherwise, insert the intermediate dataType
+                                                               } else if ( converters[ conv2 ] !== true ) {
+                                                                       current = tmp[ 0 ];
+                                                                       dataTypes.unshift( tmp[ 1 ] );
+                                                               }
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               // Apply converter (if not an equivalence)
+                               if ( conv !== true ) {
+
+                                       // Unless errors are allowed to bubble, catch and return them
+                                       if ( conv && s.throws ) {
+                                               response = conv( response );
+                                       } else {
+                                               try {
+                                                       response = conv( response );
+                                               } catch ( e ) {
+                                                       return {
+                                                               state: "parsererror",
+                                                               error: conv ? e : "No conversion from " + prev + " to " + current
+                                                       };
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       return { state: "success", data: response };
+}
+
+jQuery.extend( {
+
+       // Counter for holding the number of active queries
+       active: 0,
+
+       // Last-Modified header cache for next request
+       lastModified: {},
+       etag: {},
+
+       ajaxSettings: {
+               url: location.href,
+               type: "GET",
+               isLocal: rlocalProtocol.test( location.protocol ),
+               global: true,
+               processData: true,
+               async: true,
+               contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+
+               /*
+               timeout: 0,
+               data: null,
+               dataType: null,
+               username: null,
+               password: null,
+               cache: null,
+               throws: false,
+               traditional: false,
+               headers: {},
+               */
+
+               accepts: {
+                       "*": allTypes,
+                       text: "text/plain",
+                       html: "text/html",
+                       xml: "application/xml, text/xml",
+                       json: "application/json, text/javascript"
+               },
+
+               contents: {
+                       xml: /\bxml\b/,
+                       html: /\bhtml/,
+                       json: /\bjson\b/
+               },
+
+               responseFields: {
+                       xml: "responseXML",
+                       text: "responseText",
+                       json: "responseJSON"
+               },
+
+               // Data converters
+               // Keys separate source (or catchall "*") and destination types with a single space
+               converters: {
+
+                       // Convert anything to text
+                       "* text": String,
+
+                       // Text to html (true = no transformation)
+                       "text html": true,
+
+                       // Evaluate text as a json expression
+                       "text json": JSON.parse,
+
+                       // Parse text as xml
+                       "text xml": jQuery.parseXML
+               },
+
+               // For options that shouldn't be deep extended:
+               // you can add your own custom options here if
+               // and when you create one that shouldn't be
+               // deep extended (see ajaxExtend)
+               flatOptions: {
+                       url: true,
+                       context: true
+               }
+       },
+
+       // Creates a full fledged settings object into target
+       // with both ajaxSettings and settings fields.
+       // If target is omitted, writes into ajaxSettings.
+       ajaxSetup: function( target, settings ) {
+               return settings ?
+
+                       // Building a settings object
+                       ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+                       // Extending ajaxSettings
+                       ajaxExtend( jQuery.ajaxSettings, target );
+       },
+
+       ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+       ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+       // Main method
+       ajax: function( url, options ) {
+
+               // If url is an object, simulate pre-1.5 signature
+               if ( typeof url === "object" ) {
+                       options = url;
+                       url = undefined;
+               }
+
+               // Force options to be an object
+               options = options || {};
+
+               var transport,
+
+                       // URL without anti-cache param
+                       cacheURL,
+
+                       // Response headers
+                       responseHeadersString,
+                       responseHeaders,
+
+                       // timeout handle
+                       timeoutTimer,
+
+                       // Url cleanup var
+                       urlAnchor,
+
+                       // Request state (becomes false upon send and true upon completion)
+                       completed,
+
+                       // To know if global events are to be dispatched
+                       fireGlobals,
+
+                       // Loop variable
+                       i,
+
+                       // uncached part of the url
+                       uncached,
+
+                       // Create the final options object
+                       s = jQuery.ajaxSetup( {}, options ),
+
+                       // Callbacks context
+                       callbackContext = s.context || s,
+
+                       // Context for global events is callbackContext if it is a DOM node or jQuery collection
+                       globalEventContext = s.context &&
+                               ( callbackContext.nodeType || callbackContext.jquery ) ?
+                                       jQuery( callbackContext ) :
+                                       jQuery.event,
+
+                       // Deferreds
+                       deferred = jQuery.Deferred(),
+                       completeDeferred = jQuery.Callbacks( "once memory" ),
+
+                       // Status-dependent callbacks
+                       statusCode = s.statusCode || {},
+
+                       // Headers (they are sent all at once)
+                       requestHeaders = {},
+                       requestHeadersNames = {},
+
+                       // Default abort message
+                       strAbort = "canceled",
+
+                       // Fake xhr
+                       jqXHR = {
+                               readyState: 0,
+
+                               // Builds headers hashtable if needed
+                               getResponseHeader: function( key ) {
+                                       var match;
+                                       if ( completed ) {
+                                               if ( !responseHeaders ) {
+                                                       responseHeaders = {};
+                                                       while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
+                                                               responseHeaders[ match[ 1 ].toLowerCase() + " " ] =
+                                                                       ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] )
+                                                                               .concat( match[ 2 ] );
+                                                       }
+                                               }
+                                               match = responseHeaders[ key.toLowerCase() + " " ];
+                                       }
+                                       return match == null ? null : match.join( ", " );
+                               },
+
+                               // Raw string
+                               getAllResponseHeaders: function() {
+                                       return completed ? responseHeadersString : null;
+                               },
+
+                               // Caches the header
+                               setRequestHeader: function( name, value ) {
+                                       if ( completed == null ) {
+                                               name = requestHeadersNames[ name.toLowerCase() ] =
+                                                       requestHeadersNames[ name.toLowerCase() ] || name;
+                                               requestHeaders[ name ] = value;
+                                       }
+                                       return this;
+                               },
+
+                               // Overrides response content-type header
+                               overrideMimeType: function( type ) {
+                                       if ( completed == null ) {
+                                               s.mimeType = type;
+                                       }
+                                       return this;
+                               },
+
+                               // Status-dependent callbacks
+                               statusCode: function( map ) {
+                                       var code;
+                                       if ( map ) {
+                                               if ( completed ) {
+
+                                                       // Execute the appropriate callbacks
+                                                       jqXHR.always( map[ jqXHR.status ] );
+                                               } else {
+
+                                                       // Lazy-add the new callbacks in a way that preserves old ones
+                                                       for ( code in map ) {
+                                                               statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+                                                       }
+                                               }
+                                       }
+                                       return this;
+                               },
+
+                               // Cancel the request
+                               abort: function( statusText ) {
+                                       var finalText = statusText || strAbort;
+                                       if ( transport ) {
+                                               transport.abort( finalText );
+                                       }
+                                       done( 0, finalText );
+                                       return this;
+                               }
+                       };
+
+               // Attach deferreds
+               deferred.promise( jqXHR );
+
+               // Add protocol if not provided (prefilters might expect it)
+               // Handle falsy url in the settings object (#10093: consistency with old signature)
+               // We also use the url parameter if available
+               s.url = ( ( url || s.url || location.href ) + "" )
+                       .replace( rprotocol, location.protocol + "//" );
+
+               // Alias method option to type as per ticket #12004
+               s.type = options.method || options.type || s.method || s.type;
+
+               // Extract dataTypes list
+               s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ];
+
+               // A cross-domain request is in order when the origin doesn't match the current origin.
+               if ( s.crossDomain == null ) {
+                       urlAnchor = document.createElement( "a" );
+
+                       // Support: IE <=8 - 11, Edge 12 - 15
+                       // IE throws exception on accessing the href property if url is malformed,
+                       // e.g. http://example.com:80x/
+                       try {
+                               urlAnchor.href = s.url;
+
+                               // Support: IE <=8 - 11 only
+                               // Anchor's host property isn't correctly set when s.url is relative
+                               urlAnchor.href = urlAnchor.href;
+                               s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !==
+                                       urlAnchor.protocol + "//" + urlAnchor.host;
+                       } catch ( e ) {
+
+                               // If there is an error parsing the URL, assume it is crossDomain,
+                               // it can be rejected by the transport if it is invalid
+                               s.crossDomain = true;
+                       }
+               }
+
+               // Convert data if not already a string
+               if ( s.data && s.processData && typeof s.data !== "string" ) {
+                       s.data = jQuery.param( s.data, s.traditional );
+               }
+
+               // Apply prefilters
+               inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+               // If request was aborted inside a prefilter, stop there
+               if ( completed ) {
+                       return jqXHR;
+               }
+
+               // We can fire global events as of now if asked to
+               // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
+               fireGlobals = jQuery.event && s.global;
+
+               // Watch for a new set of requests
+               if ( fireGlobals && jQuery.active++ === 0 ) {
+                       jQuery.event.trigger( "ajaxStart" );
+               }
+
+               // Uppercase the type
+               s.type = s.type.toUpperCase();
+
+               // Determine if request has content
+               s.hasContent = !rnoContent.test( s.type );
+
+               // Save the URL in case we're toying with the If-Modified-Since
+               // and/or If-None-Match header later on
+               // Remove hash to simplify url manipulation
+               cacheURL = s.url.replace( rhash, "" );
+
+               // More options handling for requests with no content
+               if ( !s.hasContent ) {
+
+                       // Remember the hash so we can put it back
+                       uncached = s.url.slice( cacheURL.length );
+
+                       // If data is available and should be processed, append data to url
+                       if ( s.data && ( s.processData || typeof s.data === "string" ) ) {
+                               cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data;
+
+                               // #9682: remove data so that it's not used in an eventual retry
+                               delete s.data;
+                       }
+
+                       // Add or update anti-cache param if needed
+                       if ( s.cache === false ) {
+                               cacheURL = cacheURL.replace( rantiCache, "$1" );
+                               uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached;
+                       }
+
+                       // Put hash and anti-cache on the URL that will be requested (gh-1732)
+                       s.url = cacheURL + uncached;
+
+               // Change '%20' to '+' if this is encoded form body content (gh-2658)
+               } else if ( s.data && s.processData &&
+                       ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) {
+                       s.data = s.data.replace( r20, "+" );
+               }
+
+               // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+               if ( s.ifModified ) {
+                       if ( jQuery.lastModified[ cacheURL ] ) {
+                               jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+                       }
+                       if ( jQuery.etag[ cacheURL ] ) {
+                               jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+                       }
+               }
+
+               // Set the correct header, if data is being sent
+               if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+                       jqXHR.setRequestHeader( "Content-Type", s.contentType );
+               }
+
+               // Set the Accepts header for the server, depending on the dataType
+               jqXHR.setRequestHeader(
+                       "Accept",
+                       s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?
+                               s.accepts[ s.dataTypes[ 0 ] ] +
+                                       ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+                               s.accepts[ "*" ]
+               );
+
+               // Check for headers option
+               for ( i in s.headers ) {
+                       jqXHR.setRequestHeader( i, s.headers[ i ] );
+               }
+
+               // Allow custom headers/mimetypes and early abort
+               if ( s.beforeSend &&
+                       ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {
+
+                       // Abort if not done already and return
+                       return jqXHR.abort();
+               }
+
+               // Aborting is no longer a cancellation
+               strAbort = "abort";
+
+               // Install callbacks on deferreds
+               completeDeferred.add( s.complete );
+               jqXHR.done( s.success );
+               jqXHR.fail( s.error );
+
+               // Get transport
+               transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+               // If no transport, we auto-abort
+               if ( !transport ) {
+                       done( -1, "No Transport" );
+               } else {
+                       jqXHR.readyState = 1;
+
+                       // Send global event
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+                       }
+
+                       // If request was aborted inside ajaxSend, stop there
+                       if ( completed ) {
+                               return jqXHR;
+                       }
+
+                       // Timeout
+                       if ( s.async && s.timeout > 0 ) {
+                               timeoutTimer = window.setTimeout( function() {
+                                       jqXHR.abort( "timeout" );
+                               }, s.timeout );
+                       }
+
+                       try {
+                               completed = false;
+                               transport.send( requestHeaders, done );
+                       } catch ( e ) {
+
+                               // Rethrow post-completion exceptions
+                               if ( completed ) {
+                                       throw e;
+                               }
+
+                               // Propagate others as results
+                               done( -1, e );
+                       }
+               }
+
+               // Callback for when everything is done
+               function done( status, nativeStatusText, responses, headers ) {
+                       var isSuccess, success, error, response, modified,
+                               statusText = nativeStatusText;
+
+                       // Ignore repeat invocations
+                       if ( completed ) {
+                               return;
+                       }
+
+                       completed = true;
+
+                       // Clear timeout if it exists
+                       if ( timeoutTimer ) {
+                               window.clearTimeout( timeoutTimer );
+                       }
+
+                       // Dereference transport for early garbage collection
+                       // (no matter how long the jqXHR object will be used)
+                       transport = undefined;
+
+                       // Cache response headers
+                       responseHeadersString = headers || "";
+
+                       // Set readyState
+                       jqXHR.readyState = status > 0 ? 4 : 0;
+
+                       // Determine if successful
+                       isSuccess = status >= 200 && status < 300 || status === 304;
+
+                       // Get response data
+                       if ( responses ) {
+                               response = ajaxHandleResponses( s, jqXHR, responses );
+                       }
+
+                       // Convert no matter what (that way responseXXX fields are always set)
+                       response = ajaxConvert( s, response, jqXHR, isSuccess );
+
+                       // If successful, handle type chaining
+                       if ( isSuccess ) {
+
+                               // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+                               if ( s.ifModified ) {
+                                       modified = jqXHR.getResponseHeader( "Last-Modified" );
+                                       if ( modified ) {
+                                               jQuery.lastModified[ cacheURL ] = modified;
+                                       }
+                                       modified = jqXHR.getResponseHeader( "etag" );
+                                       if ( modified ) {
+                                               jQuery.etag[ cacheURL ] = modified;
+                                       }
+                               }
+
+                               // if no content
+                               if ( status === 204 || s.type === "HEAD" ) {
+                                       statusText = "nocontent";
+
+                               // if not modified
+                               } else if ( status === 304 ) {
+                                       statusText = "notmodified";
+
+                               // If we have data, let's convert it
+                               } else {
+                                       statusText = response.state;
+                                       success = response.data;
+                                       error = response.error;
+                                       isSuccess = !error;
+                               }
+                       } else {
+
+                               // Extract error from statusText and normalize for non-aborts
+                               error = statusText;
+                               if ( status || !statusText ) {
+                                       statusText = "error";
+                                       if ( status < 0 ) {
+                                               status = 0;
+                                       }
+                               }
+                       }
+
+                       // Set data for the fake xhr object
+                       jqXHR.status = status;
+                       jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+                       // Success/Error
+                       if ( isSuccess ) {
+                               deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+                       } else {
+                               deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+                       }
+
+                       // Status-dependent callbacks
+                       jqXHR.statusCode( statusCode );
+                       statusCode = undefined;
+
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+                                       [ jqXHR, s, isSuccess ? success : error ] );
+                       }
+
+                       // Complete
+                       completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+
+                               // Handle the global AJAX counter
+                               if ( !( --jQuery.active ) ) {
+                                       jQuery.event.trigger( "ajaxStop" );
+                               }
+                       }
+               }
+
+               return jqXHR;
+       },
+
+       getJSON: function( url, data, callback ) {
+               return jQuery.get( url, data, callback, "json" );
+       },
+
+       getScript: function( url, callback ) {
+               return jQuery.get( url, undefined, callback, "script" );
+       }
+} );
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+       jQuery[ method ] = function( url, data, callback, type ) {
+
+               // Shift arguments if data argument was omitted
+               if ( isFunction( data ) ) {
+                       type = type || callback;
+                       callback = data;
+                       data = undefined;
+               }
+
+               // The url can be an options object (which then must have .url)
+               return jQuery.ajax( jQuery.extend( {
+                       url: url,
+                       type: method,
+                       dataType: type,
+                       data: data,
+                       success: callback
+               }, jQuery.isPlainObject( url ) && url ) );
+       };
+} );
+
+
+jQuery._evalUrl = function( url, options ) {
+       return jQuery.ajax( {
+               url: url,
+
+               // Make this explicit, since user can override this through ajaxSetup (#11264)
+               type: "GET",
+               dataType: "script",
+               cache: true,
+               async: false,
+               global: false,
+
+               // Only evaluate the response if it is successful (gh-4126)
+               // dataFilter is not invoked for failure responses, so using it instead
+               // of the default converter is kludgy but it works.
+               converters: {
+                       "text script": function() {}
+               },
+               dataFilter: function( response ) {
+                       jQuery.globalEval( response, options );
+               }
+       } );
+};
+
+
+jQuery.fn.extend( {
+       wrapAll: function( html ) {
+               var wrap;
+
+               if ( this[ 0 ] ) {
+                       if ( isFunction( html ) ) {
+                               html = html.call( this[ 0 ] );
+                       }
+
+                       // The elements to wrap the target around
+                       wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
+
+                       if ( this[ 0 ].parentNode ) {
+                               wrap.insertBefore( this[ 0 ] );
+                       }
+
+                       wrap.map( function() {
+                               var elem = this;
+
+                               while ( elem.firstElementChild ) {
+                                       elem = elem.firstElementChild;
+                               }
+
+                               return elem;
+                       } ).append( this );
+               }
+
+               return this;
+       },
+
+       wrapInner: function( html ) {
+               if ( isFunction( html ) ) {
+                       return this.each( function( i ) {
+                               jQuery( this ).wrapInner( html.call( this, i ) );
+                       } );
+               }
+
+               return this.each( function() {
+                       var self = jQuery( this ),
+                               contents = self.contents();
+
+                       if ( contents.length ) {
+                               contents.wrapAll( html );
+
+                       } else {
+                               self.append( html );
+                       }
+               } );
+       },
+
+       wrap: function( html ) {
+               var htmlIsFunction = isFunction( html );
+
+               return this.each( function( i ) {
+                       jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );
+               } );
+       },
+
+       unwrap: function( selector ) {
+               this.parent( selector ).not( "body" ).each( function() {
+                       jQuery( this ).replaceWith( this.childNodes );
+               } );
+               return this;
+       }
+} );
+
+
+jQuery.expr.pseudos.hidden = function( elem ) {
+       return !jQuery.expr.pseudos.visible( elem );
+};
+jQuery.expr.pseudos.visible = function( elem ) {
+       return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
+};
+
+
+
+
+jQuery.ajaxSettings.xhr = function() {
+       try {
+               return new window.XMLHttpRequest();
+       } catch ( e ) {}
+};
+
+var xhrSuccessStatus = {
+
+               // File protocol always yields status code 0, assume 200
+               0: 200,
+
+               // Support: IE <=9 only
+               // #1450: sometimes IE returns 1223 when it should be 204
+               1223: 204
+       },
+       xhrSupported = jQuery.ajaxSettings.xhr();
+
+support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+support.ajax = xhrSupported = !!xhrSupported;
+
+jQuery.ajaxTransport( function( options ) {
+       var callback, errorCallback;
+
+       // Cross domain only allowed if supported through XMLHttpRequest
+       if ( support.cors || xhrSupported && !options.crossDomain ) {
+               return {
+                       send: function( headers, complete ) {
+                               var i,
+                                       xhr = options.xhr();
+
+                               xhr.open(
+                                       options.type,
+                                       options.url,
+                                       options.async,
+                                       options.username,
+                                       options.password
+                               );
+
+                               // Apply custom fields if provided
+                               if ( options.xhrFields ) {
+                                       for ( i in options.xhrFields ) {
+                                               xhr[ i ] = options.xhrFields[ i ];
+                                       }
+                               }
+
+                               // Override mime type if needed
+                               if ( options.mimeType && xhr.overrideMimeType ) {
+                                       xhr.overrideMimeType( options.mimeType );
+                               }
+
+                               // X-Requested-With header
+                               // For cross-domain requests, seeing as conditions for a preflight are
+                               // akin to a jigsaw puzzle, we simply never set it to be sure.
+                               // (it can always be set on a per-request basis or even using ajaxSetup)
+                               // For same-domain requests, won't change header if already provided.
+                               if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) {
+                                       headers[ "X-Requested-With" ] = "XMLHttpRequest";
+                               }
+
+                               // Set headers
+                               for ( i in headers ) {
+                                       xhr.setRequestHeader( i, headers[ i ] );
+                               }
+
+                               // Callback
+                               callback = function( type ) {
+                                       return function() {
+                                               if ( callback ) {
+                                                       callback = errorCallback = xhr.onload =
+                                                               xhr.onerror = xhr.onabort = xhr.ontimeout =
+                                                                       xhr.onreadystatechange = null;
+
+                                                       if ( type === "abort" ) {
+                                                               xhr.abort();
+                                                       } else if ( type === "error" ) {
+
+                                                               // Support: IE <=9 only
+                                                               // On a manual native abort, IE9 throws
+                                                               // errors on any property access that is not readyState
+                                                               if ( typeof xhr.status !== "number" ) {
+                                                                       complete( 0, "error" );
+                                                               } else {
+                                                                       complete(
+
+                                                                               // File: protocol always yields status 0; see #8605, #14207
+                                                                               xhr.status,
+                                                                               xhr.statusText
+                                                                       );
+                                                               }
+                                                       } else {
+                                                               complete(
+                                                                       xhrSuccessStatus[ xhr.status ] || xhr.status,
+                                                                       xhr.statusText,
+
+                                                                       // Support: IE <=9 only
+                                                                       // IE9 has no XHR2 but throws on binary (trac-11426)
+                                                                       // For XHR2 non-text, let the caller handle it (gh-2498)
+                                                                       ( xhr.responseType || "text" ) !== "text"  ||
+                                                                       typeof xhr.responseText !== "string" ?
+                                                                               { binary: xhr.response } :
+                                                                               { text: xhr.responseText },
+                                                                       xhr.getAllResponseHeaders()
+                                                               );
+                                                       }
+                                               }
+                                       };
+                               };
+
+                               // Listen to events
+                               xhr.onload = callback();
+                               errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" );
+
+                               // Support: IE 9 only
+                               // Use onreadystatechange to replace onabort
+                               // to handle uncaught aborts
+                               if ( xhr.onabort !== undefined ) {
+                                       xhr.onabort = errorCallback;
+                               } else {
+                                       xhr.onreadystatechange = function() {
+
+                                               // Check readyState before timeout as it changes
+                                               if ( xhr.readyState === 4 ) {
+
+                                                       // Allow onerror to be called first,
+                                                       // but that will not handle a native abort
+                                                       // Also, save errorCallback to a variable
+                                                       // as xhr.onerror cannot be accessed
+                                                       window.setTimeout( function() {
+                                                               if ( callback ) {
+                                                                       errorCallback();
+                                                               }
+                                                       } );
+                                               }
+                                       };
+                               }
+
+                               // Create the abort callback
+                               callback = callback( "abort" );
+
+                               try {
+
+                                       // Do send the request (this may raise an exception)
+                                       xhr.send( options.hasContent && options.data || null );
+                               } catch ( e ) {
+
+                                       // #14683: Only rethrow if this hasn't been notified as an error yet
+                                       if ( callback ) {
+                                               throw e;
+                                       }
+                               }
+                       },
+
+                       abort: function() {
+                               if ( callback ) {
+                                       callback();
+                               }
+                       }
+               };
+       }
+} );
+
+
+
+
+// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)
+jQuery.ajaxPrefilter( function( s ) {
+       if ( s.crossDomain ) {
+               s.contents.script = false;
+       }
+} );
+
+// Install script dataType
+jQuery.ajaxSetup( {
+       accepts: {
+               script: "text/javascript, application/javascript, " +
+                       "application/ecmascript, application/x-ecmascript"
+       },
+       contents: {
+               script: /\b(?:java|ecma)script\b/
+       },
+       converters: {
+               "text script": function( text ) {
+                       jQuery.globalEval( text );
+                       return text;
+               }
+       }
+} );
+
+// Handle cache's special case and crossDomain
+jQuery.ajaxPrefilter( "script", function( s ) {
+       if ( s.cache === undefined ) {
+               s.cache = false;
+       }
+       if ( s.crossDomain ) {
+               s.type = "GET";
+       }
+} );
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function( s ) {
+
+       // This transport only deals with cross domain or forced-by-attrs requests
+       if ( s.crossDomain || s.scriptAttrs ) {
+               var script, callback;
+               return {
+                       send: function( _, complete ) {
+                               script = jQuery( "<script>" )
+                                       .attr( s.scriptAttrs || {} )
+                                       .prop( { charset: s.scriptCharset, src: s.url } )
+                                       .on( "load error", callback = function( evt ) {
+                                               script.remove();
+                                               callback = null;
+                                               if ( evt ) {
+                                                       complete( evt.type === "error" ? 404 : 200, evt.type );
+                                               }
+                                       } );
+
+                               // Use native DOM manipulation to avoid our domManip AJAX trickery
+                               document.head.appendChild( script[ 0 ] );
+                       },
+                       abort: function() {
+                               if ( callback ) {
+                                       callback();
+                               }
+                       }
+               };
+       }
+} );
+
+
+
+
+var oldCallbacks = [],
+       rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup( {
+       jsonp: "callback",
+       jsonpCallback: function() {
+               var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+               this[ callback ] = true;
+               return callback;
+       }
+} );
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+       var callbackName, overwritten, responseContainer,
+               jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+                       "url" :
+                       typeof s.data === "string" &&
+                               ( s.contentType || "" )
+                                       .indexOf( "application/x-www-form-urlencoded" ) === 0 &&
+                               rjsonp.test( s.data ) && "data"
+               );
+
+       // Handle iff the expected data type is "jsonp" or we have a parameter to set
+       if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+               // Get callback name, remembering preexisting value associated with it
+               callbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?
+                       s.jsonpCallback() :
+                       s.jsonpCallback;
+
+               // Insert callback into url or form data
+               if ( jsonProp ) {
+                       s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+               } else if ( s.jsonp !== false ) {
+                       s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+               }
+
+               // Use data converter to retrieve json after script execution
+               s.converters[ "script json" ] = function() {
+                       if ( !responseContainer ) {
+                               jQuery.error( callbackName + " was not called" );
+                       }
+                       return responseContainer[ 0 ];
+               };
+
+               // Force json dataType
+               s.dataTypes[ 0 ] = "json";
+
+               // Install callback
+               overwritten = window[ callbackName ];
+               window[ callbackName ] = function() {
+                       responseContainer = arguments;
+               };
+
+               // Clean-up function (fires after converters)
+               jqXHR.always( function() {
+
+                       // If previous value didn't exist - remove it
+                       if ( overwritten === undefined ) {
+                               jQuery( window ).removeProp( callbackName );
+
+                       // Otherwise restore preexisting value
+                       } else {
+                               window[ callbackName ] = overwritten;
+                       }
+
+                       // Save back as free
+                       if ( s[ callbackName ] ) {
+
+                               // Make sure that re-using the options doesn't screw things around
+                               s.jsonpCallback = originalSettings.jsonpCallback;
+
+                               // Save the callback name for future use
+                               oldCallbacks.push( callbackName );
+                       }
+
+                       // Call if it was a function and we have a response
+                       if ( responseContainer && isFunction( overwritten ) ) {
+                               overwritten( responseContainer[ 0 ] );
+                       }
+
+                       responseContainer = overwritten = undefined;
+               } );
+
+               // Delegate to script
+               return "script";
+       }
+} );
+
+
+
+
+// Support: Safari 8 only
+// In Safari 8 documents created via document.implementation.createHTMLDocument
+// collapse sibling forms: the second one becomes a child of the first one.
+// Because of that, this security measure has to be disabled in Safari 8.
+// https://bugs.webkit.org/show_bug.cgi?id=137337
+support.createHTMLDocument = ( function() {
+       var body = document.implementation.createHTMLDocument( "" ).body;
+       body.innerHTML = "<form></form><form></form>";
+       return body.childNodes.length === 2;
+} )();
+
+
+// Argument "data" should be string of html
+// context (optional): If specified, the fragment will be created in this context,
+// defaults to document
+// keepScripts (optional): If true, will include scripts passed in the html string
+jQuery.parseHTML = function( data, context, keepScripts ) {
+       if ( typeof data !== "string" ) {
+               return [];
+       }
+       if ( typeof context === "boolean" ) {
+               keepScripts = context;
+               context = false;
+       }
+
+       var base, parsed, scripts;
+
+       if ( !context ) {
+
+               // Stop scripts or inline event handlers from being executed immediately
+               // by using document.implementation
+               if ( support.createHTMLDocument ) {
+                       context = document.implementation.createHTMLDocument( "" );
+
+                       // Set the base href for the created document
+                       // so any parsed elements with URLs
+                       // are based on the document's URL (gh-2965)
+                       base = context.createElement( "base" );
+                       base.href = document.location.href;
+                       context.head.appendChild( base );
+               } else {
+                       context = document;
+               }
+       }
+
+       parsed = rsingleTag.exec( data );
+       scripts = !keepScripts && [];
+
+       // Single tag
+       if ( parsed ) {
+               return [ context.createElement( parsed[ 1 ] ) ];
+       }
+
+       parsed = buildFragment( [ data ], context, scripts );
+
+       if ( scripts && scripts.length ) {
+               jQuery( scripts ).remove();
+       }
+
+       return jQuery.merge( [], parsed.childNodes );
+};
+
+
+/**
+ * Load a url into a page
+ */
+jQuery.fn.load = function( url, params, callback ) {
+       var selector, type, response,
+               self = this,
+               off = url.indexOf( " " );
+
+       if ( off > -1 ) {
+               selector = stripAndCollapse( url.slice( off ) );
+               url = url.slice( 0, off );
+       }
+
+       // If it's a function
+       if ( isFunction( params ) ) {
+
+               // We assume that it's the callback
+               callback = params;
+               params = undefined;
+
+       // Otherwise, build a param string
+       } else if ( params && typeof params === "object" ) {
+               type = "POST";
+       }
+
+       // If we have elements to modify, make the request
+       if ( self.length > 0 ) {
+               jQuery.ajax( {
+                       url: url,
+
+                       // If "type" variable is undefined, then "GET" method will be used.
+                       // Make value of this field explicit since
+                       // user can override it through ajaxSetup method
+                       type: type || "GET",
+                       dataType: "html",
+                       data: params
+               } ).done( function( responseText ) {
+
+                       // Save response for use in complete callback
+                       response = arguments;
+
+                       self.html( selector ?
+
+                               // If a selector was specified, locate the right elements in a dummy div
+                               // Exclude scripts to avoid IE 'Permission Denied' errors
+                               jQuery( "<div>" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+                               // Otherwise use the full result
+                               responseText );
+
+               // If the request succeeds, this function gets "data", "status", "jqXHR"
+               // but they are ignored because response was set above.
+               // If it fails, this function gets "jqXHR", "status", "error"
+               } ).always( callback && function( jqXHR, status ) {
+                       self.each( function() {
+                               callback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );
+                       } );
+               } );
+       }
+
+       return this;
+};
+
+
+
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [
+       "ajaxStart",
+       "ajaxStop",
+       "ajaxComplete",
+       "ajaxError",
+       "ajaxSuccess",
+       "ajaxSend"
+], function( i, type ) {
+       jQuery.fn[ type ] = function( fn ) {
+               return this.on( type, fn );
+       };
+} );
+
+
+
+
+jQuery.expr.pseudos.animated = function( elem ) {
+       return jQuery.grep( jQuery.timers, function( fn ) {
+               return elem === fn.elem;
+       } ).length;
+};
+
+
+
+
+jQuery.offset = {
+       setOffset: function( elem, options, i ) {
+               var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
+                       position = jQuery.css( elem, "position" ),
+                       curElem = jQuery( elem ),
+                       props = {};
+
+               // Set position first, in-case top/left are set even on static elem
+               if ( position === "static" ) {
+                       elem.style.position = "relative";
+               }
+
+               curOffset = curElem.offset();
+               curCSSTop = jQuery.css( elem, "top" );
+               curCSSLeft = jQuery.css( elem, "left" );
+               calculatePosition = ( position === "absolute" || position === "fixed" ) &&
+                       ( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;
+
+               // Need to be able to calculate position if either
+               // top or left is auto and position is either absolute or fixed
+               if ( calculatePosition ) {
+                       curPosition = curElem.position();
+                       curTop = curPosition.top;
+                       curLeft = curPosition.left;
+
+               } else {
+                       curTop = parseFloat( curCSSTop ) || 0;
+                       curLeft = parseFloat( curCSSLeft ) || 0;
+               }
+
+               if ( isFunction( options ) ) {
+
+                       // Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
+                       options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
+               }
+
+               if ( options.top != null ) {
+                       props.top = ( options.top - curOffset.top ) + curTop;
+               }
+               if ( options.left != null ) {
+                       props.left = ( options.left - curOffset.left ) + curLeft;
+               }
+
+               if ( "using" in options ) {
+                       options.using.call( elem, props );
+
+               } else {
+                       curElem.css( props );
+               }
+       }
+};
+
+jQuery.fn.extend( {
+
+       // offset() relates an element's border box to the document origin
+       offset: function( options ) {
+
+               // Preserve chaining for setter
+               if ( arguments.length ) {
+                       return options === undefined ?
+                               this :
+                               this.each( function( i ) {
+                                       jQuery.offset.setOffset( this, options, i );
+                               } );
+               }
+
+               var rect, win,
+                       elem = this[ 0 ];
+
+               if ( !elem ) {
+                       return;
+               }
+
+               // Return zeros for disconnected and hidden (display: none) elements (gh-2310)
+               // Support: IE <=11 only
+               // Running getBoundingClientRect on a
+               // disconnected node in IE throws an error
+               if ( !elem.getClientRects().length ) {
+                       return { top: 0, left: 0 };
+               }
+
+               // Get document-relative position by adding viewport scroll to viewport-relative gBCR
+               rect = elem.getBoundingClientRect();
+               win = elem.ownerDocument.defaultView;
+               return {
+                       top: rect.top + win.pageYOffset,
+                       left: rect.left + win.pageXOffset
+               };
+       },
+
+       // position() relates an element's margin box to its offset parent's padding box
+       // This corresponds to the behavior of CSS absolute positioning
+       position: function() {
+               if ( !this[ 0 ] ) {
+                       return;
+               }
+
+               var offsetParent, offset, doc,
+                       elem = this[ 0 ],
+                       parentOffset = { top: 0, left: 0 };
+
+               // position:fixed elements are offset from the viewport, which itself always has zero offset
+               if ( jQuery.css( elem, "position" ) === "fixed" ) {
+
+                       // Assume position:fixed implies availability of getBoundingClientRect
+                       offset = elem.getBoundingClientRect();
+
+               } else {
+                       offset = this.offset();
+
+                       // Account for the *real* offset parent, which can be the document or its root element
+                       // when a statically positioned element is identified
+                       doc = elem.ownerDocument;
+                       offsetParent = elem.offsetParent || doc.documentElement;
+                       while ( offsetParent &&
+                               ( offsetParent === doc.body || offsetParent === doc.documentElement ) &&
+                               jQuery.css( offsetParent, "position" ) === "static" ) {
+
+                               offsetParent = offsetParent.parentNode;
+                       }
+                       if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {
+
+                               // Incorporate borders into its offset, since they are outside its content origin
+                               parentOffset = jQuery( offsetParent ).offset();
+                               parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true );
+                               parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true );
+                       }
+               }
+
+               // Subtract parent offsets and element margins
+               return {
+                       top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+                       left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
+               };
+       },
+
+       // This method will return documentElement in the following cases:
+       // 1) For the element inside the iframe without offsetParent, this method will return
+       //    documentElement of the parent window
+       // 2) For the hidden or detached element
+       // 3) For body or html element, i.e. in case of the html node - it will return itself
+       //
+       // but those exceptions were never presented as a real life use-cases
+       // and might be considered as more preferable results.
+       //
+       // This logic, however, is not guaranteed and can change at any point in the future
+       offsetParent: function() {
+               return this.map( function() {
+                       var offsetParent = this.offsetParent;
+
+                       while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) {
+                               offsetParent = offsetParent.offsetParent;
+                       }
+
+                       return offsetParent || documentElement;
+               } );
+       }
+} );
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
+       var top = "pageYOffset" === prop;
+
+       jQuery.fn[ method ] = function( val ) {
+               return access( this, function( elem, method, val ) {
+
+                       // Coalesce documents and windows
+                       var win;
+                       if ( isWindow( elem ) ) {
+                               win = elem;
+                       } else if ( elem.nodeType === 9 ) {
+                               win = elem.defaultView;
+                       }
+
+                       if ( val === undefined ) {
+                               return win ? win[ prop ] : elem[ method ];
+                       }
+
+                       if ( win ) {
+                               win.scrollTo(
+                                       !top ? val : win.pageXOffset,
+                                       top ? val : win.pageYOffset
+                               );
+
+                       } else {
+                               elem[ method ] = val;
+                       }
+               }, method, val, arguments.length );
+       };
+} );
+
+// Support: Safari <=7 - 9.1, Chrome <=37 - 49
+// Add the top/left cssHooks using jQuery.fn.position
+// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347
+// getComputedStyle returns percent when specified for top/left/bottom/right;
+// rather than make the css module depend on the offset module, just check for it here
+jQuery.each( [ "top", "left" ], function( i, prop ) {
+       jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
+               function( elem, computed ) {
+                       if ( computed ) {
+                               computed = curCSS( elem, prop );
+
+                               // If curCSS returns percentage, fallback to offset
+                               return rnumnonpx.test( computed ) ?
+                                       jQuery( elem ).position()[ prop ] + "px" :
+                                       computed;
+                       }
+               }
+       );
+} );
+
+
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+       jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
+               function( defaultExtra, funcName ) {
+
+               // Margin is only for outerHeight, outerWidth
+               jQuery.fn[ funcName ] = function( margin, value ) {
+                       var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+                               extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+                       return access( this, function( elem, type, value ) {
+                               var doc;
+
+                               if ( isWindow( elem ) ) {
+
+                                       // $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
+                                       return funcName.indexOf( "outer" ) === 0 ?
+                                               elem[ "inner" + name ] :
+                                               elem.document.documentElement[ "client" + name ];
+                               }
+
+                               // Get document width or height
+                               if ( elem.nodeType === 9 ) {
+                                       doc = elem.documentElement;
+
+                                       // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
+                                       // whichever is greatest
+                                       return Math.max(
+                                               elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+                                               elem.body[ "offset" + name ], doc[ "offset" + name ],
+                                               doc[ "client" + name ]
+                                       );
+                               }
+
+                               return value === undefined ?
+
+                                       // Get width or height on the element, requesting but not forcing parseFloat
+                                       jQuery.css( elem, type, extra ) :
+
+                                       // Set width or height on the element
+                                       jQuery.style( elem, type, value, extra );
+                       }, type, chainable ? margin : undefined, chainable );
+               };
+       } );
+} );
+
+
+jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
+       "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+       "change select submit keydown keypress keyup contextmenu" ).split( " " ),
+       function( i, name ) {
+
+       // Handle event binding
+       jQuery.fn[ name ] = function( data, fn ) {
+               return arguments.length > 0 ?
+                       this.on( name, null, data, fn ) :
+                       this.trigger( name );
+       };
+} );
+
+jQuery.fn.extend( {
+       hover: function( fnOver, fnOut ) {
+               return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+       }
+} );
+
+
+
+
+jQuery.fn.extend( {
+
+       bind: function( types, data, fn ) {
+               return this.on( types, null, data, fn );
+       },
+       unbind: function( types, fn ) {
+               return this.off( types, null, fn );
+       },
+
+       delegate: function( selector, types, data, fn ) {
+               return this.on( types, selector, data, fn );
+       },
+       undelegate: function( selector, types, fn ) {
+
+               // ( namespace ) or ( selector, types [, fn] )
+               return arguments.length === 1 ?
+                       this.off( selector, "**" ) :
+                       this.off( types, selector || "**", fn );
+       }
+} );
+
+// Bind a function to a context, optionally partially applying any
+// arguments.
+// jQuery.proxy is deprecated to promote standards (specifically Function#bind)
+// However, it is not slated for removal any time soon
+jQuery.proxy = function( fn, context ) {
+       var tmp, args, proxy;
+
+       if ( typeof context === "string" ) {
+               tmp = fn[ context ];
+               context = fn;
+               fn = tmp;
+       }
+
+       // Quick check to determine if target is callable, in the spec
+       // this throws a TypeError, but we will just return undefined.
+       if ( !isFunction( fn ) ) {
+               return undefined;
+       }
+
+       // Simulated bind
+       args = slice.call( arguments, 2 );
+       proxy = function() {
+               return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
+       };
+
+       // Set the guid of unique handler to the same of original handler, so it can be removed
+       proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+       return proxy;
+};
+
+jQuery.holdReady = function( hold ) {
+       if ( hold ) {
+               jQuery.readyWait++;
+       } else {
+               jQuery.ready( true );
+       }
+};
+jQuery.isArray = Array.isArray;
+jQuery.parseJSON = JSON.parse;
+jQuery.nodeName = nodeName;
+jQuery.isFunction = isFunction;
+jQuery.isWindow = isWindow;
+jQuery.camelCase = camelCase;
+jQuery.type = toType;
+
+jQuery.now = Date.now;
+
+jQuery.isNumeric = function( obj ) {
+
+       // As of jQuery 3.0, isNumeric is limited to
+       // strings and numbers (primitives or objects)
+       // that can be coerced to finite numbers (gh-2662)
+       var type = jQuery.type( obj );
+       return ( type === "number" || type === "string" ) &&
+
+               // parseFloat NaNs numeric-cast false positives ("")
+               // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
+               // subtraction forces infinities to NaN
+               !isNaN( obj - parseFloat( obj ) );
+};
+
+
+
+
+// Register as a named AMD module, since jQuery can be concatenated with other
+// files that may use define, but not via a proper concatenation script that
+// understands anonymous AMD modules. A named AMD is safest and most robust
+// way to register. Lowercase jquery is used because AMD module names are
+// derived from file names, and jQuery is normally delivered in a lowercase
+// file name. Do this after creating the global so that if an AMD module wants
+// to call noConflict to hide this version of jQuery, it will work.
+
+// Note that for maximum portability, libraries that are not jQuery should
+// declare themselves as anonymous modules, and avoid setting a global if an
+// AMD loader is present. jQuery is a special case. For more information, see
+// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
+
+if ( typeof define === "function" && define.amd ) {
+       define( "jquery", [], function() {
+               return jQuery;
+       } );
+}
+
+
+
+
+var
+
+       // Map over jQuery in case of overwrite
+       _jQuery = window.jQuery,
+
+       // Map over the $ in case of overwrite
+       _$ = window.$;
+
+jQuery.noConflict = function( deep ) {
+       if ( window.$ === jQuery ) {
+               window.$ = _$;
+       }
+
+       if ( deep && window.jQuery === jQuery ) {
+               window.jQuery = _jQuery;
+       }
+
+       return jQuery;
+};
+
+// Expose jQuery and $ identifiers, even in AMD
+// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
+// and CommonJS for browser emulators (#13566)
+if ( !noGlobal ) {
+       window.jQuery = window.$ = jQuery;
+}
+
+
+
+
+return jQuery;
+} );
+/**!
+ * @fileOverview Kickass library to create and place poppers near their reference elements.
+ * @version 1.15.0
+ * @license
+ * Copyright (c) 2016 Federico Zivolo and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+(function (global, factory) {
+       typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+       typeof define === 'function' && define.amd ? define(factory) :
+       (global.Popper = factory());
+}(this, (function () { 'use strict';
+
+var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
+
+var longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox'];
+var timeoutDuration = 0;
+for (var i = 0; i < longerTimeoutBrowsers.length; i += 1) {
+  if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) {
+    timeoutDuration = 1;
+    break;
+  }
+}
+
+function microtaskDebounce(fn) {
+  var called = false;
+  return function () {
+    if (called) {
+      return;
+    }
+    called = true;
+    window.Promise.resolve().then(function () {
+      called = false;
+      fn();
+    });
+  };
+}
+
+function taskDebounce(fn) {
+  var scheduled = false;
+  return function () {
+    if (!scheduled) {
+      scheduled = true;
+      setTimeout(function () {
+        scheduled = false;
+        fn();
+      }, timeoutDuration);
+    }
+  };
+}
+
+var supportsMicroTasks = isBrowser && window.Promise;
+
+/**
+* Create a debounced version of a method, that's asynchronously deferred
+* but called in the minimum time possible.
+*
+* @method
+* @memberof Popper.Utils
+* @argument {Function} fn
+* @returns {Function}
+*/
+var debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce;
+
+/**
+ * Check if the given variable is a function
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Any} functionToCheck - variable to check
+ * @returns {Boolean} answer to: is a function?
+ */
+function isFunction(functionToCheck) {
+  var getType = {};
+  return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
+}
+
+/**
+ * Get CSS computed property of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Eement} element
+ * @argument {String} property
+ */
+function getStyleComputedProperty(element, property) {
+  if (element.nodeType !== 1) {
+    return [];
+  }
+  // NOTE: 1 DOM access here
+  var window = element.ownerDocument.defaultView;
+  var css = window.getComputedStyle(element, null);
+  return property ? css[property] : css;
+}
+
+/**
+ * Returns the parentNode or the host of the element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} parent
+ */
+function getParentNode(element) {
+  if (element.nodeName === 'HTML') {
+    return element;
+  }
+  return element.parentNode || element.host;
+}
+
+/**
+ * Returns the scrolling parent of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} scroll parent
+ */
+function getScrollParent(element) {
+  // Return body, `getScroll` will take care to get the correct `scrollTop` from it
+  if (!element) {
+    return document.body;
+  }
+
+  switch (element.nodeName) {
+    case 'HTML':
+    case 'BODY':
+      return element.ownerDocument.body;
+    case '#document':
+      return element.body;
+  }
+
+  // Firefox want us to check `-x` and `-y` variations as well
+
+  var _getStyleComputedProp = getStyleComputedProperty(element),
+      overflow = _getStyleComputedProp.overflow,
+      overflowX = _getStyleComputedProp.overflowX,
+      overflowY = _getStyleComputedProp.overflowY;
+
+  if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {
+    return element;
+  }
+
+  return getScrollParent(getParentNode(element));
+}
+
+var isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);
+var isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);
+
+/**
+ * Determines if the browser is Internet Explorer
+ * @method
+ * @memberof Popper.Utils
+ * @param {Number} version to check
+ * @returns {Boolean} isIE
+ */
+function isIE(version) {
+  if (version === 11) {
+    return isIE11;
+  }
+  if (version === 10) {
+    return isIE10;
+  }
+  return isIE11 || isIE10;
+}
+
+/**
+ * Returns the offset parent of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} offset parent
+ */
+function getOffsetParent(element) {
+  if (!element) {
+    return document.documentElement;
+  }
+
+  var noOffsetParent = isIE(10) ? document.body : null;
+
+  // NOTE: 1 DOM access here
+  var offsetParent = element.offsetParent || null;
+  // Skip hidden elements which don't have an offsetParent
+  while (offsetParent === noOffsetParent && element.nextElementSibling) {
+    offsetParent = (element = element.nextElementSibling).offsetParent;
+  }
+
+  var nodeName = offsetParent && offsetParent.nodeName;
+
+  if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {
+    return element ? element.ownerDocument.documentElement : document.documentElement;
+  }
+
+  // .offsetParent will return the closest TH, TD or TABLE in case
+  // no offsetParent is present, I hate this job...
+  if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {
+    return getOffsetParent(offsetParent);
+  }
+
+  return offsetParent;
+}
+
+function isOffsetContainer(element) {
+  var nodeName = element.nodeName;
+
+  if (nodeName === 'BODY') {
+    return false;
+  }
+  return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element;
+}
+
+/**
+ * Finds the root node (document, shadowDOM root) of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} node
+ * @returns {Element} root node
+ */
+function getRoot(node) {
+  if (node.parentNode !== null) {
+    return getRoot(node.parentNode);
+  }
+
+  return node;
+}
+
+/**
+ * Finds the offset parent common to the two provided nodes
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element1
+ * @argument {Element} element2
+ * @returns {Element} common offset parent
+ */
+function findCommonOffsetParent(element1, element2) {
+  // This check is needed to avoid errors in case one of the elements isn't defined for any reason
+  if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) {
+    return document.documentElement;
+  }
+
+  // Here we make sure to give as "start" the element that comes first in the DOM
+  var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING;
+  var start = order ? element1 : element2;
+  var end = order ? element2 : element1;
+
+  // Get common ancestor container
+  var range = document.createRange();
+  range.setStart(start, 0);
+  range.setEnd(end, 0);
+  var commonAncestorContainer = range.commonAncestorContainer;
+
+  // Both nodes are inside #document
+
+  if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) {
+    if (isOffsetContainer(commonAncestorContainer)) {
+      return commonAncestorContainer;
+    }
+
+    return getOffsetParent(commonAncestorContainer);
+  }
+
+  // one of the nodes is inside shadowDOM, find which one
+  var element1root = getRoot(element1);
+  if (element1root.host) {
+    return findCommonOffsetParent(element1root.host, element2);
+  } else {
+    return findCommonOffsetParent(element1, getRoot(element2).host);
+  }
+}
+
+/**
+ * Gets the scroll value of the given element in the given side (top and left)
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @argument {String} side `top` or `left`
+ * @returns {number} amount of scrolled pixels
+ */
+function getScroll(element) {
+  var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top';
+
+  var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft';
+  var nodeName = element.nodeName;
+
+  if (nodeName === 'BODY' || nodeName === 'HTML') {
+    var html = element.ownerDocument.documentElement;
+    var scrollingElement = element.ownerDocument.scrollingElement || html;
+    return scrollingElement[upperSide];
+  }
+
+  return element[upperSide];
+}
+
+/*
+ * Sum or subtract the element scroll values (left and top) from a given rect object
+ * @method
+ * @memberof Popper.Utils
+ * @param {Object} rect - Rect object you want to change
+ * @param {HTMLElement} element - The element from the function reads the scroll values
+ * @param {Boolean} subtract - set to true if you want to subtract the scroll values
+ * @return {Object} rect - The modifier rect object
+ */
+function includeScroll(rect, element) {
+  var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+  var scrollTop = getScroll(element, 'top');
+  var scrollLeft = getScroll(element, 'left');
+  var modifier = subtract ? -1 : 1;
+  rect.top += scrollTop * modifier;
+  rect.bottom += scrollTop * modifier;
+  rect.left += scrollLeft * modifier;
+  rect.right += scrollLeft * modifier;
+  return rect;
+}
+
+/*
+ * Helper to detect borders of a given element
+ * @method
+ * @memberof Popper.Utils
+ * @param {CSSStyleDeclaration} styles
+ * Result of `getStyleComputedProperty` on the given element
+ * @param {String} axis - `x` or `y`
+ * @return {number} borders - The borders size of the given axis
+ */
+
+function getBordersSize(styles, axis) {
+  var sideA = axis === 'x' ? 'Left' : 'Top';
+  var sideB = sideA === 'Left' ? 'Right' : 'Bottom';
+
+  return parseFloat(styles['border' + sideA + 'Width'], 10) + parseFloat(styles['border' + sideB + 'Width'], 10);
+}
+
+function getSize(axis, body, html, computedStyle) {
+  return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0);
+}
+
+function getWindowSizes(document) {
+  var body = document.body;
+  var html = document.documentElement;
+  var computedStyle = isIE(10) && getComputedStyle(html);
+
+  return {
+    height: getSize('Height', body, html, computedStyle),
+    width: getSize('Width', body, html, computedStyle)
+  };
+}
+
+var classCallCheck = function (instance, Constructor) {
+  if (!(instance instanceof Constructor)) {
+    throw new TypeError("Cannot call a class as a function");
+  }
+};
+
+var createClass = function () {
+  function defineProperties(target, props) {
+    for (var i = 0; i < props.length; i++) {
+      var descriptor = props[i];
+      descriptor.enumerable = descriptor.enumerable || false;
+      descriptor.configurable = true;
+      if ("value" in descriptor) descriptor.writable = true;
+      Object.defineProperty(target, descriptor.key, descriptor);
+    }
+  }
+
+  return function (Constructor, protoProps, staticProps) {
+    if (protoProps) defineProperties(Constructor.prototype, protoProps);
+    if (staticProps) defineProperties(Constructor, staticProps);
+    return Constructor;
+  };
+}();
+
+
+
+
+
+var defineProperty = function (obj, key, value) {
+  if (key in obj) {
+    Object.defineProperty(obj, key, {
+      value: value,
+      enumerable: true,
+      configurable: true,
+      writable: true
+    });
+  } else {
+    obj[key] = value;
+  }
+
+  return obj;
+};
+
+var _extends = Object.assign || function (target) {
+  for (var i = 1; i < arguments.length; i++) {
+    var source = arguments[i];
+
+    for (var key in source) {
+      if (Object.prototype.hasOwnProperty.call(source, key)) {
+        target[key] = source[key];
+      }
+    }
+  }
+
+  return target;
+};
+
+/**
+ * Given element offsets, generate an output similar to getBoundingClientRect
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Object} offsets
+ * @returns {Object} ClientRect like output
+ */
+function getClientRect(offsets) {
+  return _extends({}, offsets, {
+    right: offsets.left + offsets.width,
+    bottom: offsets.top + offsets.height
+  });
+}
+
+/**
+ * Get bounding client rect of given element
+ * @method
+ * @memberof Popper.Utils
+ * @param {HTMLElement} element
+ * @return {Object} client rect
+ */
+function getBoundingClientRect(element) {
+  var rect = {};
+
+  // IE10 10 FIX: Please, don't ask, the element isn't
+  // considered in DOM in some circumstances...
+  // This isn't reproducible in IE10 compatibility mode of IE11
+  try {
+    if (isIE(10)) {
+      rect = element.getBoundingClientRect();
+      var scrollTop = getScroll(element, 'top');
+      var scrollLeft = getScroll(element, 'left');
+      rect.top += scrollTop;
+      rect.left += scrollLeft;
+      rect.bottom += scrollTop;
+      rect.right += scrollLeft;
+    } else {
+      rect = element.getBoundingClientRect();
+    }
+  } catch (e) {}
+
+  var result = {
+    left: rect.left,
+    top: rect.top,
+    width: rect.right - rect.left,
+    height: rect.bottom - rect.top
+  };
+
+  // subtract scrollbar size from sizes
+  var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {};
+  var width = sizes.width || element.clientWidth || result.right - result.left;
+  var height = sizes.height || element.clientHeight || result.bottom - result.top;
+
+  var horizScrollbar = element.offsetWidth - width;
+  var vertScrollbar = element.offsetHeight - height;
+
+  // if an hypothetical scrollbar is detected, we must be sure it's not a `border`
+  // we make this check conditional for performance reasons
+  if (horizScrollbar || vertScrollbar) {
+    var styles = getStyleComputedProperty(element);
+    horizScrollbar -= getBordersSize(styles, 'x');
+    vertScrollbar -= getBordersSize(styles, 'y');
+
+    result.width -= horizScrollbar;
+    result.height -= vertScrollbar;
+  }
+
+  return getClientRect(result);
+}
+
+function getOffsetRectRelativeToArbitraryNode(children, parent) {
+  var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+  var isIE10 = isIE(10);
+  var isHTML = parent.nodeName === 'HTML';
+  var childrenRect = getBoundingClientRect(children);
+  var parentRect = getBoundingClientRect(parent);
+  var scrollParent = getScrollParent(children);
+
+  var styles = getStyleComputedProperty(parent);
+  var borderTopWidth = parseFloat(styles.borderTopWidth, 10);
+  var borderLeftWidth = parseFloat(styles.borderLeftWidth, 10);
+
+  // In cases where the parent is fixed, we must ignore negative scroll in offset calc
+  if (fixedPosition && isHTML) {
+    parentRect.top = Math.max(parentRect.top, 0);
+    parentRect.left = Math.max(parentRect.left, 0);
+  }
+  var offsets = getClientRect({
+    top: childrenRect.top - parentRect.top - borderTopWidth,
+    left: childrenRect.left - parentRect.left - borderLeftWidth,
+    width: childrenRect.width,
+    height: childrenRect.height
+  });
+  offsets.marginTop = 0;
+  offsets.marginLeft = 0;
+
+  // Subtract margins of documentElement in case it's being used as parent
+  // we do this only on HTML because it's the only element that behaves
+  // differently when margins are applied to it. The margins are included in
+  // the box of the documentElement, in the other cases not.
+  if (!isIE10 && isHTML) {
+    var marginTop = parseFloat(styles.marginTop, 10);
+    var marginLeft = parseFloat(styles.marginLeft, 10);
+
+    offsets.top -= borderTopWidth - marginTop;
+    offsets.bottom -= borderTopWidth - marginTop;
+    offsets.left -= borderLeftWidth - marginLeft;
+    offsets.right -= borderLeftWidth - marginLeft;
+
+    // Attach marginTop and marginLeft because in some circumstances we may need them
+    offsets.marginTop = marginTop;
+    offsets.marginLeft = marginLeft;
+  }
+
+  if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') {
+    offsets = includeScroll(offsets, parent);
+  }
+
+  return offsets;
+}
+
+function getViewportOffsetRectRelativeToArtbitraryNode(element) {
+  var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+  var html = element.ownerDocument.documentElement;
+  var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html);
+  var width = Math.max(html.clientWidth, window.innerWidth || 0);
+  var height = Math.max(html.clientHeight, window.innerHeight || 0);
+
+  var scrollTop = !excludeScroll ? getScroll(html) : 0;
+  var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0;
+
+  var offset = {
+    top: scrollTop - relativeOffset.top + relativeOffset.marginTop,
+    left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft,
+    width: width,
+    height: height
+  };
+
+  return getClientRect(offset);
+}
+
+/**
+ * Check if the given element is fixed or is inside a fixed parent
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @argument {Element} customContainer
+ * @returns {Boolean} answer to "isFixed?"
+ */
+function isFixed(element) {
+  var nodeName = element.nodeName;
+  if (nodeName === 'BODY' || nodeName === 'HTML') {
+    return false;
+  }
+  if (getStyleComputedProperty(element, 'position') === 'fixed') {
+    return true;
+  }
+  var parentNode = getParentNode(element);
+  if (!parentNode) {
+    return false;
+  }
+  return isFixed(parentNode);
+}
+
+/**
+ * Finds the first parent of an element that has a transformed property defined
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} first transformed parent or documentElement
+ */
+
+function getFixedPositionOffsetParent(element) {
+  // This check is needed to avoid errors in case one of the elements isn't defined for any reason
+  if (!element || !element.parentElement || isIE()) {
+    return document.documentElement;
+  }
+  var el = element.parentElement;
+  while (el && getStyleComputedProperty(el, 'transform') === 'none') {
+    el = el.parentElement;
+  }
+  return el || document.documentElement;
+}
+
+/**
+ * Computed the boundaries limits and return them
+ * @method
+ * @memberof Popper.Utils
+ * @param {HTMLElement} popper
+ * @param {HTMLElement} reference
+ * @param {number} padding
+ * @param {HTMLElement} boundariesElement - Element used to define the boundaries
+ * @param {Boolean} fixedPosition - Is in fixed position mode
+ * @returns {Object} Coordinates of the boundaries
+ */
+function getBoundaries(popper, reference, padding, boundariesElement) {
+  var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
+
+  // NOTE: 1 DOM access here
+
+  var boundaries = { top: 0, left: 0 };
+  var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);
+
+  // Handle viewport case
+  if (boundariesElement === 'viewport') {
+    boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition);
+  } else {
+    // Handle other cases based on DOM element used as boundaries
+    var boundariesNode = void 0;
+    if (boundariesElement === 'scrollParent') {
+      boundariesNode = getScrollParent(getParentNode(reference));
+      if (boundariesNode.nodeName === 'BODY') {
+        boundariesNode = popper.ownerDocument.documentElement;
+      }
+    } else if (boundariesElement === 'window') {
+      boundariesNode = popper.ownerDocument.documentElement;
+    } else {
+      boundariesNode = boundariesElement;
+    }
+
+    var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition);
+
+    // In case of HTML, we need a different computation
+    if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) {
+      var _getWindowSizes = getWindowSizes(popper.ownerDocument),
+          height = _getWindowSizes.height,
+          width = _getWindowSizes.width;
+
+      boundaries.top += offsets.top - offsets.marginTop;
+      boundaries.bottom = height + offsets.top;
+      boundaries.left += offsets.left - offsets.marginLeft;
+      boundaries.right = width + offsets.left;
+    } else {
+      // for all the other DOM elements, this one is good
+      boundaries = offsets;
+    }
+  }
+
+  // Add paddings
+  padding = padding || 0;
+  var isPaddingNumber = typeof padding === 'number';
+  boundaries.left += isPaddingNumber ? padding : padding.left || 0;
+  boundaries.top += isPaddingNumber ? padding : padding.top || 0;
+  boundaries.right -= isPaddingNumber ? padding : padding.right || 0;
+  boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0;
+
+  return boundaries;
+}
+
+function getArea(_ref) {
+  var width = _ref.width,
+      height = _ref.height;
+
+  return width * height;
+}
+
+/**
+ * Utility used to transform the `auto` placement to the placement with more
+ * available space.
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) {
+  var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0;
+
+  if (placement.indexOf('auto') === -1) {
+    return placement;
+  }
+
+  var boundaries = getBoundaries(popper, reference, padding, boundariesElement);
+
+  var rects = {
+    top: {
+      width: boundaries.width,
+      height: refRect.top - boundaries.top
+    },
+    right: {
+      width: boundaries.right - refRect.right,
+      height: boundaries.height
+    },
+    bottom: {
+      width: boundaries.width,
+      height: boundaries.bottom - refRect.bottom
+    },
+    left: {
+      width: refRect.left - boundaries.left,
+      height: boundaries.height
+    }
+  };
+
+  var sortedAreas = Object.keys(rects).map(function (key) {
+    return _extends({
+      key: key
+    }, rects[key], {
+      area: getArea(rects[key])
+    });
+  }).sort(function (a, b) {
+    return b.area - a.area;
+  });
+
+  var filteredAreas = sortedAreas.filter(function (_ref2) {
+    var width = _ref2.width,
+        height = _ref2.height;
+    return width >= popper.clientWidth && height >= popper.clientHeight;
+  });
+
+  var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key;
+
+  var variation = placement.split('-')[1];
+
+  return computedPlacement + (variation ? '-' + variation : '');
+}
+
+/**
+ * Get offsets to the reference element
+ * @method
+ * @memberof Popper.Utils
+ * @param {Object} state
+ * @param {Element} popper - the popper element
+ * @param {Element} reference - the reference element (the popper will be relative to this)
+ * @param {Element} fixedPosition - is in fixed position mode
+ * @returns {Object} An object containing the offsets which will be applied to the popper
+ */
+function getReferenceOffsets(state, popper, reference) {
+  var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
+
+  var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);
+  return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition);
+}
+
+/**
+ * Get the outer sizes of the given element (offset size + margins)
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Object} object containing width and height properties
+ */
+function getOuterSizes(element) {
+  var window = element.ownerDocument.defaultView;
+  var styles = window.getComputedStyle(element);
+  var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0);
+  var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0);
+  var result = {
+    width: element.offsetWidth + y,
+    height: element.offsetHeight + x
+  };
+  return result;
+}
+
+/**
+ * Get the opposite placement of the given one
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} placement
+ * @returns {String} flipped placement
+ */
+function getOppositePlacement(placement) {
+  var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' };
+  return placement.replace(/left|right|bottom|top/g, function (matched) {
+    return hash[matched];
+  });
+}
+
+/**
+ * Get offsets to the popper
+ * @method
+ * @memberof Popper.Utils
+ * @param {Object} position - CSS position the Popper will get applied
+ * @param {HTMLElement} popper - the popper element
+ * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this)
+ * @param {String} placement - one of the valid placement options
+ * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper
+ */
+function getPopperOffsets(popper, referenceOffsets, placement) {
+  placement = placement.split('-')[0];
+
+  // Get popper node sizes
+  var popperRect = getOuterSizes(popper);
+
+  // Add position, width and height to our offsets object
+  var popperOffsets = {
+    width: popperRect.width,
+    height: popperRect.height
+  };
+
+  // depending by the popper placement we have to compute its offsets slightly differently
+  var isHoriz = ['right', 'left'].indexOf(placement) !== -1;
+  var mainSide = isHoriz ? 'top' : 'left';
+  var secondarySide = isHoriz ? 'left' : 'top';
+  var measurement = isHoriz ? 'height' : 'width';
+  var secondaryMeasurement = !isHoriz ? 'height' : 'width';
+
+  popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2;
+  if (placement === secondarySide) {
+    popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement];
+  } else {
+    popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)];
+  }
+
+  return popperOffsets;
+}
+
+/**
+ * Mimics the `find` method of Array
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Array} arr
+ * @argument prop
+ * @argument value
+ * @returns index or -1
+ */
+function find(arr, check) {
+  // use native find if supported
+  if (Array.prototype.find) {
+    return arr.find(check);
+  }
+
+  // use `filter` to obtain the same behavior of `find`
+  return arr.filter(check)[0];
+}
+
+/**
+ * Return the index of the matching object
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Array} arr
+ * @argument prop
+ * @argument value
+ * @returns index or -1
+ */
+function findIndex(arr, prop, value) {
+  // use native findIndex if supported
+  if (Array.prototype.findIndex) {
+    return arr.findIndex(function (cur) {
+      return cur[prop] === value;
+    });
+  }
+
+  // use `find` + `indexOf` if `findIndex` isn't supported
+  var match = find(arr, function (obj) {
+    return obj[prop] === value;
+  });
+  return arr.indexOf(match);
+}
+
+/**
+ * Loop trough the list of modifiers and run them in order,
+ * each of them will then edit the data object.
+ * @method
+ * @memberof Popper.Utils
+ * @param {dataObject} data
+ * @param {Array} modifiers
+ * @param {String} ends - Optional modifier name used as stopper
+ * @returns {dataObject}
+ */
+function runModifiers(modifiers, data, ends) {
+  var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends));
+
+  modifiersToRun.forEach(function (modifier) {
+    if (modifier['function']) {
+      // eslint-disable-line dot-notation
+      console.warn('`modifier.function` is deprecated, use `modifier.fn`!');
+    }
+    var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation
+    if (modifier.enabled && isFunction(fn)) {
+      // Add properties to offsets to make them a complete clientRect object
+      // we do this before each modifier to make sure the previous one doesn't
+      // mess with these values
+      data.offsets.popper = getClientRect(data.offsets.popper);
+      data.offsets.reference = getClientRect(data.offsets.reference);
+
+      data = fn(data, modifier);
+    }
+  });
+
+  return data;
+}
+
+/**
+ * Updates the position of the popper, computing the new offsets and applying
+ * the new style.<br />
+ * Prefer `scheduleUpdate` over `update` because of performance reasons.
+ * @method
+ * @memberof Popper
+ */
+function update() {
+  // if popper is destroyed, don't perform any further update
+  if (this.state.isDestroyed) {
+    return;
+  }
+
+  var data = {
+    instance: this,
+    styles: {},
+    arrowStyles: {},
+    attributes: {},
+    flipped: false,
+    offsets: {}
+  };
+
+  // compute reference element offsets
+  data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed);
+
+  // compute auto placement, store placement inside the data object,
+  // modifiers will be able to edit `placement` if needed
+  // and refer to originalPlacement to know the original value
+  data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding);
+
+  // store the computed placement inside `originalPlacement`
+  data.originalPlacement = data.placement;
+
+  data.positionFixed = this.options.positionFixed;
+
+  // compute the popper offsets
+  data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement);
+
+  data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute';
+
+  // run the modifiers
+  data = runModifiers(this.modifiers, data);
+
+  // the first `update` will call `onCreate` callback
+  // the other ones will call `onUpdate` callback
+  if (!this.state.isCreated) {
+    this.state.isCreated = true;
+    this.options.onCreate(data);
+  } else {
+    this.options.onUpdate(data);
+  }
+}
+
+/**
+ * Helper used to know if the given modifier is enabled.
+ * @method
+ * @memberof Popper.Utils
+ * @returns {Boolean}
+ */
+function isModifierEnabled(modifiers, modifierName) {
+  return modifiers.some(function (_ref) {
+    var name = _ref.name,
+        enabled = _ref.enabled;
+    return enabled && name === modifierName;
+  });
+}
+
+/**
+ * Get the prefixed supported property name
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} property (camelCase)
+ * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix)
+ */
+function getSupportedPropertyName(property) {
+  var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O'];
+  var upperProp = property.charAt(0).toUpperCase() + property.slice(1);
+
+  for (var i = 0; i < prefixes.length; i++) {
+    var prefix = prefixes[i];
+    var toCheck = prefix ? '' + prefix + upperProp : property;
+    if (typeof document.body.style[toCheck] !== 'undefined') {
+      return toCheck;
+    }
+  }
+  return null;
+}
+
+/**
+ * Destroys the popper.
+ * @method
+ * @memberof Popper
+ */
+function destroy() {
+  this.state.isDestroyed = true;
+
+  // touch DOM only if `applyStyle` modifier is enabled
+  if (isModifierEnabled(this.modifiers, 'applyStyle')) {
+    this.popper.removeAttribute('x-placement');
+    this.popper.style.position = '';
+    this.popper.style.top = '';
+    this.popper.style.left = '';
+    this.popper.style.right = '';
+    this.popper.style.bottom = '';
+    this.popper.style.willChange = '';
+    this.popper.style[getSupportedPropertyName('transform')] = '';
+  }
+
+  this.disableEventListeners();
+
+  // remove the popper if user explicity asked for the deletion on destroy
+  // do not use `remove` because IE11 doesn't support it
+  if (this.options.removeOnDestroy) {
+    this.popper.parentNode.removeChild(this.popper);
+  }
+  return this;
+}
+
+/**
+ * Get the window associated with the element
+ * @argument {Element} element
+ * @returns {Window}
+ */
+function getWindow(element) {
+  var ownerDocument = element.ownerDocument;
+  return ownerDocument ? ownerDocument.defaultView : window;
+}
+
+function attachToScrollParents(scrollParent, event, callback, scrollParents) {
+  var isBody = scrollParent.nodeName === 'BODY';
+  var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent;
+  target.addEventListener(event, callback, { passive: true });
+
+  if (!isBody) {
+    attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents);
+  }
+  scrollParents.push(target);
+}
+
+/**
+ * Setup needed event listeners used to update the popper position
+ * @method
+ * @memberof Popper.Utils
+ * @private
+ */
+function setupEventListeners(reference, options, state, updateBound) {
+  // Resize event listener on window
+  state.updateBound = updateBound;
+  getWindow(reference).addEventListener('resize', state.updateBound, { passive: true });
+
+  // Scroll event listener on scroll parents
+  var scrollElement = getScrollParent(reference);
+  attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents);
+  state.scrollElement = scrollElement;
+  state.eventsEnabled = true;
+
+  return state;
+}
+
+/**
+ * It will add resize/scroll events and start recalculating
+ * position of the popper element when they are triggered.
+ * @method
+ * @memberof Popper
+ */
+function enableEventListeners() {
+  if (!this.state.eventsEnabled) {
+    this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate);
+  }
+}
+
+/**
+ * Remove event listeners used to update the popper position
+ * @method
+ * @memberof Popper.Utils
+ * @private
+ */
+function removeEventListeners(reference, state) {
+  // Remove resize event listener on window
+  getWindow(reference).removeEventListener('resize', state.updateBound);
+
+  // Remove scroll event listener on scroll parents
+  state.scrollParents.forEach(function (target) {
+    target.removeEventListener('scroll', state.updateBound);
+  });
+
+  // Reset state
+  state.updateBound = null;
+  state.scrollParents = [];
+  state.scrollElement = null;
+  state.eventsEnabled = false;
+  return state;
+}
+
+/**
+ * It will remove resize/scroll events and won't recalculate popper position
+ * when they are triggered. It also won't trigger `onUpdate` callback anymore,
+ * unless you call `update` method manually.
+ * @method
+ * @memberof Popper
+ */
+function disableEventListeners() {
+  if (this.state.eventsEnabled) {
+    cancelAnimationFrame(this.scheduleUpdate);
+    this.state = removeEventListeners(this.reference, this.state);
+  }
+}
+
+/**
+ * Tells if a given input is a number
+ * @method
+ * @memberof Popper.Utils
+ * @param {*} input to check
+ * @return {Boolean}
+ */
+function isNumeric(n) {
+  return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);
+}
+
+/**
+ * Set the style to the given popper
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element - Element to apply the style to
+ * @argument {Object} styles
+ * Object with a list of properties and values which will be applied to the element
+ */
+function setStyles(element, styles) {
+  Object.keys(styles).forEach(function (prop) {
+    var unit = '';
+    // add unit if the value is numeric and is one of the following
+    if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) {
+      unit = 'px';
+    }
+    element.style[prop] = styles[prop] + unit;
+  });
+}
+
+/**
+ * Set the attributes to the given popper
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element - Element to apply the attributes to
+ * @argument {Object} styles
+ * Object with a list of properties and values which will be applied to the element
+ */
+function setAttributes(element, attributes) {
+  Object.keys(attributes).forEach(function (prop) {
+    var value = attributes[prop];
+    if (value !== false) {
+      element.setAttribute(prop, attributes[prop]);
+    } else {
+      element.removeAttribute(prop);
+    }
+  });
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} data.styles - List of style properties - values to apply to popper element
+ * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The same data object
+ */
+function applyStyle(data) {
+  // any property present in `data.styles` will be applied to the popper,
+  // in this way we can make the 3rd party modifiers add custom styles to it
+  // Be aware, modifiers could override the properties defined in the previous
+  // lines of this modifier!
+  setStyles(data.instance.popper, data.styles);
+
+  // any property present in `data.attributes` will be applied to the popper,
+  // they will be set as HTML attributes of the element
+  setAttributes(data.instance.popper, data.attributes);
+
+  // if arrowElement is defined and arrowStyles has some properties
+  if (data.arrowElement && Object.keys(data.arrowStyles).length) {
+    setStyles(data.arrowElement, data.arrowStyles);
+  }
+
+  return data;
+}
+
+/**
+ * Set the x-placement attribute before everything else because it could be used
+ * to add margins to the popper margins needs to be calculated to get the
+ * correct popper offsets.
+ * @method
+ * @memberof Popper.modifiers
+ * @param {HTMLElement} reference - The reference element used to position the popper
+ * @param {HTMLElement} popper - The HTML element used as popper
+ * @param {Object} options - Popper.js options
+ */
+function applyStyleOnLoad(reference, popper, options, modifierOptions, state) {
+  // compute reference element offsets
+  var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed);
+
+  // compute auto placement, store placement inside the data object,
+  // modifiers will be able to edit `placement` if needed
+  // and refer to originalPlacement to know the original value
+  var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding);
+
+  popper.setAttribute('x-placement', placement);
+
+  // Apply `position` to popper before anything else because
+  // without the position applied we can't guarantee correct computations
+  setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' });
+
+  return options;
+}
+
+/**
+ * @function
+ * @memberof Popper.Utils
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Boolean} shouldRound - If the offsets should be rounded at all
+ * @returns {Object} The popper's position offsets rounded
+ *
+ * The tale of pixel-perfect positioning. It's still not 100% perfect, but as
+ * good as it can be within reason.
+ * Discussion here: https://github.com/FezVrasta/popper.js/pull/715
+ *
+ * Low DPI screens cause a popper to be blurry if not using full pixels (Safari
+ * as well on High DPI screens).
+ *
+ * Firefox prefers no rounding for positioning and does not have blurriness on
+ * high DPI screens.
+ *
+ * Only horizontal placement and left/right values need to be considered.
+ */
+function getRoundedOffsets(data, shouldRound) {
+  var _data$offsets = data.offsets,
+      popper = _data$offsets.popper,
+      reference = _data$offsets.reference;
+  var round = Math.round,
+      floor = Math.floor;
+
+  var noRound = function noRound(v) {
+    return v;
+  };
+
+  var referenceWidth = round(reference.width);
+  var popperWidth = round(popper.width);
+
+  var isVertical = ['left', 'right'].indexOf(data.placement) !== -1;
+  var isVariation = data.placement.indexOf('-') !== -1;
+  var sameWidthParity = referenceWidth % 2 === popperWidth % 2;
+  var bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1;
+
+  var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthParity ? round : floor;
+  var verticalToInteger = !shouldRound ? noRound : round;
+
+  return {
+    left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left),
+    top: verticalToInteger(popper.top),
+    bottom: verticalToInteger(popper.bottom),
+    right: horizontalToInteger(popper.right)
+  };
+}
+
+var isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent);
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function computeStyle(data, options) {
+  var x = options.x,
+      y = options.y;
+  var popper = data.offsets.popper;
+
+  // Remove this legacy support in Popper.js v2
+
+  var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) {
+    return modifier.name === 'applyStyle';
+  }).gpuAcceleration;
+  if (legacyGpuAccelerationOption !== undefined) {
+    console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');
+  }
+  var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration;
+
+  var offsetParent = getOffsetParent(data.instance.popper);
+  var offsetParentRect = getBoundingClientRect(offsetParent);
+
+  // Styles
+  var styles = {
+    position: popper.position
+  };
+
+  var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox);
+
+  var sideA = x === 'bottom' ? 'top' : 'bottom';
+  var sideB = y === 'right' ? 'left' : 'right';
+
+  // if gpuAcceleration is set to `true` and transform is supported,
+  //  we use `translate3d` to apply the position to the popper we
+  // automatically use the supported prefixed version if needed
+  var prefixedProperty = getSupportedPropertyName('transform');
+
+  // now, let's make a step back and look at this code closely (wtf?)
+  // If the content of the popper grows once it's been positioned, it
+  // may happen that the popper gets misplaced because of the new content
+  // overflowing its reference element
+  // To avoid this problem, we provide two options (x and y), which allow
+  // the consumer to define the offset origin.
+  // If we position a popper on top of a reference element, we can set
+  // `x` to `top` to make the popper grow towards its top instead of
+  // its bottom.
+  var left = void 0,
+      top = void 0;
+  if (sideA === 'bottom') {
+    // when offsetParent is <html> the positioning is relative to the bottom of the screen (excluding the scrollbar)
+    // and not the bottom of the html element
+    if (offsetParent.nodeName === 'HTML') {
+      top = -offsetParent.clientHeight + offsets.bottom;
+    } else {
+      top = -offsetParentRect.height + offsets.bottom;
+    }
+  } else {
+    top = offsets.top;
+  }
+  if (sideB === 'right') {
+    if (offsetParent.nodeName === 'HTML') {
+      left = -offsetParent.clientWidth + offsets.right;
+    } else {
+      left = -offsetParentRect.width + offsets.right;
+    }
+  } else {
+    left = offsets.left;
+  }
+  if (gpuAcceleration && prefixedProperty) {
+    styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
+    styles[sideA] = 0;
+    styles[sideB] = 0;
+    styles.willChange = 'transform';
+  } else {
+    // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties
+    var invertTop = sideA === 'bottom' ? -1 : 1;
+    var invertLeft = sideB === 'right' ? -1 : 1;
+    styles[sideA] = top * invertTop;
+    styles[sideB] = left * invertLeft;
+    styles.willChange = sideA + ', ' + sideB;
+  }
+
+  // Attributes
+  var attributes = {
+    'x-placement': data.placement
+  };
+
+  // Update `data` attributes, styles and arrowStyles
+  data.attributes = _extends({}, attributes, data.attributes);
+  data.styles = _extends({}, styles, data.styles);
+  data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles);
+
+  return data;
+}
+
+/**
+ * Helper used to know if the given modifier depends from another one.<br />
+ * It checks if the needed modifier is listed and enabled.
+ * @method
+ * @memberof Popper.Utils
+ * @param {Array} modifiers - list of modifiers
+ * @param {String} requestingName - name of requesting modifier
+ * @param {String} requestedName - name of requested modifier
+ * @returns {Boolean}
+ */
+function isModifierRequired(modifiers, requestingName, requestedName) {
+  var requesting = find(modifiers, function (_ref) {
+    var name = _ref.name;
+    return name === requestingName;
+  });
+
+  var isRequired = !!requesting && modifiers.some(function (modifier) {
+    return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order;
+  });
+
+  if (!isRequired) {
+    var _requesting = '`' + requestingName + '`';
+    var requested = '`' + requestedName + '`';
+    console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!');
+  }
+  return isRequired;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function arrow(data, options) {
+  var _data$offsets$arrow;
+
+  // arrow depends on keepTogether in order to work
+  if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) {
+    return data;
+  }
+
+  var arrowElement = options.element;
+
+  // if arrowElement is a string, suppose it's a CSS selector
+  if (typeof arrowElement === 'string') {
+    arrowElement = data.instance.popper.querySelector(arrowElement);
+
+    // if arrowElement is not found, don't run the modifier
+    if (!arrowElement) {
+      return data;
+    }
+  } else {
+    // if the arrowElement isn't a query selector we must check that the
+    // provided DOM node is child of its popper node
+    if (!data.instance.popper.contains(arrowElement)) {
+      console.warn('WARNING: `arrow.element` must be child of its popper element!');
+      return data;
+    }
+  }
+
+  var placement = data.placement.split('-')[0];
+  var _data$offsets = data.offsets,
+      popper = _data$offsets.popper,
+      reference = _data$offsets.reference;
+
+  var isVertical = ['left', 'right'].indexOf(placement) !== -1;
+
+  var len = isVertical ? 'height' : 'width';
+  var sideCapitalized = isVertical ? 'Top' : 'Left';
+  var side = sideCapitalized.toLowerCase();
+  var altSide = isVertical ? 'left' : 'top';
+  var opSide = isVertical ? 'bottom' : 'right';
+  var arrowElementSize = getOuterSizes(arrowElement)[len];
+
+  //
+  // extends keepTogether behavior making sure the popper and its
+  // reference have enough pixels in conjunction
+  //
+
+  // top/left side
+  if (reference[opSide] - arrowElementSize < popper[side]) {
+    data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize);
+  }
+  // bottom/right side
+  if (reference[side] + arrowElementSize > popper[opSide]) {
+    data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide];
+  }
+  data.offsets.popper = getClientRect(data.offsets.popper);
+
+  // compute center of the popper
+  var center = reference[side] + reference[len] / 2 - arrowElementSize / 2;
+
+  // Compute the sideValue using the updated popper offsets
+  // take popper margin in account because we don't have this info available
+  var css = getStyleComputedProperty(data.instance.popper);
+  var popperMarginSide = parseFloat(css['margin' + sideCapitalized], 10);
+  var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width'], 10);
+  var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide;
+
+  // prevent arrowElement from being placed not contiguously to its popper
+  sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0);
+
+  data.arrowElement = arrowElement;
+  data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow);
+
+  return data;
+}
+
+/**
+ * Get the opposite placement variation of the given one
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} placement variation
+ * @returns {String} flipped placement variation
+ */
+function getOppositeVariation(variation) {
+  if (variation === 'end') {
+    return 'start';
+  } else if (variation === 'start') {
+    return 'end';
+  }
+  return variation;
+}
+
+/**
+ * List of accepted placements to use as values of the `placement` option.<br />
+ * Valid placements are:
+ * - `auto`
+ * - `top`
+ * - `right`
+ * - `bottom`
+ * - `left`
+ *
+ * Each placement can have a variation from this list:
+ * - `-start`
+ * - `-end`
+ *
+ * Variations are interpreted easily if you think of them as the left to right
+ * written languages. Horizontally (`top` and `bottom`), `start` is left and `end`
+ * is right.<br />
+ * Vertically (`left` and `right`), `start` is top and `end` is bottom.
+ *
+ * Some valid examples are:
+ * - `top-end` (on top of reference, right aligned)
+ * - `right-start` (on right of reference, top aligned)
+ * - `bottom` (on bottom, centered)
+ * - `auto-end` (on the side with more space available, alignment depends by placement)
+ *
+ * @static
+ * @type {Array}
+ * @enum {String}
+ * @readonly
+ * @method placements
+ * @memberof Popper
+ */
+var placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start'];
+
+// Get rid of `auto` `auto-start` and `auto-end`
+var validPlacements = placements.slice(3);
+
+/**
+ * Given an initial placement, returns all the subsequent placements
+ * clockwise (or counter-clockwise).
+ *
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} placement - A valid placement (it accepts variations)
+ * @argument {Boolean} counter - Set to true to walk the placements counterclockwise
+ * @returns {Array} placements including their variations
+ */
+function clockwise(placement) {
+  var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+  var index = validPlacements.indexOf(placement);
+  var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index));
+  return counter ? arr.reverse() : arr;
+}
+
+var BEHAVIORS = {
+  FLIP: 'flip',
+  CLOCKWISE: 'clockwise',
+  COUNTERCLOCKWISE: 'counterclockwise'
+};
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function flip(data, options) {
+  // if `inner` modifier is enabled, we can't use the `flip` modifier
+  if (isModifierEnabled(data.instance.modifiers, 'inner')) {
+    return data;
+  }
+
+  if (data.flipped && data.placement === data.originalPlacement) {
+    // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides
+    return data;
+  }
+
+  var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed);
+
+  var placement = data.placement.split('-')[0];
+  var placementOpposite = getOppositePlacement(placement);
+  var variation = data.placement.split('-')[1] || '';
+
+  var flipOrder = [];
+
+  switch (options.behavior) {
+    case BEHAVIORS.FLIP:
+      flipOrder = [placement, placementOpposite];
+      break;
+    case BEHAVIORS.CLOCKWISE:
+      flipOrder = clockwise(placement);
+      break;
+    case BEHAVIORS.COUNTERCLOCKWISE:
+      flipOrder = clockwise(placement, true);
+      break;
+    default:
+      flipOrder = options.behavior;
+  }
+
+  flipOrder.forEach(function (step, index) {
+    if (placement !== step || flipOrder.length === index + 1) {
+      return data;
+    }
+
+    placement = data.placement.split('-')[0];
+    placementOpposite = getOppositePlacement(placement);
+
+    var popperOffsets = data.offsets.popper;
+    var refOffsets = data.offsets.reference;
+
+    // using floor because the reference offsets may contain decimals we are not going to consider here
+    var floor = Math.floor;
+    var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom);
+
+    var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left);
+    var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right);
+    var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top);
+    var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom);
+
+    var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom;
+
+    // flip the variation if required
+    var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;
+
+    // flips variation if reference element overflows boundaries
+    var flippedVariationByRef = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom);
+
+    // flips variation if popper content overflows boundaries
+    var flippedVariationByContent = !!options.flipVariationsByContent && (isVertical && variation === 'start' && overflowsRight || isVertical && variation === 'end' && overflowsLeft || !isVertical && variation === 'start' && overflowsBottom || !isVertical && variation === 'end' && overflowsTop);
+
+    var flippedVariation = flippedVariationByRef || flippedVariationByContent;
+
+    if (overlapsRef || overflowsBoundaries || flippedVariation) {
+      // this boolean to detect any flip loop
+      data.flipped = true;
+
+      if (overlapsRef || overflowsBoundaries) {
+        placement = flipOrder[index + 1];
+      }
+
+      if (flippedVariation) {
+        variation = getOppositeVariation(variation);
+      }
+
+      data.placement = placement + (variation ? '-' + variation : '');
+
+      // this object contains `position`, we want to preserve it along with
+      // any additional property we may add in the future
+      data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement));
+
+      data = runModifiers(data.instance.modifiers, data, 'flip');
+    }
+  });
+  return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function keepTogether(data) {
+  var _data$offsets = data.offsets,
+      popper = _data$offsets.popper,
+      reference = _data$offsets.reference;
+
+  var placement = data.placement.split('-')[0];
+  var floor = Math.floor;
+  var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;
+  var side = isVertical ? 'right' : 'bottom';
+  var opSide = isVertical ? 'left' : 'top';
+  var measurement = isVertical ? 'width' : 'height';
+
+  if (popper[side] < floor(reference[opSide])) {
+    data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement];
+  }
+  if (popper[opSide] > floor(reference[side])) {
+    data.offsets.popper[opSide] = floor(reference[side]);
+  }
+
+  return data;
+}
+
+/**
+ * Converts a string containing value + unit into a px value number
+ * @function
+ * @memberof {modifiers~offset}
+ * @private
+ * @argument {String} str - Value + unit string
+ * @argument {String} measurement - `height` or `width`
+ * @argument {Object} popperOffsets
+ * @argument {Object} referenceOffsets
+ * @returns {Number|String}
+ * Value in pixels, or original string if no values were extracted
+ */
+function toValue(str, measurement, popperOffsets, referenceOffsets) {
+  // separate value from unit
+  var split = str.match(/((?:\-|\+)?\d*\.?\d*)(.*)/);
+  var value = +split[1];
+  var unit = split[2];
+
+  // If it's not a number it's an operator, I guess
+  if (!value) {
+    return str;
+  }
+
+  if (unit.indexOf('%') === 0) {
+    var element = void 0;
+    switch (unit) {
+      case '%p':
+        element = popperOffsets;
+        break;
+      case '%':
+      case '%r':
+      default:
+        element = referenceOffsets;
+    }
+
+    var rect = getClientRect(element);
+    return rect[measurement] / 100 * value;
+  } else if (unit === 'vh' || unit === 'vw') {
+    // if is a vh or vw, we calculate the size based on the viewport
+    var size = void 0;
+    if (unit === 'vh') {
+      size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
+    } else {
+      size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
+    }
+    return size / 100 * value;
+  } else {
+    // if is an explicit pixel unit, we get rid of the unit and keep the value
+    // if is an implicit unit, it's px, and we return just the value
+    return value;
+  }
+}
+
+/**
+ * Parse an `offset` string to extrapolate `x` and `y` numeric offsets.
+ * @function
+ * @memberof {modifiers~offset}
+ * @private
+ * @argument {String} offset
+ * @argument {Object} popperOffsets
+ * @argument {Object} referenceOffsets
+ * @argument {String} basePlacement
+ * @returns {Array} a two cells array with x and y offsets in numbers
+ */
+function parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) {
+  var offsets = [0, 0];
+
+  // Use height if placement is left or right and index is 0 otherwise use width
+  // in this way the first offset will use an axis and the second one
+  // will use the other one
+  var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1;
+
+  // Split the offset string to obtain a list of values and operands
+  // The regex addresses values with the plus or minus sign in front (+10, -20, etc)
+  var fragments = offset.split(/(\+|\-)/).map(function (frag) {
+    return frag.trim();
+  });
+
+  // Detect if the offset string contains a pair of values or a single one
+  // they could be separated by comma or space
+  var divider = fragments.indexOf(find(fragments, function (frag) {
+    return frag.search(/,|\s/) !== -1;
+  }));
+
+  if (fragments[divider] && fragments[divider].indexOf(',') === -1) {
+    console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');
+  }
+
+  // If divider is found, we divide the list of values and operands to divide
+  // them by ofset X and Y.
+  var splitRegex = /\s*,\s*|\s+/;
+  var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments];
+
+  // Convert the values with units to absolute pixels to allow our computations
+  ops = ops.map(function (op, index) {
+    // Most of the units rely on the orientation of the popper
+    var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width';
+    var mergeWithPrevious = false;
+    return op
+    // This aggregates any `+` or `-` sign that aren't considered operators
+    // e.g.: 10 + +5 => [10, +, +5]
+    .reduce(function (a, b) {
+      if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) {
+        a[a.length - 1] = b;
+        mergeWithPrevious = true;
+        return a;
+      } else if (mergeWithPrevious) {
+        a[a.length - 1] += b;
+        mergeWithPrevious = false;
+        return a;
+      } else {
+        return a.concat(b);
+      }
+    }, [])
+    // Here we convert the string values into number values (in px)
+    .map(function (str) {
+      return toValue(str, measurement, popperOffsets, referenceOffsets);
+    });
+  });
+
+  // Loop trough the offsets arrays and execute the operations
+  ops.forEach(function (op, index) {
+    op.forEach(function (frag, index2) {
+      if (isNumeric(frag)) {
+        offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1);
+      }
+    });
+  });
+  return offsets;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @argument {Number|String} options.offset=0
+ * The offset value as described in the modifier description
+ * @returns {Object} The data object, properly modified
+ */
+function offset(data, _ref) {
+  var offset = _ref.offset;
+  var placement = data.placement,
+      _data$offsets = data.offsets,
+      popper = _data$offsets.popper,
+      reference = _data$offsets.reference;
+
+  var basePlacement = placement.split('-')[0];
+
+  var offsets = void 0;
+  if (isNumeric(+offset)) {
+    offsets = [+offset, 0];
+  } else {
+    offsets = parseOffset(offset, popper, reference, basePlacement);
+  }
+
+  if (basePlacement === 'left') {
+    popper.top += offsets[0];
+    popper.left -= offsets[1];
+  } else if (basePlacement === 'right') {
+    popper.top += offsets[0];
+    popper.left += offsets[1];
+  } else if (basePlacement === 'top') {
+    popper.left += offsets[0];
+    popper.top -= offsets[1];
+  } else if (basePlacement === 'bottom') {
+    popper.left += offsets[0];
+    popper.top += offsets[1];
+  }
+
+  data.popper = popper;
+  return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function preventOverflow(data, options) {
+  var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper);
+
+  // If offsetParent is the reference element, we really want to
+  // go one step up and use the next offsetParent as reference to
+  // avoid to make this modifier completely useless and look like broken
+  if (data.instance.reference === boundariesElement) {
+    boundariesElement = getOffsetParent(boundariesElement);
+  }
+
+  // NOTE: DOM access here
+  // resets the popper's position so that the document size can be calculated excluding
+  // the size of the popper element itself
+  var transformProp = getSupportedPropertyName('transform');
+  var popperStyles = data.instance.popper.style; // assignment to help minification
+  var top = popperStyles.top,
+      left = popperStyles.left,
+      transform = popperStyles[transformProp];
+
+  popperStyles.top = '';
+  popperStyles.left = '';
+  popperStyles[transformProp] = '';
+
+  var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed);
+
+  // NOTE: DOM access here
+  // restores the original style properties after the offsets have been computed
+  popperStyles.top = top;
+  popperStyles.left = left;
+  popperStyles[transformProp] = transform;
+
+  options.boundaries = boundaries;
+
+  var order = options.priority;
+  var popper = data.offsets.popper;
+
+  var check = {
+    primary: function primary(placement) {
+      var value = popper[placement];
+      if (popper[placement] < boundaries[placement] && !options.escapeWithReference) {
+        value = Math.max(popper[placement], boundaries[placement]);
+      }
+      return defineProperty({}, placement, value);
+    },
+    secondary: function secondary(placement) {
+      var mainSide = placement === 'right' ? 'left' : 'top';
+      var value = popper[mainSide];
+      if (popper[placement] > boundaries[placement] && !options.escapeWithReference) {
+        value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height));
+      }
+      return defineProperty({}, mainSide, value);
+    }
+  };
+
+  order.forEach(function (placement) {
+    var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';
+    popper = _extends({}, popper, check[side](placement));
+  });
+
+  data.offsets.popper = popper;
+
+  return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function shift(data) {
+  var placement = data.placement;
+  var basePlacement = placement.split('-')[0];
+  var shiftvariation = placement.split('-')[1];
+
+  // if shift shiftvariation is specified, run the modifier
+  if (shiftvariation) {
+    var _data$offsets = data.offsets,
+        reference = _data$offsets.reference,
+        popper = _data$offsets.popper;
+
+    var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1;
+    var side = isVertical ? 'left' : 'top';
+    var measurement = isVertical ? 'width' : 'height';
+
+    var shiftOffsets = {
+      start: defineProperty({}, side, reference[side]),
+      end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement])
+    };
+
+    data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]);
+  }
+
+  return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function hide(data) {
+  if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) {
+    return data;
+  }
+
+  var refRect = data.offsets.reference;
+  var bound = find(data.instance.modifiers, function (modifier) {
+    return modifier.name === 'preventOverflow';
+  }).boundaries;
+
+  if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) {
+    // Avoid unnecessary DOM access if visibility hasn't changed
+    if (data.hide === true) {
+      return data;
+    }
+
+    data.hide = true;
+    data.attributes['x-out-of-boundaries'] = '';
+  } else {
+    // Avoid unnecessary DOM access if visibility hasn't changed
+    if (data.hide === false) {
+      return data;
+    }
+
+    data.hide = false;
+    data.attributes['x-out-of-boundaries'] = false;
+  }
+
+  return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function inner(data) {
+  var placement = data.placement;
+  var basePlacement = placement.split('-')[0];
+  var _data$offsets = data.offsets,
+      popper = _data$offsets.popper,
+      reference = _data$offsets.reference;
+
+  var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1;
+
+  var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1;
+
+  popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0);
+
+  data.placement = getOppositePlacement(placement);
+  data.offsets.popper = getClientRect(popper);
+
+  return data;
+}
+
+/**
+ * Modifier function, each modifier can have a function of this type assigned
+ * to its `fn` property.<br />
+ * These functions will be called on each update, this means that you must
+ * make sure they are performant enough to avoid performance bottlenecks.
+ *
+ * @function ModifierFn
+ * @argument {dataObject} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {dataObject} The data object, properly modified
+ */
+
+/**
+ * Modifiers are plugins used to alter the behavior of your poppers.<br />
+ * Popper.js uses a set of 9 modifiers to provide all the basic functionalities
+ * needed by the library.
+ *
+ * Usually you don't want to override the `order`, `fn` and `onLoad` props.
+ * All the other properties are configurations that could be tweaked.
+ * @namespace modifiers
+ */
+var modifiers = {
+  /**
+   * Modifier used to shift the popper on the start or end of its reference
+   * element.<br />
+   * It will read the variation of the `placement` property.<br />
+   * It can be one either `-end` or `-start`.
+   * @memberof modifiers
+   * @inner
+   */
+  shift: {
+    /** @prop {number} order=100 - Index used to define the order of execution */
+    order: 100,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: shift
+  },
+
+  /**
+   * The `offset` modifier can shift your popper on both its axis.
+   *
+   * It accepts the following units:
+   * - `px` or unit-less, interpreted as pixels
+   * - `%` or `%r`, percentage relative to the length of the reference element
+   * - `%p`, percentage relative to the length of the popper element
+   * - `vw`, CSS viewport width unit
+   * - `vh`, CSS viewport height unit
+   *
+   * For length is intended the main axis relative to the placement of the popper.<br />
+   * This means that if the placement is `top` or `bottom`, the length will be the
+   * `width`. In case of `left` or `right`, it will be the `height`.
+   *
+   * You can provide a single value (as `Number` or `String`), or a pair of values
+   * as `String` divided by a comma or one (or more) white spaces.<br />
+   * The latter is a deprecated method because it leads to confusion and will be
+   * removed in v2.<br />
+   * Additionally, it accepts additions and subtractions between different units.
+   * Note that multiplications and divisions aren't supported.
+   *
+   * Valid examples are:
+   * ```
+   * 10
+   * '10%'
+   * '10, 10'
+   * '10%, 10'
+   * '10 + 10%'
+   * '10 - 5vh + 3%'
+   * '-10px + 5vh, 5px - 6%'
+   * ```
+   * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap
+   * > with their reference element, unfortunately, you will have to disable the `flip` modifier.
+   * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373).
+   *
+   * @memberof modifiers
+   * @inner
+   */
+  offset: {
+    /** @prop {number} order=200 - Index used to define the order of execution */
+    order: 200,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: offset,
+    /** @prop {Number|String} offset=0
+     * The offset value as described in the modifier description
+     */
+    offset: 0
+  },
+
+  /**
+   * Modifier used to prevent the popper from being positioned outside the boundary.
+   *
+   * A scenario exists where the reference itself is not within the boundaries.<br />
+   * We can say it has "escaped the boundaries" — or just "escaped".<br />
+   * In this case we need to decide whether the popper should either:
+   *
+   * - detach from the reference and remain "trapped" in the boundaries, or
+   * - if it should ignore the boundary and "escape with its reference"
+   *
+   * When `escapeWithReference` is set to`true` and reference is completely
+   * outside its boundaries, the popper will overflow (or completely leave)
+   * the boundaries in order to remain attached to the edge of the reference.
+   *
+   * @memberof modifiers
+   * @inner
+   */
+  preventOverflow: {
+    /** @prop {number} order=300 - Index used to define the order of execution */
+    order: 300,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: preventOverflow,
+    /**
+     * @prop {Array} [priority=['left','right','top','bottom']]
+     * Popper will try to prevent overflow following these priorities by default,
+     * then, it could overflow on the left and on top of the `boundariesElement`
+     */
+    priority: ['left', 'right', 'top', 'bottom'],
+    /**
+     * @prop {number} padding=5
+     * Amount of pixel used to define a minimum distance between the boundaries
+     * and the popper. This makes sure the popper always has a little padding
+     * between the edges of its container
+     */
+    padding: 5,
+    /**
+     * @prop {String|HTMLElement} boundariesElement='scrollParent'
+     * Boundaries used by the modifier. Can be `scrollParent`, `window`,
+     * `viewport` or any DOM element.
+     */
+    boundariesElement: 'scrollParent'
+  },
+
+  /**
+   * Modifier used to make sure the reference and its popper stay near each other
+   * without leaving any gap between the two. Especially useful when the arrow is
+   * enabled and you want to ensure that it points to its reference element.
+   * It cares only about the first axis. You can still have poppers with margin
+   * between the popper and its reference element.
+   * @memberof modifiers
+   * @inner
+   */
+  keepTogether: {
+    /** @prop {number} order=400 - Index used to define the order of execution */
+    order: 400,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: keepTogether
+  },
+
+  /**
+   * This modifier is used to move the `arrowElement` of the popper to make
+   * sure it is positioned between the reference element and its popper element.
+   * It will read the outer size of the `arrowElement` node to detect how many
+   * pixels of conjunction are needed.
+   *
+   * It has no effect if no `arrowElement` is provided.
+   * @memberof modifiers
+   * @inner
+   */
+  arrow: {
+    /** @prop {number} order=500 - Index used to define the order of execution */
+    order: 500,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: arrow,
+    /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */
+    element: '[x-arrow]'
+  },
+
+  /**
+   * Modifier used to flip the popper's placement when it starts to overlap its
+   * reference element.
+   *
+   * Requires the `preventOverflow` modifier before it in order to work.
+   *
+   * **NOTE:** this modifier will interrupt the current update cycle and will
+   * restart it if it detects the need to flip the placement.
+   * @memberof modifiers
+   * @inner
+   */
+  flip: {
+    /** @prop {number} order=600 - Index used to define the order of execution */
+    order: 600,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: flip,
+    /**
+     * @prop {String|Array} behavior='flip'
+     * The behavior used to change the popper's placement. It can be one of
+     * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid
+     * placements (with optional variations)
+     */
+    behavior: 'flip',
+    /**
+     * @prop {number} padding=5
+     * The popper will flip if it hits the edges of the `boundariesElement`
+     */
+    padding: 5,
+    /**
+     * @prop {String|HTMLElement} boundariesElement='viewport'
+     * The element which will define the boundaries of the popper position.
+     * The popper will never be placed outside of the defined boundaries
+     * (except if `keepTogether` is enabled)
+     */
+    boundariesElement: 'viewport',
+    /**
+     * @prop {Boolean} flipVariations=false
+     * The popper will switch placement variation between `-start` and `-end` when
+     * the reference element overlaps its boundaries.
+     *
+     * The original placement should have a set variation.
+     */
+    flipVariations: false,
+    /**
+     * @prop {Boolean} flipVariationsByContent=false
+     * The popper will switch placement variation between `-start` and `-end` when
+     * the popper element overlaps its reference boundaries.
+     *
+     * The original placement should have a set variation.
+     */
+    flipVariationsByContent: false
+  },
+
+  /**
+   * Modifier used to make the popper flow toward the inner of the reference element.
+   * By default, when this modifier is disabled, the popper will be placed outside
+   * the reference element.
+   * @memberof modifiers
+   * @inner
+   */
+  inner: {
+    /** @prop {number} order=700 - Index used to define the order of execution */
+    order: 700,
+    /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */
+    enabled: false,
+    /** @prop {ModifierFn} */
+    fn: inner
+  },
+
+  /**
+   * Modifier used to hide the popper when its reference element is outside of the
+   * popper boundaries. It will set a `x-out-of-boundaries` attribute which can
+   * be used to hide with a CSS selector the popper when its reference is
+   * out of boundaries.
+   *
+   * Requires the `preventOverflow` modifier before it in order to work.
+   * @memberof modifiers
+   * @inner
+   */
+  hide: {
+    /** @prop {number} order=800 - Index used to define the order of execution */
+    order: 800,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: hide
+  },
+
+  /**
+   * Computes the style that will be applied to the popper element to gets
+   * properly positioned.
+   *
+   * Note that this modifier will not touch the DOM, it just prepares the styles
+   * so that `applyStyle` modifier can apply it. This separation is useful
+   * in case you need to replace `applyStyle` with a custom implementation.
+   *
+   * This modifier has `850` as `order` value to maintain backward compatibility
+   * with previous versions of Popper.js. Expect the modifiers ordering method
+   * to change in future major versions of the library.
+   *
+   * @memberof modifiers
+   * @inner
+   */
+  computeStyle: {
+    /** @prop {number} order=850 - Index used to define the order of execution */
+    order: 850,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: computeStyle,
+    /**
+     * @prop {Boolean} gpuAcceleration=true
+     * If true, it uses the CSS 3D transformation to position the popper.
+     * Otherwise, it will use the `top` and `left` properties
+     */
+    gpuAcceleration: true,
+    /**
+     * @prop {string} [x='bottom']
+     * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin.
+     * Change this if your popper should grow in a direction different from `bottom`
+     */
+    x: 'bottom',
+    /**
+     * @prop {string} [x='left']
+     * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin.
+     * Change this if your popper should grow in a direction different from `right`
+     */
+    y: 'right'
+  },
+
+  /**
+   * Applies the computed styles to the popper element.
+   *
+   * All the DOM manipulations are limited to this modifier. This is useful in case
+   * you want to integrate Popper.js inside a framework or view library and you
+   * want to delegate all the DOM manipulations to it.
+   *
+   * Note that if you disable this modifier, you must make sure the popper element
+   * has its position set to `absolute` before Popper.js can do its work!
+   *
+   * Just disable this modifier and define your own to achieve the desired effect.
+   *
+   * @memberof modifiers
+   * @inner
+   */
+  applyStyle: {
+    /** @prop {number} order=900 - Index used to define the order of execution */
+    order: 900,
+    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+    enabled: true,
+    /** @prop {ModifierFn} */
+    fn: applyStyle,
+    /** @prop {Function} */
+    onLoad: applyStyleOnLoad,
+    /**
+     * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier
+     * @prop {Boolean} gpuAcceleration=true
+     * If true, it uses the CSS 3D transformation to position the popper.
+     * Otherwise, it will use the `top` and `left` properties
+     */
+    gpuAcceleration: undefined
+  }
+};
+
+/**
+ * The `dataObject` is an object containing all the information used by Popper.js.
+ * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks.
+ * @name dataObject
+ * @property {Object} data.instance The Popper.js instance
+ * @property {String} data.placement Placement applied to popper
+ * @property {String} data.originalPlacement Placement originally defined on init
+ * @property {Boolean} data.flipped True if popper has been flipped by flip modifier
+ * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper
+ * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier
+ * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`)
+ * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`)
+ * @property {Object} data.boundaries Offsets of the popper boundaries
+ * @property {Object} data.offsets The measurements of popper, reference and arrow elements
+ * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values
+ * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values
+ * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0
+ */
+
+/**
+ * Default options provided to Popper.js constructor.<br />
+ * These can be overridden using the `options` argument of Popper.js.<br />
+ * To override an option, simply pass an object with the same
+ * structure of the `options` object, as the 3rd argument. For example:
+ * ```
+ * new Popper(ref, pop, {
+ *   modifiers: {
+ *     preventOverflow: { enabled: false }
+ *   }
+ * })
+ * ```
+ * @type {Object}
+ * @static
+ * @memberof Popper
+ */
+var Defaults = {
+  /**
+   * Popper's placement.
+   * @prop {Popper.placements} placement='bottom'
+   */
+  placement: 'bottom',
+
+  /**
+   * Set this to true if you want popper to position it self in 'fixed' mode
+   * @prop {Boolean} positionFixed=false
+   */
+  positionFixed: false,
+
+  /**
+   * Whether events (resize, scroll) are initially enabled.
+   * @prop {Boolean} eventsEnabled=true
+   */
+  eventsEnabled: true,
+
+  /**
+   * Set to true if you want to automatically remove the popper when
+   * you call the `destroy` method.
+   * @prop {Boolean} removeOnDestroy=false
+   */
+  removeOnDestroy: false,
+
+  /**
+   * Callback called when the popper is created.<br />
+   * By default, it is set to no-op.<br />
+   * Access Popper.js instance with `data.instance`.
+   * @prop {onCreate}
+   */
+  onCreate: function onCreate() {},
+
+  /**
+   * Callback called when the popper is updated. This callback is not called
+   * on the initialization/creation of the popper, but only on subsequent
+   * updates.<br />
+   * By default, it is set to no-op.<br />
+   * Access Popper.js instance with `data.instance`.
+   * @prop {onUpdate}
+   */
+  onUpdate: function onUpdate() {},
+
+  /**
+   * List of modifiers used to modify the offsets before they are applied to the popper.
+   * They provide most of the functionalities of Popper.js.
+   * @prop {modifiers}
+   */
+  modifiers: modifiers
+};
+
+/**
+ * @callback onCreate
+ * @param {dataObject} data
+ */
+
+/**
+ * @callback onUpdate
+ * @param {dataObject} data
+ */
+
+// Utils
+// Methods
+var Popper = function () {
+  /**
+   * Creates a new Popper.js instance.
+   * @class Popper
+   * @param {Element|referenceObject} reference - The reference element used to position the popper
+   * @param {Element} popper - The HTML / XML element used as the popper
+   * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults)
+   * @return {Object} instance - The generated Popper.js instance
+   */
+  function Popper(reference, popper) {
+    var _this = this;
+
+    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+    classCallCheck(this, Popper);
+
+    this.scheduleUpdate = function () {
+      return requestAnimationFrame(_this.update);
+    };
+
+    // make update() debounced, so that it only runs at most once-per-tick
+    this.update = debounce(this.update.bind(this));
+
+    // with {} we create a new object with the options inside it
+    this.options = _extends({}, Popper.Defaults, options);
+
+    // init state
+    this.state = {
+      isDestroyed: false,
+      isCreated: false,
+      scrollParents: []
+    };
+
+    // get reference and popper elements (allow jQuery wrappers)
+    this.reference = reference && reference.jquery ? reference[0] : reference;
+    this.popper = popper && popper.jquery ? popper[0] : popper;
+
+    // Deep merge modifiers options
+    this.options.modifiers = {};
+    Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) {
+      _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {});
+    });
+
+    // Refactoring modifiers' list (Object => Array)
+    this.modifiers = Object.keys(this.options.modifiers).map(function (name) {
+      return _extends({
+        name: name
+      }, _this.options.modifiers[name]);
+    })
+    // sort the modifiers by order
+    .sort(function (a, b) {
+      return a.order - b.order;
+    });
+
+    // modifiers have the ability to execute arbitrary code when Popper.js get inited
+    // such code is executed in the same order of its modifier
+    // they could add new properties to their options configuration
+    // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`!
+    this.modifiers.forEach(function (modifierOptions) {
+      if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) {
+        modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state);
+      }
+    });
+
+    // fire the first update to position the popper in the right place
+    this.update();
+
+    var eventsEnabled = this.options.eventsEnabled;
+    if (eventsEnabled) {
+      // setup event listeners, they will take care of update the position in specific situations
+      this.enableEventListeners();
+    }
+
+    this.state.eventsEnabled = eventsEnabled;
+  }
+
+  // We can't use class properties because they don't get listed in the
+  // class prototype and break stuff like Sinon stubs
+
+
+  createClass(Popper, [{
+    key: 'update',
+    value: function update$$1() {
+      return update.call(this);
+    }
+  }, {
+    key: 'destroy',
+    value: function destroy$$1() {
+      return destroy.call(this);
+    }
+  }, {
+    key: 'enableEventListeners',
+    value: function enableEventListeners$$1() {
+      return enableEventListeners.call(this);
+    }
+  }, {
+    key: 'disableEventListeners',
+    value: function disableEventListeners$$1() {
+      return disableEventListeners.call(this);
+    }
+
+    /**
+     * Schedules an update. It will run on the next UI update available.
+     * @method scheduleUpdate
+     * @memberof Popper
+     */
+
+
+    /**
+     * Collection of utilities useful when writing custom modifiers.
+     * Starting from version 1.7, this method is available only if you
+     * include `popper-utils.js` before `popper.js`.
+     *
+     * **DEPRECATION**: This way to access PopperUtils is deprecated
+     * and will be removed in v2! Use the PopperUtils module directly instead.
+     * Due to the high instability of the methods contained in Utils, we can't
+     * guarantee them to follow semver. Use them at your own risk!
+     * @static
+     * @private
+     * @type {Object}
+     * @deprecated since version 1.8
+     * @member Utils
+     * @memberof Popper
+     */
+
+  }]);
+  return Popper;
+}();
+
+/**
+ * The `referenceObject` is an object that provides an interface compatible with Popper.js
+ * and lets you use it as replacement of a real DOM node.<br />
+ * You can use this method to position a popper relatively to a set of coordinates
+ * in case you don't have a DOM node to use as reference.
+ *
+ * ```
+ * new Popper(referenceObject, popperNode);
+ * ```
+ *
+ * NB: This feature isn't supported in Internet Explorer 10.
+ * @name referenceObject
+ * @property {Function} data.getBoundingClientRect
+ * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method.
+ * @property {number} data.clientWidth
+ * An ES6 getter that will return the width of the virtual reference element.
+ * @property {number} data.clientHeight
+ * An ES6 getter that will return the height of the virtual reference element.
+ */
+
+
+Popper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils;
+Popper.placements = placements;
+Popper.Defaults = Defaults;
+
+return Popper;
+
+})));
+//# sourceMappingURL=popper.js.map
+/*!
+  * Bootstrap v4.3.1 (https://getbootstrap.com/)
+  * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+  */
+ (function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery'), require('popper.js')) :
+  typeof define === 'function' && define.amd ? define(['exports', 'jquery', 'popper.js'], factory) :
+  (global = global || self, factory(global.bootstrap = {}, global.jQuery, global.Popper));
+}(this, function (exports, $, Popper) { 'use strict';
+
+  $ = $ && $.hasOwnProperty('default') ? $['default'] : $;
+  Popper = Popper && Popper.hasOwnProperty('default') ? Popper['default'] : Popper;
+
+  function _defineProperties(target, props) {
+    for (var i = 0; i < props.length; i++) {
+      var descriptor = props[i];
+      descriptor.enumerable = descriptor.enumerable || false;
+      descriptor.configurable = true;
+      if ("value" in descriptor) descriptor.writable = true;
+      Object.defineProperty(target, descriptor.key, descriptor);
+    }
+  }
+
+  function _createClass(Constructor, protoProps, staticProps) {
+    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
+    if (staticProps) _defineProperties(Constructor, staticProps);
+    return Constructor;
+  }
+
+  function _defineProperty(obj, key, value) {
+    if (key in obj) {
+      Object.defineProperty(obj, key, {
+        value: value,
+        enumerable: true,
+        configurable: true,
+        writable: true
+      });
+    } else {
+      obj[key] = value;
+    }
+
+    return obj;
+  }
+
+  function _objectSpread(target) {
+    for (var i = 1; i < arguments.length; i++) {
+      var source = arguments[i] != null ? arguments[i] : {};
+      var ownKeys = Object.keys(source);
+
+      if (typeof Object.getOwnPropertySymbols === 'function') {
+        ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
+          return Object.getOwnPropertyDescriptor(source, sym).enumerable;
+        }));
+      }
+
+      ownKeys.forEach(function (key) {
+        _defineProperty(target, key, source[key]);
+      });
+    }
+
+    return target;
+  }
+
+  function _inheritsLoose(subClass, superClass) {
+    subClass.prototype = Object.create(superClass.prototype);
+    subClass.prototype.constructor = subClass;
+    subClass.__proto__ = superClass;
+  }
+
+  /**
+   * --------------------------------------------------------------------------
+   * Bootstrap (v4.3.1): util.js
+   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+   * --------------------------------------------------------------------------
+   */
+  /**
+   * ------------------------------------------------------------------------
+   * Private TransitionEnd Helpers
+   * ------------------------------------------------------------------------
+   */
+
+  var TRANSITION_END = 'transitionend';
+  var MAX_UID = 1000000;
+  var MILLISECONDS_MULTIPLIER = 1000; // Shoutout AngusCroll (https://goo.gl/pxwQGp)
+
+  function toType(obj) {
+    return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase();
+  }
+
+  function getSpecialTransitionEndEvent() {
+    return {
+      bindType: TRANSITION_END,
+      delegateType: TRANSITION_END,
+      handle: function handle(event) {
+        if ($(event.target).is(this)) {
+          return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params
+        }
+
+        return undefined; // eslint-disable-line no-undefined
+      }
+    };
+  }
+
+  function transitionEndEmulator(duration) {
+    var _this = this;
+
+    var called = false;
+    $(this).one(Util.TRANSITION_END, function () {
+      called = true;
+    });
+    setTimeout(function () {
+      if (!called) {
+        Util.triggerTransitionEnd(_this);
+      }
+    }, duration);
+    return this;
+  }
+
+  function setTransitionEndSupport() {
+    $.fn.emulateTransitionEnd = transitionEndEmulator;
+    $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent();
+  }
+  /**
+   * --------------------------------------------------------------------------
+   * Public Util Api
+   * --------------------------------------------------------------------------
+   */
+
+
+  var Util = {
+    TRANSITION_END: 'bsTransitionEnd',
+    getUID: function getUID(prefix) {
+      do {
+        // eslint-disable-next-line no-bitwise
+        prefix += ~~(Math.random() * MAX_UID); // "~~" acts like a faster Math.floor() here
+      } while (document.getElementById(prefix));
+
+      return prefix;
+    },
+    getSelectorFromElement: function getSelectorFromElement(element) {
+      var selector = element.getAttribute('data-target');
+
+      if (!selector || selector === '#') {
+        var hrefAttr = element.getAttribute('href');
+        selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : '';
+      }
+
+      try {
+        return document.querySelector(selector) ? selector : null;
+      } catch (err) {
+        return null;
+      }
+    },
+    getTransitionDurationFromElement: function getTransitionDurationFromElement(element) {
+      if (!element) {
+        return 0;
+      } // Get transition-duration of the element
+
+
+      var transitionDuration = $(element).css('transition-duration');
+      var transitionDelay = $(element).css('transition-delay');
+      var floatTransitionDuration = parseFloat(transitionDuration);
+      var floatTransitionDelay = parseFloat(transitionDelay); // Return 0 if element or transition duration is not found
+
+      if (!floatTransitionDuration && !floatTransitionDelay) {
+        return 0;
+      } // If multiple durations are defined, take the first
+
+
+      transitionDuration = transitionDuration.split(',')[0];
+      transitionDelay = transitionDelay.split(',')[0];
+      return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;
+    },
+    reflow: function reflow(element) {
+      return element.offsetHeight;
+    },
+    triggerTransitionEnd: function triggerTransitionEnd(element) {
+      $(element).trigger(TRANSITION_END);
+    },
+    // TODO: Remove in v5
+    supportsTransitionEnd: function supportsTransitionEnd() {
+      return Boolean(TRANSITION_END);
+    },
+    isElement: function isElement(obj) {
+      return (obj[0] || obj).nodeType;
+    },
+    typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) {
+      for (var property in configTypes) {
+        if (Object.prototype.hasOwnProperty.call(configTypes, property)) {
+          var expectedTypes = configTypes[property];
+          var value = config[property];
+          var valueType = value && Util.isElement(value) ? 'element' : toType(value);
+
+          if (!new RegExp(expectedTypes).test(valueType)) {
+            throw new Error(componentName.toUpperCase() + ": " + ("Option \"" + property + "\" provided type \"" + valueType + "\" ") + ("but expected type \"" + expectedTypes + "\"."));
+          }
+        }
+      }
+    },
+    findShadowRoot: function findShadowRoot(element) {
+      if (!document.documentElement.attachShadow) {
+        return null;
+      } // Can find the shadow root otherwise it'll return the document
+
+
+      if (typeof element.getRootNode === 'function') {
+        var root = element.getRootNode();
+        return root instanceof ShadowRoot ? root : null;
+      }
+
+      if (element instanceof ShadowRoot) {
+        return element;
+      } // when we don't find a shadow root
+
+
+      if (!element.parentNode) {
+        return null;
+      }
+
+      return Util.findShadowRoot(element.parentNode);
+    }
+  };
+  setTransitionEndSupport();
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME = 'alert';
+  var VERSION = '4.3.1';
+  var DATA_KEY = 'bs.alert';
+  var EVENT_KEY = "." + DATA_KEY;
+  var DATA_API_KEY = '.data-api';
+  var JQUERY_NO_CONFLICT = $.fn[NAME];
+  var Selector = {
+    DISMISS: '[data-dismiss="alert"]'
+  };
+  var Event = {
+    CLOSE: "close" + EVENT_KEY,
+    CLOSED: "closed" + EVENT_KEY,
+    CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY
+  };
+  var ClassName = {
+    ALERT: 'alert',
+    FADE: 'fade',
+    SHOW: 'show'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Alert =
+  /*#__PURE__*/
+  function () {
+    function Alert(element) {
+      this._element = element;
+    } // Getters
+
+
+    var _proto = Alert.prototype;
+
+    // Public
+    _proto.close = function close(element) {
+      var rootElement = this._element;
+
+      if (element) {
+        rootElement = this._getRootElement(element);
+      }
+
+      var customEvent = this._triggerCloseEvent(rootElement);
+
+      if (customEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      this._removeElement(rootElement);
+    };
+
+    _proto.dispose = function dispose() {
+      $.removeData(this._element, DATA_KEY);
+      this._element = null;
+    } // Private
+    ;
+
+    _proto._getRootElement = function _getRootElement(element) {
+      var selector = Util.getSelectorFromElement(element);
+      var parent = false;
+
+      if (selector) {
+        parent = document.querySelector(selector);
+      }
+
+      if (!parent) {
+        parent = $(element).closest("." + ClassName.ALERT)[0];
+      }
+
+      return parent;
+    };
+
+    _proto._triggerCloseEvent = function _triggerCloseEvent(element) {
+      var closeEvent = $.Event(Event.CLOSE);
+      $(element).trigger(closeEvent);
+      return closeEvent;
+    };
+
+    _proto._removeElement = function _removeElement(element) {
+      var _this = this;
+
+      $(element).removeClass(ClassName.SHOW);
+
+      if (!$(element).hasClass(ClassName.FADE)) {
+        this._destroyElement(element);
+
+        return;
+      }
+
+      var transitionDuration = Util.getTransitionDurationFromElement(element);
+      $(element).one(Util.TRANSITION_END, function (event) {
+        return _this._destroyElement(element, event);
+      }).emulateTransitionEnd(transitionDuration);
+    };
+
+    _proto._destroyElement = function _destroyElement(element) {
+      $(element).detach().trigger(Event.CLOSED).remove();
+    } // Static
+    ;
+
+    Alert._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var $element = $(this);
+        var data = $element.data(DATA_KEY);
+
+        if (!data) {
+          data = new Alert(this);
+          $element.data(DATA_KEY, data);
+        }
+
+        if (config === 'close') {
+          data[config](this);
+        }
+      });
+    };
+
+    Alert._handleDismiss = function _handleDismiss(alertInstance) {
+      return function (event) {
+        if (event) {
+          event.preventDefault();
+        }
+
+        alertInstance.close(this);
+      };
+    };
+
+    _createClass(Alert, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION;
+      }
+    }]);
+
+    return Alert;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert()));
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME] = Alert._jQueryInterface;
+  $.fn[NAME].Constructor = Alert;
+
+  $.fn[NAME].noConflict = function () {
+    $.fn[NAME] = JQUERY_NO_CONFLICT;
+    return Alert._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$1 = 'button';
+  var VERSION$1 = '4.3.1';
+  var DATA_KEY$1 = 'bs.button';
+  var EVENT_KEY$1 = "." + DATA_KEY$1;
+  var DATA_API_KEY$1 = '.data-api';
+  var JQUERY_NO_CONFLICT$1 = $.fn[NAME$1];
+  var ClassName$1 = {
+    ACTIVE: 'active',
+    BUTTON: 'btn',
+    FOCUS: 'focus'
+  };
+  var Selector$1 = {
+    DATA_TOGGLE_CARROT: '[data-toggle^="button"]',
+    DATA_TOGGLE: '[data-toggle="buttons"]',
+    INPUT: 'input:not([type="hidden"])',
+    ACTIVE: '.active',
+    BUTTON: '.btn'
+  };
+  var Event$1 = {
+    CLICK_DATA_API: "click" + EVENT_KEY$1 + DATA_API_KEY$1,
+    FOCUS_BLUR_DATA_API: "focus" + EVENT_KEY$1 + DATA_API_KEY$1 + " " + ("blur" + EVENT_KEY$1 + DATA_API_KEY$1)
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Button =
+  /*#__PURE__*/
+  function () {
+    function Button(element) {
+      this._element = element;
+    } // Getters
+
+
+    var _proto = Button.prototype;
+
+    // Public
+    _proto.toggle = function toggle() {
+      var triggerChangeEvent = true;
+      var addAriaPressed = true;
+      var rootElement = $(this._element).closest(Selector$1.DATA_TOGGLE)[0];
+
+      if (rootElement) {
+        var input = this._element.querySelector(Selector$1.INPUT);
+
+        if (input) {
+          if (input.type === 'radio') {
+            if (input.checked && this._element.classList.contains(ClassName$1.ACTIVE)) {
+              triggerChangeEvent = false;
+            } else {
+              var activeElement = rootElement.querySelector(Selector$1.ACTIVE);
+
+              if (activeElement) {
+                $(activeElement).removeClass(ClassName$1.ACTIVE);
+              }
+            }
+          }
+
+          if (triggerChangeEvent) {
+            if (input.hasAttribute('disabled') || rootElement.hasAttribute('disabled') || input.classList.contains('disabled') || rootElement.classList.contains('disabled')) {
+              return;
+            }
+
+            input.checked = !this._element.classList.contains(ClassName$1.ACTIVE);
+            $(input).trigger('change');
+          }
+
+          input.focus();
+          addAriaPressed = false;
+        }
+      }
+
+      if (addAriaPressed) {
+        this._element.setAttribute('aria-pressed', !this._element.classList.contains(ClassName$1.ACTIVE));
+      }
+
+      if (triggerChangeEvent) {
+        $(this._element).toggleClass(ClassName$1.ACTIVE);
+      }
+    };
+
+    _proto.dispose = function dispose() {
+      $.removeData(this._element, DATA_KEY$1);
+      this._element = null;
+    } // Static
+    ;
+
+    Button._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var data = $(this).data(DATA_KEY$1);
+
+        if (!data) {
+          data = new Button(this);
+          $(this).data(DATA_KEY$1, data);
+        }
+
+        if (config === 'toggle') {
+          data[config]();
+        }
+      });
+    };
+
+    _createClass(Button, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$1;
+      }
+    }]);
+
+    return Button;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(document).on(Event$1.CLICK_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {
+    event.preventDefault();
+    var button = event.target;
+
+    if (!$(button).hasClass(ClassName$1.BUTTON)) {
+      button = $(button).closest(Selector$1.BUTTON);
+    }
+
+    Button._jQueryInterface.call($(button), 'toggle');
+  }).on(Event$1.FOCUS_BLUR_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {
+    var button = $(event.target).closest(Selector$1.BUTTON)[0];
+    $(button).toggleClass(ClassName$1.FOCUS, /^focus(in)?$/.test(event.type));
+  });
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME$1] = Button._jQueryInterface;
+  $.fn[NAME$1].Constructor = Button;
+
+  $.fn[NAME$1].noConflict = function () {
+    $.fn[NAME$1] = JQUERY_NO_CONFLICT$1;
+    return Button._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$2 = 'carousel';
+  var VERSION$2 = '4.3.1';
+  var DATA_KEY$2 = 'bs.carousel';
+  var EVENT_KEY$2 = "." + DATA_KEY$2;
+  var DATA_API_KEY$2 = '.data-api';
+  var JQUERY_NO_CONFLICT$2 = $.fn[NAME$2];
+  var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key
+
+  var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key
+
+  var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch
+
+  var SWIPE_THRESHOLD = 40;
+  var Default = {
+    interval: 5000,
+    keyboard: true,
+    slide: false,
+    pause: 'hover',
+    wrap: true,
+    touch: true
+  };
+  var DefaultType = {
+    interval: '(number|boolean)',
+    keyboard: 'boolean',
+    slide: '(boolean|string)',
+    pause: '(string|boolean)',
+    wrap: 'boolean',
+    touch: 'boolean'
+  };
+  var Direction = {
+    NEXT: 'next',
+    PREV: 'prev',
+    LEFT: 'left',
+    RIGHT: 'right'
+  };
+  var Event$2 = {
+    SLIDE: "slide" + EVENT_KEY$2,
+    SLID: "slid" + EVENT_KEY$2,
+    KEYDOWN: "keydown" + EVENT_KEY$2,
+    MOUSEENTER: "mouseenter" + EVENT_KEY$2,
+    MOUSELEAVE: "mouseleave" + EVENT_KEY$2,
+    TOUCHSTART: "touchstart" + EVENT_KEY$2,
+    TOUCHMOVE: "touchmove" + EVENT_KEY$2,
+    TOUCHEND: "touchend" + EVENT_KEY$2,
+    POINTERDOWN: "pointerdown" + EVENT_KEY$2,
+    POINTERUP: "pointerup" + EVENT_KEY$2,
+    DRAG_START: "dragstart" + EVENT_KEY$2,
+    LOAD_DATA_API: "load" + EVENT_KEY$2 + DATA_API_KEY$2,
+    CLICK_DATA_API: "click" + EVENT_KEY$2 + DATA_API_KEY$2
+  };
+  var ClassName$2 = {
+    CAROUSEL: 'carousel',
+    ACTIVE: 'active',
+    SLIDE: 'slide',
+    RIGHT: 'carousel-item-right',
+    LEFT: 'carousel-item-left',
+    NEXT: 'carousel-item-next',
+    PREV: 'carousel-item-prev',
+    ITEM: 'carousel-item',
+    POINTER_EVENT: 'pointer-event'
+  };
+  var Selector$2 = {
+    ACTIVE: '.active',
+    ACTIVE_ITEM: '.active.carousel-item',
+    ITEM: '.carousel-item',
+    ITEM_IMG: '.carousel-item img',
+    NEXT_PREV: '.carousel-item-next, .carousel-item-prev',
+    INDICATORS: '.carousel-indicators',
+    DATA_SLIDE: '[data-slide], [data-slide-to]',
+    DATA_RIDE: '[data-ride="carousel"]'
+  };
+  var PointerType = {
+    TOUCH: 'touch',
+    PEN: 'pen'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Carousel =
+  /*#__PURE__*/
+  function () {
+    function Carousel(element, config) {
+      this._items = null;
+      this._interval = null;
+      this._activeElement = null;
+      this._isPaused = false;
+      this._isSliding = false;
+      this.touchTimeout = null;
+      this.touchStartX = 0;
+      this.touchDeltaX = 0;
+      this._config = this._getConfig(config);
+      this._element = element;
+      this._indicatorsElement = this._element.querySelector(Selector$2.INDICATORS);
+      this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;
+      this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent);
+
+      this._addEventListeners();
+    } // Getters
+
+
+    var _proto = Carousel.prototype;
+
+    // Public
+    _proto.next = function next() {
+      if (!this._isSliding) {
+        this._slide(Direction.NEXT);
+      }
+    };
+
+    _proto.nextWhenVisible = function nextWhenVisible() {
+      // Don't call next when the page isn't visible
+      // or the carousel or its parent isn't visible
+      if (!document.hidden && $(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden') {
+        this.next();
+      }
+    };
+
+    _proto.prev = function prev() {
+      if (!this._isSliding) {
+        this._slide(Direction.PREV);
+      }
+    };
+
+    _proto.pause = function pause(event) {
+      if (!event) {
+        this._isPaused = true;
+      }
+
+      if (this._element.querySelector(Selector$2.NEXT_PREV)) {
+        Util.triggerTransitionEnd(this._element);
+        this.cycle(true);
+      }
+
+      clearInterval(this._interval);
+      this._interval = null;
+    };
+
+    _proto.cycle = function cycle(event) {
+      if (!event) {
+        this._isPaused = false;
+      }
+
+      if (this._interval) {
+        clearInterval(this._interval);
+        this._interval = null;
+      }
+
+      if (this._config.interval && !this._isPaused) {
+        this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval);
+      }
+    };
+
+    _proto.to = function to(index) {
+      var _this = this;
+
+      this._activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);
+
+      var activeIndex = this._getItemIndex(this._activeElement);
+
+      if (index > this._items.length - 1 || index < 0) {
+        return;
+      }
+
+      if (this._isSliding) {
+        $(this._element).one(Event$2.SLID, function () {
+          return _this.to(index);
+        });
+        return;
+      }
+
+      if (activeIndex === index) {
+        this.pause();
+        this.cycle();
+        return;
+      }
+
+      var direction = index > activeIndex ? Direction.NEXT : Direction.PREV;
+
+      this._slide(direction, this._items[index]);
+    };
+
+    _proto.dispose = function dispose() {
+      $(this._element).off(EVENT_KEY$2);
+      $.removeData(this._element, DATA_KEY$2);
+      this._items = null;
+      this._config = null;
+      this._element = null;
+      this._interval = null;
+      this._isPaused = null;
+      this._isSliding = null;
+      this._activeElement = null;
+      this._indicatorsElement = null;
+    } // Private
+    ;
+
+    _proto._getConfig = function _getConfig(config) {
+      config = _objectSpread({}, Default, config);
+      Util.typeCheckConfig(NAME$2, config, DefaultType);
+      return config;
+    };
+
+    _proto._handleSwipe = function _handleSwipe() {
+      var absDeltax = Math.abs(this.touchDeltaX);
+
+      if (absDeltax <= SWIPE_THRESHOLD) {
+        return;
+      }
+
+      var direction = absDeltax / this.touchDeltaX; // swipe left
+
+      if (direction > 0) {
+        this.prev();
+      } // swipe right
+
+
+      if (direction < 0) {
+        this.next();
+      }
+    };
+
+    _proto._addEventListeners = function _addEventListeners() {
+      var _this2 = this;
+
+      if (this._config.keyboard) {
+        $(this._element).on(Event$2.KEYDOWN, function (event) {
+          return _this2._keydown(event);
+        });
+      }
+
+      if (this._config.pause === 'hover') {
+        $(this._element).on(Event$2.MOUSEENTER, function (event) {
+          return _this2.pause(event);
+        }).on(Event$2.MOUSELEAVE, function (event) {
+          return _this2.cycle(event);
+        });
+      }
+
+      if (this._config.touch) {
+        this._addTouchEventListeners();
+      }
+    };
+
+    _proto._addTouchEventListeners = function _addTouchEventListeners() {
+      var _this3 = this;
+
+      if (!this._touchSupported) {
+        return;
+      }
+
+      var start = function start(event) {
+        if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
+          _this3.touchStartX = event.originalEvent.clientX;
+        } else if (!_this3._pointerEvent) {
+          _this3.touchStartX = event.originalEvent.touches[0].clientX;
+        }
+      };
+
+      var move = function move(event) {
+        // ensure swiping with one touch and not pinching
+        if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {
+          _this3.touchDeltaX = 0;
+        } else {
+          _this3.touchDeltaX = event.originalEvent.touches[0].clientX - _this3.touchStartX;
+        }
+      };
+
+      var end = function end(event) {
+        if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
+          _this3.touchDeltaX = event.originalEvent.clientX - _this3.touchStartX;
+        }
+
+        _this3._handleSwipe();
+
+        if (_this3._config.pause === 'hover') {
+          // If it's a touch-enabled device, mouseenter/leave are fired as
+          // part of the mouse compatibility events on first tap - the carousel
+          // would stop cycling until user tapped out of it;
+          // here, we listen for touchend, explicitly pause the carousel
+          // (as if it's the second time we tap on it, mouseenter compat event
+          // is NOT fired) and after a timeout (to allow for mouse compatibility
+          // events to fire) we explicitly restart cycling
+          _this3.pause();
+
+          if (_this3.touchTimeout) {
+            clearTimeout(_this3.touchTimeout);
+          }
+
+          _this3.touchTimeout = setTimeout(function (event) {
+            return _this3.cycle(event);
+          }, TOUCHEVENT_COMPAT_WAIT + _this3._config.interval);
+        }
+      };
+
+      $(this._element.querySelectorAll(Selector$2.ITEM_IMG)).on(Event$2.DRAG_START, function (e) {
+        return e.preventDefault();
+      });
+
+      if (this._pointerEvent) {
+        $(this._element).on(Event$2.POINTERDOWN, function (event) {
+          return start(event);
+        });
+        $(this._element).on(Event$2.POINTERUP, function (event) {
+          return end(event);
+        });
+
+        this._element.classList.add(ClassName$2.POINTER_EVENT);
+      } else {
+        $(this._element).on(Event$2.TOUCHSTART, function (event) {
+          return start(event);
+        });
+        $(this._element).on(Event$2.TOUCHMOVE, function (event) {
+          return move(event);
+        });
+        $(this._element).on(Event$2.TOUCHEND, function (event) {
+          return end(event);
+        });
+      }
+    };
+
+    _proto._keydown = function _keydown(event) {
+      if (/input|textarea/i.test(event.target.tagName)) {
+        return;
+      }
+
+      switch (event.which) {
+        case ARROW_LEFT_KEYCODE:
+          event.preventDefault();
+          this.prev();
+          break;
+
+        case ARROW_RIGHT_KEYCODE:
+          event.preventDefault();
+          this.next();
+          break;
+
+        default:
+      }
+    };
+
+    _proto._getItemIndex = function _getItemIndex(element) {
+      this._items = element && element.parentNode ? [].slice.call(element.parentNode.querySelectorAll(Selector$2.ITEM)) : [];
+      return this._items.indexOf(element);
+    };
+
+    _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) {
+      var isNextDirection = direction === Direction.NEXT;
+      var isPrevDirection = direction === Direction.PREV;
+
+      var activeIndex = this._getItemIndex(activeElement);
+
+      var lastItemIndex = this._items.length - 1;
+      var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex;
+
+      if (isGoingToWrap && !this._config.wrap) {
+        return activeElement;
+      }
+
+      var delta = direction === Direction.PREV ? -1 : 1;
+      var itemIndex = (activeIndex + delta) % this._items.length;
+      return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex];
+    };
+
+    _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) {
+      var targetIndex = this._getItemIndex(relatedTarget);
+
+      var fromIndex = this._getItemIndex(this._element.querySelector(Selector$2.ACTIVE_ITEM));
+
+      var slideEvent = $.Event(Event$2.SLIDE, {
+        relatedTarget: relatedTarget,
+        direction: eventDirectionName,
+        from: fromIndex,
+        to: targetIndex
+      });
+      $(this._element).trigger(slideEvent);
+      return slideEvent;
+    };
+
+    _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) {
+      if (this._indicatorsElement) {
+        var indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector$2.ACTIVE));
+        $(indicators).removeClass(ClassName$2.ACTIVE);
+
+        var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)];
+
+        if (nextIndicator) {
+          $(nextIndicator).addClass(ClassName$2.ACTIVE);
+        }
+      }
+    };
+
+    _proto._slide = function _slide(direction, element) {
+      var _this4 = this;
+
+      var activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);
+
+      var activeElementIndex = this._getItemIndex(activeElement);
+
+      var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement);
+
+      var nextElementIndex = this._getItemIndex(nextElement);
+
+      var isCycling = Boolean(this._interval);
+      var directionalClassName;
+      var orderClassName;
+      var eventDirectionName;
+
+      if (direction === Direction.NEXT) {
+        directionalClassName = ClassName$2.LEFT;
+        orderClassName = ClassName$2.NEXT;
+        eventDirectionName = Direction.LEFT;
+      } else {
+        directionalClassName = ClassName$2.RIGHT;
+        orderClassName = ClassName$2.PREV;
+        eventDirectionName = Direction.RIGHT;
+      }
+
+      if (nextElement && $(nextElement).hasClass(ClassName$2.ACTIVE)) {
+        this._isSliding = false;
+        return;
+      }
+
+      var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName);
+
+      if (slideEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      if (!activeElement || !nextElement) {
+        // Some weirdness is happening, so we bail
+        return;
+      }
+
+      this._isSliding = true;
+
+      if (isCycling) {
+        this.pause();
+      }
+
+      this._setActiveIndicatorElement(nextElement);
+
+      var slidEvent = $.Event(Event$2.SLID, {
+        relatedTarget: nextElement,
+        direction: eventDirectionName,
+        from: activeElementIndex,
+        to: nextElementIndex
+      });
+
+      if ($(this._element).hasClass(ClassName$2.SLIDE)) {
+        $(nextElement).addClass(orderClassName);
+        Util.reflow(nextElement);
+        $(activeElement).addClass(directionalClassName);
+        $(nextElement).addClass(directionalClassName);
+        var nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10);
+
+        if (nextElementInterval) {
+          this._config.defaultInterval = this._config.defaultInterval || this._config.interval;
+          this._config.interval = nextElementInterval;
+        } else {
+          this._config.interval = this._config.defaultInterval || this._config.interval;
+        }
+
+        var transitionDuration = Util.getTransitionDurationFromElement(activeElement);
+        $(activeElement).one(Util.TRANSITION_END, function () {
+          $(nextElement).removeClass(directionalClassName + " " + orderClassName).addClass(ClassName$2.ACTIVE);
+          $(activeElement).removeClass(ClassName$2.ACTIVE + " " + orderClassName + " " + directionalClassName);
+          _this4._isSliding = false;
+          setTimeout(function () {
+            return $(_this4._element).trigger(slidEvent);
+          }, 0);
+        }).emulateTransitionEnd(transitionDuration);
+      } else {
+        $(activeElement).removeClass(ClassName$2.ACTIVE);
+        $(nextElement).addClass(ClassName$2.ACTIVE);
+        this._isSliding = false;
+        $(this._element).trigger(slidEvent);
+      }
+
+      if (isCycling) {
+        this.cycle();
+      }
+    } // Static
+    ;
+
+    Carousel._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var data = $(this).data(DATA_KEY$2);
+
+        var _config = _objectSpread({}, Default, $(this).data());
+
+        if (typeof config === 'object') {
+          _config = _objectSpread({}, _config, config);
+        }
+
+        var action = typeof config === 'string' ? config : _config.slide;
+
+        if (!data) {
+          data = new Carousel(this, _config);
+          $(this).data(DATA_KEY$2, data);
+        }
+
+        if (typeof config === 'number') {
+          data.to(config);
+        } else if (typeof action === 'string') {
+          if (typeof data[action] === 'undefined') {
+            throw new TypeError("No method named \"" + action + "\"");
+          }
+
+          data[action]();
+        } else if (_config.interval && _config.ride) {
+          data.pause();
+          data.cycle();
+        }
+      });
+    };
+
+    Carousel._dataApiClickHandler = function _dataApiClickHandler(event) {
+      var selector = Util.getSelectorFromElement(this);
+
+      if (!selector) {
+        return;
+      }
+
+      var target = $(selector)[0];
+
+      if (!target || !$(target).hasClass(ClassName$2.CAROUSEL)) {
+        return;
+      }
+
+      var config = _objectSpread({}, $(target).data(), $(this).data());
+
+      var slideIndex = this.getAttribute('data-slide-to');
+
+      if (slideIndex) {
+        config.interval = false;
+      }
+
+      Carousel._jQueryInterface.call($(target), config);
+
+      if (slideIndex) {
+        $(target).data(DATA_KEY$2).to(slideIndex);
+      }
+
+      event.preventDefault();
+    };
+
+    _createClass(Carousel, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$2;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default;
+      }
+    }]);
+
+    return Carousel;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(document).on(Event$2.CLICK_DATA_API, Selector$2.DATA_SLIDE, Carousel._dataApiClickHandler);
+  $(window).on(Event$2.LOAD_DATA_API, function () {
+    var carousels = [].slice.call(document.querySelectorAll(Selector$2.DATA_RIDE));
+
+    for (var i = 0, len = carousels.length; i < len; i++) {
+      var $carousel = $(carousels[i]);
+
+      Carousel._jQueryInterface.call($carousel, $carousel.data());
+    }
+  });
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME$2] = Carousel._jQueryInterface;
+  $.fn[NAME$2].Constructor = Carousel;
+
+  $.fn[NAME$2].noConflict = function () {
+    $.fn[NAME$2] = JQUERY_NO_CONFLICT$2;
+    return Carousel._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$3 = 'collapse';
+  var VERSION$3 = '4.3.1';
+  var DATA_KEY$3 = 'bs.collapse';
+  var EVENT_KEY$3 = "." + DATA_KEY$3;
+  var DATA_API_KEY$3 = '.data-api';
+  var JQUERY_NO_CONFLICT$3 = $.fn[NAME$3];
+  var Default$1 = {
+    toggle: true,
+    parent: ''
+  };
+  var DefaultType$1 = {
+    toggle: 'boolean',
+    parent: '(string|element)'
+  };
+  var Event$3 = {
+    SHOW: "show" + EVENT_KEY$3,
+    SHOWN: "shown" + EVENT_KEY$3,
+    HIDE: "hide" + EVENT_KEY$3,
+    HIDDEN: "hidden" + EVENT_KEY$3,
+    CLICK_DATA_API: "click" + EVENT_KEY$3 + DATA_API_KEY$3
+  };
+  var ClassName$3 = {
+    SHOW: 'show',
+    COLLAPSE: 'collapse',
+    COLLAPSING: 'collapsing',
+    COLLAPSED: 'collapsed'
+  };
+  var Dimension = {
+    WIDTH: 'width',
+    HEIGHT: 'height'
+  };
+  var Selector$3 = {
+    ACTIVES: '.show, .collapsing',
+    DATA_TOGGLE: '[data-toggle="collapse"]'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Collapse =
+  /*#__PURE__*/
+  function () {
+    function Collapse(element, config) {
+      this._isTransitioning = false;
+      this._element = element;
+      this._config = this._getConfig(config);
+      this._triggerArray = [].slice.call(document.querySelectorAll("[data-toggle=\"collapse\"][href=\"#" + element.id + "\"]," + ("[data-toggle=\"collapse\"][data-target=\"#" + element.id + "\"]")));
+      var toggleList = [].slice.call(document.querySelectorAll(Selector$3.DATA_TOGGLE));
+
+      for (var i = 0, len = toggleList.length; i < len; i++) {
+        var elem = toggleList[i];
+        var selector = Util.getSelectorFromElement(elem);
+        var filterElement = [].slice.call(document.querySelectorAll(selector)).filter(function (foundElem) {
+          return foundElem === element;
+        });
+
+        if (selector !== null && filterElement.length > 0) {
+          this._selector = selector;
+
+          this._triggerArray.push(elem);
+        }
+      }
+
+      this._parent = this._config.parent ? this._getParent() : null;
+
+      if (!this._config.parent) {
+        this._addAriaAndCollapsedClass(this._element, this._triggerArray);
+      }
+
+      if (this._config.toggle) {
+        this.toggle();
+      }
+    } // Getters
+
+
+    var _proto = Collapse.prototype;
+
+    // Public
+    _proto.toggle = function toggle() {
+      if ($(this._element).hasClass(ClassName$3.SHOW)) {
+        this.hide();
+      } else {
+        this.show();
+      }
+    };
+
+    _proto.show = function show() {
+      var _this = this;
+
+      if (this._isTransitioning || $(this._element).hasClass(ClassName$3.SHOW)) {
+        return;
+      }
+
+      var actives;
+      var activesData;
+
+      if (this._parent) {
+        actives = [].slice.call(this._parent.querySelectorAll(Selector$3.ACTIVES)).filter(function (elem) {
+          if (typeof _this._config.parent === 'string') {
+            return elem.getAttribute('data-parent') === _this._config.parent;
+          }
+
+          return elem.classList.contains(ClassName$3.COLLAPSE);
+        });
+
+        if (actives.length === 0) {
+          actives = null;
+        }
+      }
+
+      if (actives) {
+        activesData = $(actives).not(this._selector).data(DATA_KEY$3);
+
+        if (activesData && activesData._isTransitioning) {
+          return;
+        }
+      }
+
+      var startEvent = $.Event(Event$3.SHOW);
+      $(this._element).trigger(startEvent);
+
+      if (startEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      if (actives) {
+        Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide');
+
+        if (!activesData) {
+          $(actives).data(DATA_KEY$3, null);
+        }
+      }
+
+      var dimension = this._getDimension();
+
+      $(this._element).removeClass(ClassName$3.COLLAPSE).addClass(ClassName$3.COLLAPSING);
+      this._element.style[dimension] = 0;
+
+      if (this._triggerArray.length) {
+        $(this._triggerArray).removeClass(ClassName$3.COLLAPSED).attr('aria-expanded', true);
+      }
+
+      this.setTransitioning(true);
+
+      var complete = function complete() {
+        $(_this._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).addClass(ClassName$3.SHOW);
+        _this._element.style[dimension] = '';
+
+        _this.setTransitioning(false);
+
+        $(_this._element).trigger(Event$3.SHOWN);
+      };
+
+      var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);
+      var scrollSize = "scroll" + capitalizedDimension;
+      var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+      $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+      this._element.style[dimension] = this._element[scrollSize] + "px";
+    };
+
+    _proto.hide = function hide() {
+      var _this2 = this;
+
+      if (this._isTransitioning || !$(this._element).hasClass(ClassName$3.SHOW)) {
+        return;
+      }
+
+      var startEvent = $.Event(Event$3.HIDE);
+      $(this._element).trigger(startEvent);
+
+      if (startEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      var dimension = this._getDimension();
+
+      this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + "px";
+      Util.reflow(this._element);
+      $(this._element).addClass(ClassName$3.COLLAPSING).removeClass(ClassName$3.COLLAPSE).removeClass(ClassName$3.SHOW);
+      var triggerArrayLength = this._triggerArray.length;
+
+      if (triggerArrayLength > 0) {
+        for (var i = 0; i < triggerArrayLength; i++) {
+          var trigger = this._triggerArray[i];
+          var selector = Util.getSelectorFromElement(trigger);
+
+          if (selector !== null) {
+            var $elem = $([].slice.call(document.querySelectorAll(selector)));
+
+            if (!$elem.hasClass(ClassName$3.SHOW)) {
+              $(trigger).addClass(ClassName$3.COLLAPSED).attr('aria-expanded', false);
+            }
+          }
+        }
+      }
+
+      this.setTransitioning(true);
+
+      var complete = function complete() {
+        _this2.setTransitioning(false);
+
+        $(_this2._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).trigger(Event$3.HIDDEN);
+      };
+
+      this._element.style[dimension] = '';
+      var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+      $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+    };
+
+    _proto.setTransitioning = function setTransitioning(isTransitioning) {
+      this._isTransitioning = isTransitioning;
+    };
+
+    _proto.dispose = function dispose() {
+      $.removeData(this._element, DATA_KEY$3);
+      this._config = null;
+      this._parent = null;
+      this._element = null;
+      this._triggerArray = null;
+      this._isTransitioning = null;
+    } // Private
+    ;
+
+    _proto._getConfig = function _getConfig(config) {
+      config = _objectSpread({}, Default$1, config);
+      config.toggle = Boolean(config.toggle); // Coerce string values
+
+      Util.typeCheckConfig(NAME$3, config, DefaultType$1);
+      return config;
+    };
+
+    _proto._getDimension = function _getDimension() {
+      var hasWidth = $(this._element).hasClass(Dimension.WIDTH);
+      return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT;
+    };
+
+    _proto._getParent = function _getParent() {
+      var _this3 = this;
+
+      var parent;
+
+      if (Util.isElement(this._config.parent)) {
+        parent = this._config.parent; // It's a jQuery object
+
+        if (typeof this._config.parent.jquery !== 'undefined') {
+          parent = this._config.parent[0];
+        }
+      } else {
+        parent = document.querySelector(this._config.parent);
+      }
+
+      var selector = "[data-toggle=\"collapse\"][data-parent=\"" + this._config.parent + "\"]";
+      var children = [].slice.call(parent.querySelectorAll(selector));
+      $(children).each(function (i, element) {
+        _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]);
+      });
+      return parent;
+    };
+
+    _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) {
+      var isOpen = $(element).hasClass(ClassName$3.SHOW);
+
+      if (triggerArray.length) {
+        $(triggerArray).toggleClass(ClassName$3.COLLAPSED, !isOpen).attr('aria-expanded', isOpen);
+      }
+    } // Static
+    ;
+
+    Collapse._getTargetFromElement = function _getTargetFromElement(element) {
+      var selector = Util.getSelectorFromElement(element);
+      return selector ? document.querySelector(selector) : null;
+    };
+
+    Collapse._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var $this = $(this);
+        var data = $this.data(DATA_KEY$3);
+
+        var _config = _objectSpread({}, Default$1, $this.data(), typeof config === 'object' && config ? config : {});
+
+        if (!data && _config.toggle && /show|hide/.test(config)) {
+          _config.toggle = false;
+        }
+
+        if (!data) {
+          data = new Collapse(this, _config);
+          $this.data(DATA_KEY$3, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config]();
+        }
+      });
+    };
+
+    _createClass(Collapse, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$3;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default$1;
+      }
+    }]);
+
+    return Collapse;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(document).on(Event$3.CLICK_DATA_API, Selector$3.DATA_TOGGLE, function (event) {
+    // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
+    if (event.currentTarget.tagName === 'A') {
+      event.preventDefault();
+    }
+
+    var $trigger = $(this);
+    var selector = Util.getSelectorFromElement(this);
+    var selectors = [].slice.call(document.querySelectorAll(selector));
+    $(selectors).each(function () {
+      var $target = $(this);
+      var data = $target.data(DATA_KEY$3);
+      var config = data ? 'toggle' : $trigger.data();
+
+      Collapse._jQueryInterface.call($target, config);
+    });
+  });
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME$3] = Collapse._jQueryInterface;
+  $.fn[NAME$3].Constructor = Collapse;
+
+  $.fn[NAME$3].noConflict = function () {
+    $.fn[NAME$3] = JQUERY_NO_CONFLICT$3;
+    return Collapse._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$4 = 'dropdown';
+  var VERSION$4 = '4.3.1';
+  var DATA_KEY$4 = 'bs.dropdown';
+  var EVENT_KEY$4 = "." + DATA_KEY$4;
+  var DATA_API_KEY$4 = '.data-api';
+  var JQUERY_NO_CONFLICT$4 = $.fn[NAME$4];
+  var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key
+
+  var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key
+
+  var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key
+
+  var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key
+
+  var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key
+
+  var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse)
+
+  var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + "|" + ARROW_DOWN_KEYCODE + "|" + ESCAPE_KEYCODE);
+  var Event$4 = {
+    HIDE: "hide" + EVENT_KEY$4,
+    HIDDEN: "hidden" + EVENT_KEY$4,
+    SHOW: "show" + EVENT_KEY$4,
+    SHOWN: "shown" + EVENT_KEY$4,
+    CLICK: "click" + EVENT_KEY$4,
+    CLICK_DATA_API: "click" + EVENT_KEY$4 + DATA_API_KEY$4,
+    KEYDOWN_DATA_API: "keydown" + EVENT_KEY$4 + DATA_API_KEY$4,
+    KEYUP_DATA_API: "keyup" + EVENT_KEY$4 + DATA_API_KEY$4
+  };
+  var ClassName$4 = {
+    DISABLED: 'disabled',
+    SHOW: 'show',
+    DROPUP: 'dropup',
+    DROPRIGHT: 'dropright',
+    DROPLEFT: 'dropleft',
+    MENURIGHT: 'dropdown-menu-right',
+    MENULEFT: 'dropdown-menu-left',
+    POSITION_STATIC: 'position-static'
+  };
+  var Selector$4 = {
+    DATA_TOGGLE: '[data-toggle="dropdown"]',
+    FORM_CHILD: '.dropdown form',
+    MENU: '.dropdown-menu',
+    NAVBAR_NAV: '.navbar-nav',
+    VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
+  };
+  var AttachmentMap = {
+    TOP: 'top-start',
+    TOPEND: 'top-end',
+    BOTTOM: 'bottom-start',
+    BOTTOMEND: 'bottom-end',
+    RIGHT: 'right-start',
+    RIGHTEND: 'right-end',
+    LEFT: 'left-start',
+    LEFTEND: 'left-end'
+  };
+  var Default$2 = {
+    offset: 0,
+    flip: true,
+    boundary: 'scrollParent',
+    reference: 'toggle',
+    display: 'dynamic'
+  };
+  var DefaultType$2 = {
+    offset: '(number|string|function)',
+    flip: 'boolean',
+    boundary: '(string|element)',
+    reference: '(string|element)',
+    display: 'string'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Dropdown =
+  /*#__PURE__*/
+  function () {
+    function Dropdown(element, config) {
+      this._element = element;
+      this._popper = null;
+      this._config = this._getConfig(config);
+      this._menu = this._getMenuElement();
+      this._inNavbar = this._detectNavbar();
+
+      this._addEventListeners();
+    } // Getters
+
+
+    var _proto = Dropdown.prototype;
+
+    // Public
+    _proto.toggle = function toggle() {
+      if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED)) {
+        return;
+      }
+
+      var parent = Dropdown._getParentFromElement(this._element);
+
+      var isActive = $(this._menu).hasClass(ClassName$4.SHOW);
+
+      Dropdown._clearMenus();
+
+      if (isActive) {
+        return;
+      }
+
+      var relatedTarget = {
+        relatedTarget: this._element
+      };
+      var showEvent = $.Event(Event$4.SHOW, relatedTarget);
+      $(parent).trigger(showEvent);
+
+      if (showEvent.isDefaultPrevented()) {
+        return;
+      } // Disable totally Popper.js for Dropdown in Navbar
+
+
+      if (!this._inNavbar) {
+        /**
+         * Check for Popper dependency
+         * Popper - https://popper.js.org
+         */
+        if (typeof Popper === 'undefined') {
+          throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org/)');
+        }
+
+        var referenceElement = this._element;
+
+        if (this._config.reference === 'parent') {
+          referenceElement = parent;
+        } else if (Util.isElement(this._config.reference)) {
+          referenceElement = this._config.reference; // Check if it's jQuery element
+
+          if (typeof this._config.reference.jquery !== 'undefined') {
+            referenceElement = this._config.reference[0];
+          }
+        } // If boundary is not `scrollParent`, then set position to `static`
+        // to allow the menu to "escape" the scroll parent's boundaries
+        // https://github.com/twbs/bootstrap/issues/24251
+
+
+        if (this._config.boundary !== 'scrollParent') {
+          $(parent).addClass(ClassName$4.POSITION_STATIC);
+        }
+
+        this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig());
+      } // If this is a touch-enabled device we add extra
+      // empty mouseover listeners to the body's immediate children;
+      // only needed because of broken event delegation on iOS
+      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
+
+
+      if ('ontouchstart' in document.documentElement && $(parent).closest(Selector$4.NAVBAR_NAV).length === 0) {
+        $(document.body).children().on('mouseover', null, $.noop);
+      }
+
+      this._element.focus();
+
+      this._element.setAttribute('aria-expanded', true);
+
+      $(this._menu).toggleClass(ClassName$4.SHOW);
+      $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));
+    };
+
+    _proto.show = function show() {
+      if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || $(this._menu).hasClass(ClassName$4.SHOW)) {
+        return;
+      }
+
+      var relatedTarget = {
+        relatedTarget: this._element
+      };
+      var showEvent = $.Event(Event$4.SHOW, relatedTarget);
+
+      var parent = Dropdown._getParentFromElement(this._element);
+
+      $(parent).trigger(showEvent);
+
+      if (showEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      $(this._menu).toggleClass(ClassName$4.SHOW);
+      $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));
+    };
+
+    _proto.hide = function hide() {
+      if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || !$(this._menu).hasClass(ClassName$4.SHOW)) {
+        return;
+      }
+
+      var relatedTarget = {
+        relatedTarget: this._element
+      };
+      var hideEvent = $.Event(Event$4.HIDE, relatedTarget);
+
+      var parent = Dropdown._getParentFromElement(this._element);
+
+      $(parent).trigger(hideEvent);
+
+      if (hideEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      $(this._menu).toggleClass(ClassName$4.SHOW);
+      $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));
+    };
+
+    _proto.dispose = function dispose() {
+      $.removeData(this._element, DATA_KEY$4);
+      $(this._element).off(EVENT_KEY$4);
+      this._element = null;
+      this._menu = null;
+
+      if (this._popper !== null) {
+        this._popper.destroy();
+
+        this._popper = null;
+      }
+    };
+
+    _proto.update = function update() {
+      this._inNavbar = this._detectNavbar();
+
+      if (this._popper !== null) {
+        this._popper.scheduleUpdate();
+      }
+    } // Private
+    ;
+
+    _proto._addEventListeners = function _addEventListeners() {
+      var _this = this;
+
+      $(this._element).on(Event$4.CLICK, function (event) {
+        event.preventDefault();
+        event.stopPropagation();
+
+        _this.toggle();
+      });
+    };
+
+    _proto._getConfig = function _getConfig(config) {
+      config = _objectSpread({}, this.constructor.Default, $(this._element).data(), config);
+      Util.typeCheckConfig(NAME$4, config, this.constructor.DefaultType);
+      return config;
+    };
+
+    _proto._getMenuElement = function _getMenuElement() {
+      if (!this._menu) {
+        var parent = Dropdown._getParentFromElement(this._element);
+
+        if (parent) {
+          this._menu = parent.querySelector(Selector$4.MENU);
+        }
+      }
+
+      return this._menu;
+    };
+
+    _proto._getPlacement = function _getPlacement() {
+      var $parentDropdown = $(this._element.parentNode);
+      var placement = AttachmentMap.BOTTOM; // Handle dropup
+
+      if ($parentDropdown.hasClass(ClassName$4.DROPUP)) {
+        placement = AttachmentMap.TOP;
+
+        if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {
+          placement = AttachmentMap.TOPEND;
+        }
+      } else if ($parentDropdown.hasClass(ClassName$4.DROPRIGHT)) {
+        placement = AttachmentMap.RIGHT;
+      } else if ($parentDropdown.hasClass(ClassName$4.DROPLEFT)) {
+        placement = AttachmentMap.LEFT;
+      } else if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {
+        placement = AttachmentMap.BOTTOMEND;
+      }
+
+      return placement;
+    };
+
+    _proto._detectNavbar = function _detectNavbar() {
+      return $(this._element).closest('.navbar').length > 0;
+    };
+
+    _proto._getOffset = function _getOffset() {
+      var _this2 = this;
+
+      var offset = {};
+
+      if (typeof this._config.offset === 'function') {
+        offset.fn = function (data) {
+          data.offsets = _objectSpread({}, data.offsets, _this2._config.offset(data.offsets, _this2._element) || {});
+          return data;
+        };
+      } else {
+        offset.offset = this._config.offset;
+      }
+
+      return offset;
+    };
+
+    _proto._getPopperConfig = function _getPopperConfig() {
+      var popperConfig = {
+        placement: this._getPlacement(),
+        modifiers: {
+          offset: this._getOffset(),
+          flip: {
+            enabled: this._config.flip
+          },
+          preventOverflow: {
+            boundariesElement: this._config.boundary
+          }
+        } // Disable Popper.js if we have a static display
+
+      };
+
+      if (this._config.display === 'static') {
+        popperConfig.modifiers.applyStyle = {
+          enabled: false
+        };
+      }
+
+      return popperConfig;
+    } // Static
+    ;
+
+    Dropdown._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var data = $(this).data(DATA_KEY$4);
+
+        var _config = typeof config === 'object' ? config : null;
+
+        if (!data) {
+          data = new Dropdown(this, _config);
+          $(this).data(DATA_KEY$4, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config]();
+        }
+      });
+    };
+
+    Dropdown._clearMenus = function _clearMenus(event) {
+      if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) {
+        return;
+      }
+
+      var toggles = [].slice.call(document.querySelectorAll(Selector$4.DATA_TOGGLE));
+
+      for (var i = 0, len = toggles.length; i < len; i++) {
+        var parent = Dropdown._getParentFromElement(toggles[i]);
+
+        var context = $(toggles[i]).data(DATA_KEY$4);
+        var relatedTarget = {
+          relatedTarget: toggles[i]
+        };
+
+        if (event && event.type === 'click') {
+          relatedTarget.clickEvent = event;
+        }
+
+        if (!context) {
+          continue;
+        }
+
+        var dropdownMenu = context._menu;
+
+        if (!$(parent).hasClass(ClassName$4.SHOW)) {
+          continue;
+        }
+
+        if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $.contains(parent, event.target)) {
+          continue;
+        }
+
+        var hideEvent = $.Event(Event$4.HIDE, relatedTarget);
+        $(parent).trigger(hideEvent);
+
+        if (hideEvent.isDefaultPrevented()) {
+          continue;
+        } // If this is a touch-enabled device we remove the extra
+        // empty mouseover listeners we added for iOS support
+
+
+        if ('ontouchstart' in document.documentElement) {
+          $(document.body).children().off('mouseover', null, $.noop);
+        }
+
+        toggles[i].setAttribute('aria-expanded', 'false');
+        $(dropdownMenu).removeClass(ClassName$4.SHOW);
+        $(parent).removeClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));
+      }
+    };
+
+    Dropdown._getParentFromElement = function _getParentFromElement(element) {
+      var parent;
+      var selector = Util.getSelectorFromElement(element);
+
+      if (selector) {
+        parent = document.querySelector(selector);
+      }
+
+      return parent || element.parentNode;
+    } // eslint-disable-next-line complexity
+    ;
+
+    Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) {
+      // If not input/textarea:
+      //  - And not a key in REGEXP_KEYDOWN => not a dropdown command
+      // If input/textarea:
+      //  - If space key => not a dropdown command
+      //  - If key is other than escape
+      //    - If key is not up or down => not a dropdown command
+      //    - If trigger inside the menu => not a dropdown command
+      if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $(event.target).closest(Selector$4.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {
+        return;
+      }
+
+      event.preventDefault();
+      event.stopPropagation();
+
+      if (this.disabled || $(this).hasClass(ClassName$4.DISABLED)) {
+        return;
+      }
+
+      var parent = Dropdown._getParentFromElement(this);
+
+      var isActive = $(parent).hasClass(ClassName$4.SHOW);
+
+      if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {
+        if (event.which === ESCAPE_KEYCODE) {
+          var toggle = parent.querySelector(Selector$4.DATA_TOGGLE);
+          $(toggle).trigger('focus');
+        }
+
+        $(this).trigger('click');
+        return;
+      }
+
+      var items = [].slice.call(parent.querySelectorAll(Selector$4.VISIBLE_ITEMS));
+
+      if (items.length === 0) {
+        return;
+      }
+
+      var index = items.indexOf(event.target);
+
+      if (event.which === ARROW_UP_KEYCODE && index > 0) {
+        // Up
+        index--;
+      }
+
+      if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) {
+        // Down
+        index++;
+      }
+
+      if (index < 0) {
+        index = 0;
+      }
+
+      items[index].focus();
+    };
+
+    _createClass(Dropdown, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$4;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default$2;
+      }
+    }, {
+      key: "DefaultType",
+      get: function get() {
+        return DefaultType$2;
+      }
+    }]);
+
+    return Dropdown;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(document).on(Event$4.KEYDOWN_DATA_API, Selector$4.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event$4.KEYDOWN_DATA_API, Selector$4.MENU, Dropdown._dataApiKeydownHandler).on(Event$4.CLICK_DATA_API + " " + Event$4.KEYUP_DATA_API, Dropdown._clearMenus).on(Event$4.CLICK_DATA_API, Selector$4.DATA_TOGGLE, function (event) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    Dropdown._jQueryInterface.call($(this), 'toggle');
+  }).on(Event$4.CLICK_DATA_API, Selector$4.FORM_CHILD, function (e) {
+    e.stopPropagation();
+  });
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME$4] = Dropdown._jQueryInterface;
+  $.fn[NAME$4].Constructor = Dropdown;
+
+  $.fn[NAME$4].noConflict = function () {
+    $.fn[NAME$4] = JQUERY_NO_CONFLICT$4;
+    return Dropdown._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$5 = 'modal';
+  var VERSION$5 = '4.3.1';
+  var DATA_KEY$5 = 'bs.modal';
+  var EVENT_KEY$5 = "." + DATA_KEY$5;
+  var DATA_API_KEY$5 = '.data-api';
+  var JQUERY_NO_CONFLICT$5 = $.fn[NAME$5];
+  var ESCAPE_KEYCODE$1 = 27; // KeyboardEvent.which value for Escape (Esc) key
+
+  var Default$3 = {
+    backdrop: true,
+    keyboard: true,
+    focus: true,
+    show: true
+  };
+  var DefaultType$3 = {
+    backdrop: '(boolean|string)',
+    keyboard: 'boolean',
+    focus: 'boolean',
+    show: 'boolean'
+  };
+  var Event$5 = {
+    HIDE: "hide" + EVENT_KEY$5,
+    HIDDEN: "hidden" + EVENT_KEY$5,
+    SHOW: "show" + EVENT_KEY$5,
+    SHOWN: "shown" + EVENT_KEY$5,
+    FOCUSIN: "focusin" + EVENT_KEY$5,
+    RESIZE: "resize" + EVENT_KEY$5,
+    CLICK_DISMISS: "click.dismiss" + EVENT_KEY$5,
+    KEYDOWN_DISMISS: "keydown.dismiss" + EVENT_KEY$5,
+    MOUSEUP_DISMISS: "mouseup.dismiss" + EVENT_KEY$5,
+    MOUSEDOWN_DISMISS: "mousedown.dismiss" + EVENT_KEY$5,
+    CLICK_DATA_API: "click" + EVENT_KEY$5 + DATA_API_KEY$5
+  };
+  var ClassName$5 = {
+    SCROLLABLE: 'modal-dialog-scrollable',
+    SCROLLBAR_MEASURER: 'modal-scrollbar-measure',
+    BACKDROP: 'modal-backdrop',
+    OPEN: 'modal-open',
+    FADE: 'fade',
+    SHOW: 'show'
+  };
+  var Selector$5 = {
+    DIALOG: '.modal-dialog',
+    MODAL_BODY: '.modal-body',
+    DATA_TOGGLE: '[data-toggle="modal"]',
+    DATA_DISMISS: '[data-dismiss="modal"]',
+    FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
+    STICKY_CONTENT: '.sticky-top'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Modal =
+  /*#__PURE__*/
+  function () {
+    function Modal(element, config) {
+      this._config = this._getConfig(config);
+      this._element = element;
+      this._dialog = element.querySelector(Selector$5.DIALOG);
+      this._backdrop = null;
+      this._isShown = false;
+      this._isBodyOverflowing = false;
+      this._ignoreBackdropClick = false;
+      this._isTransitioning = false;
+      this._scrollbarWidth = 0;
+    } // Getters
+
+
+    var _proto = Modal.prototype;
+
+    // Public
+    _proto.toggle = function toggle(relatedTarget) {
+      return this._isShown ? this.hide() : this.show(relatedTarget);
+    };
+
+    _proto.show = function show(relatedTarget) {
+      var _this = this;
+
+      if (this._isShown || this._isTransitioning) {
+        return;
+      }
+
+      if ($(this._element).hasClass(ClassName$5.FADE)) {
+        this._isTransitioning = true;
+      }
+
+      var showEvent = $.Event(Event$5.SHOW, {
+        relatedTarget: relatedTarget
+      });
+      $(this._element).trigger(showEvent);
+
+      if (this._isShown || showEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      this._isShown = true;
+
+      this._checkScrollbar();
+
+      this._setScrollbar();
+
+      this._adjustDialog();
+
+      this._setEscapeEvent();
+
+      this._setResizeEvent();
+
+      $(this._element).on(Event$5.CLICK_DISMISS, Selector$5.DATA_DISMISS, function (event) {
+        return _this.hide(event);
+      });
+      $(this._dialog).on(Event$5.MOUSEDOWN_DISMISS, function () {
+        $(_this._element).one(Event$5.MOUSEUP_DISMISS, function (event) {
+          if ($(event.target).is(_this._element)) {
+            _this._ignoreBackdropClick = true;
+          }
+        });
+      });
+
+      this._showBackdrop(function () {
+        return _this._showElement(relatedTarget);
+      });
+    };
+
+    _proto.hide = function hide(event) {
+      var _this2 = this;
+
+      if (event) {
+        event.preventDefault();
+      }
+
+      if (!this._isShown || this._isTransitioning) {
+        return;
+      }
+
+      var hideEvent = $.Event(Event$5.HIDE);
+      $(this._element).trigger(hideEvent);
+
+      if (!this._isShown || hideEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      this._isShown = false;
+      var transition = $(this._element).hasClass(ClassName$5.FADE);
+
+      if (transition) {
+        this._isTransitioning = true;
+      }
+
+      this._setEscapeEvent();
+
+      this._setResizeEvent();
+
+      $(document).off(Event$5.FOCUSIN);
+      $(this._element).removeClass(ClassName$5.SHOW);
+      $(this._element).off(Event$5.CLICK_DISMISS);
+      $(this._dialog).off(Event$5.MOUSEDOWN_DISMISS);
+
+      if (transition) {
+        var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+        $(this._element).one(Util.TRANSITION_END, function (event) {
+          return _this2._hideModal(event);
+        }).emulateTransitionEnd(transitionDuration);
+      } else {
+        this._hideModal();
+      }
+    };
+
+    _proto.dispose = function dispose() {
+      [window, this._element, this._dialog].forEach(function (htmlElement) {
+        return $(htmlElement).off(EVENT_KEY$5);
+      });
+      /**
+       * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`
+       * Do not move `document` in `htmlElements` array
+       * It will remove `Event.CLICK_DATA_API` event that should remain
+       */
+
+      $(document).off(Event$5.FOCUSIN);
+      $.removeData(this._element, DATA_KEY$5);
+      this._config = null;
+      this._element = null;
+      this._dialog = null;
+      this._backdrop = null;
+      this._isShown = null;
+      this._isBodyOverflowing = null;
+      this._ignoreBackdropClick = null;
+      this._isTransitioning = null;
+      this._scrollbarWidth = null;
+    };
+
+    _proto.handleUpdate = function handleUpdate() {
+      this._adjustDialog();
+    } // Private
+    ;
+
+    _proto._getConfig = function _getConfig(config) {
+      config = _objectSpread({}, Default$3, config);
+      Util.typeCheckConfig(NAME$5, config, DefaultType$3);
+      return config;
+    };
+
+    _proto._showElement = function _showElement(relatedTarget) {
+      var _this3 = this;
+
+      var transition = $(this._element).hasClass(ClassName$5.FADE);
+
+      if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
+        // Don't move modal's DOM position
+        document.body.appendChild(this._element);
+      }
+
+      this._element.style.display = 'block';
+
+      this._element.removeAttribute('aria-hidden');
+
+      this._element.setAttribute('aria-modal', true);
+
+      if ($(this._dialog).hasClass(ClassName$5.SCROLLABLE)) {
+        this._dialog.querySelector(Selector$5.MODAL_BODY).scrollTop = 0;
+      } else {
+        this._element.scrollTop = 0;
+      }
+
+      if (transition) {
+        Util.reflow(this._element);
+      }
+
+      $(this._element).addClass(ClassName$5.SHOW);
+
+      if (this._config.focus) {
+        this._enforceFocus();
+      }
+
+      var shownEvent = $.Event(Event$5.SHOWN, {
+        relatedTarget: relatedTarget
+      });
+
+      var transitionComplete = function transitionComplete() {
+        if (_this3._config.focus) {
+          _this3._element.focus();
+        }
+
+        _this3._isTransitioning = false;
+        $(_this3._element).trigger(shownEvent);
+      };
+
+      if (transition) {
+        var transitionDuration = Util.getTransitionDurationFromElement(this._dialog);
+        $(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(transitionDuration);
+      } else {
+        transitionComplete();
+      }
+    };
+
+    _proto._enforceFocus = function _enforceFocus() {
+      var _this4 = this;
+
+      $(document).off(Event$5.FOCUSIN) // Guard against infinite focus loop
+      .on(Event$5.FOCUSIN, function (event) {
+        if (document !== event.target && _this4._element !== event.target && $(_this4._element).has(event.target).length === 0) {
+          _this4._element.focus();
+        }
+      });
+    };
+
+    _proto._setEscapeEvent = function _setEscapeEvent() {
+      var _this5 = this;
+
+      if (this._isShown && this._config.keyboard) {
+        $(this._element).on(Event$5.KEYDOWN_DISMISS, function (event) {
+          if (event.which === ESCAPE_KEYCODE$1) {
+            event.preventDefault();
+
+            _this5.hide();
+          }
+        });
+      } else if (!this._isShown) {
+        $(this._element).off(Event$5.KEYDOWN_DISMISS);
+      }
+    };
+
+    _proto._setResizeEvent = function _setResizeEvent() {
+      var _this6 = this;
+
+      if (this._isShown) {
+        $(window).on(Event$5.RESIZE, function (event) {
+          return _this6.handleUpdate(event);
+        });
+      } else {
+        $(window).off(Event$5.RESIZE);
+      }
+    };
+
+    _proto._hideModal = function _hideModal() {
+      var _this7 = this;
+
+      this._element.style.display = 'none';
+
+      this._element.setAttribute('aria-hidden', true);
+
+      this._element.removeAttribute('aria-modal');
+
+      this._isTransitioning = false;
+
+      this._showBackdrop(function () {
+        $(document.body).removeClass(ClassName$5.OPEN);
+
+        _this7._resetAdjustments();
+
+        _this7._resetScrollbar();
+
+        $(_this7._element).trigger(Event$5.HIDDEN);
+      });
+    };
+
+    _proto._removeBackdrop = function _removeBackdrop() {
+      if (this._backdrop) {
+        $(this._backdrop).remove();
+        this._backdrop = null;
+      }
+    };
+
+    _proto._showBackdrop = function _showBackdrop(callback) {
+      var _this8 = this;
+
+      var animate = $(this._element).hasClass(ClassName$5.FADE) ? ClassName$5.FADE : '';
+
+      if (this._isShown && this._config.backdrop) {
+        this._backdrop = document.createElement('div');
+        this._backdrop.className = ClassName$5.BACKDROP;
+
+        if (animate) {
+          this._backdrop.classList.add(animate);
+        }
+
+        $(this._backdrop).appendTo(document.body);
+        $(this._element).on(Event$5.CLICK_DISMISS, function (event) {
+          if (_this8._ignoreBackdropClick) {
+            _this8._ignoreBackdropClick = false;
+            return;
+          }
+
+          if (event.target !== event.currentTarget) {
+            return;
+          }
+
+          if (_this8._config.backdrop === 'static') {
+            _this8._element.focus();
+          } else {
+            _this8.hide();
+          }
+        });
+
+        if (animate) {
+          Util.reflow(this._backdrop);
+        }
+
+        $(this._backdrop).addClass(ClassName$5.SHOW);
+
+        if (!callback) {
+          return;
+        }
+
+        if (!animate) {
+          callback();
+          return;
+        }
+
+        var backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);
+        $(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(backdropTransitionDuration);
+      } else if (!this._isShown && this._backdrop) {
+        $(this._backdrop).removeClass(ClassName$5.SHOW);
+
+        var callbackRemove = function callbackRemove() {
+          _this8._removeBackdrop();
+
+          if (callback) {
+            callback();
+          }
+        };
+
+        if ($(this._element).hasClass(ClassName$5.FADE)) {
+          var _backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);
+
+          $(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(_backdropTransitionDuration);
+        } else {
+          callbackRemove();
+        }
+      } else if (callback) {
+        callback();
+      }
+    } // ----------------------------------------------------------------------
+    // the following methods are used to handle overflowing modals
+    // todo (fat): these should probably be refactored out of modal.js
+    // ----------------------------------------------------------------------
+    ;
+
+    _proto._adjustDialog = function _adjustDialog() {
+      var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;
+
+      if (!this._isBodyOverflowing && isModalOverflowing) {
+        this._element.style.paddingLeft = this._scrollbarWidth + "px";
+      }
+
+      if (this._isBodyOverflowing && !isModalOverflowing) {
+        this._element.style.paddingRight = this._scrollbarWidth + "px";
+      }
+    };
+
+    _proto._resetAdjustments = function _resetAdjustments() {
+      this._element.style.paddingLeft = '';
+      this._element.style.paddingRight = '';
+    };
+
+    _proto._checkScrollbar = function _checkScrollbar() {
+      var rect = document.body.getBoundingClientRect();
+      this._isBodyOverflowing = rect.left + rect.right < window.innerWidth;
+      this._scrollbarWidth = this._getScrollbarWidth();
+    };
+
+    _proto._setScrollbar = function _setScrollbar() {
+      var _this9 = this;
+
+      if (this._isBodyOverflowing) {
+        // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
+        //   while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
+        var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));
+        var stickyContent = [].slice.call(document.querySelectorAll(Selector$5.STICKY_CONTENT)); // Adjust fixed content padding
+
+        $(fixedContent).each(function (index, element) {
+          var actualPadding = element.style.paddingRight;
+          var calculatedPadding = $(element).css('padding-right');
+          $(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this9._scrollbarWidth + "px");
+        }); // Adjust sticky content margin
+
+        $(stickyContent).each(function (index, element) {
+          var actualMargin = element.style.marginRight;
+          var calculatedMargin = $(element).css('margin-right');
+          $(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this9._scrollbarWidth + "px");
+        }); // Adjust body padding
+
+        var actualPadding = document.body.style.paddingRight;
+        var calculatedPadding = $(document.body).css('padding-right');
+        $(document.body).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + "px");
+      }
+
+      $(document.body).addClass(ClassName$5.OPEN);
+    };
+
+    _proto._resetScrollbar = function _resetScrollbar() {
+      // Restore fixed content padding
+      var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));
+      $(fixedContent).each(function (index, element) {
+        var padding = $(element).data('padding-right');
+        $(element).removeData('padding-right');
+        element.style.paddingRight = padding ? padding : '';
+      }); // Restore sticky content
+
+      var elements = [].slice.call(document.querySelectorAll("" + Selector$5.STICKY_CONTENT));
+      $(elements).each(function (index, element) {
+        var margin = $(element).data('margin-right');
+
+        if (typeof margin !== 'undefined') {
+          $(element).css('margin-right', margin).removeData('margin-right');
+        }
+      }); // Restore body padding
+
+      var padding = $(document.body).data('padding-right');
+      $(document.body).removeData('padding-right');
+      document.body.style.paddingRight = padding ? padding : '';
+    };
+
+    _proto._getScrollbarWidth = function _getScrollbarWidth() {
+      // thx d.walsh
+      var scrollDiv = document.createElement('div');
+      scrollDiv.className = ClassName$5.SCROLLBAR_MEASURER;
+      document.body.appendChild(scrollDiv);
+      var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
+      document.body.removeChild(scrollDiv);
+      return scrollbarWidth;
+    } // Static
+    ;
+
+    Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) {
+      return this.each(function () {
+        var data = $(this).data(DATA_KEY$5);
+
+        var _config = _objectSpread({}, Default$3, $(this).data(), typeof config === 'object' && config ? config : {});
+
+        if (!data) {
+          data = new Modal(this, _config);
+          $(this).data(DATA_KEY$5, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config](relatedTarget);
+        } else if (_config.show) {
+          data.show(relatedTarget);
+        }
+      });
+    };
+
+    _createClass(Modal, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$5;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default$3;
+      }
+    }]);
+
+    return Modal;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(document).on(Event$5.CLICK_DATA_API, Selector$5.DATA_TOGGLE, function (event) {
+    var _this10 = this;
+
+    var target;
+    var selector = Util.getSelectorFromElement(this);
+
+    if (selector) {
+      target = document.querySelector(selector);
+    }
+
+    var config = $(target).data(DATA_KEY$5) ? 'toggle' : _objectSpread({}, $(target).data(), $(this).data());
+
+    if (this.tagName === 'A' || this.tagName === 'AREA') {
+      event.preventDefault();
+    }
+
+    var $target = $(target).one(Event$5.SHOW, function (showEvent) {
+      if (showEvent.isDefaultPrevented()) {
+        // Only register focus restorer if modal will actually get shown
+        return;
+      }
+
+      $target.one(Event$5.HIDDEN, function () {
+        if ($(_this10).is(':visible')) {
+          _this10.focus();
+        }
+      });
+    });
+
+    Modal._jQueryInterface.call($(target), config, this);
+  });
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME$5] = Modal._jQueryInterface;
+  $.fn[NAME$5].Constructor = Modal;
+
+  $.fn[NAME$5].noConflict = function () {
+    $.fn[NAME$5] = JQUERY_NO_CONFLICT$5;
+    return Modal._jQueryInterface;
+  };
+
+  /**
+   * --------------------------------------------------------------------------
+   * Bootstrap (v4.3.1): tools/sanitizer.js
+   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+   * --------------------------------------------------------------------------
+   */
+  var uriAttrs = ['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href'];
+  var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i;
+  var DefaultWhitelist = {
+    // Global attributes allowed on any supplied element below.
+    '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
+    a: ['target', 'href', 'title', 'rel'],
+    area: [],
+    b: [],
+    br: [],
+    col: [],
+    code: [],
+    div: [],
+    em: [],
+    hr: [],
+    h1: [],
+    h2: [],
+    h3: [],
+    h4: [],
+    h5: [],
+    h6: [],
+    i: [],
+    img: ['src', 'alt', 'title', 'width', 'height'],
+    li: [],
+    ol: [],
+    p: [],
+    pre: [],
+    s: [],
+    small: [],
+    span: [],
+    sub: [],
+    sup: [],
+    strong: [],
+    u: [],
+    ul: []
+    /**
+     * A pattern that recognizes a commonly useful subset of URLs that are safe.
+     *
+     * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+     */
+
+  };
+  var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;
+  /**
+   * A pattern that matches safe data URLs. Only matches image, video and audio types.
+   *
+   * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+   */
+
+  var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;
+
+  function allowedAttribute(attr, allowedAttributeList) {
+    var attrName = attr.nodeName.toLowerCase();
+
+    if (allowedAttributeList.indexOf(attrName) !== -1) {
+      if (uriAttrs.indexOf(attrName) !== -1) {
+        return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN));
+      }
+
+      return true;
+    }
+
+    var regExp = allowedAttributeList.filter(function (attrRegex) {
+      return attrRegex instanceof RegExp;
+    }); // Check if a regular expression validates the attribute.
+
+    for (var i = 0, l = regExp.length; i < l; i++) {
+      if (attrName.match(regExp[i])) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
+    if (unsafeHtml.length === 0) {
+      return unsafeHtml;
+    }
+
+    if (sanitizeFn && typeof sanitizeFn === 'function') {
+      return sanitizeFn(unsafeHtml);
+    }
+
+    var domParser = new window.DOMParser();
+    var createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');
+    var whitelistKeys = Object.keys(whiteList);
+    var elements = [].slice.call(createdDocument.body.querySelectorAll('*'));
+
+    var _loop = function _loop(i, len) {
+      var el = elements[i];
+      var elName = el.nodeName.toLowerCase();
+
+      if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {
+        el.parentNode.removeChild(el);
+        return "continue";
+      }
+
+      var attributeList = [].slice.call(el.attributes);
+      var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []);
+      attributeList.forEach(function (attr) {
+        if (!allowedAttribute(attr, whitelistedAttributes)) {
+          el.removeAttribute(attr.nodeName);
+        }
+      });
+    };
+
+    for (var i = 0, len = elements.length; i < len; i++) {
+      var _ret = _loop(i, len);
+
+      if (_ret === "continue") continue;
+    }
+
+    return createdDocument.body.innerHTML;
+  }
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$6 = 'tooltip';
+  var VERSION$6 = '4.3.1';
+  var DATA_KEY$6 = 'bs.tooltip';
+  var EVENT_KEY$6 = "." + DATA_KEY$6;
+  var JQUERY_NO_CONFLICT$6 = $.fn[NAME$6];
+  var CLASS_PREFIX = 'bs-tooltip';
+  var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g');
+  var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn'];
+  var DefaultType$4 = {
+    animation: 'boolean',
+    template: 'string',
+    title: '(string|element|function)',
+    trigger: 'string',
+    delay: '(number|object)',
+    html: 'boolean',
+    selector: '(string|boolean)',
+    placement: '(string|function)',
+    offset: '(number|string|function)',
+    container: '(string|element|boolean)',
+    fallbackPlacement: '(string|array)',
+    boundary: '(string|element)',
+    sanitize: 'boolean',
+    sanitizeFn: '(null|function)',
+    whiteList: 'object'
+  };
+  var AttachmentMap$1 = {
+    AUTO: 'auto',
+    TOP: 'top',
+    RIGHT: 'right',
+    BOTTOM: 'bottom',
+    LEFT: 'left'
+  };
+  var Default$4 = {
+    animation: true,
+    template: '<div class="tooltip" role="tooltip">' + '<div class="arrow"></div>' + '<div class="tooltip-inner"></div></div>',
+    trigger: 'hover focus',
+    title: '',
+    delay: 0,
+    html: false,
+    selector: false,
+    placement: 'top',
+    offset: 0,
+    container: false,
+    fallbackPlacement: 'flip',
+    boundary: 'scrollParent',
+    sanitize: true,
+    sanitizeFn: null,
+    whiteList: DefaultWhitelist
+  };
+  var HoverState = {
+    SHOW: 'show',
+    OUT: 'out'
+  };
+  var Event$6 = {
+    HIDE: "hide" + EVENT_KEY$6,
+    HIDDEN: "hidden" + EVENT_KEY$6,
+    SHOW: "show" + EVENT_KEY$6,
+    SHOWN: "shown" + EVENT_KEY$6,
+    INSERTED: "inserted" + EVENT_KEY$6,
+    CLICK: "click" + EVENT_KEY$6,
+    FOCUSIN: "focusin" + EVENT_KEY$6,
+    FOCUSOUT: "focusout" + EVENT_KEY$6,
+    MOUSEENTER: "mouseenter" + EVENT_KEY$6,
+    MOUSELEAVE: "mouseleave" + EVENT_KEY$6
+  };
+  var ClassName$6 = {
+    FADE: 'fade',
+    SHOW: 'show'
+  };
+  var Selector$6 = {
+    TOOLTIP: '.tooltip',
+    TOOLTIP_INNER: '.tooltip-inner',
+    ARROW: '.arrow'
+  };
+  var Trigger = {
+    HOVER: 'hover',
+    FOCUS: 'focus',
+    CLICK: 'click',
+    MANUAL: 'manual'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Tooltip =
+  /*#__PURE__*/
+  function () {
+    function Tooltip(element, config) {
+      /**
+       * Check for Popper dependency
+       * Popper - https://popper.js.org
+       */
+      if (typeof Popper === 'undefined') {
+        throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org/)');
+      } // private
+
+
+      this._isEnabled = true;
+      this._timeout = 0;
+      this._hoverState = '';
+      this._activeTrigger = {};
+      this._popper = null; // Protected
+
+      this.element = element;
+      this.config = this._getConfig(config);
+      this.tip = null;
+
+      this._setListeners();
+    } // Getters
+
+
+    var _proto = Tooltip.prototype;
+
+    // Public
+    _proto.enable = function enable() {
+      this._isEnabled = true;
+    };
+
+    _proto.disable = function disable() {
+      this._isEnabled = false;
+    };
+
+    _proto.toggleEnabled = function toggleEnabled() {
+      this._isEnabled = !this._isEnabled;
+    };
+
+    _proto.toggle = function toggle(event) {
+      if (!this._isEnabled) {
+        return;
+      }
+
+      if (event) {
+        var dataKey = this.constructor.DATA_KEY;
+        var context = $(event.currentTarget).data(dataKey);
+
+        if (!context) {
+          context = new this.constructor(event.currentTarget, this._getDelegateConfig());
+          $(event.currentTarget).data(dataKey, context);
+        }
+
+        context._activeTrigger.click = !context._activeTrigger.click;
+
+        if (context._isWithActiveTrigger()) {
+          context._enter(null, context);
+        } else {
+          context._leave(null, context);
+        }
+      } else {
+        if ($(this.getTipElement()).hasClass(ClassName$6.SHOW)) {
+          this._leave(null, this);
+
+          return;
+        }
+
+        this._enter(null, this);
+      }
+    };
+
+    _proto.dispose = function dispose() {
+      clearTimeout(this._timeout);
+      $.removeData(this.element, this.constructor.DATA_KEY);
+      $(this.element).off(this.constructor.EVENT_KEY);
+      $(this.element).closest('.modal').off('hide.bs.modal');
+
+      if (this.tip) {
+        $(this.tip).remove();
+      }
+
+      this._isEnabled = null;
+      this._timeout = null;
+      this._hoverState = null;
+      this._activeTrigger = null;
+
+      if (this._popper !== null) {
+        this._popper.destroy();
+      }
+
+      this._popper = null;
+      this.element = null;
+      this.config = null;
+      this.tip = null;
+    };
+
+    _proto.show = function show() {
+      var _this = this;
+
+      if ($(this.element).css('display') === 'none') {
+        throw new Error('Please use show on visible elements');
+      }
+
+      var showEvent = $.Event(this.constructor.Event.SHOW);
+
+      if (this.isWithContent() && this._isEnabled) {
+        $(this.element).trigger(showEvent);
+        var shadowRoot = Util.findShadowRoot(this.element);
+        var isInTheDom = $.contains(shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement, this.element);
+
+        if (showEvent.isDefaultPrevented() || !isInTheDom) {
+          return;
+        }
+
+        var tip = this.getTipElement();
+        var tipId = Util.getUID(this.constructor.NAME);
+        tip.setAttribute('id', tipId);
+        this.element.setAttribute('aria-describedby', tipId);
+        this.setContent();
+
+        if (this.config.animation) {
+          $(tip).addClass(ClassName$6.FADE);
+        }
+
+        var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement;
+
+        var attachment = this._getAttachment(placement);
+
+        this.addAttachmentClass(attachment);
+
+        var container = this._getContainer();
+
+        $(tip).data(this.constructor.DATA_KEY, this);
+
+        if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {
+          $(tip).appendTo(container);
+        }
+
+        $(this.element).trigger(this.constructor.Event.INSERTED);
+        this._popper = new Popper(this.element, tip, {
+          placement: attachment,
+          modifiers: {
+            offset: this._getOffset(),
+            flip: {
+              behavior: this.config.fallbackPlacement
+            },
+            arrow: {
+              element: Selector$6.ARROW
+            },
+            preventOverflow: {
+              boundariesElement: this.config.boundary
+            }
+          },
+          onCreate: function onCreate(data) {
+            if (data.originalPlacement !== data.placement) {
+              _this._handlePopperPlacementChange(data);
+            }
+          },
+          onUpdate: function onUpdate(data) {
+            return _this._handlePopperPlacementChange(data);
+          }
+        });
+        $(tip).addClass(ClassName$6.SHOW); // If this is a touch-enabled device we add extra
+        // empty mouseover listeners to the body's immediate children;
+        // only needed because of broken event delegation on iOS
+        // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
+
+        if ('ontouchstart' in document.documentElement) {
+          $(document.body).children().on('mouseover', null, $.noop);
+        }
+
+        var complete = function complete() {
+          if (_this.config.animation) {
+            _this._fixTransition();
+          }
+
+          var prevHoverState = _this._hoverState;
+          _this._hoverState = null;
+          $(_this.element).trigger(_this.constructor.Event.SHOWN);
+
+          if (prevHoverState === HoverState.OUT) {
+            _this._leave(null, _this);
+          }
+        };
+
+        if ($(this.tip).hasClass(ClassName$6.FADE)) {
+          var transitionDuration = Util.getTransitionDurationFromElement(this.tip);
+          $(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+        } else {
+          complete();
+        }
+      }
+    };
+
+    _proto.hide = function hide(callback) {
+      var _this2 = this;
+
+      var tip = this.getTipElement();
+      var hideEvent = $.Event(this.constructor.Event.HIDE);
+
+      var complete = function complete() {
+        if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) {
+          tip.parentNode.removeChild(tip);
+        }
+
+        _this2._cleanTipClass();
+
+        _this2.element.removeAttribute('aria-describedby');
+
+        $(_this2.element).trigger(_this2.constructor.Event.HIDDEN);
+
+        if (_this2._popper !== null) {
+          _this2._popper.destroy();
+        }
+
+        if (callback) {
+          callback();
+        }
+      };
+
+      $(this.element).trigger(hideEvent);
+
+      if (hideEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      $(tip).removeClass(ClassName$6.SHOW); // If this is a touch-enabled device we remove the extra
+      // empty mouseover listeners we added for iOS support
+
+      if ('ontouchstart' in document.documentElement) {
+        $(document.body).children().off('mouseover', null, $.noop);
+      }
+
+      this._activeTrigger[Trigger.CLICK] = false;
+      this._activeTrigger[Trigger.FOCUS] = false;
+      this._activeTrigger[Trigger.HOVER] = false;
+
+      if ($(this.tip).hasClass(ClassName$6.FADE)) {
+        var transitionDuration = Util.getTransitionDurationFromElement(tip);
+        $(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+      } else {
+        complete();
+      }
+
+      this._hoverState = '';
+    };
+
+    _proto.update = function update() {
+      if (this._popper !== null) {
+        this._popper.scheduleUpdate();
+      }
+    } // Protected
+    ;
+
+    _proto.isWithContent = function isWithContent() {
+      return Boolean(this.getTitle());
+    };
+
+    _proto.addAttachmentClass = function addAttachmentClass(attachment) {
+      $(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment);
+    };
+
+    _proto.getTipElement = function getTipElement() {
+      this.tip = this.tip || $(this.config.template)[0];
+      return this.tip;
+    };
+
+    _proto.setContent = function setContent() {
+      var tip = this.getTipElement();
+      this.setElementContent($(tip.querySelectorAll(Selector$6.TOOLTIP_INNER)), this.getTitle());
+      $(tip).removeClass(ClassName$6.FADE + " " + ClassName$6.SHOW);
+    };
+
+    _proto.setElementContent = function setElementContent($element, content) {
+      if (typeof content === 'object' && (content.nodeType || content.jquery)) {
+        // Content is a DOM node or a jQuery
+        if (this.config.html) {
+          if (!$(content).parent().is($element)) {
+            $element.empty().append(content);
+          }
+        } else {
+          $element.text($(content).text());
+        }
+
+        return;
+      }
+
+      if (this.config.html) {
+        if (this.config.sanitize) {
+          content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn);
+        }
+
+        $element.html(content);
+      } else {
+        $element.text(content);
+      }
+    };
+
+    _proto.getTitle = function getTitle() {
+      var title = this.element.getAttribute('data-original-title');
+
+      if (!title) {
+        title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title;
+      }
+
+      return title;
+    } // Private
+    ;
+
+    _proto._getOffset = function _getOffset() {
+      var _this3 = this;
+
+      var offset = {};
+
+      if (typeof this.config.offset === 'function') {
+        offset.fn = function (data) {
+          data.offsets = _objectSpread({}, data.offsets, _this3.config.offset(data.offsets, _this3.element) || {});
+          return data;
+        };
+      } else {
+        offset.offset = this.config.offset;
+      }
+
+      return offset;
+    };
+
+    _proto._getContainer = function _getContainer() {
+      if (this.config.container === false) {
+        return document.body;
+      }
+
+      if (Util.isElement(this.config.container)) {
+        return $(this.config.container);
+      }
+
+      return $(document).find(this.config.container);
+    };
+
+    _proto._getAttachment = function _getAttachment(placement) {
+      return AttachmentMap$1[placement.toUpperCase()];
+    };
+
+    _proto._setListeners = function _setListeners() {
+      var _this4 = this;
+
+      var triggers = this.config.trigger.split(' ');
+      triggers.forEach(function (trigger) {
+        if (trigger === 'click') {
+          $(_this4.element).on(_this4.constructor.Event.CLICK, _this4.config.selector, function (event) {
+            return _this4.toggle(event);
+          });
+        } else if (trigger !== Trigger.MANUAL) {
+          var eventIn = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSEENTER : _this4.constructor.Event.FOCUSIN;
+          var eventOut = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSELEAVE : _this4.constructor.Event.FOCUSOUT;
+          $(_this4.element).on(eventIn, _this4.config.selector, function (event) {
+            return _this4._enter(event);
+          }).on(eventOut, _this4.config.selector, function (event) {
+            return _this4._leave(event);
+          });
+        }
+      });
+      $(this.element).closest('.modal').on('hide.bs.modal', function () {
+        if (_this4.element) {
+          _this4.hide();
+        }
+      });
+
+      if (this.config.selector) {
+        this.config = _objectSpread({}, this.config, {
+          trigger: 'manual',
+          selector: ''
+        });
+      } else {
+        this._fixTitle();
+      }
+    };
+
+    _proto._fixTitle = function _fixTitle() {
+      var titleType = typeof this.element.getAttribute('data-original-title');
+
+      if (this.element.getAttribute('title') || titleType !== 'string') {
+        this.element.setAttribute('data-original-title', this.element.getAttribute('title') || '');
+        this.element.setAttribute('title', '');
+      }
+    };
+
+    _proto._enter = function _enter(event, context) {
+      var dataKey = this.constructor.DATA_KEY;
+      context = context || $(event.currentTarget).data(dataKey);
+
+      if (!context) {
+        context = new this.constructor(event.currentTarget, this._getDelegateConfig());
+        $(event.currentTarget).data(dataKey, context);
+      }
+
+      if (event) {
+        context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true;
+      }
+
+      if ($(context.getTipElement()).hasClass(ClassName$6.SHOW) || context._hoverState === HoverState.SHOW) {
+        context._hoverState = HoverState.SHOW;
+        return;
+      }
+
+      clearTimeout(context._timeout);
+      context._hoverState = HoverState.SHOW;
+
+      if (!context.config.delay || !context.config.delay.show) {
+        context.show();
+        return;
+      }
+
+      context._timeout = setTimeout(function () {
+        if (context._hoverState === HoverState.SHOW) {
+          context.show();
+        }
+      }, context.config.delay.show);
+    };
+
+    _proto._leave = function _leave(event, context) {
+      var dataKey = this.constructor.DATA_KEY;
+      context = context || $(event.currentTarget).data(dataKey);
+
+      if (!context) {
+        context = new this.constructor(event.currentTarget, this._getDelegateConfig());
+        $(event.currentTarget).data(dataKey, context);
+      }
+
+      if (event) {
+        context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false;
+      }
+
+      if (context._isWithActiveTrigger()) {
+        return;
+      }
+
+      clearTimeout(context._timeout);
+      context._hoverState = HoverState.OUT;
+
+      if (!context.config.delay || !context.config.delay.hide) {
+        context.hide();
+        return;
+      }
+
+      context._timeout = setTimeout(function () {
+        if (context._hoverState === HoverState.OUT) {
+          context.hide();
+        }
+      }, context.config.delay.hide);
+    };
+
+    _proto._isWithActiveTrigger = function _isWithActiveTrigger() {
+      for (var trigger in this._activeTrigger) {
+        if (this._activeTrigger[trigger]) {
+          return true;
+        }
+      }
+
+      return false;
+    };
+
+    _proto._getConfig = function _getConfig(config) {
+      var dataAttributes = $(this.element).data();
+      Object.keys(dataAttributes).forEach(function (dataAttr) {
+        if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
+          delete dataAttributes[dataAttr];
+        }
+      });
+      config = _objectSpread({}, this.constructor.Default, dataAttributes, typeof config === 'object' && config ? config : {});
+
+      if (typeof config.delay === 'number') {
+        config.delay = {
+          show: config.delay,
+          hide: config.delay
+        };
+      }
+
+      if (typeof config.title === 'number') {
+        config.title = config.title.toString();
+      }
+
+      if (typeof config.content === 'number') {
+        config.content = config.content.toString();
+      }
+
+      Util.typeCheckConfig(NAME$6, config, this.constructor.DefaultType);
+
+      if (config.sanitize) {
+        config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn);
+      }
+
+      return config;
+    };
+
+    _proto._getDelegateConfig = function _getDelegateConfig() {
+      var config = {};
+
+      if (this.config) {
+        for (var key in this.config) {
+          if (this.constructor.Default[key] !== this.config[key]) {
+            config[key] = this.config[key];
+          }
+        }
+      }
+
+      return config;
+    };
+
+    _proto._cleanTipClass = function _cleanTipClass() {
+      var $tip = $(this.getTipElement());
+      var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX);
+
+      if (tabClass !== null && tabClass.length) {
+        $tip.removeClass(tabClass.join(''));
+      }
+    };
+
+    _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(popperData) {
+      var popperInstance = popperData.instance;
+      this.tip = popperInstance.popper;
+
+      this._cleanTipClass();
+
+      this.addAttachmentClass(this._getAttachment(popperData.placement));
+    };
+
+    _proto._fixTransition = function _fixTransition() {
+      var tip = this.getTipElement();
+      var initConfigAnimation = this.config.animation;
+
+      if (tip.getAttribute('x-placement') !== null) {
+        return;
+      }
+
+      $(tip).removeClass(ClassName$6.FADE);
+      this.config.animation = false;
+      this.hide();
+      this.show();
+      this.config.animation = initConfigAnimation;
+    } // Static
+    ;
+
+    Tooltip._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var data = $(this).data(DATA_KEY$6);
+
+        var _config = typeof config === 'object' && config;
+
+        if (!data && /dispose|hide/.test(config)) {
+          return;
+        }
+
+        if (!data) {
+          data = new Tooltip(this, _config);
+          $(this).data(DATA_KEY$6, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config]();
+        }
+      });
+    };
+
+    _createClass(Tooltip, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$6;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default$4;
+      }
+    }, {
+      key: "NAME",
+      get: function get() {
+        return NAME$6;
+      }
+    }, {
+      key: "DATA_KEY",
+      get: function get() {
+        return DATA_KEY$6;
+      }
+    }, {
+      key: "Event",
+      get: function get() {
+        return Event$6;
+      }
+    }, {
+      key: "EVENT_KEY",
+      get: function get() {
+        return EVENT_KEY$6;
+      }
+    }, {
+      key: "DefaultType",
+      get: function get() {
+        return DefaultType$4;
+      }
+    }]);
+
+    return Tooltip;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+
+  $.fn[NAME$6] = Tooltip._jQueryInterface;
+  $.fn[NAME$6].Constructor = Tooltip;
+
+  $.fn[NAME$6].noConflict = function () {
+    $.fn[NAME$6] = JQUERY_NO_CONFLICT$6;
+    return Tooltip._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$7 = 'popover';
+  var VERSION$7 = '4.3.1';
+  var DATA_KEY$7 = 'bs.popover';
+  var EVENT_KEY$7 = "." + DATA_KEY$7;
+  var JQUERY_NO_CONFLICT$7 = $.fn[NAME$7];
+  var CLASS_PREFIX$1 = 'bs-popover';
+  var BSCLS_PREFIX_REGEX$1 = new RegExp("(^|\\s)" + CLASS_PREFIX$1 + "\\S+", 'g');
+
+  var Default$5 = _objectSpread({}, Tooltip.Default, {
+    placement: 'right',
+    trigger: 'click',
+    content: '',
+    template: '<div class="popover" role="tooltip">' + '<div class="arrow"></div>' + '<h3 class="popover-header"></h3>' + '<div class="popover-body"></div></div>'
+  });
+
+  var DefaultType$5 = _objectSpread({}, Tooltip.DefaultType, {
+    content: '(string|element|function)'
+  });
+
+  var ClassName$7 = {
+    FADE: 'fade',
+    SHOW: 'show'
+  };
+  var Selector$7 = {
+    TITLE: '.popover-header',
+    CONTENT: '.popover-body'
+  };
+  var Event$7 = {
+    HIDE: "hide" + EVENT_KEY$7,
+    HIDDEN: "hidden" + EVENT_KEY$7,
+    SHOW: "show" + EVENT_KEY$7,
+    SHOWN: "shown" + EVENT_KEY$7,
+    INSERTED: "inserted" + EVENT_KEY$7,
+    CLICK: "click" + EVENT_KEY$7,
+    FOCUSIN: "focusin" + EVENT_KEY$7,
+    FOCUSOUT: "focusout" + EVENT_KEY$7,
+    MOUSEENTER: "mouseenter" + EVENT_KEY$7,
+    MOUSELEAVE: "mouseleave" + EVENT_KEY$7
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Popover =
+  /*#__PURE__*/
+  function (_Tooltip) {
+    _inheritsLoose(Popover, _Tooltip);
+
+    function Popover() {
+      return _Tooltip.apply(this, arguments) || this;
+    }
+
+    var _proto = Popover.prototype;
+
+    // Overrides
+    _proto.isWithContent = function isWithContent() {
+      return this.getTitle() || this._getContent();
+    };
+
+    _proto.addAttachmentClass = function addAttachmentClass(attachment) {
+      $(this.getTipElement()).addClass(CLASS_PREFIX$1 + "-" + attachment);
+    };
+
+    _proto.getTipElement = function getTipElement() {
+      this.tip = this.tip || $(this.config.template)[0];
+      return this.tip;
+    };
+
+    _proto.setContent = function setContent() {
+      var $tip = $(this.getTipElement()); // We use append for html objects to maintain js events
+
+      this.setElementContent($tip.find(Selector$7.TITLE), this.getTitle());
+
+      var content = this._getContent();
+
+      if (typeof content === 'function') {
+        content = content.call(this.element);
+      }
+
+      this.setElementContent($tip.find(Selector$7.CONTENT), content);
+      $tip.removeClass(ClassName$7.FADE + " " + ClassName$7.SHOW);
+    } // Private
+    ;
+
+    _proto._getContent = function _getContent() {
+      return this.element.getAttribute('data-content') || this.config.content;
+    };
+
+    _proto._cleanTipClass = function _cleanTipClass() {
+      var $tip = $(this.getTipElement());
+      var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX$1);
+
+      if (tabClass !== null && tabClass.length > 0) {
+        $tip.removeClass(tabClass.join(''));
+      }
+    } // Static
+    ;
+
+    Popover._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var data = $(this).data(DATA_KEY$7);
+
+        var _config = typeof config === 'object' ? config : null;
+
+        if (!data && /dispose|hide/.test(config)) {
+          return;
+        }
+
+        if (!data) {
+          data = new Popover(this, _config);
+          $(this).data(DATA_KEY$7, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config]();
+        }
+      });
+    };
+
+    _createClass(Popover, null, [{
+      key: "VERSION",
+      // Getters
+      get: function get() {
+        return VERSION$7;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default$5;
+      }
+    }, {
+      key: "NAME",
+      get: function get() {
+        return NAME$7;
+      }
+    }, {
+      key: "DATA_KEY",
+      get: function get() {
+        return DATA_KEY$7;
+      }
+    }, {
+      key: "Event",
+      get: function get() {
+        return Event$7;
+      }
+    }, {
+      key: "EVENT_KEY",
+      get: function get() {
+        return EVENT_KEY$7;
+      }
+    }, {
+      key: "DefaultType",
+      get: function get() {
+        return DefaultType$5;
+      }
+    }]);
+
+    return Popover;
+  }(Tooltip);
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+
+  $.fn[NAME$7] = Popover._jQueryInterface;
+  $.fn[NAME$7].Constructor = Popover;
+
+  $.fn[NAME$7].noConflict = function () {
+    $.fn[NAME$7] = JQUERY_NO_CONFLICT$7;
+    return Popover._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$8 = 'scrollspy';
+  var VERSION$8 = '4.3.1';
+  var DATA_KEY$8 = 'bs.scrollspy';
+  var EVENT_KEY$8 = "." + DATA_KEY$8;
+  var DATA_API_KEY$6 = '.data-api';
+  var JQUERY_NO_CONFLICT$8 = $.fn[NAME$8];
+  var Default$6 = {
+    offset: 10,
+    method: 'auto',
+    target: ''
+  };
+  var DefaultType$6 = {
+    offset: 'number',
+    method: 'string',
+    target: '(string|element)'
+  };
+  var Event$8 = {
+    ACTIVATE: "activate" + EVENT_KEY$8,
+    SCROLL: "scroll" + EVENT_KEY$8,
+    LOAD_DATA_API: "load" + EVENT_KEY$8 + DATA_API_KEY$6
+  };
+  var ClassName$8 = {
+    DROPDOWN_ITEM: 'dropdown-item',
+    DROPDOWN_MENU: 'dropdown-menu',
+    ACTIVE: 'active'
+  };
+  var Selector$8 = {
+    DATA_SPY: '[data-spy="scroll"]',
+    ACTIVE: '.active',
+    NAV_LIST_GROUP: '.nav, .list-group',
+    NAV_LINKS: '.nav-link',
+    NAV_ITEMS: '.nav-item',
+    LIST_ITEMS: '.list-group-item',
+    DROPDOWN: '.dropdown',
+    DROPDOWN_ITEMS: '.dropdown-item',
+    DROPDOWN_TOGGLE: '.dropdown-toggle'
+  };
+  var OffsetMethod = {
+    OFFSET: 'offset',
+    POSITION: 'position'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var ScrollSpy =
+  /*#__PURE__*/
+  function () {
+    function ScrollSpy(element, config) {
+      var _this = this;
+
+      this._element = element;
+      this._scrollElement = element.tagName === 'BODY' ? window : element;
+      this._config = this._getConfig(config);
+      this._selector = this._config.target + " " + Selector$8.NAV_LINKS + "," + (this._config.target + " " + Selector$8.LIST_ITEMS + ",") + (this._config.target + " " + Selector$8.DROPDOWN_ITEMS);
+      this._offsets = [];
+      this._targets = [];
+      this._activeTarget = null;
+      this._scrollHeight = 0;
+      $(this._scrollElement).on(Event$8.SCROLL, function (event) {
+        return _this._process(event);
+      });
+      this.refresh();
+
+      this._process();
+    } // Getters
+
+
+    var _proto = ScrollSpy.prototype;
+
+    // Public
+    _proto.refresh = function refresh() {
+      var _this2 = this;
+
+      var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION;
+      var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method;
+      var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0;
+      this._offsets = [];
+      this._targets = [];
+      this._scrollHeight = this._getScrollHeight();
+      var targets = [].slice.call(document.querySelectorAll(this._selector));
+      targets.map(function (element) {
+        var target;
+        var targetSelector = Util.getSelectorFromElement(element);
+
+        if (targetSelector) {
+          target = document.querySelector(targetSelector);
+        }
+
+        if (target) {
+          var targetBCR = target.getBoundingClientRect();
+
+          if (targetBCR.width || targetBCR.height) {
+            // TODO (fat): remove sketch reliance on jQuery position/offset
+            return [$(target)[offsetMethod]().top + offsetBase, targetSelector];
+          }
+        }
+
+        return null;
+      }).filter(function (item) {
+        return item;
+      }).sort(function (a, b) {
+        return a[0] - b[0];
+      }).forEach(function (item) {
+        _this2._offsets.push(item[0]);
+
+        _this2._targets.push(item[1]);
+      });
+    };
+
+    _proto.dispose = function dispose() {
+      $.removeData(this._element, DATA_KEY$8);
+      $(this._scrollElement).off(EVENT_KEY$8);
+      this._element = null;
+      this._scrollElement = null;
+      this._config = null;
+      this._selector = null;
+      this._offsets = null;
+      this._targets = null;
+      this._activeTarget = null;
+      this._scrollHeight = null;
+    } // Private
+    ;
+
+    _proto._getConfig = function _getConfig(config) {
+      config = _objectSpread({}, Default$6, typeof config === 'object' && config ? config : {});
+
+      if (typeof config.target !== 'string') {
+        var id = $(config.target).attr('id');
+
+        if (!id) {
+          id = Util.getUID(NAME$8);
+          $(config.target).attr('id', id);
+        }
+
+        config.target = "#" + id;
+      }
+
+      Util.typeCheckConfig(NAME$8, config, DefaultType$6);
+      return config;
+    };
+
+    _proto._getScrollTop = function _getScrollTop() {
+      return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop;
+    };
+
+    _proto._getScrollHeight = function _getScrollHeight() {
+      return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
+    };
+
+    _proto._getOffsetHeight = function _getOffsetHeight() {
+      return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height;
+    };
+
+    _proto._process = function _process() {
+      var scrollTop = this._getScrollTop() + this._config.offset;
+
+      var scrollHeight = this._getScrollHeight();
+
+      var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight();
+
+      if (this._scrollHeight !== scrollHeight) {
+        this.refresh();
+      }
+
+      if (scrollTop >= maxScroll) {
+        var target = this._targets[this._targets.length - 1];
+
+        if (this._activeTarget !== target) {
+          this._activate(target);
+        }
+
+        return;
+      }
+
+      if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
+        this._activeTarget = null;
+
+        this._clear();
+
+        return;
+      }
+
+      var offsetLength = this._offsets.length;
+
+      for (var i = offsetLength; i--;) {
+        var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]);
+
+        if (isActiveTarget) {
+          this._activate(this._targets[i]);
+        }
+      }
+    };
+
+    _proto._activate = function _activate(target) {
+      this._activeTarget = target;
+
+      this._clear();
+
+      var queries = this._selector.split(',').map(function (selector) {
+        return selector + "[data-target=\"" + target + "\"]," + selector + "[href=\"" + target + "\"]";
+      });
+
+      var $link = $([].slice.call(document.querySelectorAll(queries.join(','))));
+
+      if ($link.hasClass(ClassName$8.DROPDOWN_ITEM)) {
+        $link.closest(Selector$8.DROPDOWN).find(Selector$8.DROPDOWN_TOGGLE).addClass(ClassName$8.ACTIVE);
+        $link.addClass(ClassName$8.ACTIVE);
+      } else {
+        // Set triggered link as active
+        $link.addClass(ClassName$8.ACTIVE); // Set triggered links parents as active
+        // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
+
+        $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_LINKS + ", " + Selector$8.LIST_ITEMS).addClass(ClassName$8.ACTIVE); // Handle special case when .nav-link is inside .nav-item
+
+        $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_ITEMS).children(Selector$8.NAV_LINKS).addClass(ClassName$8.ACTIVE);
+      }
+
+      $(this._scrollElement).trigger(Event$8.ACTIVATE, {
+        relatedTarget: target
+      });
+    };
+
+    _proto._clear = function _clear() {
+      [].slice.call(document.querySelectorAll(this._selector)).filter(function (node) {
+        return node.classList.contains(ClassName$8.ACTIVE);
+      }).forEach(function (node) {
+        return node.classList.remove(ClassName$8.ACTIVE);
+      });
+    } // Static
+    ;
+
+    ScrollSpy._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var data = $(this).data(DATA_KEY$8);
+
+        var _config = typeof config === 'object' && config;
+
+        if (!data) {
+          data = new ScrollSpy(this, _config);
+          $(this).data(DATA_KEY$8, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config]();
+        }
+      });
+    };
+
+    _createClass(ScrollSpy, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$8;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default$6;
+      }
+    }]);
+
+    return ScrollSpy;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(window).on(Event$8.LOAD_DATA_API, function () {
+    var scrollSpys = [].slice.call(document.querySelectorAll(Selector$8.DATA_SPY));
+    var scrollSpysLength = scrollSpys.length;
+
+    for (var i = scrollSpysLength; i--;) {
+      var $spy = $(scrollSpys[i]);
+
+      ScrollSpy._jQueryInterface.call($spy, $spy.data());
+    }
+  });
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME$8] = ScrollSpy._jQueryInterface;
+  $.fn[NAME$8].Constructor = ScrollSpy;
+
+  $.fn[NAME$8].noConflict = function () {
+    $.fn[NAME$8] = JQUERY_NO_CONFLICT$8;
+    return ScrollSpy._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$9 = 'tab';
+  var VERSION$9 = '4.3.1';
+  var DATA_KEY$9 = 'bs.tab';
+  var EVENT_KEY$9 = "." + DATA_KEY$9;
+  var DATA_API_KEY$7 = '.data-api';
+  var JQUERY_NO_CONFLICT$9 = $.fn[NAME$9];
+  var Event$9 = {
+    HIDE: "hide" + EVENT_KEY$9,
+    HIDDEN: "hidden" + EVENT_KEY$9,
+    SHOW: "show" + EVENT_KEY$9,
+    SHOWN: "shown" + EVENT_KEY$9,
+    CLICK_DATA_API: "click" + EVENT_KEY$9 + DATA_API_KEY$7
+  };
+  var ClassName$9 = {
+    DROPDOWN_MENU: 'dropdown-menu',
+    ACTIVE: 'active',
+    DISABLED: 'disabled',
+    FADE: 'fade',
+    SHOW: 'show'
+  };
+  var Selector$9 = {
+    DROPDOWN: '.dropdown',
+    NAV_LIST_GROUP: '.nav, .list-group',
+    ACTIVE: '.active',
+    ACTIVE_UL: '> li > .active',
+    DATA_TOGGLE: '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',
+    DROPDOWN_TOGGLE: '.dropdown-toggle',
+    DROPDOWN_ACTIVE_CHILD: '> .dropdown-menu .active'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Tab =
+  /*#__PURE__*/
+  function () {
+    function Tab(element) {
+      this._element = element;
+    } // Getters
+
+
+    var _proto = Tab.prototype;
+
+    // Public
+    _proto.show = function show() {
+      var _this = this;
+
+      if (this._element.parentNode && this._element.parentNode.nodeType === Node.ELEMENT_NODE && $(this._element).hasClass(ClassName$9.ACTIVE) || $(this._element).hasClass(ClassName$9.DISABLED)) {
+        return;
+      }
+
+      var target;
+      var previous;
+      var listElement = $(this._element).closest(Selector$9.NAV_LIST_GROUP)[0];
+      var selector = Util.getSelectorFromElement(this._element);
+
+      if (listElement) {
+        var itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector$9.ACTIVE_UL : Selector$9.ACTIVE;
+        previous = $.makeArray($(listElement).find(itemSelector));
+        previous = previous[previous.length - 1];
+      }
+
+      var hideEvent = $.Event(Event$9.HIDE, {
+        relatedTarget: this._element
+      });
+      var showEvent = $.Event(Event$9.SHOW, {
+        relatedTarget: previous
+      });
+
+      if (previous) {
+        $(previous).trigger(hideEvent);
+      }
+
+      $(this._element).trigger(showEvent);
+
+      if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) {
+        return;
+      }
+
+      if (selector) {
+        target = document.querySelector(selector);
+      }
+
+      this._activate(this._element, listElement);
+
+      var complete = function complete() {
+        var hiddenEvent = $.Event(Event$9.HIDDEN, {
+          relatedTarget: _this._element
+        });
+        var shownEvent = $.Event(Event$9.SHOWN, {
+          relatedTarget: previous
+        });
+        $(previous).trigger(hiddenEvent);
+        $(_this._element).trigger(shownEvent);
+      };
+
+      if (target) {
+        this._activate(target, target.parentNode, complete);
+      } else {
+        complete();
+      }
+    };
+
+    _proto.dispose = function dispose() {
+      $.removeData(this._element, DATA_KEY$9);
+      this._element = null;
+    } // Private
+    ;
+
+    _proto._activate = function _activate(element, container, callback) {
+      var _this2 = this;
+
+      var activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') ? $(container).find(Selector$9.ACTIVE_UL) : $(container).children(Selector$9.ACTIVE);
+      var active = activeElements[0];
+      var isTransitioning = callback && active && $(active).hasClass(ClassName$9.FADE);
+
+      var complete = function complete() {
+        return _this2._transitionComplete(element, active, callback);
+      };
+
+      if (active && isTransitioning) {
+        var transitionDuration = Util.getTransitionDurationFromElement(active);
+        $(active).removeClass(ClassName$9.SHOW).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+      } else {
+        complete();
+      }
+    };
+
+    _proto._transitionComplete = function _transitionComplete(element, active, callback) {
+      if (active) {
+        $(active).removeClass(ClassName$9.ACTIVE);
+        var dropdownChild = $(active.parentNode).find(Selector$9.DROPDOWN_ACTIVE_CHILD)[0];
+
+        if (dropdownChild) {
+          $(dropdownChild).removeClass(ClassName$9.ACTIVE);
+        }
+
+        if (active.getAttribute('role') === 'tab') {
+          active.setAttribute('aria-selected', false);
+        }
+      }
+
+      $(element).addClass(ClassName$9.ACTIVE);
+
+      if (element.getAttribute('role') === 'tab') {
+        element.setAttribute('aria-selected', true);
+      }
+
+      Util.reflow(element);
+
+      if (element.classList.contains(ClassName$9.FADE)) {
+        element.classList.add(ClassName$9.SHOW);
+      }
+
+      if (element.parentNode && $(element.parentNode).hasClass(ClassName$9.DROPDOWN_MENU)) {
+        var dropdownElement = $(element).closest(Selector$9.DROPDOWN)[0];
+
+        if (dropdownElement) {
+          var dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(Selector$9.DROPDOWN_TOGGLE));
+          $(dropdownToggleList).addClass(ClassName$9.ACTIVE);
+        }
+
+        element.setAttribute('aria-expanded', true);
+      }
+
+      if (callback) {
+        callback();
+      }
+    } // Static
+    ;
+
+    Tab._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var $this = $(this);
+        var data = $this.data(DATA_KEY$9);
+
+        if (!data) {
+          data = new Tab(this);
+          $this.data(DATA_KEY$9, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config]();
+        }
+      });
+    };
+
+    _createClass(Tab, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$9;
+      }
+    }]);
+
+    return Tab;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+
+  $(document).on(Event$9.CLICK_DATA_API, Selector$9.DATA_TOGGLE, function (event) {
+    event.preventDefault();
+
+    Tab._jQueryInterface.call($(this), 'show');
+  });
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME$9] = Tab._jQueryInterface;
+  $.fn[NAME$9].Constructor = Tab;
+
+  $.fn[NAME$9].noConflict = function () {
+    $.fn[NAME$9] = JQUERY_NO_CONFLICT$9;
+    return Tab._jQueryInterface;
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME$a = 'toast';
+  var VERSION$a = '4.3.1';
+  var DATA_KEY$a = 'bs.toast';
+  var EVENT_KEY$a = "." + DATA_KEY$a;
+  var JQUERY_NO_CONFLICT$a = $.fn[NAME$a];
+  var Event$a = {
+    CLICK_DISMISS: "click.dismiss" + EVENT_KEY$a,
+    HIDE: "hide" + EVENT_KEY$a,
+    HIDDEN: "hidden" + EVENT_KEY$a,
+    SHOW: "show" + EVENT_KEY$a,
+    SHOWN: "shown" + EVENT_KEY$a
+  };
+  var ClassName$a = {
+    FADE: 'fade',
+    HIDE: 'hide',
+    SHOW: 'show',
+    SHOWING: 'showing'
+  };
+  var DefaultType$7 = {
+    animation: 'boolean',
+    autohide: 'boolean',
+    delay: 'number'
+  };
+  var Default$7 = {
+    animation: true,
+    autohide: true,
+    delay: 500
+  };
+  var Selector$a = {
+    DATA_DISMISS: '[data-dismiss="toast"]'
+    /**
+     * ------------------------------------------------------------------------
+     * Class Definition
+     * ------------------------------------------------------------------------
+     */
+
+  };
+
+  var Toast =
+  /*#__PURE__*/
+  function () {
+    function Toast(element, config) {
+      this._element = element;
+      this._config = this._getConfig(config);
+      this._timeout = null;
+
+      this._setListeners();
+    } // Getters
+
+
+    var _proto = Toast.prototype;
+
+    // Public
+    _proto.show = function show() {
+      var _this = this;
+
+      $(this._element).trigger(Event$a.SHOW);
+
+      if (this._config.animation) {
+        this._element.classList.add(ClassName$a.FADE);
+      }
+
+      var complete = function complete() {
+        _this._element.classList.remove(ClassName$a.SHOWING);
+
+        _this._element.classList.add(ClassName$a.SHOW);
+
+        $(_this._element).trigger(Event$a.SHOWN);
+
+        if (_this._config.autohide) {
+          _this.hide();
+        }
+      };
+
+      this._element.classList.remove(ClassName$a.HIDE);
+
+      this._element.classList.add(ClassName$a.SHOWING);
+
+      if (this._config.animation) {
+        var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+        $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+      } else {
+        complete();
+      }
+    };
+
+    _proto.hide = function hide(withoutTimeout) {
+      var _this2 = this;
+
+      if (!this._element.classList.contains(ClassName$a.SHOW)) {
+        return;
+      }
+
+      $(this._element).trigger(Event$a.HIDE);
+
+      if (withoutTimeout) {
+        this._close();
+      } else {
+        this._timeout = setTimeout(function () {
+          _this2._close();
+        }, this._config.delay);
+      }
+    };
+
+    _proto.dispose = function dispose() {
+      clearTimeout(this._timeout);
+      this._timeout = null;
+
+      if (this._element.classList.contains(ClassName$a.SHOW)) {
+        this._element.classList.remove(ClassName$a.SHOW);
+      }
+
+      $(this._element).off(Event$a.CLICK_DISMISS);
+      $.removeData(this._element, DATA_KEY$a);
+      this._element = null;
+      this._config = null;
+    } // Private
+    ;
+
+    _proto._getConfig = function _getConfig(config) {
+      config = _objectSpread({}, Default$7, $(this._element).data(), typeof config === 'object' && config ? config : {});
+      Util.typeCheckConfig(NAME$a, config, this.constructor.DefaultType);
+      return config;
+    };
+
+    _proto._setListeners = function _setListeners() {
+      var _this3 = this;
+
+      $(this._element).on(Event$a.CLICK_DISMISS, Selector$a.DATA_DISMISS, function () {
+        return _this3.hide(true);
+      });
+    };
+
+    _proto._close = function _close() {
+      var _this4 = this;
+
+      var complete = function complete() {
+        _this4._element.classList.add(ClassName$a.HIDE);
+
+        $(_this4._element).trigger(Event$a.HIDDEN);
+      };
+
+      this._element.classList.remove(ClassName$a.SHOW);
+
+      if (this._config.animation) {
+        var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+        $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+      } else {
+        complete();
+      }
+    } // Static
+    ;
+
+    Toast._jQueryInterface = function _jQueryInterface(config) {
+      return this.each(function () {
+        var $element = $(this);
+        var data = $element.data(DATA_KEY$a);
+
+        var _config = typeof config === 'object' && config;
+
+        if (!data) {
+          data = new Toast(this, _config);
+          $element.data(DATA_KEY$a, data);
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError("No method named \"" + config + "\"");
+          }
+
+          data[config](this);
+        }
+      });
+    };
+
+    _createClass(Toast, null, [{
+      key: "VERSION",
+      get: function get() {
+        return VERSION$a;
+      }
+    }, {
+      key: "DefaultType",
+      get: function get() {
+        return DefaultType$7;
+      }
+    }, {
+      key: "Default",
+      get: function get() {
+        return Default$7;
+      }
+    }]);
+
+    return Toast;
+  }();
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+
+  $.fn[NAME$a] = Toast._jQueryInterface;
+  $.fn[NAME$a].Constructor = Toast;
+
+  $.fn[NAME$a].noConflict = function () {
+    $.fn[NAME$a] = JQUERY_NO_CONFLICT$a;
+    return Toast._jQueryInterface;
+  };
+
+  /**
+   * --------------------------------------------------------------------------
+   * Bootstrap (v4.3.1): index.js
+   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+   * --------------------------------------------------------------------------
+   */
+
+  (function () {
+    if (typeof $ === 'undefined') {
+      throw new TypeError('Bootstrap\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\'s JavaScript.');
+    }
+
+    var version = $.fn.jquery.split(' ')[0].split('.');
+    var minMajor = 1;
+    var ltMajor = 2;
+    var minMinor = 9;
+    var minPatch = 1;
+    var maxMajor = 4;
+
+    if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {
+      throw new Error('Bootstrap\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0');
+    }
+  })();
+
+  exports.Util = Util;
+  exports.Alert = Alert;
+  exports.Button = Button;
+  exports.Carousel = Carousel;
+  exports.Collapse = Collapse;
+  exports.Dropdown = Dropdown;
+  exports.Modal = Modal;
+  exports.Popover = Popover;
+  exports.Scrollspy = ScrollSpy;
+  exports.Tab = Tab;
+  exports.Toast = Toast;
+  exports.Tooltip = Tooltip;
+
+  Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
+//# sourceMappingURL=bootstrap.js.map
+"use strict";
+var Platform = {};
+
+(function () {
+
+  Platform.detectDevice = function () {
+    var body = document.body;
+    var ua = navigator.userAgent;
+    var checker = {
+      // OS
+      Windows: ua.match(/Windows/),
+      MacOS: ua.match(/Mac/),
+      Android: ua.match(/Android/),
+
+      // Browser
+      Msie: ua.match(/Trident/),
+      Edge: ua.match(/Edge/),
+      Chrome: ua.match(/Chrome/),
+      Firefox: ua.match(/Firefox/),
+      Safari: ua.match(/Safari/),
+
+      // Device
+      isApple: ua.match(/(iPhone|iPod|iPad)/),
+      iPhone: ua.match(/iPhone/),
+      iPad: ua.match(/iPad/),
+      iPod: ua.match(/iPod/),
+    };
+
+    if (checker.isApple) {
+      // Apple
+      body.classList.add('isApple');
+
+      if (checker.iPhone) {
+        // Apple iPhone
+        body.classList.add('iphone');
+      } else if (checker.iPad) {
+        // Apple iPad
+        body.classList.add('ipad');
+      } else if (checker.iPod) {
+        // Apple iPod
+        body.classList.add('ipod');
+      }
+
+    } else  if (checker.Windows){
+      // Windows OS
+      body.classList.add('windowsOS');
+
+      if (checker.Edge){
+        // Edge Browser
+        body.classList.add('edge');
+      } else if (checker.Chrome){
+        // Chrome Browser
+        body.classList.add('chrome');
+      } else if(checker.Safari){
+        // Safari Browser
+        body.classList.add('safari');
+      } else if(checker.Firefox){
+        // Firefox Browser
+        body.classList.add('firefox');
+      } else if(checker.Msie){
+        // IE Browser
+        body.classList.add('msie');
+      }
+
+    } else if (checker.MacOS){
+      // Mac OS
+      body.classList.add('macOS');
+
+      if (checker.Chrome){
+        // Chrome Browser
+        body.classList.add('chrome');
+      } else if(checker.Safari){
+        // Safari Browser
+        body.classList.add('safari');
+      } else if(checker.Firefox){
+        // Firefox Browser
+        body.classList.add('firefox');
+      }
+
+    } else if (checker.Android){
+      // Android OS
+      body.classList.add('AndroidOS');
+    }
+
+  }
+
+  Platform.detectDevice();
+
+})($);
+
+"use strict";
+
+
+jQuery(document).ready(function() {
+    //removeIf(production)
+    console.log("document ready");
+    //endRemoveIf(production)
+});
+
+function copyCodeToClipboard(event, element) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    const textInput = element.nextSibling.nextSibling;
+    textInput.select();
+
+       try {
+               if (document.execCommand('copy')) {
+            element.innerHTML = 'Copied';
+            
+            setTimeout(function() {
+                element.innerHTML = 'Copy';
+            }, 3000);
+               }
+       } catch (err) {
+               alert('Please use CTRL/CMD + C to copy.');
+               console.log('Oops, unable to copy', err);
+       }
+
+    return false;
+}
+
+//# sourceMappingURL=main.js.map
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/js/main.js.map b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/assets/js/main.js.map
new file mode 100644 (file)
index 0000000..e4bed97
--- /dev/null
@@ -0,0 +1 @@
+{"version":3,"sources":["jquery.js","popper.js","bootstrap.js","crossPlatform.js","main.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACr2UA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnjFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACl1IA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"main.js","sourcesContent":["/*!\n * jQuery JavaScript Library v3.4.1\n * https://jquery.com/\n *\n * Includes Sizzle.js\n * https://sizzlejs.com/\n *\n * Copyright JS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.org/license\n *\n * Date: 2019-05-01T21:04Z\n */\n( function( global, factory ) {\n\n\t\"use strict\";\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\n\t\t// For CommonJS and CommonJS-like environments where a proper `window`\n\t\t// is present, execute the factory and get jQuery.\n\t\t// For environments that do not have a `window` with a `document`\n\t\t// (such as Node.js), expose a factory as module.exports.\n\t\t// This accentuates the need for the creation of a real `window`.\n\t\t// e.g. var jQuery = require(\"jquery\")(window);\n\t\t// See ticket #14549 for more info.\n\t\tmodule.exports = global.document ?\n\t\t\tfactory( global, true ) :\n\t\t\tfunction( w ) {\n\t\t\t\tif ( !w.document ) {\n\t\t\t\t\tthrow new Error( \"jQuery requires a window with a document\" );\n\t\t\t\t}\n\t\t\t\treturn factory( w );\n\t\t\t};\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n} )( typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1\n// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode\n// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common\n// enough that all such attempts are guarded in a try block.\n\"use strict\";\n\nvar arr = [];\n\nvar document = window.document;\n\nvar getProto = Object.getPrototypeOf;\n\nvar slice = arr.slice;\n\nvar concat = arr.concat;\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar fnToString = hasOwn.toString;\n\nvar ObjectFunctionString = fnToString.call( Object );\n\nvar support = {};\n\nvar isFunction = function isFunction( obj ) {\n\n      // Support: Chrome <=57, Firefox <=52\n      // In some browsers, typeof returns \"function\" for HTML <object> elements\n      // (i.e., `typeof document.createElement( \"object\" ) === \"function\"`).\n      // We don't want to classify *any* DOM node as a function.\n      return typeof obj === \"function\" && typeof obj.nodeType !== \"number\";\n  };\n\n\nvar isWindow = function isWindow( obj ) {\n\t\treturn obj != null && obj === obj.window;\n\t};\n\n\n\n\n\tvar preservedScriptAttributes = {\n\t\ttype: true,\n\t\tsrc: true,\n\t\tnonce: true,\n\t\tnoModule: true\n\t};\n\n\tfunction DOMEval( code, node, doc ) {\n\t\tdoc = doc || document;\n\n\t\tvar i, val,\n\t\t\tscript = doc.createElement( \"script\" );\n\n\t\tscript.text = code;\n\t\tif ( node ) {\n\t\t\tfor ( i in preservedScriptAttributes ) {\n\n\t\t\t\t// Support: Firefox 64+, Edge 18+\n\t\t\t\t// Some browsers don't support the \"nonce\" property on scripts.\n\t\t\t\t// On the other hand, just using `getAttribute` is not enough as\n\t\t\t\t// the `nonce` attribute is reset to an empty string whenever it\n\t\t\t\t// becomes browsing-context connected.\n\t\t\t\t// See https://github.com/whatwg/html/issues/2369\n\t\t\t\t// See https://html.spec.whatwg.org/#nonce-attributes\n\t\t\t\t// The `node.getAttribute` check was added for the sake of\n\t\t\t\t// `jQuery.globalEval` so that it can fake a nonce-containing node\n\t\t\t\t// via an object.\n\t\t\t\tval = node[ i ] || node.getAttribute && node.getAttribute( i );\n\t\t\t\tif ( val ) {\n\t\t\t\t\tscript.setAttribute( i, val );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdoc.head.appendChild( script ).parentNode.removeChild( script );\n\t}\n\n\nfunction toType( obj ) {\n\tif ( obj == null ) {\n\t\treturn obj + \"\";\n\t}\n\n\t// Support: Android <=2.3 only (functionish RegExp)\n\treturn typeof obj === \"object\" || typeof obj === \"function\" ?\n\t\tclass2type[ toString.call( obj ) ] || \"object\" :\n\t\ttypeof obj;\n}\n/* global Symbol */\n// Defining this global in .eslintrc.json would create a danger of using the global\n// unguarded in another place, it seems safer to define global only for this module\n\n\n\nvar\n\tversion = \"3.4.1\",\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t},\n\n\t// Support: Android <=4.0 only\n\t// Make sure we trim BOM and NBSP\n\trtrim = /^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g;\n\njQuery.fn = jQuery.prototype = {\n\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\n\t\t// Return all the elements in a clean array\n\t\tif ( num == null ) {\n\t\t\treturn slice.call( this );\n\t\t}\n\n\t\t// Return just the one element from the set\n\t\treturn num < 0 ? this[ num + this.length ] : this[ num ];\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\teach: function( callback ) {\n\t\treturn jQuery.each( this, callback );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map( this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t} ) );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor();\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: arr.sort,\n\tsplice: arr.splice\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[ 0 ] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// Skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !isFunction( target ) ) {\n\t\ttarget = {};\n\t}\n\n\t// Extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\n\t\t// Only deal with non-null/undefined values\n\t\tif ( ( options = arguments[ i ] ) != null ) {\n\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent Object.prototype pollution\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( name === \"__proto__\" || target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject( copy ) ||\n\t\t\t\t\t( copyIsArray = Array.isArray( copy ) ) ) ) {\n\t\t\t\t\tsrc = target[ name ];\n\n\t\t\t\t\t// Ensure proper type for the source value\n\t\t\t\t\tif ( copyIsArray && !Array.isArray( src ) ) {\n\t\t\t\t\t\tclone = [];\n\t\t\t\t\t} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {\n\t\t\t\t\t\tclone = {};\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src;\n\t\t\t\t\t}\n\t\t\t\t\tcopyIsArray = false;\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend( {\n\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\tisPlainObject: function( obj ) {\n\t\tvar proto, Ctor;\n\n\t\t// Detect obvious negatives\n\t\t// Use toString instead of jQuery.type to catch host objects\n\t\tif ( !obj || toString.call( obj ) !== \"[object Object]\" ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tproto = getProto( obj );\n\n\t\t// Objects with no prototype (e.g., `Object.create( null )`) are plain\n\t\tif ( !proto ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Objects with prototype are plain iff they were constructed by a global Object function\n\t\tCtor = hasOwn.call( proto, \"constructor\" ) && proto.constructor;\n\t\treturn typeof Ctor === \"function\" && fnToString.call( Ctor ) === ObjectFunctionString;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tvar name;\n\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\t// Evaluates a script in a global context\n\tglobalEval: function( code, options ) {\n\t\tDOMEval( code, { nonce: options && options.nonce } );\n\t},\n\n\teach: function( obj, callback ) {\n\t\tvar length, i = 0;\n\n\t\tif ( isArrayLike( obj ) ) {\n\t\t\tlength = obj.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( i in obj ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t// Support: Android <=4.0 only\n\ttrim: function( text ) {\n\t\treturn text == null ?\n\t\t\t\"\" :\n\t\t\t( text + \"\" ).replace( rtrim, \"\" );\n\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArrayLike( Object( arr ) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t// push.apply(_, arraylike) throws on ancient WebKit\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar length, value,\n\t\t\ti = 0,\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArrayLike( elems ) ) {\n\t\t\tlength = elems.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn concat.apply( [], ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n} );\n\nif ( typeof Symbol === \"function\" ) {\n\tjQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];\n}\n\n// Populate the class2type map\njQuery.each( \"Boolean Number String Function Array Date RegExp Object Error Symbol\".split( \" \" ),\nfunction( i, name ) {\n\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n} );\n\nfunction isArrayLike( obj ) {\n\n\t// Support: real iOS 8.2 only (not reproducible in simulator)\n\t// `in` check used to prevent JIT error (gh-2145)\n\t// hasOwn isn't used here due to false negatives\n\t// regarding Nodelist length in IE\n\tvar length = !!obj && \"length\" in obj && obj.length,\n\t\ttype = toType( obj );\n\n\tif ( isFunction( obj ) || isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\nvar Sizzle =\n/*!\n * Sizzle CSS Selector Engine v2.3.4\n * https://sizzlejs.com/\n *\n * Copyright JS Foundation and other contributors\n * Released under the MIT license\n * https://js.foundation/\n *\n * Date: 2019-04-08\n */\n(function( window ) {\n\nvar i,\n\tsupport,\n\tExpr,\n\tgetText,\n\tisXML,\n\ttokenize,\n\tcompile,\n\tselect,\n\toutermostContext,\n\tsortInput,\n\thasDuplicate,\n\n\t// Local document vars\n\tsetDocument,\n\tdocument,\n\tdocElem,\n\tdocumentIsHTML,\n\trbuggyQSA,\n\trbuggyMatches,\n\tmatches,\n\tcontains,\n\n\t// Instance-specific data\n\texpando = \"sizzle\" + 1 * new Date(),\n\tpreferredDoc = window.document,\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\ttokenCache = createCache(),\n\tcompilerCache = createCache(),\n\tnonnativeSelectorCache = createCache(),\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t}\n\t\treturn 0;\n\t},\n\n\t// Instance methods\n\thasOwn = ({}).hasOwnProperty,\n\tarr = [],\n\tpop = arr.pop,\n\tpush_native = arr.push,\n\tpush = arr.push,\n\tslice = arr.slice,\n\t// Use a stripped-down indexOf as it's faster than native\n\t// https://jsperf.com/thor-indexof-vs-for/5\n\tindexOf = function( list, elem ) {\n\t\tvar i = 0,\n\t\t\tlen = list.length;\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tif ( list[i] === elem ) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t},\n\n\tbooleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",\n\n\t// Regular expressions\n\n\t// http://www.w3.org/TR/css3-selectors/#whitespace\n\twhitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n\n\t// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\n\tidentifier = \"(?:\\\\\\\\.|[\\\\w-]|[^\\0-\\\\xa0])+\",\n\n\t// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors\n\tattributes = \"\\\\[\" + whitespace + \"*(\" + identifier + \")(?:\" + whitespace +\n\t\t// Operator (capture 2)\n\t\t\"*([*^$|!~]?=)\" + whitespace +\n\t\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" + whitespace +\n\t\t\"*\\\\]\",\n\n\tpseudos = \":(\" + identifier + \")(?:\\\\((\" +\n\t\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\t\t// 2. simple (capture 6)\n\t\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\t\t// 3. anything else (capture 2)\n\t\t\".*\" +\n\t\t\")\\\\)|)\",\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trwhitespace = new RegExp( whitespace + \"+\", \"g\" ),\n\trtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\", \"g\" ),\n\n\trcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n\trcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace + \"*\" ),\n\trdescend = new RegExp( whitespace + \"|>\" ),\n\n\trpseudo = new RegExp( pseudos ),\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = {\n\t\t\"ID\": new RegExp( \"^#(\" + identifier + \")\" ),\n\t\t\"CLASS\": new RegExp( \"^\\\\.(\" + identifier + \")\" ),\n\t\t\"TAG\": new RegExp( \"^(\" + identifier + \"|[*])\" ),\n\t\t\"ATTR\": new RegExp( \"^\" + attributes ),\n\t\t\"PSEUDO\": new RegExp( \"^\" + pseudos ),\n\t\t\"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" + whitespace +\n\t\t\t\"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" + whitespace +\n\t\t\t\"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n\t\t\"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\t\"needsContext\": new RegExp( \"^\" + whitespace + \"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" +\n\t\t\twhitespace + \"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t},\n\n\trhtml = /HTML$/i,\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\trnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\trsibling = /[+~]/,\n\n\t// CSS escapes\n\t// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\trunescape = new RegExp( \"\\\\\\\\([\\\\da-f]{1,6}\" + whitespace + \"?|(\" + whitespace + \")|.)\", \"ig\" ),\n\tfunescape = function( _, escaped, escapedWhitespace ) {\n\t\tvar high = \"0x\" + escaped - 0x10000;\n\t\t// NaN means non-codepoint\n\t\t// Support: Firefox<24\n\t\t// Workaround erroneous numeric interpretation of +\"0x\"\n\t\treturn high !== high || escapedWhitespace ?\n\t\t\tescaped :\n\t\t\thigh < 0 ?\n\t\t\t\t// BMP codepoint\n\t\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\t\t// Supplemental Plane codepoint (surrogate pair)\n\t\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t},\n\n\t// CSS string/identifier serialization\n\t// https://drafts.csswg.org/cssom/#common-serializing-idioms\n\trcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,\n\tfcssescape = function( ch, asCodePoint ) {\n\t\tif ( asCodePoint ) {\n\n\t\t\t// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER\n\t\t\tif ( ch === \"\\0\" ) {\n\t\t\t\treturn \"\\uFFFD\";\n\t\t\t}\n\n\t\t\t// Control characters and (dependent upon position) numbers get escaped as code points\n\t\t\treturn ch.slice( 0, -1 ) + \"\\\\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + \" \";\n\t\t}\n\n\t\t// Other potentially-special ASCII characters get backslash-escaped\n\t\treturn \"\\\\\" + ch;\n\t},\n\n\t// Used for iframes\n\t// See setDocument()\n\t// Removing the function wrapper causes a \"Permission Denied\"\n\t// error in IE\n\tunloadHandler = function() {\n\t\tsetDocument();\n\t},\n\n\tinDisabledFieldset = addCombinator(\n\t\tfunction( elem ) {\n\t\t\treturn elem.disabled === true && elem.nodeName.toLowerCase() === \"fieldset\";\n\t\t},\n\t\t{ dir: \"parentNode\", next: \"legend\" }\n\t);\n\n// Optimize for push.apply( _, NodeList )\ntry {\n\tpush.apply(\n\t\t(arr = slice.call( preferredDoc.childNodes )),\n\t\tpreferredDoc.childNodes\n\t);\n\t// Support: Android<4.0\n\t// Detect silently failing push.apply\n\tarr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n\tpush = { apply: arr.length ?\n\n\t\t// Leverage slice if possible\n\t\tfunction( target, els ) {\n\t\t\tpush_native.apply( target, slice.call(els) );\n\t\t} :\n\n\t\t// Support: IE<9\n\t\t// Otherwise append directly\n\t\tfunction( target, els ) {\n\t\t\tvar j = target.length,\n\t\t\t\ti = 0;\n\t\t\t// Can't trust NodeList.length\n\t\t\twhile ( (target[j++] = els[i++]) ) {}\n\t\t\ttarget.length = j - 1;\n\t\t}\n\t};\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n\tvar m, i, elem, nid, match, groups, newSelector,\n\t\tnewContext = context && context.ownerDocument,\n\n\t\t// nodeType defaults to 9, since context defaults to document\n\t\tnodeType = context ? context.nodeType : 9;\n\n\tresults = results || [];\n\n\t// Return early from calls with invalid selector or context\n\tif ( typeof selector !== \"string\" || !selector ||\n\t\tnodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {\n\n\t\treturn results;\n\t}\n\n\t// Try to shortcut find operations (as opposed to filters) in HTML documents\n\tif ( !seed ) {\n\n\t\tif ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {\n\t\t\tsetDocument( context );\n\t\t}\n\t\tcontext = context || document;\n\n\t\tif ( documentIsHTML ) {\n\n\t\t\t// If the selector is sufficiently simple, try using a \"get*By*\" DOM method\n\t\t\t// (excepting DocumentFragment context, where the methods don't exist)\n\t\t\tif ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {\n\n\t\t\t\t// ID selector\n\t\t\t\tif ( (m = match[1]) ) {\n\n\t\t\t\t\t// Document context\n\t\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\t\tif ( (elem = context.getElementById( m )) ) {\n\n\t\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\t\tif ( elem.id === m ) {\n\t\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t// Element context\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\tif ( newContext && (elem = newContext.getElementById( m )) &&\n\t\t\t\t\t\t\tcontains( context, elem ) &&\n\t\t\t\t\t\t\telem.id === m ) {\n\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t// Type selector\n\t\t\t\t} else if ( match[2] ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\t\treturn results;\n\n\t\t\t\t// Class selector\n\t\t\t\t} else if ( (m = match[3]) && support.getElementsByClassName &&\n\t\t\t\t\tcontext.getElementsByClassName ) {\n\n\t\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\t\treturn results;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Take advantage of querySelectorAll\n\t\t\tif ( support.qsa &&\n\t\t\t\t!nonnativeSelectorCache[ selector + \" \" ] &&\n\t\t\t\t(!rbuggyQSA || !rbuggyQSA.test( selector )) &&\n\n\t\t\t\t// Support: IE 8 only\n\t\t\t\t// Exclude object elements\n\t\t\t\t(nodeType !== 1 || context.nodeName.toLowerCase() !== \"object\") ) {\n\n\t\t\t\tnewSelector = selector;\n\t\t\t\tnewContext = context;\n\n\t\t\t\t// qSA considers elements outside a scoping root when evaluating child or\n\t\t\t\t// descendant combinators, which is not what we want.\n\t\t\t\t// In such cases, we work around the behavior by prefixing every selector in the\n\t\t\t\t// list with an ID selector referencing the scope context.\n\t\t\t\t// Thanks to Andrew Dupont for this technique.\n\t\t\t\tif ( nodeType === 1 && rdescend.test( selector ) ) {\n\n\t\t\t\t\t// Capture the context ID, setting it first if necessary\n\t\t\t\t\tif ( (nid = context.getAttribute( \"id\" )) ) {\n\t\t\t\t\t\tnid = nid.replace( rcssescape, fcssescape );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontext.setAttribute( \"id\", (nid = expando) );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prefix every selector in the list\n\t\t\t\t\tgroups = tokenize( selector );\n\t\t\t\t\ti = groups.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tgroups[i] = \"#\" + nid + \" \" + toSelector( groups[i] );\n\t\t\t\t\t}\n\t\t\t\t\tnewSelector = groups.join( \",\" );\n\n\t\t\t\t\t// Expand context for sibling selectors\n\t\t\t\t\tnewContext = rsibling.test( selector ) && testContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t);\n\t\t\t\t\treturn results;\n\t\t\t\t} catch ( qsaError ) {\n\t\t\t\t\tnonnativeSelectorCache( selector, true );\n\t\t\t\t} finally {\n\t\t\t\t\tif ( nid === expando ) {\n\t\t\t\t\t\tcontext.removeAttribute( \"id\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {function(string, object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\t\t// Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n\t\tif ( keys.push( key + \" \" ) > Expr.cacheLength ) {\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn (cache[ key + \" \" ] = value);\n\t}\n\treturn cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ expando ] = true;\n\treturn fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created element and returns a boolean result\n */\nfunction assert( fn ) {\n\tvar el = document.createElement(\"fieldset\");\n\n\ttry {\n\t\treturn !!fn( el );\n\t} catch (e) {\n\t\treturn false;\n\t} finally {\n\t\t// Remove from its parent by default\n\t\tif ( el.parentNode ) {\n\t\t\tel.parentNode.removeChild( el );\n\t\t}\n\t\t// release memory in IE\n\t\tel = null;\n\t}\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n\tvar arr = attrs.split(\"|\"),\n\t\ti = arr.length;\n\n\twhile ( i-- ) {\n\t\tExpr.attrHandle[ arr[i] ] = handler;\n\t}\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n\tvar cur = b && a,\n\t\tdiff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n\t\t\ta.sourceIndex - b.sourceIndex;\n\n\t// Use IE sourceIndex if available on both nodes\n\tif ( diff ) {\n\t\treturn diff;\n\t}\n\n\t// Check if b follows a\n\tif ( cur ) {\n\t\twhile ( (cur = cur.nextSibling) ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn name === \"input\" && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn (name === \"input\" || name === \"button\") && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for :enabled/:disabled\n * @param {Boolean} disabled true for :disabled; false for :enabled\n */\nfunction createDisabledPseudo( disabled ) {\n\n\t// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable\n\treturn function( elem ) {\n\n\t\t// Only certain elements can match :enabled or :disabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled\n\t\tif ( \"form\" in elem ) {\n\n\t\t\t// Check for inherited disabledness on relevant non-disabled elements:\n\t\t\t// * listed form-associated elements in a disabled fieldset\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#category-listed\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled\n\t\t\t// * option elements in a disabled optgroup\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled\n\t\t\t// All such elements have a \"form\" property.\n\t\t\tif ( elem.parentNode && elem.disabled === false ) {\n\n\t\t\t\t// Option elements defer to a parent optgroup if present\n\t\t\t\tif ( \"label\" in elem ) {\n\t\t\t\t\tif ( \"label\" in elem.parentNode ) {\n\t\t\t\t\t\treturn elem.parentNode.disabled === disabled;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn elem.disabled === disabled;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Support: IE 6 - 11\n\t\t\t\t// Use the isDisabled shortcut property to check for disabled fieldset ancestors\n\t\t\t\treturn elem.isDisabled === disabled ||\n\n\t\t\t\t\t// Where there is no isDisabled, check manually\n\t\t\t\t\t/* jshint -W018 */\n\t\t\t\t\telem.isDisabled !== !disabled &&\n\t\t\t\t\t\tinDisabledFieldset( elem ) === disabled;\n\t\t\t}\n\n\t\t\treturn elem.disabled === disabled;\n\n\t\t// Try to winnow out elements that can't be disabled before trusting the disabled property.\n\t\t// Some victims get caught in our net (label, legend, menu, track), but it shouldn't\n\t\t// even exist on them, let alone have a boolean value.\n\t\t} else if ( \"label\" in elem ) {\n\t\t\treturn elem.disabled === disabled;\n\t\t}\n\n\t\t// Remaining elements are neither :enabled nor :disabled\n\t\treturn false;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction(function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction(function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ (j = matchIndexes[i]) ] ) {\n\t\t\t\t\tseed[j] = !(matches[j] = seed[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Checks a node for validity as a Sizzle context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== \"undefined\" && context;\n}\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Detects XML nodes\n * @param {Element|Object} elem An element or a document\n * @returns {Boolean} True iff elem is a non-HTML XML node\n */\nisXML = Sizzle.isXML = function( elem ) {\n\tvar namespace = elem.namespaceURI,\n\t\tdocElem = (elem.ownerDocument || elem).documentElement;\n\n\t// Support: IE <=8\n\t// Assume HTML when documentElement doesn't yet exist, such as inside loading iframes\n\t// https://bugs.jquery.com/ticket/4833\n\treturn !rhtml.test( namespace || docElem && docElem.nodeName || \"HTML\" );\n};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n\tvar hasCompare, subWindow,\n\t\tdoc = node ? node.ownerDocument || node : preferredDoc;\n\n\t// Return early if doc is invalid or already selected\n\tif ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {\n\t\treturn document;\n\t}\n\n\t// Update global variables\n\tdocument = doc;\n\tdocElem = document.documentElement;\n\tdocumentIsHTML = !isXML( document );\n\n\t// Support: IE 9-11, Edge\n\t// Accessing iframe documents after unload throws \"permission denied\" errors (jQuery #13936)\n\tif ( preferredDoc !== document &&\n\t\t(subWindow = document.defaultView) && subWindow.top !== subWindow ) {\n\n\t\t// Support: IE 11, Edge\n\t\tif ( subWindow.addEventListener ) {\n\t\t\tsubWindow.addEventListener( \"unload\", unloadHandler, false );\n\n\t\t// Support: IE 9 - 10 only\n\t\t} else if ( subWindow.attachEvent ) {\n\t\t\tsubWindow.attachEvent( \"onunload\", unloadHandler );\n\t\t}\n\t}\n\n\t/* Attributes\n\t---------------------------------------------------------------------- */\n\n\t// Support: IE<8\n\t// Verify that getAttribute really returns attributes and not properties\n\t// (excepting IE8 booleans)\n\tsupport.attributes = assert(function( el ) {\n\t\tel.className = \"i\";\n\t\treturn !el.getAttribute(\"className\");\n\t});\n\n\t/* getElement(s)By*\n\t---------------------------------------------------------------------- */\n\n\t// Check if getElementsByTagName(\"*\") returns only elements\n\tsupport.getElementsByTagName = assert(function( el ) {\n\t\tel.appendChild( document.createComment(\"\") );\n\t\treturn !el.getElementsByTagName(\"*\").length;\n\t});\n\n\t// Support: IE<9\n\tsupport.getElementsByClassName = rnative.test( document.getElementsByClassName );\n\n\t// Support: IE<10\n\t// Check if getElementById returns elements by name\n\t// The broken getElementById methods don't pick up programmatically-set names,\n\t// so use a roundabout getElementsByName test\n\tsupport.getById = assert(function( el ) {\n\t\tdocElem.appendChild( el ).id = expando;\n\t\treturn !document.getElementsByName || !document.getElementsByName( expando ).length;\n\t});\n\n\t// ID filter and find\n\tif ( support.getById ) {\n\t\tExpr.filter[\"ID\"] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute(\"id\") === attrId;\n\t\t\t};\n\t\t};\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar elem = context.getElementById( id );\n\t\t\t\treturn elem ? [ elem ] : [];\n\t\t\t}\n\t\t};\n\t} else {\n\t\tExpr.filter[\"ID\"] =  function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\tvar node = typeof elem.getAttributeNode !== \"undefined\" &&\n\t\t\t\t\telem.getAttributeNode(\"id\");\n\t\t\t\treturn node && node.value === attrId;\n\t\t\t};\n\t\t};\n\n\t\t// Support: IE 6 - 7 only\n\t\t// getElementById is not reliable as a find shortcut\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar node, i, elems,\n\t\t\t\t\telem = context.getElementById( id );\n\n\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t// Verify the id attribute\n\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fall back on getElementsByName\n\t\t\t\t\telems = context.getElementsByName( id );\n\t\t\t\t\ti = 0;\n\t\t\t\t\twhile ( (elem = elems[i++]) ) {\n\t\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn [];\n\t\t\t}\n\t\t};\n\t}\n\n\t// Tag\n\tExpr.find[\"TAG\"] = support.getElementsByTagName ?\n\t\tfunction( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\n\t\t\t// DocumentFragment nodes don't have gEBTN\n\t\t\t} else if ( support.qsa ) {\n\t\t\t\treturn context.querySelectorAll( tag );\n\t\t\t}\n\t\t} :\n\n\t\tfunction( tag, context ) {\n\t\t\tvar elem,\n\t\t\t\ttmp = [],\n\t\t\t\ti = 0,\n\t\t\t\t// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too\n\t\t\t\tresults = context.getElementsByTagName( tag );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( tag === \"*\" ) {\n\t\t\t\twhile ( (elem = results[i++]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn tmp;\n\t\t\t}\n\t\t\treturn results;\n\t\t};\n\n\t// Class\n\tExpr.find[\"CLASS\"] = support.getElementsByClassName && function( className, context ) {\n\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML ) {\n\t\t\treturn context.getElementsByClassName( className );\n\t\t}\n\t};\n\n\t/* QSA/matchesSelector\n\t---------------------------------------------------------------------- */\n\n\t// QSA and matchesSelector support\n\n\t// matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n\trbuggyMatches = [];\n\n\t// qSa(:focus) reports false when true (Chrome 21)\n\t// We allow this because of a bug in IE8/9 that throws an error\n\t// whenever `document.activeElement` is accessed on an iframe\n\t// So, we allow :focus to pass through QSA all the time to avoid the IE error\n\t// See https://bugs.jquery.com/ticket/13378\n\trbuggyQSA = [];\n\n\tif ( (support.qsa = rnative.test( document.querySelectorAll )) ) {\n\t\t// Build QSA regex\n\t\t// Regex strategy adopted from Diego Perini\n\t\tassert(function( el ) {\n\t\t\t// Select is set to empty string on purpose\n\t\t\t// This is to test IE's treatment of not explicitly\n\t\t\t// setting a boolean content attribute,\n\t\t\t// since its presence should be enough\n\t\t\t// https://bugs.jquery.com/ticket/12359\n\t\t\tdocElem.appendChild( el ).innerHTML = \"<a id='\" + expando + \"'></a>\" +\n\t\t\t\t\"<select id='\" + expando + \"-\\r\\\\' msallowcapture=''>\" +\n\t\t\t\t\"<option selected=''></option></select>\";\n\n\t\t\t// Support: IE8, Opera 11-12.16\n\t\t\t// Nothing should be selected when empty strings follow ^= or $= or *=\n\t\t\t// The test attribute must be unknown in Opera but \"safe\" for WinRT\n\t\t\t// https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section\n\t\t\tif ( el.querySelectorAll(\"[msallowcapture^='']\").length ) {\n\t\t\t\trbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Support: IE8\n\t\t\t// Boolean attributes and \"value\" are not treated correctly\n\t\t\tif ( !el.querySelectorAll(\"[selected]\").length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n\t\t\t}\n\n\t\t\t// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+\n\t\t\tif ( !el.querySelectorAll( \"[id~=\" + expando + \"-]\" ).length ) {\n\t\t\t\trbuggyQSA.push(\"~=\");\n\t\t\t}\n\n\t\t\t// Webkit/Opera - :checked should return selected option elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !el.querySelectorAll(\":checked\").length ) {\n\t\t\t\trbuggyQSA.push(\":checked\");\n\t\t\t}\n\n\t\t\t// Support: Safari 8+, iOS 8+\n\t\t\t// https://bugs.webkit.org/show_bug.cgi?id=136851\n\t\t\t// In-page `selector#id sibling-combinator selector` fails\n\t\t\tif ( !el.querySelectorAll( \"a#\" + expando + \"+*\" ).length ) {\n\t\t\t\trbuggyQSA.push(\".#.+[+~]\");\n\t\t\t}\n\t\t});\n\n\t\tassert(function( el ) {\n\t\t\tel.innerHTML = \"<a href='' disabled='disabled'></a>\" +\n\t\t\t\t\"<select disabled='disabled'><option/></select>\";\n\n\t\t\t// Support: Windows 8 Native Apps\n\t\t\t// The type and name attributes are restricted during .innerHTML assignment\n\t\t\tvar input = document.createElement(\"input\");\n\t\t\tinput.setAttribute( \"type\", \"hidden\" );\n\t\t\tel.appendChild( input ).setAttribute( \"name\", \"D\" );\n\n\t\t\t// Support: IE8\n\t\t\t// Enforce case-sensitivity of name attribute\n\t\t\tif ( el.querySelectorAll(\"[name=d]\").length ) {\n\t\t\t\trbuggyQSA.push( \"name\" + whitespace + \"*[*^$|!~]?=\" );\n\t\t\t}\n\n\t\t\t// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( el.querySelectorAll(\":enabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Support: IE9-11+\n\t\t\t// IE's :disabled selector does not pick up the children of disabled fieldsets\n\t\t\tdocElem.appendChild( el ).disabled = true;\n\t\t\tif ( el.querySelectorAll(\":disabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Opera 10-11 does not throw on post-comma invalid pseudos\n\t\t\tel.querySelectorAll(\"*,:x\");\n\t\t\trbuggyQSA.push(\",.*:\");\n\t\t});\n\t}\n\n\tif ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||\n\t\tdocElem.webkitMatchesSelector ||\n\t\tdocElem.mozMatchesSelector ||\n\t\tdocElem.oMatchesSelector ||\n\t\tdocElem.msMatchesSelector) )) ) {\n\n\t\tassert(function( el ) {\n\t\t\t// Check to see if it's possible to do matchesSelector\n\t\t\t// on a disconnected node (IE 9)\n\t\t\tsupport.disconnectedMatch = matches.call( el, \"*\" );\n\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\tmatches.call( el, \"[s!='']:x\" );\n\t\t\trbuggyMatches.push( \"!=\", pseudos );\n\t\t});\n\t}\n\n\trbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join(\"|\") );\n\trbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join(\"|\") );\n\n\t/* Contains\n\t---------------------------------------------------------------------- */\n\thasCompare = rnative.test( docElem.compareDocumentPosition );\n\n\t// Element contains another\n\t// Purposefully self-exclusive\n\t// As in, an element does not contain itself\n\tcontains = hasCompare || rnative.test( docElem.contains ) ?\n\t\tfunction( a, b ) {\n\t\t\tvar adown = a.nodeType === 9 ? a.documentElement : a,\n\t\t\t\tbup = b && b.parentNode;\n\t\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\t\t\t\tadown.contains ?\n\t\t\t\t\tadown.contains( bup ) :\n\t\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t\t));\n\t\t} :\n\t\tfunction( a, b ) {\n\t\t\tif ( b ) {\n\t\t\t\twhile ( (b = b.parentNode) ) {\n\t\t\t\t\tif ( b === a ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t/* Sorting\n\t---------------------------------------------------------------------- */\n\n\t// Document order sorting\n\tsortOrder = hasCompare ?\n\tfunction( a, b ) {\n\n\t\t// Flag for duplicate removal\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Sort on method existence if only one input has compareDocumentPosition\n\t\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\t\tif ( compare ) {\n\t\t\treturn compare;\n\t\t}\n\n\t\t// Calculate position if both inputs belong to the same document\n\t\tcompare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?\n\t\t\ta.compareDocumentPosition( b ) :\n\n\t\t\t// Otherwise we know they are disconnected\n\t\t\t1;\n\n\t\t// Disconnected nodes\n\t\tif ( compare & 1 ||\n\t\t\t(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {\n\n\t\t\t// Choose the first element that is related to our preferred document\n\t\t\tif ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// Maintain original order\n\t\t\treturn sortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\t\t}\n\n\t\treturn compare & 4 ? -1 : 1;\n\t} :\n\tfunction( a, b ) {\n\t\t// Exit early if the nodes are identical\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tap = [ a ],\n\t\t\tbp = [ b ];\n\n\t\t// Parentless nodes are either documents or disconnected\n\t\tif ( !aup || !bup ) {\n\t\t\treturn a === document ? -1 :\n\t\t\t\tb === document ? 1 :\n\t\t\t\taup ? -1 :\n\t\t\t\tbup ? 1 :\n\t\t\t\tsortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\n\t\t// If the nodes are siblings, we can do a quick check\n\t\t} else if ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\t\t}\n\n\t\t// Otherwise we need full lists of their ancestors for comparison\n\t\tcur = a;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tap.unshift( cur );\n\t\t}\n\t\tcur = b;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tbp.unshift( cur );\n\t\t}\n\n\t\t// Walk down the tree looking for a discrepancy\n\t\twhile ( ap[i] === bp[i] ) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i ?\n\t\t\t// Do a sibling check if the nodes have a common ancestor\n\t\t\tsiblingCheck( ap[i], bp[i] ) :\n\n\t\t\t// Otherwise nodes in our document sort first\n\t\t\tap[i] === preferredDoc ? -1 :\n\t\t\tbp[i] === preferredDoc ? 1 :\n\t\t\t0;\n\t};\n\n\treturn document;\n};\n\nSizzle.matches = function( expr, elements ) {\n\treturn Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tif ( support.matchesSelector && documentIsHTML &&\n\t\t!nonnativeSelectorCache[ expr + \" \" ] &&\n\t\t( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n\t\t( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\tvar ret = matches.call( elem, expr );\n\n\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\tif ( ret || support.disconnectedMatch ||\n\t\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t\t// fragment in IE 9\n\t\t\t\t\telem.document && elem.document.nodeType !== 11 ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tnonnativeSelectorCache( expr, true );\n\t\t}\n\t}\n\n\treturn Sizzle( expr, document, null, [ elem ] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n\t// Set document vars if needed\n\tif ( ( context.ownerDocument || context ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\treturn contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tvar fn = Expr.attrHandle[ name.toLowerCase() ],\n\t\t// Don't get fooled by Object.prototype properties (jQuery #13807)\n\t\tval = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n\t\t\tfn( elem, name, !documentIsHTML ) :\n\t\t\tundefined;\n\n\treturn val !== undefined ?\n\t\tval :\n\t\tsupport.attributes || !documentIsHTML ?\n\t\t\telem.getAttribute( name ) :\n\t\t\t(val = elem.getAttributeNode(name)) && val.specified ?\n\t\t\t\tval.value :\n\t\t\t\tnull;\n};\n\nSizzle.escape = function( sel ) {\n\treturn (sel + \"\").replace( rcssescape, fcssescape );\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\t// Unless we *know* we can detect duplicates, assume their presence\n\thasDuplicate = !support.detectDuplicates;\n\tsortInput = !support.sortStable && results.slice( 0 );\n\tresults.sort( sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( (elem = results[i++]) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tresults.splice( duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\t// Clear input after sorting to release objects\n\t// See https://github.com/jquery/sizzle/pull/225\n\tsortInput = null;\n\n\treturn results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n\tvar node,\n\t\tret = \"\",\n\t\ti = 0,\n\t\tnodeType = elem.nodeType;\n\n\tif ( !nodeType ) {\n\t\t// If no nodeType, this is expected to be an array\n\t\twhile ( (node = elem[i++]) ) {\n\t\t\t// Do not traverse comment nodes\n\t\t\tret += getText( node );\n\t\t}\n\t} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n\t\t// Use textContent for elements\n\t\t// innerText usage removed for consistency of new lines (jQuery #11153)\n\t\tif ( typeof elem.textContent === \"string\" ) {\n\t\t\treturn elem.textContent;\n\t\t} else {\n\t\t\t// Traverse its children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tret += getText( elem );\n\t\t\t}\n\t\t}\n\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\treturn elem.nodeValue;\n\t}\n\t// Do not include comment or processing instruction nodes\n\n\treturn ret;\n};\n\nExpr = Sizzle.selectors = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tattrHandle: {},\n\n\tfind: {},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: {\n\t\t\"ATTR\": function( match ) {\n\t\t\tmatch[1] = match[1].replace( runescape, funescape );\n\n\t\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\t\tmatch[3] = ( match[3] || match[4] || match[5] || \"\" ).replace( runescape, funescape );\n\n\t\t\tif ( match[2] === \"~=\" ) {\n\t\t\t\tmatch[3] = \" \" + match[3] + \" \";\n\t\t\t}\n\n\t\t\treturn match.slice( 0, 4 );\n\t\t},\n\n\t\t\"CHILD\": function( match ) {\n\t\t\t/* matches from matchExpr[\"CHILD\"]\n\t\t\t\t1 type (only|nth|...)\n\t\t\t\t2 what (child|of-type)\n\t\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t\t5 sign of xn-component\n\t\t\t\t6 x of xn-component\n\t\t\t\t7 sign of y-component\n\t\t\t\t8 y of y-component\n\t\t\t*/\n\t\t\tmatch[1] = match[1].toLowerCase();\n\n\t\t\tif ( match[1].slice( 0, 3 ) === \"nth\" ) {\n\t\t\t\t// nth-* requires argument\n\t\t\t\tif ( !match[3] ) {\n\t\t\t\t\tSizzle.error( match[0] );\n\t\t\t\t}\n\n\t\t\t\t// numeric x and y parameters for Expr.filter.CHILD\n\t\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\t\tmatch[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === \"even\" || match[3] === \"odd\" ) );\n\t\t\t\tmatch[5] = +( ( match[7] + match[8] ) || match[3] === \"odd\" );\n\n\t\t\t// other types prohibit arguments\n\t\t\t} else if ( match[3] ) {\n\t\t\t\tSizzle.error( match[0] );\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\t\"PSEUDO\": function( match ) {\n\t\t\tvar excess,\n\t\t\t\tunquoted = !match[6] && match[2];\n\n\t\t\tif ( matchExpr[\"CHILD\"].test( match[0] ) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Accept quoted arguments as-is\n\t\t\tif ( match[3] ) {\n\t\t\t\tmatch[2] = match[4] || match[5] || \"\";\n\n\t\t\t// Strip excess characters from unquoted arguments\n\t\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\t\t\t\t// Get excess from tokenize (recursively)\n\t\t\t\t(excess = tokenize( unquoted, true )) &&\n\t\t\t\t// advance to the next closing parenthesis\n\t\t\t\t(excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length) ) {\n\n\t\t\t\t// excess is a negative index\n\t\t\t\tmatch[0] = match[0].slice( 0, excess );\n\t\t\t\tmatch[2] = unquoted.slice( 0, excess );\n\t\t\t}\n\n\t\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\t\treturn match.slice( 0, 3 );\n\t\t}\n\t},\n\n\tfilter: {\n\n\t\t\"TAG\": function( nodeNameSelector ) {\n\t\t\tvar nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\t\t\t\tfunction() { return true; } :\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n\t\t\t\t};\n\t\t},\n\n\t\t\"CLASS\": function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t(pattern = new RegExp( \"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\" )) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test( typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\" );\n\t\t\t\t});\n\t\t},\n\n\t\t\"ATTR\": function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = Sizzle.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\treturn operator === \"=\" ? result === check :\n\t\t\t\t\toperator === \"!=\" ? result !== check :\n\t\t\t\t\toperator === \"^=\" ? check && result.indexOf( check ) === 0 :\n\t\t\t\t\toperator === \"*=\" ? check && result.indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"$=\" ? check && result.slice( -check.length ) === check :\n\t\t\t\t\toperator === \"~=\" ? ( \" \" + result.replace( rwhitespace, \" \" ) + \" \" ).indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n\t\t\t\t\tfalse;\n\t\t\t};\n\t\t},\n\n\t\t\"CHILD\": function( type, what, argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tvar cache, uniqueCache, outerCache, node, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType,\n\t\t\t\t\t\tdiff = false;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( (node = node[ dir ]) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) {\n\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\n\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\tnode = parent;\n\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\tdiff = nodeIndex && cache[ 2 ];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\t\tdiff = nodeIndex;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// xml :nth-child(...)\n\t\t\t\t\t\t\t// or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t\tif ( diff === false ) {\n\t\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t\tif ( ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) &&\n\t\t\t\t\t\t\t\t\t\t++diff ) {\n\n\t\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\t\"PSEUDO\": function( pseudo, argument ) {\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// http://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar args,\n\t\t\t\tfn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\t\tSizzle.error( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as Sizzle does\n\t\t\tif ( fn[ expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\t// But maintain support for old signatures\n\t\t\tif ( fn.length > 1 ) {\n\t\t\t\targs = [ pseudo, pseudo, \"\", argument ];\n\t\t\t\treturn Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n\t\t\t\t\tmarkFunction(function( seed, matches ) {\n\t\t\t\t\t\tvar idx,\n\t\t\t\t\t\t\tmatched = fn( seed, argument ),\n\t\t\t\t\t\t\ti = matched.length;\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tidx = indexOf( seed, matched[i] );\n\t\t\t\t\t\t\tseed[ idx ] = !( matches[ idx ] = matched[i] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}) :\n\t\t\t\t\tfunction( elem ) {\n\t\t\t\t\t\treturn fn( elem, 0, args );\n\t\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\t\t// Potentially complex pseudos\n\t\t\"not\": markFunction(function( selector ) {\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n\t\t\treturn matcher[ expando ] ?\n\t\t\t\tmarkFunction(function( seed, matches, context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = unmatched[i]) ) {\n\t\t\t\t\t\t\tseed[i] = !(matches[i] = elem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}) :\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tinput[0] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\t\t\t\t\t// Don't keep the element (issue #299)\n\t\t\t\t\tinput[0] = null;\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t}),\n\n\t\t\"has\": markFunction(function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn Sizzle( selector, elem ).length > 0;\n\t\t\t};\n\t\t}),\n\n\t\t\"contains\": markFunction(function( text ) {\n\t\t\ttext = text.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t}),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// http://www.w3.org/TR/selectors/#lang-pseudo\n\t\t\"lang\": markFunction( function( lang ) {\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test(lang || \"\") ) {\n\t\t\t\tSizzle.error( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = lang.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( (elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( (elem = elem.parentNode) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t}),\n\n\t\t// Miscellaneous\n\t\t\"target\": function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\t\"root\": function( elem ) {\n\t\t\treturn elem === docElem;\n\t\t},\n\n\t\t\"focus\": function( elem ) {\n\t\t\treturn elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\n\t\t},\n\n\t\t// Boolean properties\n\t\t\"enabled\": createDisabledPseudo( false ),\n\t\t\"disabled\": createDisabledPseudo( true ),\n\n\t\t\"checked\": function( elem ) {\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\tvar nodeName = elem.nodeName.toLowerCase();\n\t\t\treturn (nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected);\n\t\t},\n\n\t\t\"selected\": function( elem ) {\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\t\"empty\": function( elem ) {\n\t\t\t// http://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t//   but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\t\"parent\": function( elem ) {\n\t\t\treturn !Expr.pseudos[\"empty\"]( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\t\"header\": function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\t\"input\": function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\t\"button\": function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && elem.type === \"button\" || name === \"button\";\n\t\t},\n\n\t\t\"text\": function( elem ) {\n\t\t\tvar attr;\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" &&\n\t\t\t\telem.type === \"text\" &&\n\n\t\t\t\t// Support: IE<8\n\t\t\t\t// New HTML5 attribute values (e.g., \"search\") appear with elem.type === \"text\"\n\t\t\t\t( (attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\" );\n\t\t},\n\n\t\t// Position-in-collection\n\t\t\"first\": createPositionalPseudo(function() {\n\t\t\treturn [ 0 ];\n\t\t}),\n\n\t\t\"last\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t}),\n\n\t\t\"eq\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t}),\n\n\t\t\"even\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"odd\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"lt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ?\n\t\t\t\targument + length :\n\t\t\t\targument > length ?\n\t\t\t\t\tlength :\n\t\t\t\t\targument;\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"gt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t})\n\t}\n};\n\nExpr.pseudos[\"nth\"] = Expr.pseudos[\"eq\"];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tExpr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tExpr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\ntokenize = Sizzle.tokenize = function( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = Expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || (match = rcomma.exec( soFar )) ) {\n\t\t\tif ( match ) {\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[0].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( (tokens = []) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( (match = rcombinators.exec( soFar )) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push({\n\t\t\t\tvalue: matched,\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[0].replace( rtrim, \" \" )\n\t\t\t});\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\n\t\t\t\t(match = preFilters[ type ]( match ))) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push({\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t});\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\treturn parseOnly ?\n\t\tsoFar.length :\n\t\tsoFar ?\n\t\t\tSizzle.error( selector ) :\n\t\t\t// Cache the tokens\n\t\t\ttokenCache( selector, groups ).slice( 0 );\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[i].value;\n\t}\n\treturn selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tskip = combinator.next,\n\t\tkey = skip || dir,\n\t\tcheckNonElements = base && key === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, uniqueCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ expando ] || (elem[ expando ] = {});\n\n\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\tuniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});\n\n\t\t\t\t\t\tif ( skip && skip === elem.nodeName.toLowerCase() ) {\n\t\t\t\t\t\t\telem = elem[ dir ] || elem;\n\t\t\t\t\t\t} else if ( (oldCache = uniqueCache[ key ]) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn (newCache[ 2 ] = oldCache[ 2 ]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\tuniqueCache[ key ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[i]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[0];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tSizzle( selector, contexts[i], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (elem = unmatched[i]) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction(function( seed, results, context, xml ) {\n\t\tvar temp, i, elem,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed || multipleContexts( selector || \"*\", context.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems,\n\n\t\t\tmatcherOut = matcher ?\n\t\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n\t\t\t\tpostFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t\t[] :\n\n\t\t\t\t\t// ...otherwise use results directly\n\t\t\t\t\tresults :\n\t\t\t\tmatcherIn;\n\n\t\t// Find primary matches\n\t\tif ( matcher ) {\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( (elem = temp[i]) ) {\n\t\t\t\t\tmatcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = matcherOut[i]) ) {\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( (matcherIn[i] = elem) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, (matcherOut = []), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( (elem = matcherOut[i]) &&\n\t\t\t\t\t\t(temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {\n\n\t\t\t\t\t\tseed[temp] = !(results[temp] = elem);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t});\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = Expr.relative[ tokens[0].type ],\n\t\timplicitRelative = leadingRelative || Expr.relative[\" \"],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\t\t\tvar ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n\t\t\t\t(checkContext = context).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\t\t\t// Avoid hanging onto element (issue #299)\n\t\t\tcheckContext = null;\n\t\t\treturn ret;\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (matcher = Expr.relative[ tokens[i].type ]) ) {\n\t\t\tmatchers = [ addCombinator(elementMatcher( matchers ), matcher) ];\n\t\t} else {\n\t\t\tmatcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ expando ] ) {\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( Expr.relative[ tokens[j].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" })\n\t\t\t\t\t).replace( rtrim, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && Expr.find[\"TAG\"]( \"*\", outermost ),\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),\n\t\t\t\tlen = elems.length;\n\n\t\t\tif ( outermost ) {\n\t\t\t\toutermostContext = context === document || context || outermost;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\t// Support: IE<9, Safari\n\t\t\t// Tolerate NodeList properties (IE: \"length\"; Safari: <number>) matching elements by id\n\t\t\tfor ( ; i !== len && (elem = elems[i]) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\tif ( !context && elem.ownerDocument !== document ) {\n\t\t\t\t\t\tsetDocument( elem );\n\t\t\t\t\t\txml = !documentIsHTML;\n\t\t\t\t\t}\n\t\t\t\t\twhile ( (matcher = elementMatchers[j++]) ) {\n\t\t\t\t\t\tif ( matcher( elem, context || document, xml) ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( (elem = !matcher && elem) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// `i` is now the count of elements visited above, and adding it to `matchedCount`\n\t\t\t// makes the latter nonnegative.\n\t\t\tmatchedCount += i;\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\t// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`\n\t\t\t// equals `i`), unless we didn't visit _any_ elements in the above loop because we have\n\t\t\t// no element matchers and no seed.\n\t\t\t// Incrementing an initially-string \"0\" `i` allows `i` to remain a string only in that\n\t\t\t// case, which will result in a \"00\" `matchedCount` that differs from `i` but is also\n\t\t\t// numerically zero.\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (matcher = setMatchers[j++]) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !(unmatched[i] || setMatched[i]) ) {\n\t\t\t\t\t\t\t\tsetMatched[i] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tSizzle.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[i] );\n\t\t\tif ( cached[ expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n};\n\n/**\n * A low-level selection function that works with Sizzle's compiled\n *  selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n *  selector function built with Sizzle.compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nselect = Sizzle.select = function( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( (selector = compiled.selector || selector) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is only one selector in the list and no seed\n\t// (the latter of which guarantees us context)\n\tif ( match.length === 1 ) {\n\n\t\t// Reduce context if the leading compound selector is an ID\n\t\ttokens = match[0] = match[0].slice( 0 );\n\t\tif ( tokens.length > 2 && (token = tokens[0]).type === \"ID\" &&\n\t\t\t\tcontext.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {\n\n\t\t\tcontext = ( Expr.find[\"ID\"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr[\"needsContext\"].test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[i];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( Expr.relative[ (type = token.type) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( (find = Expr.find[ type ]) ) {\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( (seed = find(\n\t\t\t\t\ttoken.matches[0].replace( runescape, funescape ),\n\t\t\t\t\trsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context\n\t\t\t\t)) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\t!context || rsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n};\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split(\"\").sort( sortOrder ).join(\"\") === expando;\n\n// Support: Chrome 14-35+\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = !!hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert(function( el ) {\n\t// Should return 1, but returns 4 (following)\n\treturn el.compareDocumentPosition( document.createElement(\"fieldset\") ) & 1;\n});\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert(function( el ) {\n\tel.innerHTML = \"<a href='#'></a>\";\n\treturn el.firstChild.getAttribute(\"href\") === \"#\" ;\n}) ) {\n\taddHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n\t\tif ( !isXML ) {\n\t\t\treturn elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert(function( el ) {\n\tel.innerHTML = \"<input/>\";\n\tel.firstChild.setAttribute( \"value\", \"\" );\n\treturn el.firstChild.getAttribute( \"value\" ) === \"\";\n}) ) {\n\taddHandle( \"value\", function( elem, name, isXML ) {\n\t\tif ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n\t\t\treturn elem.defaultValue;\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert(function( el ) {\n\treturn el.getAttribute(\"disabled\") == null;\n}) ) {\n\taddHandle( booleans, function( elem, name, isXML ) {\n\t\tvar val;\n\t\tif ( !isXML ) {\n\t\t\treturn elem[ name ] === true ? name.toLowerCase() :\n\t\t\t\t\t(val = elem.getAttributeNode( name )) && val.specified ?\n\t\t\t\t\tval.value :\n\t\t\t\tnull;\n\t\t}\n\t});\n}\n\nreturn Sizzle;\n\n})( window );\n\n\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\n\n// Deprecated\njQuery.expr[ \":\" ] = jQuery.expr.pseudos;\njQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\njQuery.escapeSelector = Sizzle.escape;\n\n\n\n\nvar dir = function( elem, dir, until ) {\n\tvar matched = [],\n\t\ttruncate = until !== undefined;\n\n\twhile ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {\n\t\tif ( elem.nodeType === 1 ) {\n\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatched.push( elem );\n\t\t}\n\t}\n\treturn matched;\n};\n\n\nvar siblings = function( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\tmatched.push( n );\n\t\t}\n\t}\n\n\treturn matched;\n};\n\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\n\n\nfunction nodeName( elem, name ) {\n\n  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n\n};\nvar rsingleTag = ( /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i );\n\n\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( isFunction( qualifier ) ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t} );\n\t}\n\n\t// Single element\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t} );\n\t}\n\n\t// Arraylike of elements (jQuery, arguments, Array)\n\tif ( typeof qualifier !== \"string\" ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not;\n\t\t} );\n\t}\n\n\t// Filtered directly for both simple and complex selectors\n\treturn jQuery.filter( qualifier, elements, not );\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\tif ( elems.length === 1 && elem.nodeType === 1 ) {\n\t\treturn jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];\n\t}\n\n\treturn jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\treturn elem.nodeType === 1;\n\t} ) );\n};\n\njQuery.fn.extend( {\n\tfind: function( selector ) {\n\t\tvar i, ret,\n\t\t\tlen = this.length,\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter( function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tret = this.pushStack( [] );\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\treturn len > 1 ? jQuery.uniqueSort( ret ) : ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], false ) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], true ) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n} );\n\n\n// Initialize a jQuery object\n\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n\t// Strict HTML recognition (#11290: must start with <)\n\t// Shortcut simple #id case for speed\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/,\n\n\tinit = jQuery.fn.init = function( selector, context, root ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Method init() accepts an alternate rootjQuery\n\t\t// so migrate can support jQuery.sub (gh-2101)\n\t\troot = root || rootjQuery;\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\tif ( selector[ 0 ] === \"<\" &&\n\t\t\t\tselector[ selector.length - 1 ] === \">\" &&\n\t\t\t\tselector.length >= 3 ) {\n\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\tif ( match && ( match[ 1 ] || !context ) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[ 1 ] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[ 0 ] : context;\n\n\t\t\t\t\t// Option to run scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[ 1 ],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( isFunction( this[ match ] ) ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[ 2 ] );\n\n\t\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t\t// Inject the element directly into the jQuery object\n\t\t\t\t\t\tthis[ 0 ] = elem;\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || root ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\t} else if ( selector.nodeType ) {\n\t\t\tthis[ 0 ] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( isFunction( selector ) ) {\n\t\t\treturn root.ready !== undefined ?\n\t\t\t\troot.ready( selector ) :\n\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document );\n\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\n\t// Methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.fn.extend( {\n\thas: function( target ) {\n\t\tvar targets = jQuery( target, this ),\n\t\t\tl = targets.length;\n\n\t\treturn this.filter( function() {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[ i ] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\ttargets = typeof selectors !== \"string\" && jQuery( selectors );\n\n\t\t// Positional selectors never match, since there's no _selection_ context\n\t\tif ( !rneedsContext.test( selectors ) ) {\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tfor ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {\n\n\t\t\t\t\t// Always skip document fragments\n\t\t\t\t\tif ( cur.nodeType < 11 && ( targets ?\n\t\t\t\t\t\ttargets.index( cur ) > -1 :\n\n\t\t\t\t\t\t// Don't pass non-elements to Sizzle\n\t\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\t\tjQuery.find.matchesSelector( cur, selectors ) ) ) {\n\n\t\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within the set\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// Index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn indexOf.call( jQuery( elem ), this[ 0 ] );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn indexOf.call( this,\n\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[ 0 ] : elem\n\t\t);\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.uniqueSort(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter( selector )\n\t\t);\n\t}\n} );\n\nfunction sibling( cur, dir ) {\n\twhile ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}\n\treturn cur;\n}\n\njQuery.each( {\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn siblings( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn siblings( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n\t\tif ( typeof elem.contentDocument !== \"undefined\" ) {\n\t\t\treturn elem.contentDocument;\n\t\t}\n\n\t\t// Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only\n\t\t// Treat the template element as a regular one in browsers that\n\t\t// don't support it.\n\t\tif ( nodeName( elem, \"template\" ) ) {\n\t\t\telem = elem.content || elem;\n\t\t}\n\n\t\treturn jQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar matched = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tmatched = jQuery.filter( selector, matched );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tjQuery.uniqueSort( matched );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tmatched.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched );\n\t};\n} );\nvar rnothtmlwhite = ( /[^\\x20\\t\\r\\n\\f]+/g );\n\n\n\n// Convert String-formatted options into Object-formatted ones\nfunction createOptions( options ) {\n\tvar object = {};\n\tjQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {\n\t\tobject[ flag ] = true;\n\t} );\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\toptions: an optional list of space-separated options that will change how\n *\t\t\tthe callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n\t// Convert options from String-formatted to Object-formatted if needed\n\t// (we check in cache first)\n\toptions = typeof options === \"string\" ?\n\t\tcreateOptions( options ) :\n\t\tjQuery.extend( {}, options );\n\n\tvar // Flag to know if list is currently firing\n\t\tfiring,\n\n\t\t// Last fire value for non-forgettable lists\n\t\tmemory,\n\n\t\t// Flag to know if list was already fired\n\t\tfired,\n\n\t\t// Flag to prevent firing\n\t\tlocked,\n\n\t\t// Actual callback list\n\t\tlist = [],\n\n\t\t// Queue of execution data for repeatable lists\n\t\tqueue = [],\n\n\t\t// Index of currently firing callback (modified by add/remove as needed)\n\t\tfiringIndex = -1,\n\n\t\t// Fire callbacks\n\t\tfire = function() {\n\n\t\t\t// Enforce single-firing\n\t\t\tlocked = locked || options.once;\n\n\t\t\t// Execute callbacks for all pending executions,\n\t\t\t// respecting firingIndex overrides and runtime changes\n\t\t\tfired = firing = true;\n\t\t\tfor ( ; queue.length; firingIndex = -1 ) {\n\t\t\t\tmemory = queue.shift();\n\t\t\t\twhile ( ++firingIndex < list.length ) {\n\n\t\t\t\t\t// Run callback and check for early termination\n\t\t\t\t\tif ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&\n\t\t\t\t\t\toptions.stopOnFalse ) {\n\n\t\t\t\t\t\t// Jump to end and forget the data so .add doesn't re-fire\n\t\t\t\t\t\tfiringIndex = list.length;\n\t\t\t\t\t\tmemory = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Forget the data if we're done with it\n\t\t\tif ( !options.memory ) {\n\t\t\t\tmemory = false;\n\t\t\t}\n\n\t\t\tfiring = false;\n\n\t\t\t// Clean up if we're done firing for good\n\t\t\tif ( locked ) {\n\n\t\t\t\t// Keep an empty list if we have data for future add calls\n\t\t\t\tif ( memory ) {\n\t\t\t\t\tlist = [];\n\n\t\t\t\t// Otherwise, this object is spent\n\t\t\t\t} else {\n\t\t\t\t\tlist = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t// Actual Callbacks object\n\t\tself = {\n\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\n\t\t\t\t\t// If we have memory from a past run, we should fire after adding\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfiringIndex = list.length - 1;\n\t\t\t\t\t\tqueue.push( memory );\n\t\t\t\t\t}\n\n\t\t\t\t\t( function add( args ) {\n\t\t\t\t\t\tjQuery.each( args, function( _, arg ) {\n\t\t\t\t\t\t\tif ( isFunction( arg ) ) {\n\t\t\t\t\t\t\t\tif ( !options.unique || !self.has( arg ) ) {\n\t\t\t\t\t\t\t\t\tlist.push( arg );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if ( arg && arg.length && toType( arg ) !== \"string\" ) {\n\n\t\t\t\t\t\t\t\t// Inspect recursively\n\t\t\t\t\t\t\t\tadd( arg );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} );\n\t\t\t\t\t} )( arguments );\n\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tjQuery.each( arguments, function( _, arg ) {\n\t\t\t\t\tvar index;\n\t\t\t\t\twhile ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n\t\t\t\t\t\tlist.splice( index, 1 );\n\n\t\t\t\t\t\t// Handle firing indexes\n\t\t\t\t\t\tif ( index <= firingIndex ) {\n\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Check if a given callback is in the list.\n\t\t\t// If no argument is given, return whether or not list has callbacks attached.\n\t\t\thas: function( fn ) {\n\t\t\t\treturn fn ?\n\t\t\t\t\tjQuery.inArray( fn, list ) > -1 :\n\t\t\t\t\tlist.length > 0;\n\t\t\t},\n\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tlist = [];\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Disable .fire and .add\n\t\t\t// Abort any current/pending executions\n\t\t\t// Clear all callbacks and values\n\t\t\tdisable: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tlist = memory = \"\";\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\n\t\t\t// Disable .fire\n\t\t\t// Also disable .add unless we have memory (since it would have no effect)\n\t\t\t// Abort any pending executions\n\t\t\tlock: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tif ( !memory && !firing ) {\n\t\t\t\t\tlist = memory = \"\";\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tlocked: function() {\n\t\t\t\treturn !!locked;\n\t\t\t},\n\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( !locked ) {\n\t\t\t\t\targs = args || [];\n\t\t\t\t\targs = [ context, args.slice ? args.slice() : args ];\n\t\t\t\t\tqueue.push( args );\n\t\t\t\t\tif ( !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!fired;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\nfunction Identity( v ) {\n\treturn v;\n}\nfunction Thrower( ex ) {\n\tthrow ex;\n}\n\nfunction adoptValue( value, resolve, reject, noValue ) {\n\tvar method;\n\n\ttry {\n\n\t\t// Check for promise aspect first to privilege synchronous behavior\n\t\tif ( value && isFunction( ( method = value.promise ) ) ) {\n\t\t\tmethod.call( value ).done( resolve ).fail( reject );\n\n\t\t// Other thenables\n\t\t} else if ( value && isFunction( ( method = value.then ) ) ) {\n\t\t\tmethod.call( value, resolve, reject );\n\n\t\t// Other non-thenables\n\t\t} else {\n\n\t\t\t// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:\n\t\t\t// * false: [ value ].slice( 0 ) => resolve( value )\n\t\t\t// * true: [ value ].slice( 1 ) => resolve()\n\t\t\tresolve.apply( undefined, [ value ].slice( noValue ) );\n\t\t}\n\n\t// For Promises/A+, convert exceptions into rejections\n\t// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in\n\t// Deferred#then to conditionally suppress rejection.\n\t} catch ( value ) {\n\n\t\t// Support: Android 4.0 only\n\t\t// Strict mode functions invoked without .call/.apply get global-object context\n\t\treject.apply( undefined, [ value ] );\n\t}\n}\n\njQuery.extend( {\n\n\tDeferred: function( func ) {\n\t\tvar tuples = [\n\n\t\t\t\t// action, add listener, callbacks,\n\t\t\t\t// ... .then handlers, argument index, [final state]\n\t\t\t\t[ \"notify\", \"progress\", jQuery.Callbacks( \"memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"memory\" ), 2 ],\n\t\t\t\t[ \"resolve\", \"done\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 0, \"resolved\" ],\n\t\t\t\t[ \"reject\", \"fail\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 1, \"rejected\" ]\n\t\t\t],\n\t\t\tstate = \"pending\",\n\t\t\tpromise = {\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done( arguments ).fail( arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\t\"catch\": function( fn ) {\n\t\t\t\t\treturn promise.then( null, fn );\n\t\t\t\t},\n\n\t\t\t\t// Keep pipe for back-compat\n\t\t\t\tpipe: function( /* fnDone, fnFail, fnProgress */ ) {\n\t\t\t\t\tvar fns = arguments;\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( tuples, function( i, tuple ) {\n\n\t\t\t\t\t\t\t// Map tuples (progress, done, fail) to arguments (done, fail, progress)\n\t\t\t\t\t\t\tvar fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];\n\n\t\t\t\t\t\t\t// deferred.progress(function() { bind to newDefer or newDefer.notify })\n\t\t\t\t\t\t\t// deferred.done(function() { bind to newDefer or newDefer.resolve })\n\t\t\t\t\t\t\t// deferred.fail(function() { bind to newDefer or newDefer.reject })\n\t\t\t\t\t\t\tdeferred[ tuple[ 1 ] ]( function() {\n\t\t\t\t\t\t\t\tvar returned = fn && fn.apply( this, arguments );\n\t\t\t\t\t\t\t\tif ( returned && isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\treturned.promise()\n\t\t\t\t\t\t\t\t\t\t.progress( newDefer.notify )\n\t\t\t\t\t\t\t\t\t\t.done( newDefer.resolve )\n\t\t\t\t\t\t\t\t\t\t.fail( newDefer.reject );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tnewDefer[ tuple[ 0 ] + \"With\" ](\n\t\t\t\t\t\t\t\t\t\tthis,\n\t\t\t\t\t\t\t\t\t\tfn ? [ returned ] : arguments\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t} );\n\t\t\t\t\t\tfns = null;\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\t\t\t\tthen: function( onFulfilled, onRejected, onProgress ) {\n\t\t\t\t\tvar maxDepth = 0;\n\t\t\t\t\tfunction resolve( depth, deferred, handler, special ) {\n\t\t\t\t\t\treturn function() {\n\t\t\t\t\t\t\tvar that = this,\n\t\t\t\t\t\t\t\targs = arguments,\n\t\t\t\t\t\t\t\tmightThrow = function() {\n\t\t\t\t\t\t\t\t\tvar returned, then;\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.3\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-59\n\t\t\t\t\t\t\t\t\t// Ignore double-resolution attempts\n\t\t\t\t\t\t\t\t\tif ( depth < maxDepth ) {\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturned = handler.apply( that, args );\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.1\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-48\n\t\t\t\t\t\t\t\t\tif ( returned === deferred.promise() ) {\n\t\t\t\t\t\t\t\t\t\tthrow new TypeError( \"Thenable self-resolution\" );\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ sections 2.3.3.1, 3.5\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-54\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-75\n\t\t\t\t\t\t\t\t\t// Retrieve `then` only once\n\t\t\t\t\t\t\t\t\tthen = returned &&\n\n\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.4\n\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-64\n\t\t\t\t\t\t\t\t\t\t// Only check objects and functions for thenability\n\t\t\t\t\t\t\t\t\t\t( typeof returned === \"object\" ||\n\t\t\t\t\t\t\t\t\t\t\ttypeof returned === \"function\" ) &&\n\t\t\t\t\t\t\t\t\t\treturned.then;\n\n\t\t\t\t\t\t\t\t\t// Handle a returned thenable\n\t\t\t\t\t\t\t\t\tif ( isFunction( then ) ) {\n\n\t\t\t\t\t\t\t\t\t\t// Special processors (notify) just wait for resolution\n\t\t\t\t\t\t\t\t\t\tif ( special ) {\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special )\n\t\t\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\t\t// Normal processors (resolve) also hook into progress\n\t\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t\t// ...and disregard older resolution values\n\t\t\t\t\t\t\t\t\t\t\tmaxDepth++;\n\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdeferred.notifyWith )\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Handle all other returned values\n\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\tif ( handler !== Identity ) {\n\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\targs = [ returned ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Process the value(s)\n\t\t\t\t\t\t\t\t\t\t// Default process is resolve\n\t\t\t\t\t\t\t\t\t\t( special || deferred.resolveWith )( that, args );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\n\t\t\t\t\t\t\t\t// Only normal processors (resolve) catch and reject exceptions\n\t\t\t\t\t\t\t\tprocess = special ?\n\t\t\t\t\t\t\t\t\tmightThrow :\n\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\tmightThrow();\n\t\t\t\t\t\t\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t\t\t\t\t\t\tif ( jQuery.Deferred.exceptionHook ) {\n\t\t\t\t\t\t\t\t\t\t\t\tjQuery.Deferred.exceptionHook( e,\n\t\t\t\t\t\t\t\t\t\t\t\t\tprocess.stackTrace );\n\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.4.1\n\t\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-61\n\t\t\t\t\t\t\t\t\t\t\t// Ignore post-resolution exceptions\n\t\t\t\t\t\t\t\t\t\t\tif ( depth + 1 >= maxDepth ) {\n\n\t\t\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\t\t\tif ( handler !== Thrower ) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\t\t\targs = [ e ];\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t\tdeferred.rejectWith( that, args );\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.1\n\t\t\t\t\t\t\t// https://promisesaplus.com/#point-57\n\t\t\t\t\t\t\t// Re-resolve promises immediately to dodge false rejection from\n\t\t\t\t\t\t\t// subsequent errors\n\t\t\t\t\t\t\tif ( depth ) {\n\t\t\t\t\t\t\t\tprocess();\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t// Call an optional hook to record the stack, in case of exception\n\t\t\t\t\t\t\t\t// since it's otherwise lost when execution goes async\n\t\t\t\t\t\t\t\tif ( jQuery.Deferred.getStackHook ) {\n\t\t\t\t\t\t\t\t\tprocess.stackTrace = jQuery.Deferred.getStackHook();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\twindow.setTimeout( process );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\n\t\t\t\t\t\t// progress_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 0 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onProgress ) ?\n\t\t\t\t\t\t\t\t\tonProgress :\n\t\t\t\t\t\t\t\t\tIdentity,\n\t\t\t\t\t\t\t\tnewDefer.notifyWith\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// fulfilled_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 1 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onFulfilled ) ?\n\t\t\t\t\t\t\t\t\tonFulfilled :\n\t\t\t\t\t\t\t\t\tIdentity\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// rejected_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 2 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onRejected ) ?\n\t\t\t\t\t\t\t\t\tonRejected :\n\t\t\t\t\t\t\t\t\tThrower\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\treturn obj != null ? jQuery.extend( obj, promise ) : promise;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = {};\n\n\t\t// Add list-specific methods\n\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\tvar list = tuple[ 2 ],\n\t\t\t\tstateString = tuple[ 5 ];\n\n\t\t\t// promise.progress = list.add\n\t\t\t// promise.done = list.add\n\t\t\t// promise.fail = list.add\n\t\t\tpromise[ tuple[ 1 ] ] = list.add;\n\n\t\t\t// Handle state\n\t\t\tif ( stateString ) {\n\t\t\t\tlist.add(\n\t\t\t\t\tfunction() {\n\n\t\t\t\t\t\t// state = \"resolved\" (i.e., fulfilled)\n\t\t\t\t\t\t// state = \"rejected\"\n\t\t\t\t\t\tstate = stateString;\n\t\t\t\t\t},\n\n\t\t\t\t\t// rejected_callbacks.disable\n\t\t\t\t\t// fulfilled_callbacks.disable\n\t\t\t\t\ttuples[ 3 - i ][ 2 ].disable,\n\n\t\t\t\t\t// rejected_handlers.disable\n\t\t\t\t\t// fulfilled_handlers.disable\n\t\t\t\t\ttuples[ 3 - i ][ 3 ].disable,\n\n\t\t\t\t\t// progress_callbacks.lock\n\t\t\t\t\ttuples[ 0 ][ 2 ].lock,\n\n\t\t\t\t\t// progress_handlers.lock\n\t\t\t\t\ttuples[ 0 ][ 3 ].lock\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// progress_handlers.fire\n\t\t\t// fulfilled_handlers.fire\n\t\t\t// rejected_handlers.fire\n\t\t\tlist.add( tuple[ 3 ].fire );\n\n\t\t\t// deferred.notify = function() { deferred.notifyWith(...) }\n\t\t\t// deferred.resolve = function() { deferred.resolveWith(...) }\n\t\t\t// deferred.reject = function() { deferred.rejectWith(...) }\n\t\t\tdeferred[ tuple[ 0 ] ] = function() {\n\t\t\t\tdeferred[ tuple[ 0 ] + \"With\" ]( this === deferred ? undefined : this, arguments );\n\t\t\t\treturn this;\n\t\t\t};\n\n\t\t\t// deferred.notifyWith = list.fireWith\n\t\t\t// deferred.resolveWith = list.fireWith\n\t\t\t// deferred.rejectWith = list.fireWith\n\t\t\tdeferred[ tuple[ 0 ] + \"With\" ] = list.fireWith;\n\t\t} );\n\n\t\t// Make the deferred a promise\n\t\tpromise.promise( deferred );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( singleValue ) {\n\t\tvar\n\n\t\t\t// count of uncompleted subordinates\n\t\t\tremaining = arguments.length,\n\n\t\t\t// count of unprocessed arguments\n\t\t\ti = remaining,\n\n\t\t\t// subordinate fulfillment data\n\t\t\tresolveContexts = Array( i ),\n\t\t\tresolveValues = slice.call( arguments ),\n\n\t\t\t// the master Deferred\n\t\t\tmaster = jQuery.Deferred(),\n\n\t\t\t// subordinate callback factory\n\t\t\tupdateFunc = function( i ) {\n\t\t\t\treturn function( value ) {\n\t\t\t\t\tresolveContexts[ i ] = this;\n\t\t\t\t\tresolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;\n\t\t\t\t\tif ( !( --remaining ) ) {\n\t\t\t\t\t\tmaster.resolveWith( resolveContexts, resolveValues );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t};\n\n\t\t// Single- and empty arguments are adopted like Promise.resolve\n\t\tif ( remaining <= 1 ) {\n\t\t\tadoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,\n\t\t\t\t!remaining );\n\n\t\t\t// Use .then() to unwrap secondary thenables (cf. gh-3000)\n\t\t\tif ( master.state() === \"pending\" ||\n\t\t\t\tisFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {\n\n\t\t\t\treturn master.then();\n\t\t\t}\n\t\t}\n\n\t\t// Multiple arguments are aggregated like Promise.all array elements\n\t\twhile ( i-- ) {\n\t\t\tadoptValue( resolveValues[ i ], updateFunc( i ), master.reject );\n\t\t}\n\n\t\treturn master.promise();\n\t}\n} );\n\n\n// These usually indicate a programmer mistake during development,\n// warn about them ASAP rather than swallowing them by default.\nvar rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;\n\njQuery.Deferred.exceptionHook = function( error, stack ) {\n\n\t// Support: IE 8 - 9 only\n\t// Console exists when dev tools are open, which can happen at any time\n\tif ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {\n\t\twindow.console.warn( \"jQuery.Deferred exception: \" + error.message, error.stack, stack );\n\t}\n};\n\n\n\n\njQuery.readyException = function( error ) {\n\twindow.setTimeout( function() {\n\t\tthrow error;\n\t} );\n};\n\n\n\n\n// The deferred used on DOM ready\nvar readyList = jQuery.Deferred();\n\njQuery.fn.ready = function( fn ) {\n\n\treadyList\n\t\t.then( fn )\n\n\t\t// Wrap jQuery.readyException in a function so that the lookup\n\t\t// happens at the time of error handling instead of callback\n\t\t// registration.\n\t\t.catch( function( error ) {\n\t\t\tjQuery.readyException( error );\n\t\t} );\n\n\treturn this;\n};\n\njQuery.extend( {\n\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See #6781\n\treadyWait: 1,\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are functions bound, to execute\n\t\treadyList.resolveWith( document, [ jQuery ] );\n\t}\n} );\n\njQuery.ready.then = readyList.then;\n\n// The ready event handler and self cleanup method\nfunction completed() {\n\tdocument.removeEventListener( \"DOMContentLoaded\", completed );\n\twindow.removeEventListener( \"load\", completed );\n\tjQuery.ready();\n}\n\n// Catch cases where $(document).ready() is called\n// after the browser event has already occurred.\n// Support: IE <=9 - 10 only\n// Older IE sometimes signals \"interactive\" too soon\nif ( document.readyState === \"complete\" ||\n\t( document.readyState !== \"loading\" && !document.documentElement.doScroll ) ) {\n\n\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\twindow.setTimeout( jQuery.ready );\n\n} else {\n\n\t// Use the handy event callback\n\tdocument.addEventListener( \"DOMContentLoaded\", completed );\n\n\t// A fallback to window.onload, that will always work\n\twindow.addEventListener( \"load\", completed );\n}\n\n\n\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nvar access = function( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( toType( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\taccess( elems, fn, i, key[ i ], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( !isFunction( value ) ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn(\n\t\t\t\t\telems[ i ], key, raw ?\n\t\t\t\t\tvalue :\n\t\t\t\t\tvalue.call( elems[ i ], i, fn( elems[ i ], key ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( chainable ) {\n\t\treturn elems;\n\t}\n\n\t// Gets\n\tif ( bulk ) {\n\t\treturn fn.call( elems );\n\t}\n\n\treturn len ? fn( elems[ 0 ], key ) : emptyGet;\n};\n\n\n// Matches dashed string for camelizing\nvar rmsPrefix = /^-ms-/,\n\trdashAlpha = /-([a-z])/g;\n\n// Used by camelCase as callback to replace()\nfunction fcamelCase( all, letter ) {\n\treturn letter.toUpperCase();\n}\n\n// Convert dashed to camelCase; used by the css and data modules\n// Support: IE <=9 - 11, Edge 12 - 15\n// Microsoft forgot to hump their vendor prefix (#9572)\nfunction camelCase( string ) {\n\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n}\nvar acceptData = function( owner ) {\n\n\t// Accepts only:\n\t//  - Node\n\t//    - Node.ELEMENT_NODE\n\t//    - Node.DOCUMENT_NODE\n\t//  - Object\n\t//    - Any\n\treturn owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );\n};\n\n\n\n\nfunction Data() {\n\tthis.expando = jQuery.expando + Data.uid++;\n}\n\nData.uid = 1;\n\nData.prototype = {\n\n\tcache: function( owner ) {\n\n\t\t// Check if the owner object already has a cache\n\t\tvar value = owner[ this.expando ];\n\n\t\t// If not, create one\n\t\tif ( !value ) {\n\t\t\tvalue = {};\n\n\t\t\t// We can accept data for non-element nodes in modern browsers,\n\t\t\t// but we should not, see #8335.\n\t\t\t// Always return an empty object.\n\t\t\tif ( acceptData( owner ) ) {\n\n\t\t\t\t// If it is a node unlikely to be stringify-ed or looped over\n\t\t\t\t// use plain assignment\n\t\t\t\tif ( owner.nodeType ) {\n\t\t\t\t\towner[ this.expando ] = value;\n\n\t\t\t\t// Otherwise secure it in a non-enumerable property\n\t\t\t\t// configurable must be true to allow the property to be\n\t\t\t\t// deleted when data is removed\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty( owner, this.expando, {\n\t\t\t\t\t\tvalue: value,\n\t\t\t\t\t\tconfigurable: true\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t},\n\tset: function( owner, data, value ) {\n\t\tvar prop,\n\t\t\tcache = this.cache( owner );\n\n\t\t// Handle: [ owner, key, value ] args\n\t\t// Always use camelCase key (gh-2257)\n\t\tif ( typeof data === \"string\" ) {\n\t\t\tcache[ camelCase( data ) ] = value;\n\n\t\t// Handle: [ owner, { properties } ] args\n\t\t} else {\n\n\t\t\t// Copy the properties one-by-one to the cache object\n\t\t\tfor ( prop in data ) {\n\t\t\t\tcache[ camelCase( prop ) ] = data[ prop ];\n\t\t\t}\n\t\t}\n\t\treturn cache;\n\t},\n\tget: function( owner, key ) {\n\t\treturn key === undefined ?\n\t\t\tthis.cache( owner ) :\n\n\t\t\t// Always use camelCase key (gh-2257)\n\t\t\towner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];\n\t},\n\taccess: function( owner, key, value ) {\n\n\t\t// In cases where either:\n\t\t//\n\t\t//   1. No key was specified\n\t\t//   2. A string key was specified, but no value provided\n\t\t//\n\t\t// Take the \"read\" path and allow the get method to determine\n\t\t// which value to return, respectively either:\n\t\t//\n\t\t//   1. The entire cache object\n\t\t//   2. The data stored at the key\n\t\t//\n\t\tif ( key === undefined ||\n\t\t\t\t( ( key && typeof key === \"string\" ) && value === undefined ) ) {\n\n\t\t\treturn this.get( owner, key );\n\t\t}\n\n\t\t// When the key is not a string, or both a key and value\n\t\t// are specified, set or extend (existing objects) with either:\n\t\t//\n\t\t//   1. An object of properties\n\t\t//   2. A key and value\n\t\t//\n\t\tthis.set( owner, key, value );\n\n\t\t// Since the \"set\" path can have two possible entry points\n\t\t// return the expected data based on which path was taken[*]\n\t\treturn value !== undefined ? value : key;\n\t},\n\tremove: function( owner, key ) {\n\t\tvar i,\n\t\t\tcache = owner[ this.expando ];\n\n\t\tif ( cache === undefined ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( key !== undefined ) {\n\n\t\t\t// Support array or space separated string of keys\n\t\t\tif ( Array.isArray( key ) ) {\n\n\t\t\t\t// If key is an array of keys...\n\t\t\t\t// We always set camelCase keys, so remove that.\n\t\t\t\tkey = key.map( camelCase );\n\t\t\t} else {\n\t\t\t\tkey = camelCase( key );\n\n\t\t\t\t// If a key with the spaces exists, use it.\n\t\t\t\t// Otherwise, create an array by matching non-whitespace\n\t\t\t\tkey = key in cache ?\n\t\t\t\t\t[ key ] :\n\t\t\t\t\t( key.match( rnothtmlwhite ) || [] );\n\t\t\t}\n\n\t\t\ti = key.length;\n\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete cache[ key[ i ] ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if there's no more data\n\t\tif ( key === undefined || jQuery.isEmptyObject( cache ) ) {\n\n\t\t\t// Support: Chrome <=35 - 45\n\t\t\t// Webkit & Blink performance suffers when deleting properties\n\t\t\t// from DOM nodes, so set to undefined instead\n\t\t\t// https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)\n\t\t\tif ( owner.nodeType ) {\n\t\t\t\towner[ this.expando ] = undefined;\n\t\t\t} else {\n\t\t\t\tdelete owner[ this.expando ];\n\t\t\t}\n\t\t}\n\t},\n\thasData: function( owner ) {\n\t\tvar cache = owner[ this.expando ];\n\t\treturn cache !== undefined && !jQuery.isEmptyObject( cache );\n\t}\n};\nvar dataPriv = new Data();\n\nvar dataUser = new Data();\n\n\n\n//\tImplementation Summary\n//\n//\t1. Enforce API surface and semantic compatibility with 1.9.x branch\n//\t2. Improve the module's maintainability by reducing the storage\n//\t\tpaths to a single mechanism.\n//\t3. Use the same single mechanism to support \"private\" and \"user\" data.\n//\t4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n//\t5. Avoid exposing implementation details on user objects (eg. expando properties)\n//\t6. Provide a clear path for implementation upgrade to WeakMap in 2014\n\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /[A-Z]/g;\n\nfunction getData( data ) {\n\tif ( data === \"true\" ) {\n\t\treturn true;\n\t}\n\n\tif ( data === \"false\" ) {\n\t\treturn false;\n\t}\n\n\tif ( data === \"null\" ) {\n\t\treturn null;\n\t}\n\n\t// Only convert to a number if it doesn't change the string\n\tif ( data === +data + \"\" ) {\n\t\treturn +data;\n\t}\n\n\tif ( rbrace.test( data ) ) {\n\t\treturn JSON.parse( data );\n\t}\n\n\treturn data;\n}\n\nfunction dataAttr( elem, key, data ) {\n\tvar name;\n\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\t\tname = \"data-\" + key.replace( rmultiDash, \"-$&\" ).toLowerCase();\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = getData( data );\n\t\t\t} catch ( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tdataUser.set( elem, key, data );\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\treturn data;\n}\n\njQuery.extend( {\n\thasData: function( elem ) {\n\t\treturn dataUser.hasData( elem ) || dataPriv.hasData( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn dataUser.access( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\tdataUser.remove( elem, name );\n\t},\n\n\t// TODO: Now that all calls to _data and _removeData have been replaced\n\t// with direct calls to dataPriv methods, these can be deprecated.\n\t_data: function( elem, name, data ) {\n\t\treturn dataPriv.access( elem, name, data );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\tdataPriv.remove( elem, name );\n\t}\n} );\n\njQuery.fn.extend( {\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[ 0 ],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = dataUser.get( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !dataPriv.get( elem, \"hasDataAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE 11 only\n\t\t\t\t\t\t// The attrs elements can be null (#14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = camelCase( name.slice( 5 ) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdataPriv.set( elem, \"hasDataAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tdataUser.set( this, key );\n\t\t\t} );\n\t\t}\n\n\t\treturn access( this, function( value ) {\n\t\t\tvar data;\n\n\t\t\t// The calling jQuery object (element matches) is not empty\n\t\t\t// (and therefore has an element appears at this[ 0 ]) and the\n\t\t\t// `value` parameter was not undefined. An empty jQuery object\n\t\t\t// will result in `undefined` for elem = this[ 0 ] which will\n\t\t\t// throw an exception if an attempt to read a data cache is made.\n\t\t\tif ( elem && value === undefined ) {\n\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// The key will always be camelCased in Data\n\t\t\t\tdata = dataUser.get( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to \"discover\" the data in\n\t\t\t\t// HTML5 custom data-* attrs\n\t\t\t\tdata = dataAttr( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// We tried really hard, but the data doesn't exist.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the data...\n\t\t\tthis.each( function() {\n\n\t\t\t\t// We always store the camelCased key\n\t\t\t\tdataUser.set( this, key, value );\n\t\t\t} );\n\t\t}, null, value, arguments.length > 1, null, true );\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each( function() {\n\t\t\tdataUser.remove( this, key );\n\t\t} );\n\t}\n} );\n\n\njQuery.extend( {\n\tqueue: function( elem, type, data ) {\n\t\tvar queue;\n\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tqueue = dataPriv.get( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !queue || Array.isArray( data ) ) {\n\t\t\t\t\tqueue = dataPriv.access( elem, type, jQuery.makeArray( data ) );\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn queue || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tstartLength = queue.length,\n\t\t\tfn = queue.shift(),\n\t\t\thooks = jQuery._queueHooks( elem, type ),\n\t\t\tnext = function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t\tstartLength--;\n\t\t}\n\n\t\tif ( fn ) {\n\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\t// Clear up the last queue stop function\n\t\t\tdelete hooks.stop;\n\t\t\tfn.call( elem, next, hooks );\n\t\t}\n\n\t\tif ( !startLength && hooks ) {\n\t\t\thooks.empty.fire();\n\t\t}\n\t},\n\n\t// Not public - generate a queueHooks object, or return the current one\n\t_queueHooks: function( elem, type ) {\n\t\tvar key = type + \"queueHooks\";\n\t\treturn dataPriv.get( elem, key ) || dataPriv.access( elem, key, {\n\t\t\tempty: jQuery.Callbacks( \"once memory\" ).add( function() {\n\t\t\t\tdataPriv.remove( elem, [ type + \"queue\", key ] );\n\t\t\t} )\n\t\t} );\n\t}\n} );\n\njQuery.fn.extend( {\n\tqueue: function( type, data ) {\n\t\tvar setter = 2;\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t\tsetter--;\n\t\t}\n\n\t\tif ( arguments.length < setter ) {\n\t\t\treturn jQuery.queue( this[ 0 ], type );\n\t\t}\n\n\t\treturn data === undefined ?\n\t\t\tthis :\n\t\t\tthis.each( function() {\n\t\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\t\t// Ensure a hooks for this queue\n\t\t\t\tjQuery._queueHooks( this, type );\n\n\t\t\t\tif ( type === \"fx\" && queue[ 0 ] !== \"inprogress\" ) {\n\t\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t\t}\n\t\t\t} );\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t} );\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, obj ) {\n\t\tvar tmp,\n\t\t\tcount = 1,\n\t\t\tdefer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = this.length,\n\t\t\tresolve = function() {\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t\t}\n\t\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobj = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\n\t\twhile ( i-- ) {\n\t\t\ttmp = dataPriv.get( elements[ i ], type + \"queueHooks\" );\n\t\t\tif ( tmp && tmp.empty ) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.empty.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise( obj );\n\t}\n} );\nvar pnum = ( /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/ ).source;\n\nvar rcssNum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" );\n\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar documentElement = document.documentElement;\n\n\n\n\tvar isAttached = function( elem ) {\n\t\t\treturn jQuery.contains( elem.ownerDocument, elem );\n\t\t},\n\t\tcomposed = { composed: true };\n\n\t// Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only\n\t// Check attachment across shadow DOM boundaries when possible (gh-3504)\n\t// Support: iOS 10.0-10.2 only\n\t// Early iOS 10 versions support `attachShadow` but not `getRootNode`,\n\t// leading to errors. We need to check for `getRootNode`.\n\tif ( documentElement.getRootNode ) {\n\t\tisAttached = function( elem ) {\n\t\t\treturn jQuery.contains( elem.ownerDocument, elem ) ||\n\t\t\t\telem.getRootNode( composed ) === elem.ownerDocument;\n\t\t};\n\t}\nvar isHiddenWithinTree = function( elem, el ) {\n\n\t\t// isHiddenWithinTree might be called from jQuery#filter function;\n\t\t// in that case, element will be second argument\n\t\telem = el || elem;\n\n\t\t// Inline style trumps all\n\t\treturn elem.style.display === \"none\" ||\n\t\t\telem.style.display === \"\" &&\n\n\t\t\t// Otherwise, check computed style\n\t\t\t// Support: Firefox <=43 - 45\n\t\t\t// Disconnected elements can have computed display: none, so first confirm that elem is\n\t\t\t// in the document.\n\t\t\tisAttached( elem ) &&\n\n\t\t\tjQuery.css( elem, \"display\" ) === \"none\";\n\t};\n\nvar swap = function( elem, options, callback, args ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.apply( elem, args || [] );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n};\n\n\n\n\nfunction adjustCSS( elem, prop, valueParts, tween ) {\n\tvar adjusted, scale,\n\t\tmaxIterations = 20,\n\t\tcurrentValue = tween ?\n\t\t\tfunction() {\n\t\t\t\treturn tween.cur();\n\t\t\t} :\n\t\t\tfunction() {\n\t\t\t\treturn jQuery.css( elem, prop, \"\" );\n\t\t\t},\n\t\tinitial = currentValue(),\n\t\tunit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n\t\t// Starting value computation is required for potential unit mismatches\n\t\tinitialInUnit = elem.nodeType &&\n\t\t\t( jQuery.cssNumber[ prop ] || unit !== \"px\" && +initial ) &&\n\t\t\trcssNum.exec( jQuery.css( elem, prop ) );\n\n\tif ( initialInUnit && initialInUnit[ 3 ] !== unit ) {\n\n\t\t// Support: Firefox <=54\n\t\t// Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)\n\t\tinitial = initial / 2;\n\n\t\t// Trust units reported by jQuery.css\n\t\tunit = unit || initialInUnit[ 3 ];\n\n\t\t// Iteratively approximate from a nonzero starting point\n\t\tinitialInUnit = +initial || 1;\n\n\t\twhile ( maxIterations-- ) {\n\n\t\t\t// Evaluate and update our best guess (doubling guesses that zero out).\n\t\t\t// Finish if the scale equals or crosses 1 (making the old*new product non-positive).\n\t\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\t\t\tif ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {\n\t\t\t\tmaxIterations = 0;\n\t\t\t}\n\t\t\tinitialInUnit = initialInUnit / scale;\n\n\t\t}\n\n\t\tinitialInUnit = initialInUnit * 2;\n\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\n\t\t// Make sure we update the tween properties later on\n\t\tvalueParts = valueParts || [];\n\t}\n\n\tif ( valueParts ) {\n\t\tinitialInUnit = +initialInUnit || +initial || 0;\n\n\t\t// Apply relative offset (+=/-=) if specified\n\t\tadjusted = valueParts[ 1 ] ?\n\t\t\tinitialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :\n\t\t\t+valueParts[ 2 ];\n\t\tif ( tween ) {\n\t\t\ttween.unit = unit;\n\t\t\ttween.start = initialInUnit;\n\t\t\ttween.end = adjusted;\n\t\t}\n\t}\n\treturn adjusted;\n}\n\n\nvar defaultDisplayMap = {};\n\nfunction getDefaultDisplay( elem ) {\n\tvar temp,\n\t\tdoc = elem.ownerDocument,\n\t\tnodeName = elem.nodeName,\n\t\tdisplay = defaultDisplayMap[ nodeName ];\n\n\tif ( display ) {\n\t\treturn display;\n\t}\n\n\ttemp = doc.body.appendChild( doc.createElement( nodeName ) );\n\tdisplay = jQuery.css( temp, \"display\" );\n\n\ttemp.parentNode.removeChild( temp );\n\n\tif ( display === \"none\" ) {\n\t\tdisplay = \"block\";\n\t}\n\tdefaultDisplayMap[ nodeName ] = display;\n\n\treturn display;\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\t// Determine new display value for elements that need to change\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\n\t\t\t// Since we force visibility upon cascade-hidden elements, an immediate (and slow)\n\t\t\t// check is required in this first loop unless we have a nonempty display value (either\n\t\t\t// inline or about-to-be-restored)\n\t\t\tif ( display === \"none\" ) {\n\t\t\t\tvalues[ index ] = dataPriv.get( elem, \"display\" ) || null;\n\t\t\t\tif ( !values[ index ] ) {\n\t\t\t\t\telem.style.display = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( elem.style.display === \"\" && isHiddenWithinTree( elem ) ) {\n\t\t\t\tvalues[ index ] = getDefaultDisplay( elem );\n\t\t\t}\n\t\t} else {\n\t\t\tif ( display !== \"none\" ) {\n\t\t\t\tvalues[ index ] = \"none\";\n\n\t\t\t\t// Remember what we're overwriting\n\t\t\t\tdataPriv.set( elem, \"display\", display );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of the elements in a second loop to avoid constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\tif ( values[ index ] != null ) {\n\t\t\telements[ index ].style.display = values[ index ];\n\t\t}\n\t}\n\n\treturn elements;\n}\n\njQuery.fn.extend( {\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tif ( isHiddenWithinTree( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t} );\n\t}\n} );\nvar rcheckableType = ( /^(?:checkbox|radio)$/i );\n\nvar rtagName = ( /<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)/i );\n\nvar rscriptType = ( /^$|^module$|\\/(?:java|ecma)script/i );\n\n\n\n// We have to close these tags to support XHTML (#13200)\nvar wrapMap = {\n\n\t// Support: IE <=9 only\n\toption: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\n\t// XHTML parsers do not magically insert elements in the\n\t// same way that tag soup parsers do. So we cannot shorten\n\t// this by omitting <tbody> or other required elements.\n\tthead: [ 1, \"<table>\", \"</table>\" ],\n\tcol: [ 2, \"<table><colgroup>\", \"</colgroup></table>\" ],\n\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n\t_default: [ 0, \"\", \"\" ]\n};\n\n// Support: IE <=9 only\nwrapMap.optgroup = wrapMap.option;\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\n\nfunction getAll( context, tag ) {\n\n\t// Support: IE <=9 - 11 only\n\t// Use typeof to avoid zero-argument method invocation on host objects (#15151)\n\tvar ret;\n\n\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\tret = context.getElementsByTagName( tag || \"*\" );\n\n\t} else if ( typeof context.querySelectorAll !== \"undefined\" ) {\n\t\tret = context.querySelectorAll( tag || \"*\" );\n\n\t} else {\n\t\tret = [];\n\t}\n\n\tif ( tag === undefined || tag && nodeName( context, tag ) ) {\n\t\treturn jQuery.merge( [ context ], ret );\n\t}\n\n\treturn ret;\n}\n\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar i = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\tdataPriv.set(\n\t\t\telems[ i ],\n\t\t\t\"globalEval\",\n\t\t\t!refElements || dataPriv.get( refElements[ i ], \"globalEval\" )\n\t\t);\n\t}\n}\n\n\nvar rhtml = /<|&#?\\w+;/;\n\nfunction buildFragment( elems, context, scripts, selection, ignored ) {\n\tvar elem, tmp, tag, wrap, attached, j,\n\t\tfragment = context.createDocumentFragment(),\n\t\tnodes = [],\n\t\ti = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\telem = elems[ i ];\n\n\t\tif ( elem || elem === 0 ) {\n\n\t\t\t// Add nodes directly\n\t\t\tif ( toType( elem ) === \"object\" ) {\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t// Convert non-html into a text node\n\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t// Convert html into DOM nodes\n\t\t\t} else {\n\t\t\t\ttmp = tmp || fragment.appendChild( context.createElement( \"div\" ) );\n\n\t\t\t\t// Deserialize a standard representation\n\t\t\t\ttag = ( rtagName.exec( elem ) || [ \"\", \"\" ] )[ 1 ].toLowerCase();\n\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default;\n\t\t\t\ttmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];\n\n\t\t\t\t// Descend through wrappers to the right content\n\t\t\t\tj = wrap[ 0 ];\n\t\t\t\twhile ( j-- ) {\n\t\t\t\t\ttmp = tmp.lastChild;\n\t\t\t\t}\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t// Remember the top-level container\n\t\t\t\ttmp = fragment.firstChild;\n\n\t\t\t\t// Ensure the created nodes are orphaned (#12392)\n\t\t\t\ttmp.textContent = \"\";\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove wrapper from fragment\n\tfragment.textContent = \"\";\n\n\ti = 0;\n\twhile ( ( elem = nodes[ i++ ] ) ) {\n\n\t\t// Skip elements already in the context collection (trac-4087)\n\t\tif ( selection && jQuery.inArray( elem, selection ) > -1 ) {\n\t\t\tif ( ignored ) {\n\t\t\t\tignored.push( elem );\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tattached = isAttached( elem );\n\n\t\t// Append to fragment\n\t\ttmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n\t\t// Preserve script evaluation history\n\t\tif ( attached ) {\n\t\t\tsetGlobalEval( tmp );\n\t\t}\n\n\t\t// Capture executables\n\t\tif ( scripts ) {\n\t\t\tj = 0;\n\t\t\twhile ( ( elem = tmp[ j++ ] ) ) {\n\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\tscripts.push( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fragment;\n}\n\n\n( function() {\n\tvar fragment = document.createDocumentFragment(),\n\t\tdiv = fragment.appendChild( document.createElement( \"div\" ) ),\n\t\tinput = document.createElement( \"input\" );\n\n\t// Support: Android 4.0 - 4.3 only\n\t// Check state lost if the name is set (#11217)\n\t// Support: Windows Web Apps (WWA)\n\t// `name` and `type` must use .setAttribute for WWA (#14901)\n\tinput.setAttribute( \"type\", \"radio\" );\n\tinput.setAttribute( \"checked\", \"checked\" );\n\tinput.setAttribute( \"name\", \"t\" );\n\n\tdiv.appendChild( input );\n\n\t// Support: Android <=4.1 only\n\t// Older WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Support: IE <=11 only\n\t// Make sure textarea (and checkbox) defaultValue is properly cloned\n\tdiv.innerHTML = \"<textarea>x</textarea>\";\n\tsupport.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;\n} )();\n\n\nvar\n\trkeyEvent = /^key/,\n\trmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,\n\trtypenamespace = /^([^.]*)(?:\\.(.+)|)/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\n// Support: IE <=9 - 11+\n// focus() and blur() are asynchronous, except when they are no-op.\n// So expect focus to be synchronous when the element is already active,\n// and blur to be synchronous when the element is not already active.\n// (focus and blur are always synchronous in other supported browsers,\n// this just defines when we can count on it).\nfunction expectSync( elem, type ) {\n\treturn ( elem === safeActiveElement() ) === ( type === \"focus\" );\n}\n\n// Support: IE <=9 only\n// Accessing document.activeElement can throw unexpectedly\n// https://bugs.jquery.com/ticket/13393\nfunction safeActiveElement() {\n\ttry {\n\t\treturn document.activeElement;\n\t} catch ( err ) { }\n}\n\nfunction on( elem, types, selector, data, fn, one ) {\n\tvar origFn, type;\n\n\t// Types can be a map of types/handlers\n\tif ( typeof types === \"object\" ) {\n\n\t\t// ( types-Object, selector, data )\n\t\tif ( typeof selector !== \"string\" ) {\n\n\t\t\t// ( types-Object, data )\n\t\t\tdata = data || selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tfor ( type in types ) {\n\t\t\ton( elem, type, selector, data, types[ type ], one );\n\t\t}\n\t\treturn elem;\n\t}\n\n\tif ( data == null && fn == null ) {\n\n\t\t// ( types, fn )\n\t\tfn = selector;\n\t\tdata = selector = undefined;\n\t} else if ( fn == null ) {\n\t\tif ( typeof selector === \"string\" ) {\n\n\t\t\t// ( types, selector, fn )\n\t\t\tfn = data;\n\t\t\tdata = undefined;\n\t\t} else {\n\n\t\t\t// ( types, data, fn )\n\t\t\tfn = data;\n\t\t\tdata = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t}\n\tif ( fn === false ) {\n\t\tfn = returnFalse;\n\t} else if ( !fn ) {\n\t\treturn elem;\n\t}\n\n\tif ( one === 1 ) {\n\t\torigFn = fn;\n\t\tfn = function( event ) {\n\n\t\t\t// Can use an empty set, since event contains the info\n\t\t\tjQuery().off( event );\n\t\t\treturn origFn.apply( this, arguments );\n\t\t};\n\n\t\t// Use same guid so caller can remove using origFn\n\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t}\n\treturn elem.each( function() {\n\t\tjQuery.event.add( this, types, fn, data, selector );\n\t} );\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tglobal: {},\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar handleObjIn, eventHandle, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.get( elem );\n\n\t\t// Don't attach events to noData or text/comment nodes (but allow plain objects)\n\t\tif ( !elemData ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Ensure that invalid selectors throw exceptions at attach time\n\t\t// Evaluate against documentElement in case elem is a non-element node (e.g., document)\n\t\tif ( selector ) {\n\t\t\tjQuery.find.matchesSelector( documentElement, selector );\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !( events = elemData.events ) ) {\n\t\t\tevents = elemData.events = {};\n\t\t}\n\t\tif ( !( eventHandle = elemData.handle ) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== \"undefined\" && jQuery.event.triggered !== e.type ?\n\t\t\t\t\tjQuery.event.dispatch.apply( elem, arguments ) : undefined;\n\t\t\t};\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend( {\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join( \".\" )\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !( handlers = events[ type ] ) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener if the special events handler returns false\n\t\t\t\tif ( !special.setup ||\n\t\t\t\t\tspecial.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar j, origCount, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.hasData( elem ) && dataPriv.get( elem );\n\n\t\tif ( !elemData || !( events = elemData.events ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[ 2 ] &&\n\t\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector ||\n\t\t\t\t\t\tselector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown ||\n\t\t\t\t\tspecial.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove data and the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdataPriv.remove( elem, \"handle events\" );\n\t\t}\n\t},\n\n\tdispatch: function( nativeEvent ) {\n\n\t\t// Make a writable jQuery.Event from the native event object\n\t\tvar event = jQuery.event.fix( nativeEvent );\n\n\t\tvar i, j, ret, matched, handleObj, handlerQueue,\n\t\t\targs = new Array( arguments.length ),\n\t\t\thandlers = ( dataPriv.get( this, \"events\" ) || {} )[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[ 0 ] = event;\n\n\t\tfor ( i = 1; i < arguments.length; i++ ) {\n\t\t\targs[ i ] = arguments[ i ];\n\t\t}\n\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( ( handleObj = matched.handlers[ j++ ] ) &&\n\t\t\t\t!event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// If the event is namespaced, then each handler is only invoked if it is\n\t\t\t\t// specially universal or its namespaces are a superset of the event's.\n\t\t\t\tif ( !event.rnamespace || handleObj.namespace === false ||\n\t\t\t\t\tevent.rnamespace.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||\n\t\t\t\t\t\thandleObj.handler ).apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( ( event.result = ret ) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar i, handleObj, sel, matchedHandlers, matchedSelectors,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\tif ( delegateCount &&\n\n\t\t\t// Support: IE <=9\n\t\t\t// Black-hole SVG <use> instance trees (trac-13180)\n\t\t\tcur.nodeType &&\n\n\t\t\t// Support: Firefox <=42\n\t\t\t// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)\n\t\t\t// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click\n\t\t\t// Support: IE 11 only\n\t\t\t// ...but not arrow key \"clicks\" of radio inputs, which can have `button` -1 (gh-2343)\n\t\t\t!( event.type === \"click\" && event.button >= 1 ) ) {\n\n\t\t\tfor ( ; cur !== this; cur = cur.parentNode || this ) {\n\n\t\t\t\t// Don't check non-elements (#13208)\n\t\t\t\t// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)\n\t\t\t\tif ( cur.nodeType === 1 && !( event.type === \"click\" && cur.disabled === true ) ) {\n\t\t\t\t\tmatchedHandlers = [];\n\t\t\t\t\tmatchedSelectors = {};\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (#13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatchedSelectors[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) > -1 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] ) {\n\t\t\t\t\t\t\tmatchedHandlers.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matchedHandlers.length ) {\n\t\t\t\t\t\thandlerQueue.push( { elem: cur, handlers: matchedHandlers } );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tcur = this;\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\taddProp: function( name, hook ) {\n\t\tObject.defineProperty( jQuery.Event.prototype, name, {\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\n\t\t\tget: isFunction( hook ) ?\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn hook( this.originalEvent );\n\t\t\t\t\t}\n\t\t\t\t} :\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn this.originalEvent[ name ];\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\tset: function( value ) {\n\t\t\t\tObject.defineProperty( this, name, {\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\twritable: true,\n\t\t\t\t\tvalue: value\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t},\n\n\tfix: function( originalEvent ) {\n\t\treturn originalEvent[ jQuery.expando ] ?\n\t\t\toriginalEvent :\n\t\t\tnew jQuery.Event( originalEvent );\n\t},\n\n\tspecial: {\n\t\tload: {\n\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tclick: {\n\n\t\t\t// Utilize native event to ensure correct state for checkable inputs\n\t\t\tsetup: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Claim the first handler\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\t// dataPriv.set( el, \"click\", ... )\n\t\t\t\t\tleverageNative( el, \"click\", returnTrue );\n\t\t\t\t}\n\n\t\t\t\t// Return false to allow normal processing in the caller\n\t\t\t\treturn false;\n\t\t\t},\n\t\t\ttrigger: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Force setup before triggering a click\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\tleverageNative( el, \"click\" );\n\t\t\t\t}\n\n\t\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, suppress native .click() on links\n\t\t\t// Also prevent it if we're currently inside a leveraged native-event stack\n\t\t\t_default: function( event ) {\n\t\t\t\tvar target = event.target;\n\t\t\t\treturn rcheckableType.test( target.type ) &&\n\t\t\t\t\ttarget.click && nodeName( target, \"input\" ) &&\n\t\t\t\t\tdataPriv.get( target, \"click\" ) ||\n\t\t\t\t\tnodeName( target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\n\t\t\t\t// Support: Firefox 20+\n\t\t\t\t// Firefox doesn't alert if the returnValue field is not set.\n\t\t\t\tif ( event.result !== undefined && event.originalEvent ) {\n\t\t\t\t\tevent.originalEvent.returnValue = event.result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Ensure the presence of an event listener that handles manually-triggered\n// synthetic events by interrupting progress until reinvoked in response to\n// *native* events that it fires directly, ensuring that state changes have\n// already occurred before other listeners are invoked.\nfunction leverageNative( el, type, expectSync ) {\n\n\t// Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add\n\tif ( !expectSync ) {\n\t\tif ( dataPriv.get( el, type ) === undefined ) {\n\t\t\tjQuery.event.add( el, type, returnTrue );\n\t\t}\n\t\treturn;\n\t}\n\n\t// Register the controller as a special universal handler for all event namespaces\n\tdataPriv.set( el, type, false );\n\tjQuery.event.add( el, type, {\n\t\tnamespace: false,\n\t\thandler: function( event ) {\n\t\t\tvar notAsync, result,\n\t\t\t\tsaved = dataPriv.get( this, type );\n\n\t\t\tif ( ( event.isTrigger & 1 ) && this[ type ] ) {\n\n\t\t\t\t// Interrupt processing of the outer synthetic .trigger()ed event\n\t\t\t\t// Saved data should be false in such cases, but might be a leftover capture object\n\t\t\t\t// from an async native handler (gh-4350)\n\t\t\t\tif ( !saved.length ) {\n\n\t\t\t\t\t// Store arguments for use when handling the inner native event\n\t\t\t\t\t// There will always be at least one argument (an event object), so this array\n\t\t\t\t\t// will not be confused with a leftover capture object.\n\t\t\t\t\tsaved = slice.call( arguments );\n\t\t\t\t\tdataPriv.set( this, type, saved );\n\n\t\t\t\t\t// Trigger the native event and capture its result\n\t\t\t\t\t// Support: IE <=9 - 11+\n\t\t\t\t\t// focus() and blur() are asynchronous\n\t\t\t\t\tnotAsync = expectSync( this, type );\n\t\t\t\t\tthis[ type ]();\n\t\t\t\t\tresult = dataPriv.get( this, type );\n\t\t\t\t\tif ( saved !== result || notAsync ) {\n\t\t\t\t\t\tdataPriv.set( this, type, false );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult = {};\n\t\t\t\t\t}\n\t\t\t\t\tif ( saved !== result ) {\n\n\t\t\t\t\t\t// Cancel the outer synthetic event\n\t\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\treturn result.value;\n\t\t\t\t\t}\n\n\t\t\t\t// If this is an inner synthetic event for an event with a bubbling surrogate\n\t\t\t\t// (focus or blur), assume that the surrogate already propagated from triggering the\n\t\t\t\t// native event and prevent that from happening again here.\n\t\t\t\t// This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the\n\t\t\t\t// bubbling surrogate propagates *after* the non-bubbling base), but that seems\n\t\t\t\t// less bad than duplication.\n\t\t\t\t} else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t}\n\n\t\t\t// If this is a native event triggered above, everything is now in order\n\t\t\t// Fire an inner synthetic event with the original arguments\n\t\t\t} else if ( saved.length ) {\n\n\t\t\t\t// ...and capture the result\n\t\t\t\tdataPriv.set( this, type, {\n\t\t\t\t\tvalue: jQuery.event.trigger(\n\n\t\t\t\t\t\t// Support: IE <=9 - 11+\n\t\t\t\t\t\t// Extend with the prototype to reset the above stopImmediatePropagation()\n\t\t\t\t\t\tjQuery.extend( saved[ 0 ], jQuery.Event.prototype ),\n\t\t\t\t\t\tsaved.slice( 1 ),\n\t\t\t\t\t\tthis\n\t\t\t\t\t)\n\t\t\t\t} );\n\n\t\t\t\t// Abort handling of the native event\n\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t}\n\t\t}\n\t} );\n}\n\njQuery.removeEvent = function( elem, type, handle ) {\n\n\t// This \"if\" is needed for plain objects\n\tif ( elem.removeEventListener ) {\n\t\telem.removeEventListener( type, handle );\n\t}\n};\n\njQuery.Event = function( src, props ) {\n\n\t// Allow instantiation without the 'new' keyword\n\tif ( !( this instanceof jQuery.Event ) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ||\n\t\t\t\tsrc.defaultPrevented === undefined &&\n\n\t\t\t\t// Support: Android <=2.3 only\n\t\t\t\tsrc.returnValue === false ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t\t// Create target properties\n\t\t// Support: Safari <=6 - 7 only\n\t\t// Target should not be a text node (#504, #13143)\n\t\tthis.target = ( src.target && src.target.nodeType === 3 ) ?\n\t\t\tsrc.target.parentNode :\n\t\t\tsrc.target;\n\n\t\tthis.currentTarget = src.currentTarget;\n\t\tthis.relatedTarget = src.relatedTarget;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || Date.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tconstructor: jQuery.Event,\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\tisSimulated: false,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.preventDefault();\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Includes all common event props including KeyEvent and MouseEvent specific props\njQuery.each( {\n\taltKey: true,\n\tbubbles: true,\n\tcancelable: true,\n\tchangedTouches: true,\n\tctrlKey: true,\n\tdetail: true,\n\teventPhase: true,\n\tmetaKey: true,\n\tpageX: true,\n\tpageY: true,\n\tshiftKey: true,\n\tview: true,\n\t\"char\": true,\n\tcode: true,\n\tcharCode: true,\n\tkey: true,\n\tkeyCode: true,\n\tbutton: true,\n\tbuttons: true,\n\tclientX: true,\n\tclientY: true,\n\toffsetX: true,\n\toffsetY: true,\n\tpointerId: true,\n\tpointerType: true,\n\tscreenX: true,\n\tscreenY: true,\n\ttargetTouches: true,\n\ttoElement: true,\n\ttouches: true,\n\n\twhich: function( event ) {\n\t\tvar button = event.button;\n\n\t\t// Add which for key events\n\t\tif ( event.which == null && rkeyEvent.test( event.type ) ) {\n\t\t\treturn event.charCode != null ? event.charCode : event.keyCode;\n\t\t}\n\n\t\t// Add which for click: 1 === left; 2 === middle; 3 === right\n\t\tif ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {\n\t\t\tif ( button & 1 ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\tif ( button & 2 ) {\n\t\t\t\treturn 3;\n\t\t\t}\n\n\t\t\tif ( button & 4 ) {\n\t\t\t\treturn 2;\n\t\t\t}\n\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn event.which;\n\t}\n}, jQuery.event.addProp );\n\njQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( type, delegateType ) {\n\tjQuery.event.special[ type ] = {\n\n\t\t// Utilize native event if possible so blur/focus sequence is correct\n\t\tsetup: function() {\n\n\t\t\t// Claim the first handler\n\t\t\t// dataPriv.set( this, \"focus\", ... )\n\t\t\t// dataPriv.set( this, \"blur\", ... )\n\t\t\tleverageNative( this, type, expectSync );\n\n\t\t\t// Return false to allow normal processing in the caller\n\t\t\treturn false;\n\t\t},\n\t\ttrigger: function() {\n\n\t\t\t// Force setup before trigger\n\t\t\tleverageNative( this, type );\n\n\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\treturn true;\n\t\t},\n\n\t\tdelegateType: delegateType\n\t};\n} );\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// so that event delegation works in jQuery.\n// Do the same for pointerenter/pointerleave and pointerover/pointerout\n//\n// Support: Safari 7 only\n// Safari sends mouseenter too often; see:\n// https://bugs.chromium.org/p/chromium/issues/detail?id=470258\n// for the description of the bug (it existed in older Chrome versions as well).\njQuery.each( {\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mouseenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n} );\n\njQuery.fn.extend( {\n\n\ton: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn );\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ?\n\t\t\t\t\thandleObj.origType + \".\" + handleObj.namespace :\n\t\t\t\t\thandleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t} );\n\t}\n} );\n\n\nvar\n\n\t/* eslint-disable max-len */\n\n\t// See https://github.com/eslint/eslint/issues/3229\n\trxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)[^>]*)\\/>/gi,\n\n\t/* eslint-enable */\n\n\t// Support: IE <=10 - 11, Edge 12 - 13 only\n\t// In IE/Edge using regex groups here causes severe slowdowns.\n\t// See https://connect.microsoft.com/IE/feedback/details/1736512/\n\trnoInnerhtml = /<script|<style|<link/i,\n\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\trcleanScript = /^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g;\n\n// Prefer a tbody over its parent table for containing new rows\nfunction manipulationTarget( elem, content ) {\n\tif ( nodeName( elem, \"table\" ) &&\n\t\tnodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ) {\n\n\t\treturn jQuery( elem ).children( \"tbody\" )[ 0 ] || elem;\n\t}\n\n\treturn elem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = ( elem.getAttribute( \"type\" ) !== null ) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tif ( ( elem.type || \"\" ).slice( 0, 5 ) === \"true/\" ) {\n\t\telem.type = elem.type.slice( 5 );\n\t} else {\n\t\telem.removeAttribute( \"type\" );\n\t}\n\n\treturn elem;\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\tvar i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;\n\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// 1. Copy private data: events, handlers, etc.\n\tif ( dataPriv.hasData( src ) ) {\n\t\tpdataOld = dataPriv.access( src );\n\t\tpdataCur = dataPriv.set( dest, pdataOld );\n\t\tevents = pdataOld.events;\n\n\t\tif ( events ) {\n\t\t\tdelete pdataCur.handle;\n\t\t\tpdataCur.events = {};\n\n\t\t\tfor ( type in events ) {\n\t\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Copy user data\n\tif ( dataUser.hasData( src ) ) {\n\t\tudataOld = dataUser.access( src );\n\t\tudataCur = jQuery.extend( {}, udataOld );\n\n\t\tdataUser.set( dest, udataCur );\n\t}\n}\n\n// Fix IE bugs, see support tests\nfunction fixInput( src, dest ) {\n\tvar nodeName = dest.nodeName.toLowerCase();\n\n\t// Fails to persist the checked state of a cloned checkbox or radio button.\n\tif ( nodeName === \"input\" && rcheckableType.test( src.type ) ) {\n\t\tdest.checked = src.checked;\n\n\t// Fails to return the selected option to the default selected state when cloning options\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n}\n\nfunction domManip( collection, args, callback, ignored ) {\n\n\t// Flatten any nested arrays\n\targs = concat.apply( [], args );\n\n\tvar fragment, first, scripts, hasScripts, node, doc,\n\t\ti = 0,\n\t\tl = collection.length,\n\t\tiNoClone = l - 1,\n\t\tvalue = args[ 0 ],\n\t\tvalueIsFunction = isFunction( value );\n\n\t// We can't cloneNode fragments that contain checked, in WebKit\n\tif ( valueIsFunction ||\n\t\t\t( l > 1 && typeof value === \"string\" &&\n\t\t\t\t!support.checkClone && rchecked.test( value ) ) ) {\n\t\treturn collection.each( function( index ) {\n\t\t\tvar self = collection.eq( index );\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\targs[ 0 ] = value.call( this, index, self.html() );\n\t\t\t}\n\t\t\tdomManip( self, args, callback, ignored );\n\t\t} );\n\t}\n\n\tif ( l ) {\n\t\tfragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );\n\t\tfirst = fragment.firstChild;\n\n\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\tfragment = first;\n\t\t}\n\n\t\t// Require either new content or an interest in ignored elements to invoke the callback\n\t\tif ( first || ignored ) {\n\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\thasScripts = scripts.length;\n\n\t\t\t// Use the original fragment for the last item\n\t\t\t// instead of the first because it can end up\n\t\t\t// being emptied incorrectly in certain situations (#8070).\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tnode = fragment;\n\n\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\tif ( hasScripts ) {\n\n\t\t\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcallback.call( collection[ i ], node, i );\n\t\t\t}\n\n\t\t\tif ( hasScripts ) {\n\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t// Reenable scripts\n\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t!dataPriv.access( node, \"globalEval\" ) &&\n\t\t\t\t\t\tjQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\tif ( node.src && ( node.type || \"\" ).toLowerCase()  !== \"module\" ) {\n\n\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\tif ( jQuery._evalUrl && !node.noModule ) {\n\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src, {\n\t\t\t\t\t\t\t\t\tnonce: node.nonce || node.getAttribute( \"nonce\" )\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tDOMEval( node.textContent.replace( rcleanScript, \"\" ), node, doc );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn collection;\n}\n\nfunction remove( elem, selector, keepData ) {\n\tvar node,\n\t\tnodes = selector ? jQuery.filter( selector, elem ) : elem,\n\t\ti = 0;\n\n\tfor ( ; ( node = nodes[ i ] ) != null; i++ ) {\n\t\tif ( !keepData && node.nodeType === 1 ) {\n\t\t\tjQuery.cleanData( getAll( node ) );\n\t\t}\n\n\t\tif ( node.parentNode ) {\n\t\t\tif ( keepData && isAttached( node ) ) {\n\t\t\t\tsetGlobalEval( getAll( node, \"script\" ) );\n\t\t\t}\n\t\t\tnode.parentNode.removeChild( node );\n\t\t}\n\t}\n\n\treturn elem;\n}\n\njQuery.extend( {\n\thtmlPrefilter: function( html ) {\n\t\treturn html.replace( rxhtmlTag, \"<$1></$2>\" );\n\t},\n\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar i, l, srcElements, destElements,\n\t\t\tclone = elem.cloneNode( true ),\n\t\t\tinPage = isAttached( elem );\n\n\t\t// Fix IE cloning issues\n\t\tif ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&\n\t\t\t\t!jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\tfixInput( srcElements[ i ], destElements[ i ] );\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[ i ], destElements[ i ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, elem, type,\n\t\t\tspecial = jQuery.event.special,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {\n\t\t\tif ( acceptData( elem ) ) {\n\t\t\t\tif ( ( data = elem[ dataPriv.expando ] ) ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataPriv.expando ] = undefined;\n\t\t\t\t}\n\t\t\t\tif ( elem[ dataUser.expando ] ) {\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataUser.expando ] = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n} );\n\njQuery.fn.extend( {\n\tdetach: function( selector ) {\n\t\treturn remove( this, selector, true );\n\t},\n\n\tremove: function( selector ) {\n\t\treturn remove( this, selector );\n\t},\n\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().each( function() {\n\t\t\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\t\t\tthis.textContent = value;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t} );\n\t},\n\n\tprepend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t} );\n\t},\n\n\tbefore: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t} );\n\t},\n\n\tafter: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t} );\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = this[ i ] ) != null; i++ ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\n\t\t\t\t// Prevent memory leaks\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\n\t\t\t\t// Remove any remaining nodes\n\t\t\t\telem.textContent = \"\";\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map( function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t} );\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined && elem.nodeType === 1 ) {\n\t\t\t\treturn elem.innerHTML;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t!wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = jQuery.htmlPrefilter( value );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\t\telem = this[ i ] || {};\n\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch ( e ) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar ignored = [];\n\n\t\t// Make the changes, replacing each non-ignored context element with the new content\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tvar parent = this.parentNode;\n\n\t\t\tif ( jQuery.inArray( this, ignored ) < 0 ) {\n\t\t\t\tjQuery.cleanData( getAll( this ) );\n\t\t\t\tif ( parent ) {\n\t\t\t\t\tparent.replaceChild( elem, this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Force callback invocation\n\t\t}, ignored );\n\t}\n} );\n\njQuery.each( {\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1,\n\t\t\ti = 0;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone( true );\n\t\t\tjQuery( insert[ i ] )[ original ]( elems );\n\n\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t// .get() because push.apply(_, arraylike) throws on ancient WebKit\n\t\t\tpush.apply( ret, elems.get() );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n} );\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\nvar getStyles = function( elem ) {\n\n\t\t// Support: IE <=11 only, Firefox <=30 (#15098, #14150)\n\t\t// IE throws on elements created in popups\n\t\t// FF meanwhile throws on frame elements through \"defaultView.getComputedStyle\"\n\t\tvar view = elem.ownerDocument.defaultView;\n\n\t\tif ( !view || !view.opener ) {\n\t\t\tview = window;\n\t\t}\n\n\t\treturn view.getComputedStyle( elem );\n\t};\n\nvar rboxStyle = new RegExp( cssExpand.join( \"|\" ), \"i\" );\n\n\n\n( function() {\n\n\t// Executing both pixelPosition & boxSizingReliable tests require only one layout\n\t// so they're executed at the same time to save the second computation.\n\tfunction computeStyleTests() {\n\n\t\t// This is a singleton, we need to execute it only once\n\t\tif ( !div ) {\n\t\t\treturn;\n\t\t}\n\n\t\tcontainer.style.cssText = \"position:absolute;left:-11111px;width:60px;\" +\n\t\t\t\"margin-top:1px;padding:0;border:0\";\n\t\tdiv.style.cssText =\n\t\t\t\"position:relative;display:block;box-sizing:border-box;overflow:scroll;\" +\n\t\t\t\"margin:auto;border:1px;padding:1px;\" +\n\t\t\t\"width:60%;top:1%\";\n\t\tdocumentElement.appendChild( container ).appendChild( div );\n\n\t\tvar divStyle = window.getComputedStyle( div );\n\t\tpixelPositionVal = divStyle.top !== \"1%\";\n\n\t\t// Support: Android 4.0 - 4.3 only, Firefox <=3 - 44\n\t\treliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;\n\n\t\t// Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3\n\t\t// Some styles come back with percentage values, even though they shouldn't\n\t\tdiv.style.right = \"60%\";\n\t\tpixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;\n\n\t\t// Support: IE 9 - 11 only\n\t\t// Detect misreporting of content dimensions for box-sizing:border-box elements\n\t\tboxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;\n\n\t\t// Support: IE 9 only\n\t\t// Detect overflow:scroll screwiness (gh-3699)\n\t\t// Support: Chrome <=64\n\t\t// Don't get tricked when zoom affects offsetWidth (gh-4029)\n\t\tdiv.style.position = \"absolute\";\n\t\tscrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12;\n\n\t\tdocumentElement.removeChild( container );\n\n\t\t// Nullify the div so it wouldn't be stored in the memory and\n\t\t// it will also be a sign that checks already performed\n\t\tdiv = null;\n\t}\n\n\tfunction roundPixelMeasures( measure ) {\n\t\treturn Math.round( parseFloat( measure ) );\n\t}\n\n\tvar pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,\n\t\treliableMarginLeftVal,\n\t\tcontainer = document.createElement( \"div\" ),\n\t\tdiv = document.createElement( \"div\" );\n\n\t// Finish early in limited (non-browser) environments\n\tif ( !div.style ) {\n\t\treturn;\n\t}\n\n\t// Support: IE <=9 - 11 only\n\t// Style of cloned element affects source element cloned (#8908)\n\tdiv.style.backgroundClip = \"content-box\";\n\tdiv.cloneNode( true ).style.backgroundClip = \"\";\n\tsupport.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n\tjQuery.extend( support, {\n\t\tboxSizingReliable: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn boxSizingReliableVal;\n\t\t},\n\t\tpixelBoxStyles: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelBoxStylesVal;\n\t\t},\n\t\tpixelPosition: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelPositionVal;\n\t\t},\n\t\treliableMarginLeft: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn reliableMarginLeftVal;\n\t\t},\n\t\tscrollboxSize: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn scrollboxSizeVal;\n\t\t}\n\t} );\n} )();\n\n\nfunction curCSS( elem, name, computed ) {\n\tvar width, minWidth, maxWidth, ret,\n\n\t\t// Support: Firefox 51+\n\t\t// Retrieving style before computed somehow\n\t\t// fixes an issue with getting wrong values\n\t\t// on detached elements\n\t\tstyle = elem.style;\n\n\tcomputed = computed || getStyles( elem );\n\n\t// getPropertyValue is needed for:\n\t//   .css('filter') (IE 9 only, #12537)\n\t//   .css('--customProperty) (#3144)\n\tif ( computed ) {\n\t\tret = computed.getPropertyValue( name ) || computed[ name ];\n\n\t\tif ( ret === \"\" && !isAttached( elem ) ) {\n\t\t\tret = jQuery.style( elem, name );\n\t\t}\n\n\t\t// A tribute to the \"awesome hack by Dean Edwards\"\n\t\t// Android Browser returns percentage for some values,\n\t\t// but width seems to be reliably pixels.\n\t\t// This is against the CSSOM draft spec:\n\t\t// https://drafts.csswg.org/cssom/#resolved-values\n\t\tif ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {\n\n\t\t\t// Remember the original values\n\t\t\twidth = style.width;\n\t\t\tminWidth = style.minWidth;\n\t\t\tmaxWidth = style.maxWidth;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tstyle.minWidth = style.maxWidth = style.width = ret;\n\t\t\tret = computed.width;\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.width = width;\n\t\t\tstyle.minWidth = minWidth;\n\t\t\tstyle.maxWidth = maxWidth;\n\t\t}\n\t}\n\n\treturn ret !== undefined ?\n\n\t\t// Support: IE <=9 - 11 only\n\t\t// IE returns zIndex value as an integer.\n\t\tret + \"\" :\n\t\tret;\n}\n\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\n\t// Define the hook, we'll check on the first run if it's really needed.\n\treturn {\n\t\tget: function() {\n\t\t\tif ( conditionFn() ) {\n\n\t\t\t\t// Hook not needed (or it's not possible to use it due\n\t\t\t\t// to missing dependency), remove it.\n\t\t\t\tdelete this.get;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hook needed; redefine it so that the support test is not executed again.\n\t\t\treturn ( this.get = hookFn ).apply( this, arguments );\n\t\t}\n\t};\n}\n\n\nvar cssPrefixes = [ \"Webkit\", \"Moz\", \"ms\" ],\n\temptyStyle = document.createElement( \"div\" ).style,\n\tvendorProps = {};\n\n// Return a vendor-prefixed property or undefined\nfunction vendorPropName( name ) {\n\n\t// Check for vendor prefixed names\n\tvar capName = name[ 0 ].toUpperCase() + name.slice( 1 ),\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in emptyStyle ) {\n\t\t\treturn name;\n\t\t}\n\t}\n}\n\n// Return a potentially-mapped jQuery.cssProps or vendor prefixed property\nfunction finalPropName( name ) {\n\tvar final = jQuery.cssProps[ name ] || vendorProps[ name ];\n\n\tif ( final ) {\n\t\treturn final;\n\t}\n\tif ( name in emptyStyle ) {\n\t\treturn name;\n\t}\n\treturn vendorProps[ name ] = vendorPropName( name ) || name;\n}\n\n\nvar\n\n\t// Swappable if display is none or starts with table\n\t// except \"table\", \"table-cell\", or \"table-caption\"\n\t// See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n\trdisplayswap = /^(none|table(?!-c[ea]).+)/,\n\trcustomProp = /^--/,\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t};\n\nfunction setPositiveNumber( elem, value, subtract ) {\n\n\t// Any relative (+/-) values have already been\n\t// normalized at this point\n\tvar matches = rcssNum.exec( value );\n\treturn matches ?\n\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {\n\tvar i = dimension === \"width\" ? 1 : 0,\n\t\textra = 0,\n\t\tdelta = 0;\n\n\t// Adjustment may not be necessary\n\tif ( box === ( isBorderBox ? \"border\" : \"content\" ) ) {\n\t\treturn 0;\n\t}\n\n\tfor ( ; i < 4; i += 2 ) {\n\n\t\t// Both box models exclude margin\n\t\tif ( box === \"margin\" ) {\n\t\t\tdelta += jQuery.css( elem, box + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\t// If we get here with a content-box, we're seeking \"padding\" or \"border\" or \"margin\"\n\t\tif ( !isBorderBox ) {\n\n\t\t\t// Add padding\n\t\t\tdelta += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// For \"border\" or \"margin\", add border\n\t\t\tif ( box !== \"padding\" ) {\n\t\t\t\tdelta += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\n\t\t\t// But still keep track of it otherwise\n\t\t\t} else {\n\t\t\t\textra += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\n\t\t// If we get here with a border-box (content + padding + border), we're seeking \"content\" or\n\t\t// \"padding\" or \"margin\"\n\t\t} else {\n\n\t\t\t// For \"content\", subtract padding\n\t\t\tif ( box === \"content\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// For \"content\" or \"padding\", subtract border\n\t\t\tif ( box !== \"margin\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Account for positive content-box scroll gutter when requested by providing computedVal\n\tif ( !isBorderBox && computedVal >= 0 ) {\n\n\t\t// offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border\n\t\t// Assuming integer scroll gutter, subtract the rest and round down\n\t\tdelta += Math.max( 0, Math.ceil(\n\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\tcomputedVal -\n\t\t\tdelta -\n\t\t\textra -\n\t\t\t0.5\n\n\t\t// If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter\n\t\t// Use an explicit zero to avoid NaN (gh-3964)\n\t\t) ) || 0;\n\t}\n\n\treturn delta;\n}\n\nfunction getWidthOrHeight( elem, dimension, extra ) {\n\n\t// Start with computed style\n\tvar styles = getStyles( elem ),\n\n\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).\n\t\t// Fake content-box until we know it's needed to know the true value.\n\t\tboxSizingNeeded = !support.boxSizingReliable() || extra,\n\t\tisBorderBox = boxSizingNeeded &&\n\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\tvalueIsBorderBox = isBorderBox,\n\n\t\tval = curCSS( elem, dimension, styles ),\n\t\toffsetProp = \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );\n\n\t// Support: Firefox <=54\n\t// Return a confounding non-pixel value or feign ignorance, as appropriate.\n\tif ( rnumnonpx.test( val ) ) {\n\t\tif ( !extra ) {\n\t\t\treturn val;\n\t\t}\n\t\tval = \"auto\";\n\t}\n\n\n\t// Fall back to offsetWidth/offsetHeight when value is \"auto\"\n\t// This happens for inline elements with no explicit setting (gh-3571)\n\t// Support: Android <=4.1 - 4.3 only\n\t// Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)\n\t// Support: IE 9-11 only\n\t// Also use offsetWidth/offsetHeight for when box sizing is unreliable\n\t// We use getClientRects() to check for hidden/disconnected.\n\t// In those cases, the computed value can be trusted to be border-box\n\tif ( ( !support.boxSizingReliable() && isBorderBox ||\n\t\tval === \"auto\" ||\n\t\t!parseFloat( val ) && jQuery.css( elem, \"display\", false, styles ) === \"inline\" ) &&\n\t\telem.getClientRects().length ) {\n\n\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n\t\t// Where available, offsetWidth/offsetHeight approximate border box dimensions.\n\t\t// Where not available (e.g., SVG), assume unreliable box-sizing and interpret the\n\t\t// retrieved value as a content box dimension.\n\t\tvalueIsBorderBox = offsetProp in elem;\n\t\tif ( valueIsBorderBox ) {\n\t\t\tval = elem[ offsetProp ];\n\t\t}\n\t}\n\n\t// Normalize \"\" and auto\n\tval = parseFloat( val ) || 0;\n\n\t// Adjust for the element's box model\n\treturn ( val +\n\t\tboxModelAdjustment(\n\t\t\telem,\n\t\t\tdimension,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles,\n\n\t\t\t// Provide the current computed size to request scroll gutter calculation (gh-3589)\n\t\t\tval\n\t\t)\n\t) + \"px\";\n}\n\njQuery.extend( {\n\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Don't automatically add \"px\" to these possibly-unitless properties\n\tcssNumber: {\n\t\t\"animationIterationCount\": true,\n\t\t\"columnCount\": true,\n\t\t\"fillOpacity\": true,\n\t\t\"flexGrow\": true,\n\t\t\"flexShrink\": true,\n\t\t\"fontWeight\": true,\n\t\t\"gridArea\": true,\n\t\t\"gridColumn\": true,\n\t\t\"gridColumnEnd\": true,\n\t\t\"gridColumnStart\": true,\n\t\t\"gridRow\": true,\n\t\t\"gridRowEnd\": true,\n\t\t\"gridRowStart\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"order\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name ),\n\t\t\tstyle = elem.style;\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to query the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Gets hook for the prefixed version, then unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// Convert \"+=\" or \"-=\" to relative numbers (#7345)\n\t\t\tif ( type === \"string\" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {\n\t\t\t\tvalue = adjustCSS( elem, name, ret );\n\n\t\t\t\t// Fixes bug #9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set (#7116)\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add the unit (except for certain CSS properties)\n\t\t\t// The isCustomProp check can be removed in jQuery 4.0 when we only auto-append\n\t\t\t// \"px\" to a few hardcoded values.\n\t\t\tif ( type === \"number\" && !isCustomProp ) {\n\t\t\t\tvalue += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? \"\" : \"px\" );\n\t\t\t}\n\n\t\t\t// background-* props affect original clone's values\n\t\t\tif ( !support.clearCloneStyle && value === \"\" && name.indexOf( \"background\" ) === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !( \"set\" in hooks ) ||\n\t\t\t\t( value = hooks.set( elem, value, extra ) ) !== undefined ) {\n\n\t\t\t\tif ( isCustomProp ) {\n\t\t\t\t\tstyle.setProperty( name, value );\n\t\t\t\t} else {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks &&\n\t\t\t\t( ret = hooks.get( elem, false, extra ) ) !== undefined ) {\n\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar val, num, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name );\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to modify the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Try prefixed name followed by the unprefixed name\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t// Convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Make numeric if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || isFinite( num ) ? num || 0 : val;\n\t\t}\n\n\t\treturn val;\n\t}\n} );\n\njQuery.each( [ \"height\", \"width\" ], function( i, dimension ) {\n\tjQuery.cssHooks[ dimension ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\n\t\t\t\t// Certain elements can have dimension info if we invisibly show them\n\t\t\t\t// but it must have a current display style that would benefit\n\t\t\t\treturn rdisplayswap.test( jQuery.css( elem, \"display\" ) ) &&\n\n\t\t\t\t\t// Support: Safari 8+\n\t\t\t\t\t// Table columns in Safari have non-zero offsetWidth & zero\n\t\t\t\t\t// getBoundingClientRect().width unless display is changed.\n\t\t\t\t\t// Support: IE <=11 only\n\t\t\t\t\t// Running getBoundingClientRect on a disconnected node\n\t\t\t\t\t// in IE throws an error.\n\t\t\t\t\t( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?\n\t\t\t\t\t\tswap( elem, cssShow, function() {\n\t\t\t\t\t\t\treturn getWidthOrHeight( elem, dimension, extra );\n\t\t\t\t\t\t} ) :\n\t\t\t\t\t\tgetWidthOrHeight( elem, dimension, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar matches,\n\t\t\t\tstyles = getStyles( elem ),\n\n\t\t\t\t// Only read styles.position if the test has a chance to fail\n\t\t\t\t// to avoid forcing a reflow.\n\t\t\t\tscrollboxSizeBuggy = !support.scrollboxSize() &&\n\t\t\t\t\tstyles.position === \"absolute\",\n\n\t\t\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)\n\t\t\t\tboxSizingNeeded = scrollboxSizeBuggy || extra,\n\t\t\t\tisBorderBox = boxSizingNeeded &&\n\t\t\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\tsubtract = extra ?\n\t\t\t\t\tboxModelAdjustment(\n\t\t\t\t\t\telem,\n\t\t\t\t\t\tdimension,\n\t\t\t\t\t\textra,\n\t\t\t\t\t\tisBorderBox,\n\t\t\t\t\t\tstyles\n\t\t\t\t\t) :\n\t\t\t\t\t0;\n\n\t\t\t// Account for unreliable border-box dimensions by comparing offset* to computed and\n\t\t\t// faking a content-box to get border and padding (gh-3699)\n\t\t\tif ( isBorderBox && scrollboxSizeBuggy ) {\n\t\t\t\tsubtract -= Math.ceil(\n\t\t\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\t\t\tparseFloat( styles[ dimension ] ) -\n\t\t\t\t\tboxModelAdjustment( elem, dimension, \"border\", false, styles ) -\n\t\t\t\t\t0.5\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Convert to pixels if value adjustment is needed\n\t\t\tif ( subtract && ( matches = rcssNum.exec( value ) ) &&\n\t\t\t\t( matches[ 3 ] || \"px\" ) !== \"px\" ) {\n\n\t\t\t\telem.style[ dimension ] = value;\n\t\t\t\tvalue = jQuery.css( elem, dimension );\n\t\t\t}\n\n\t\t\treturn setPositiveNumber( elem, value, subtract );\n\t\t}\n\t};\n} );\n\njQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,\n\tfunction( elem, computed ) {\n\t\tif ( computed ) {\n\t\t\treturn ( parseFloat( curCSS( elem, \"marginLeft\" ) ) ||\n\t\t\t\telem.getBoundingClientRect().left -\n\t\t\t\t\tswap( elem, { marginLeft: 0 }, function() {\n\t\t\t\t\t\treturn elem.getBoundingClientRect().left;\n\t\t\t\t\t} )\n\t\t\t\t) + \"px\";\n\t\t}\n\t}\n);\n\n// These hooks are used by animate to expand properties\njQuery.each( {\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// Assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split( \" \" ) : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( prefix !== \"margin\" ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n} );\n\njQuery.fn.extend( {\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( Array.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t}\n} );\n\n\nfunction Tween( elem, options, prop, end, easing ) {\n\treturn new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n\tconstructor: Tween,\n\tinit: function( elem, options, prop, end, easing, unit ) {\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\t\tthis.easing = easing || jQuery.easing._default;\n\t\tthis.options = options;\n\t\tthis.start = this.now = this.cur();\n\t\tthis.end = end;\n\t\tthis.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n\t},\n\tcur: function() {\n\t\tvar hooks = Tween.propHooks[ this.prop ];\n\n\t\treturn hooks && hooks.get ?\n\t\t\thooks.get( this ) :\n\t\t\tTween.propHooks._default.get( this );\n\t},\n\trun: function( percent ) {\n\t\tvar eased,\n\t\t\thooks = Tween.propHooks[ this.prop ];\n\n\t\tif ( this.options.duration ) {\n\t\t\tthis.pos = eased = jQuery.easing[ this.easing ](\n\t\t\t\tpercent, this.options.duration * percent, 0, 1, this.options.duration\n\t\t\t);\n\t\t} else {\n\t\t\tthis.pos = eased = percent;\n\t\t}\n\t\tthis.now = ( this.end - this.start ) * eased + this.start;\n\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\tif ( hooks && hooks.set ) {\n\t\t\thooks.set( this );\n\t\t} else {\n\t\t\tTween.propHooks._default.set( this );\n\t\t}\n\t\treturn this;\n\t}\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n\t_default: {\n\t\tget: function( tween ) {\n\t\t\tvar result;\n\n\t\t\t// Use a property on the element directly when it is not a DOM element,\n\t\t\t// or when there is no matching style property that exists.\n\t\t\tif ( tween.elem.nodeType !== 1 ||\n\t\t\t\ttween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {\n\t\t\t\treturn tween.elem[ tween.prop ];\n\t\t\t}\n\n\t\t\t// Passing an empty string as a 3rd parameter to .css will automatically\n\t\t\t// attempt a parseFloat and fallback to a string if the parse fails.\n\t\t\t// Simple values such as \"10px\" are parsed to Float;\n\t\t\t// complex values such as \"rotate(1rad)\" are returned as-is.\n\t\t\tresult = jQuery.css( tween.elem, tween.prop, \"\" );\n\n\t\t\t// Empty strings, null, undefined and \"auto\" are converted to 0.\n\t\t\treturn !result || result === \"auto\" ? 0 : result;\n\t\t},\n\t\tset: function( tween ) {\n\n\t\t\t// Use step hook for back compat.\n\t\t\t// Use cssHook if its there.\n\t\t\t// Use .style if available and use plain properties where available.\n\t\t\tif ( jQuery.fx.step[ tween.prop ] ) {\n\t\t\t\tjQuery.fx.step[ tween.prop ]( tween );\n\t\t\t} else if ( tween.elem.nodeType === 1 && (\n\t\t\t\t\tjQuery.cssHooks[ tween.prop ] ||\n\t\t\t\t\ttween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {\n\t\t\t\tjQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n\t\t\t} else {\n\t\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Support: IE <=9 only\n// Panic based approach to setting things on disconnected nodes\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n\tset: function( tween ) {\n\t\tif ( tween.elem.nodeType && tween.elem.parentNode ) {\n\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t}\n\t}\n};\n\njQuery.easing = {\n\tlinear: function( p ) {\n\t\treturn p;\n\t},\n\tswing: function( p ) {\n\t\treturn 0.5 - Math.cos( p * Math.PI ) / 2;\n\t},\n\t_default: \"swing\"\n};\n\njQuery.fx = Tween.prototype.init;\n\n// Back compat <1.8 extension point\njQuery.fx.step = {};\n\n\n\n\nvar\n\tfxNow, inProgress,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trrun = /queueHooks$/;\n\nfunction schedule() {\n\tif ( inProgress ) {\n\t\tif ( document.hidden === false && window.requestAnimationFrame ) {\n\t\t\twindow.requestAnimationFrame( schedule );\n\t\t} else {\n\t\t\twindow.setTimeout( schedule, jQuery.fx.interval );\n\t\t}\n\n\t\tjQuery.fx.tick();\n\t}\n}\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\twindow.setTimeout( function() {\n\t\tfxNow = undefined;\n\t} );\n\treturn ( fxNow = Date.now() );\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n\tvar which,\n\t\ti = 0,\n\t\tattrs = { height: type };\n\n\t// If we include width, step value is 1 to do all cssExpand values,\n\t// otherwise step value is 2 to skip over Left and Right\n\tincludeWidth = includeWidth ? 1 : 0;\n\tfor ( ; i < 4; i += 2 - includeWidth ) {\n\t\twhich = cssExpand[ i ];\n\t\tattrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n\t}\n\n\tif ( includeWidth ) {\n\t\tattrs.opacity = attrs.width = type;\n\t}\n\n\treturn attrs;\n}\n\nfunction createTween( value, prop, animation ) {\n\tvar tween,\n\t\tcollection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ \"*\" ] ),\n\t\tindex = 0,\n\t\tlength = collection.length;\n\tfor ( ; index < length; index++ ) {\n\t\tif ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {\n\n\t\t\t// We're done with this property\n\t\t\treturn tween;\n\t\t}\n\t}\n}\n\nfunction defaultPrefilter( elem, props, opts ) {\n\tvar prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,\n\t\tisBox = \"width\" in props || \"height\" in props,\n\t\tanim = this,\n\t\torig = {},\n\t\tstyle = elem.style,\n\t\thidden = elem.nodeType && isHiddenWithinTree( elem ),\n\t\tdataShow = dataPriv.get( elem, \"fxshow\" );\n\n\t// Queue-skipping animations hijack the fx hooks\n\tif ( !opts.queue ) {\n\t\thooks = jQuery._queueHooks( elem, \"fx\" );\n\t\tif ( hooks.unqueued == null ) {\n\t\t\thooks.unqueued = 0;\n\t\t\toldfire = hooks.empty.fire;\n\t\t\thooks.empty.fire = function() {\n\t\t\t\tif ( !hooks.unqueued ) {\n\t\t\t\t\toldfire();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\thooks.unqueued++;\n\n\t\tanim.always( function() {\n\n\t\t\t// Ensure the complete handler is called before this completes\n\t\t\tanim.always( function() {\n\t\t\t\thooks.unqueued--;\n\t\t\t\tif ( !jQuery.queue( elem, \"fx\" ).length ) {\n\t\t\t\t\thooks.empty.fire();\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t}\n\n\t// Detect show/hide animations\n\tfor ( prop in props ) {\n\t\tvalue = props[ prop ];\n\t\tif ( rfxtypes.test( value ) ) {\n\t\t\tdelete props[ prop ];\n\t\t\ttoggle = toggle || value === \"toggle\";\n\t\t\tif ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n\t\t\t\t// Pretend to be hidden if this is a \"show\" and\n\t\t\t\t// there is still data from a stopped show/hide\n\t\t\t\tif ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n\t\t\t\t\thidden = true;\n\n\t\t\t\t// Ignore all other no-op show/hide data\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\torig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n\t\t}\n\t}\n\n\t// Bail out if this is a no-op like .hide().hide()\n\tpropTween = !jQuery.isEmptyObject( props );\n\tif ( !propTween && jQuery.isEmptyObject( orig ) ) {\n\t\treturn;\n\t}\n\n\t// Restrict \"overflow\" and \"display\" styles during box animations\n\tif ( isBox && elem.nodeType === 1 ) {\n\n\t\t// Support: IE <=9 - 11, Edge 12 - 15\n\t\t// Record all 3 overflow attributes because IE does not infer the shorthand\n\t\t// from identically-valued overflowX and overflowY and Edge just mirrors\n\t\t// the overflowX value there.\n\t\topts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n\t\t// Identify a display type, preferring old show/hide data over the CSS cascade\n\t\trestoreDisplay = dataShow && dataShow.display;\n\t\tif ( restoreDisplay == null ) {\n\t\t\trestoreDisplay = dataPriv.get( elem, \"display\" );\n\t\t}\n\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\tif ( display === \"none\" ) {\n\t\t\tif ( restoreDisplay ) {\n\t\t\t\tdisplay = restoreDisplay;\n\t\t\t} else {\n\n\t\t\t\t// Get nonempty value(s) by temporarily forcing visibility\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t\trestoreDisplay = elem.style.display || restoreDisplay;\n\t\t\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\t\t\tshowHide( [ elem ] );\n\t\t\t}\n\t\t}\n\n\t\t// Animate inline elements as inline-block\n\t\tif ( display === \"inline\" || display === \"inline-block\" && restoreDisplay != null ) {\n\t\t\tif ( jQuery.css( elem, \"float\" ) === \"none\" ) {\n\n\t\t\t\t// Restore the original display value at the end of pure show/hide animations\n\t\t\t\tif ( !propTween ) {\n\t\t\t\t\tanim.done( function() {\n\t\t\t\t\t\tstyle.display = restoreDisplay;\n\t\t\t\t\t} );\n\t\t\t\t\tif ( restoreDisplay == null ) {\n\t\t\t\t\t\tdisplay = style.display;\n\t\t\t\t\t\trestoreDisplay = display === \"none\" ? \"\" : display;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstyle.display = \"inline-block\";\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( opts.overflow ) {\n\t\tstyle.overflow = \"hidden\";\n\t\tanim.always( function() {\n\t\t\tstyle.overflow = opts.overflow[ 0 ];\n\t\t\tstyle.overflowX = opts.overflow[ 1 ];\n\t\t\tstyle.overflowY = opts.overflow[ 2 ];\n\t\t} );\n\t}\n\n\t// Implement show/hide animations\n\tpropTween = false;\n\tfor ( prop in orig ) {\n\n\t\t// General show/hide setup for this element animation\n\t\tif ( !propTween ) {\n\t\t\tif ( dataShow ) {\n\t\t\t\tif ( \"hidden\" in dataShow ) {\n\t\t\t\t\thidden = dataShow.hidden;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdataShow = dataPriv.access( elem, \"fxshow\", { display: restoreDisplay } );\n\t\t\t}\n\n\t\t\t// Store hidden/visible for toggle so `.stop().toggle()` \"reverses\"\n\t\t\tif ( toggle ) {\n\t\t\t\tdataShow.hidden = !hidden;\n\t\t\t}\n\n\t\t\t// Show elements before animating them\n\t\t\tif ( hidden ) {\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t}\n\n\t\t\t/* eslint-disable no-loop-func */\n\n\t\t\tanim.done( function() {\n\n\t\t\t/* eslint-enable no-loop-func */\n\n\t\t\t\t// The final step of a \"hide\" animation is actually hiding the element\n\t\t\t\tif ( !hidden ) {\n\t\t\t\t\tshowHide( [ elem ] );\n\t\t\t\t}\n\t\t\t\tdataPriv.remove( elem, \"fxshow\" );\n\t\t\t\tfor ( prop in orig ) {\n\t\t\t\t\tjQuery.style( elem, prop, orig[ prop ] );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\t// Per-property setup\n\t\tpropTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\t\tif ( !( prop in dataShow ) ) {\n\t\t\tdataShow[ prop ] = propTween.start;\n\t\t\tif ( hidden ) {\n\t\t\t\tpropTween.end = propTween.start;\n\t\t\t\tpropTween.start = 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction propFilter( props, specialEasing ) {\n\tvar index, name, easing, value, hooks;\n\n\t// camelCase, specialEasing and expand cssHook pass\n\tfor ( index in props ) {\n\t\tname = camelCase( index );\n\t\teasing = specialEasing[ name ];\n\t\tvalue = props[ index ];\n\t\tif ( Array.isArray( value ) ) {\n\t\t\teasing = value[ 1 ];\n\t\t\tvalue = props[ index ] = value[ 0 ];\n\t\t}\n\n\t\tif ( index !== name ) {\n\t\t\tprops[ name ] = value;\n\t\t\tdelete props[ index ];\n\t\t}\n\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tif ( hooks && \"expand\" in hooks ) {\n\t\t\tvalue = hooks.expand( value );\n\t\t\tdelete props[ name ];\n\n\t\t\t// Not quite $.extend, this won't overwrite existing keys.\n\t\t\t// Reusing 'index' because we have the correct \"name\"\n\t\t\tfor ( index in value ) {\n\t\t\t\tif ( !( index in props ) ) {\n\t\t\t\t\tprops[ index ] = value[ index ];\n\t\t\t\t\tspecialEasing[ index ] = easing;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tspecialEasing[ name ] = easing;\n\t\t}\n\t}\n}\n\nfunction Animation( elem, properties, options ) {\n\tvar result,\n\t\tstopped,\n\t\tindex = 0,\n\t\tlength = Animation.prefilters.length,\n\t\tdeferred = jQuery.Deferred().always( function() {\n\n\t\t\t// Don't match elem in the :animated selector\n\t\t\tdelete tick.elem;\n\t\t} ),\n\t\ttick = function() {\n\t\t\tif ( stopped ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar currentTime = fxNow || createFxNow(),\n\t\t\t\tremaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n\n\t\t\t\t// Support: Android 2.3 only\n\t\t\t\t// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)\n\t\t\t\ttemp = remaining / animation.duration || 0,\n\t\t\t\tpercent = 1 - temp,\n\t\t\t\tindex = 0,\n\t\t\t\tlength = animation.tweens.length;\n\n\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\tanimation.tweens[ index ].run( percent );\n\t\t\t}\n\n\t\t\tdeferred.notifyWith( elem, [ animation, percent, remaining ] );\n\n\t\t\t// If there's more to do, yield\n\t\t\tif ( percent < 1 && length ) {\n\t\t\t\treturn remaining;\n\t\t\t}\n\n\t\t\t// If this was an empty animation, synthesize a final progress notification\n\t\t\tif ( !length ) {\n\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t}\n\n\t\t\t// Resolve the animation and report its conclusion\n\t\t\tdeferred.resolveWith( elem, [ animation ] );\n\t\t\treturn false;\n\t\t},\n\t\tanimation = deferred.promise( {\n\t\t\telem: elem,\n\t\t\tprops: jQuery.extend( {}, properties ),\n\t\t\topts: jQuery.extend( true, {\n\t\t\t\tspecialEasing: {},\n\t\t\t\teasing: jQuery.easing._default\n\t\t\t}, options ),\n\t\t\toriginalProperties: properties,\n\t\t\toriginalOptions: options,\n\t\t\tstartTime: fxNow || createFxNow(),\n\t\t\tduration: options.duration,\n\t\t\ttweens: [],\n\t\t\tcreateTween: function( prop, end ) {\n\t\t\t\tvar tween = jQuery.Tween( elem, animation.opts, prop, end,\n\t\t\t\t\t\tanimation.opts.specialEasing[ prop ] || animation.opts.easing );\n\t\t\t\tanimation.tweens.push( tween );\n\t\t\t\treturn tween;\n\t\t\t},\n\t\t\tstop: function( gotoEnd ) {\n\t\t\t\tvar index = 0,\n\n\t\t\t\t\t// If we are going to the end, we want to run all the tweens\n\t\t\t\t\t// otherwise we skip this part\n\t\t\t\t\tlength = gotoEnd ? animation.tweens.length : 0;\n\t\t\t\tif ( stopped ) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tstopped = true;\n\t\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\t\tanimation.tweens[ index ].run( 1 );\n\t\t\t\t}\n\n\t\t\t\t// Resolve when we played the last frame; otherwise, reject\n\t\t\t\tif ( gotoEnd ) {\n\t\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t\t\tdeferred.resolveWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t} else {\n\t\t\t\t\tdeferred.rejectWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t}\n\t\t} ),\n\t\tprops = animation.props;\n\n\tpropFilter( props, animation.opts.specialEasing );\n\n\tfor ( ; index < length; index++ ) {\n\t\tresult = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );\n\t\tif ( result ) {\n\t\t\tif ( isFunction( result.stop ) ) {\n\t\t\t\tjQuery._queueHooks( animation.elem, animation.opts.queue ).stop =\n\t\t\t\t\tresult.stop.bind( result );\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tjQuery.map( props, createTween, animation );\n\n\tif ( isFunction( animation.opts.start ) ) {\n\t\tanimation.opts.start.call( elem, animation );\n\t}\n\n\t// Attach callbacks from options\n\tanimation\n\t\t.progress( animation.opts.progress )\n\t\t.done( animation.opts.done, animation.opts.complete )\n\t\t.fail( animation.opts.fail )\n\t\t.always( animation.opts.always );\n\n\tjQuery.fx.timer(\n\t\tjQuery.extend( tick, {\n\t\t\telem: elem,\n\t\t\tanim: animation,\n\t\t\tqueue: animation.opts.queue\n\t\t} )\n\t);\n\n\treturn animation;\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\n\ttweeners: {\n\t\t\"*\": [ function( prop, value ) {\n\t\t\tvar tween = this.createTween( prop, value );\n\t\t\tadjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );\n\t\t\treturn tween;\n\t\t} ]\n\t},\n\n\ttweener: function( props, callback ) {\n\t\tif ( isFunction( props ) ) {\n\t\t\tcallback = props;\n\t\t\tprops = [ \"*\" ];\n\t\t} else {\n\t\t\tprops = props.match( rnothtmlwhite );\n\t\t}\n\n\t\tvar prop,\n\t\t\tindex = 0,\n\t\t\tlength = props.length;\n\n\t\tfor ( ; index < length; index++ ) {\n\t\t\tprop = props[ index ];\n\t\t\tAnimation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];\n\t\t\tAnimation.tweeners[ prop ].unshift( callback );\n\t\t}\n\t},\n\n\tprefilters: [ defaultPrefilter ],\n\n\tprefilter: function( callback, prepend ) {\n\t\tif ( prepend ) {\n\t\t\tAnimation.prefilters.unshift( callback );\n\t\t} else {\n\t\t\tAnimation.prefilters.push( callback );\n\t\t}\n\t}\n} );\n\njQuery.speed = function( speed, easing, fn ) {\n\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\tcomplete: fn || !fn && easing ||\n\t\t\tisFunction( speed ) && speed,\n\t\tduration: speed,\n\t\teasing: fn && easing || easing && !isFunction( easing ) && easing\n\t};\n\n\t// Go to the end state if fx are off\n\tif ( jQuery.fx.off ) {\n\t\topt.duration = 0;\n\n\t} else {\n\t\tif ( typeof opt.duration !== \"number\" ) {\n\t\t\tif ( opt.duration in jQuery.fx.speeds ) {\n\t\t\t\topt.duration = jQuery.fx.speeds[ opt.duration ];\n\n\t\t\t} else {\n\t\t\t\topt.duration = jQuery.fx.speeds._default;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Normalize opt.queue - true/undefined/null -> \"fx\"\n\tif ( opt.queue == null || opt.queue === true ) {\n\t\topt.queue = \"fx\";\n\t}\n\n\t// Queueing\n\topt.old = opt.complete;\n\n\topt.complete = function() {\n\t\tif ( isFunction( opt.old ) ) {\n\t\t\topt.old.call( this );\n\t\t}\n\n\t\tif ( opt.queue ) {\n\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t}\n\t};\n\n\treturn opt;\n};\n\njQuery.fn.extend( {\n\tfadeTo: function( speed, to, easing, callback ) {\n\n\t\t// Show any hidden elements after setting opacity to 0\n\t\treturn this.filter( isHiddenWithinTree ).css( \"opacity\", 0 ).show()\n\n\t\t\t// Animate to the value specified\n\t\t\t.end().animate( { opacity: to }, speed, easing, callback );\n\t},\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar empty = jQuery.isEmptyObject( prop ),\n\t\t\toptall = jQuery.speed( speed, easing, callback ),\n\t\t\tdoAnimation = function() {\n\n\t\t\t\t// Operate on a copy of prop so per-property easing won't be lost\n\t\t\t\tvar anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n\t\t\t\t// Empty animations, or finishing resolves immediately\n\t\t\t\tif ( empty || dataPriv.get( this, \"finish\" ) ) {\n\t\t\t\t\tanim.stop( true );\n\t\t\t\t}\n\t\t\t};\n\t\t\tdoAnimation.finish = doAnimation;\n\n\t\treturn empty || optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tvar stopQueue = function( hooks ) {\n\t\t\tvar stop = hooks.stop;\n\t\t\tdelete hooks.stop;\n\t\t\tstop( gotoEnd );\n\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue && type !== false ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar dequeue = true,\n\t\t\t\tindex = type != null && type + \"queueHooks\",\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = dataPriv.get( this );\n\n\t\t\tif ( index ) {\n\t\t\t\tif ( data[ index ] && data[ index ].stop ) {\n\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n\t\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this &&\n\t\t\t\t\t( type == null || timers[ index ].queue === type ) ) {\n\n\t\t\t\t\ttimers[ index ].anim.stop( gotoEnd );\n\t\t\t\t\tdequeue = false;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Start the next in the queue if the last step wasn't forced.\n\t\t\t// Timers currently will call their complete callbacks, which\n\t\t\t// will dequeue but only if they were gotoEnd.\n\t\t\tif ( dequeue || !gotoEnd ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t} );\n\t},\n\tfinish: function( type ) {\n\t\tif ( type !== false ) {\n\t\t\ttype = type || \"fx\";\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tvar index,\n\t\t\t\tdata = dataPriv.get( this ),\n\t\t\t\tqueue = data[ type + \"queue\" ],\n\t\t\t\thooks = data[ type + \"queueHooks\" ],\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tlength = queue ? queue.length : 0;\n\n\t\t\t// Enable finishing flag on private data\n\t\t\tdata.finish = true;\n\n\t\t\t// Empty the queue first\n\t\t\tjQuery.queue( this, type, [] );\n\n\t\t\tif ( hooks && hooks.stop ) {\n\t\t\t\thooks.stop.call( this, true );\n\t\t\t}\n\n\t\t\t// Look for any active animations, and finish them\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n\t\t\t\t\ttimers[ index ].anim.stop( true );\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Look for any animations in the old queue and finish them\n\t\t\tfor ( index = 0; index < length; index++ ) {\n\t\t\t\tif ( queue[ index ] && queue[ index ].finish ) {\n\t\t\t\t\tqueue[ index ].finish.call( this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Turn off finishing flag\n\t\t\tdelete data.finish;\n\t\t} );\n\t}\n} );\n\njQuery.each( [ \"toggle\", \"show\", \"hide\" ], function( i, name ) {\n\tvar cssFn = jQuery.fn[ name ];\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn speed == null || typeof speed === \"boolean\" ?\n\t\t\tcssFn.apply( this, arguments ) :\n\t\t\tthis.animate( genFx( name, true ), speed, easing, callback );\n\t};\n} );\n\n// Generate shortcuts for custom animations\njQuery.each( {\n\tslideDown: genFx( \"show\" ),\n\tslideUp: genFx( \"hide\" ),\n\tslideToggle: genFx( \"toggle\" ),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n} );\n\njQuery.timers = [];\njQuery.fx.tick = function() {\n\tvar timer,\n\t\ti = 0,\n\t\ttimers = jQuery.timers;\n\n\tfxNow = Date.now();\n\n\tfor ( ; i < timers.length; i++ ) {\n\t\ttimer = timers[ i ];\n\n\t\t// Run the timer and safely remove it when done (allowing for external removal)\n\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\ttimers.splice( i--, 1 );\n\t\t}\n\t}\n\n\tif ( !timers.length ) {\n\t\tjQuery.fx.stop();\n\t}\n\tfxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n\tjQuery.timers.push( timer );\n\tjQuery.fx.start();\n};\n\njQuery.fx.interval = 13;\njQuery.fx.start = function() {\n\tif ( inProgress ) {\n\t\treturn;\n\t}\n\n\tinProgress = true;\n\tschedule();\n};\n\njQuery.fx.stop = function() {\n\tinProgress = null;\n};\n\njQuery.fx.speeds = {\n\tslow: 600,\n\tfast: 200,\n\n\t// Default speed\n\t_default: 400\n};\n\n\n// Based off of the plugin by Clint Helfers, with permission.\n// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/\njQuery.fn.delay = function( time, type ) {\n\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\ttype = type || \"fx\";\n\n\treturn this.queue( type, function( next, hooks ) {\n\t\tvar timeout = window.setTimeout( next, time );\n\t\thooks.stop = function() {\n\t\t\twindow.clearTimeout( timeout );\n\t\t};\n\t} );\n};\n\n\n( function() {\n\tvar input = document.createElement( \"input\" ),\n\t\tselect = document.createElement( \"select\" ),\n\t\topt = select.appendChild( document.createElement( \"option\" ) );\n\n\tinput.type = \"checkbox\";\n\n\t// Support: Android <=4.3 only\n\t// Default value for a checkbox should be \"on\"\n\tsupport.checkOn = input.value !== \"\";\n\n\t// Support: IE <=11 only\n\t// Must access selectedIndex to make default options select\n\tsupport.optSelected = opt.selected;\n\n\t// Support: IE <=11 only\n\t// An input loses its value after becoming a radio\n\tinput = document.createElement( \"input\" );\n\tinput.value = \"t\";\n\tinput.type = \"radio\";\n\tsupport.radioValue = input.value === \"t\";\n} )();\n\n\nvar boolHook,\n\tattrHandle = jQuery.expr.attrHandle;\n\njQuery.fn.extend( {\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tattr: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set attributes on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// Attribute hooks are determined by the lowercase version\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\thooks = jQuery.attrHooks[ name.toLowerCase() ] ||\n\t\t\t\t( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, value + \"\" );\n\t\t\treturn value;\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tret = jQuery.find.attr( elem, name );\n\n\t\t// Non-existent attributes return null, we normalize to undefined\n\t\treturn ret == null ? undefined : ret;\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( !support.radioValue && value === \"radio\" &&\n\t\t\t\t\tnodeName( elem, \"input\" ) ) {\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name,\n\t\t\ti = 0,\n\n\t\t\t// Attribute names can contain non-HTML whitespace characters\n\t\t\t// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n\t\t\tattrNames = value && value.match( rnothtmlwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( ( name = attrNames[ i++ ] ) ) {\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Hooks for boolean attributes\nboolHook = {\n\tset: function( elem, value, name ) {\n\t\tif ( value === false ) {\n\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else {\n\t\t\telem.setAttribute( name, name );\n\t\t}\n\t\treturn name;\n\t}\n};\n\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( i, name ) {\n\tvar getter = attrHandle[ name ] || jQuery.find.attr;\n\n\tattrHandle[ name ] = function( elem, name, isXML ) {\n\t\tvar ret, handle,\n\t\t\tlowercaseName = name.toLowerCase();\n\n\t\tif ( !isXML ) {\n\n\t\t\t// Avoid an infinite loop by temporarily removing this function from the getter\n\t\t\thandle = attrHandle[ lowercaseName ];\n\t\t\tattrHandle[ lowercaseName ] = ret;\n\t\t\tret = getter( elem, name, isXML ) != null ?\n\t\t\t\tlowercaseName :\n\t\t\t\tnull;\n\t\t\tattrHandle[ lowercaseName ] = handle;\n\t\t}\n\t\treturn ret;\n\t};\n} );\n\n\n\n\nvar rfocusable = /^(?:input|select|textarea|button)$/i,\n\trclickable = /^(?:a|area)$/i;\n\njQuery.fn.extend( {\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tdelete this[ jQuery.propFix[ name ] || name ];\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set properties on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\treturn ( elem[ name ] = value );\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\treturn elem[ name ];\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\t// Support: IE <=9 - 11 only\n\t\t\t\t// elem.tabIndex doesn't always return the\n\t\t\t\t// correct value when it hasn't been explicitly set\n\t\t\t\t// https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/\n\t\t\t\t// Use proper attribute retrieval(#12072)\n\t\t\t\tvar tabindex = jQuery.find.attr( elem, \"tabindex\" );\n\n\t\t\t\tif ( tabindex ) {\n\t\t\t\t\treturn parseInt( tabindex, 10 );\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\trfocusable.test( elem.nodeName ) ||\n\t\t\t\t\trclickable.test( elem.nodeName ) &&\n\t\t\t\t\telem.href\n\t\t\t\t) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t},\n\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t}\n} );\n\n// Support: IE <=11 only\n// Accessing the selectedIndex property\n// forces the browser to respect setting selected\n// on the option\n// The getter ensures a default option is selected\n// when in an optgroup\n// eslint rule \"no-unused-expressions\" is disabled for this code\n// since it considers such accessions noop\nif ( !support.optSelected ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent && parent.parentNode ) {\n\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tset: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent ) {\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\njQuery.each( [\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n} );\n\n\n\n\n\t// Strip and collapse whitespace according to HTML spec\n\t// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace\n\tfunction stripAndCollapse( value ) {\n\t\tvar tokens = value.match( rnothtmlwhite ) || [];\n\t\treturn tokens.join( \" \" );\n\t}\n\n\nfunction getClass( elem ) {\n\treturn elem.getAttribute && elem.getAttribute( \"class\" ) || \"\";\n}\n\nfunction classesToArray( value ) {\n\tif ( Array.isArray( value ) ) {\n\t\treturn value;\n\t}\n\tif ( typeof value === \"string\" ) {\n\t\treturn value.match( rnothtmlwhite ) || [];\n\t}\n\treturn [];\n}\n\njQuery.fn.extend( {\n\taddClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tclasses = classesToArray( value );\n\n\t\tif ( classes.length ) {\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + clazz + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += clazz + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tif ( !arguments.length ) {\n\t\t\treturn this.attr( \"class\", \"\" );\n\t\t}\n\n\t\tclasses = classesToArray( value );\n\n\t\tif ( classes.length ) {\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + clazz + \" \" ) > -1 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + clazz + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar type = typeof value,\n\t\t\tisValidValue = type === \"string\" || Array.isArray( value );\n\n\t\tif ( typeof stateVal === \"boolean\" && isValidValue ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).toggleClass(\n\t\t\t\t\tvalue.call( this, i, getClass( this ), stateVal ),\n\t\t\t\t\tstateVal\n\t\t\t\t);\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar className, i, self, classNames;\n\n\t\t\tif ( isValidValue ) {\n\n\t\t\t\t// Toggle individual class names\n\t\t\t\ti = 0;\n\t\t\t\tself = jQuery( this );\n\t\t\t\tclassNames = classesToArray( value );\n\n\t\t\t\twhile ( ( className = classNames[ i++ ] ) ) {\n\n\t\t\t\t\t// Check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Toggle whole class name\n\t\t\t} else if ( value === undefined || type === \"boolean\" ) {\n\t\t\t\tclassName = getClass( this );\n\t\t\t\tif ( className ) {\n\n\t\t\t\t\t// Store className if set\n\t\t\t\t\tdataPriv.set( this, \"__className__\", className );\n\t\t\t\t}\n\n\t\t\t\t// If the element has a class name or if we're passed `false`,\n\t\t\t\t// then remove the whole classname (if there was one, the above saved it).\n\t\t\t\t// Otherwise bring back whatever was previously saved (if anything),\n\t\t\t\t// falling back to the empty string if nothing was stored.\n\t\t\t\tif ( this.setAttribute ) {\n\t\t\t\t\tthis.setAttribute( \"class\",\n\t\t\t\t\t\tclassName || value === false ?\n\t\t\t\t\t\t\"\" :\n\t\t\t\t\t\tdataPriv.get( this, \"__className__\" ) || \"\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className, elem,\n\t\t\ti = 0;\n\n\t\tclassName = \" \" + selector + \" \";\n\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\tif ( elem.nodeType === 1 &&\n\t\t\t\t( \" \" + stripAndCollapse( getClass( elem ) ) + \" \" ).indexOf( className ) > -1 ) {\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n} );\n\n\n\n\nvar rreturn = /\\r/g;\n\njQuery.fn.extend( {\n\tval: function( value ) {\n\t\tvar hooks, ret, valueIsFunction,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] ||\n\t\t\t\t\tjQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks &&\n\t\t\t\t\t\"get\" in hooks &&\n\t\t\t\t\t( ret = hooks.get( elem, \"value\" ) ) !== undefined\n\t\t\t\t) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\t// Handle most common string cases\n\t\t\t\tif ( typeof ret === \"string\" ) {\n\t\t\t\t\treturn ret.replace( rreturn, \"\" );\n\t\t\t\t}\n\n\t\t\t\t// Handle cases where value is null/undef or number\n\t\t\t\treturn ret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tvalueIsFunction = isFunction( value );\n\n\t\treturn this.each( function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\n\t\t\t} else if ( Array.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !( \"set\" in hooks ) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\tvar val = jQuery.find.attr( elem, \"value\" );\n\t\t\t\treturn val != null ?\n\t\t\t\t\tval :\n\n\t\t\t\t\t// Support: IE <=10 - 11 only\n\t\t\t\t\t// option.text throws exceptions (#14686, #14858)\n\t\t\t\t\t// Strip and collapse whitespace\n\t\t\t\t\t// https://html.spec.whatwg.org/#strip-and-collapse-whitespace\n\t\t\t\t\tstripAndCollapse( jQuery.text( elem ) );\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option, i,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\",\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length;\n\n\t\t\t\tif ( index < 0 ) {\n\t\t\t\t\ti = max;\n\n\t\t\t\t} else {\n\t\t\t\t\ti = one ? index : 0;\n\t\t\t\t}\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t// IE8-9 doesn't update selected after form reset (#2551)\n\t\t\t\t\tif ( ( option.selected || i === index ) &&\n\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t!option.disabled &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled ||\n\t\t\t\t\t\t\t\t!nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t/* eslint-disable no-cond-assign */\n\n\t\t\t\t\tif ( option.selected =\n\t\t\t\t\t\tjQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1\n\t\t\t\t\t) {\n\t\t\t\t\t\toptionSet = true;\n\t\t\t\t\t}\n\n\t\t\t\t\t/* eslint-enable no-cond-assign */\n\t\t\t\t}\n\n\t\t\t\t// Force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Radios and checkboxes getter/setter\njQuery.each( [ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( Array.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );\n\t\t\t}\n\t\t}\n\t};\n\tif ( !support.checkOn ) {\n\t\tjQuery.valHooks[ this ].get = function( elem ) {\n\t\t\treturn elem.getAttribute( \"value\" ) === null ? \"on\" : elem.value;\n\t\t};\n\t}\n} );\n\n\n\n\n// Return jQuery for attributes-only inclusion\n\n\nsupport.focusin = \"onfocusin\" in window;\n\n\nvar rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\tstopPropagationCallback = function( e ) {\n\t\te.stopPropagation();\n\t};\n\njQuery.extend( jQuery.event, {\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\n\t\tvar i, cur, tmp, bubbleType, ontype, handle, special, lastElement,\n\t\t\teventPath = [ elem || document ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split( \".\" ) : [];\n\n\t\tcur = lastElement = tmp = elem = elem || document;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf( \".\" ) > -1 ) {\n\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split( \".\" );\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf( \":\" ) < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join( \".\" );\n\t\tevent.rnamespace = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (#9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === ( elem.ownerDocument || document ) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tlastElement = cur;\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( dataPriv.get( cur, \"events\" ) || {} )[ event.type ] &&\n\t\t\t\tdataPriv.get( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( ( !special._default ||\n\t\t\t\tspecial._default.apply( eventPath.pop(), data ) === false ) &&\n\t\t\t\tacceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name as the event.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (#6170)\n\t\t\t\tif ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.addEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\telem[ type ]();\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.removeEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\t// Piggyback on a donor event to simulate a different one\n\t// Used only for `focus(in | out)` events\n\tsimulate: function( type, elem, event ) {\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true\n\t\t\t}\n\t\t);\n\n\t\tjQuery.event.trigger( e, null, elem );\n\t}\n\n} );\n\njQuery.fn.extend( {\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t} );\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[ 0 ];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n} );\n\n\n// Support: Firefox <=44\n// Firefox doesn't have focus(in | out) events\n// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787\n//\n// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1\n// focus(in | out) events fire after focus & blur events,\n// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order\n// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857\nif ( !support.focusin ) {\n\tjQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler on the document while someone wants focusin/focusout\n\t\tvar handler = function( event ) {\n\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );\n\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix );\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t\tdataPriv.access( doc, fix, ( attaches || 0 ) + 1 );\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix ) - 1;\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.removeEventListener( orig, handler, true );\n\t\t\t\t\tdataPriv.remove( doc, fix );\n\n\t\t\t\t} else {\n\t\t\t\t\tdataPriv.access( doc, fix, attaches );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t} );\n}\nvar location = window.location;\n\nvar nonce = Date.now();\n\nvar rquery = ( /\\?/ );\n\n\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\n\t// Support: IE 9 - 11 only\n\t// IE throws on parseFromString with invalid input.\n\ttry {\n\t\txml = ( new window.DOMParser() ).parseFromString( data, \"text/xml\" );\n\t} catch ( e ) {\n\t\txml = undefined;\n\t}\n\n\tif ( !xml || xml.getElementsByTagName( \"parsererror\" ).length ) {\n\t\tjQuery.error( \"Invalid XML: \" + data );\n\t}\n\treturn xml;\n};\n\n\nvar\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( Array.isArray( obj ) ) {\n\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams(\n\t\t\t\t\tprefix + \"[\" + ( typeof v === \"object\" && v != null ? i : \"\" ) + \"]\",\n\t\t\t\t\tv,\n\t\t\t\t\ttraditional,\n\t\t\t\t\tadd\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\n\t} else if ( !traditional && toType( obj ) === \"object\" ) {\n\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, valueOrFunction ) {\n\n\t\t\t// If value is a function, invoke it and use its return value\n\t\t\tvar value = isFunction( valueOrFunction ) ?\n\t\t\t\tvalueOrFunction() :\n\t\t\t\tvalueOrFunction;\n\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" +\n\t\t\t\tencodeURIComponent( value == null ? \"\" : value );\n\t\t};\n\n\tif ( a == null ) {\n\t\treturn \"\";\n\t}\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t} );\n\n\t} else {\n\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" );\n};\n\njQuery.fn.extend( {\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map( function() {\n\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t} )\n\t\t.filter( function() {\n\t\t\tvar type = this.type;\n\n\t\t\t// Use .is( \":disabled\" ) so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t} )\n\t\t.map( function( i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\tif ( val == null ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\treturn jQuery.map( val, function( val ) {\n\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t} ).get();\n\t}\n} );\n\n\nvar\n\tr20 = /%20/g,\n\trhash = /#.*$/,\n\trantiCache = /([?&])_=[^&]*/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg,\n\n\t// #7653, #8125, #8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t *    - BEFORE asking for a transport\n\t *    - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n\tallTypes = \"*/\".concat( \"*\" ),\n\n\t// Anchor tag for parsing the document origin\n\toriginAnchor = document.createElement( \"a\" );\n\toriginAnchor.href = location.href;\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tvar dataType,\n\t\t\ti = 0,\n\t\t\tdataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];\n\n\t\tif ( isFunction( func ) ) {\n\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\twhile ( ( dataType = dataTypes[ i++ ] ) ) {\n\n\t\t\t\t// Prepend if requested\n\t\t\t\tif ( dataType[ 0 ] === \"+\" ) {\n\t\t\t\t\tdataType = dataType.slice( 1 ) || \"*\";\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );\n\n\t\t\t\t// Otherwise append\n\t\t\t\t} else {\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).push( func );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n\tvar inspected = {},\n\t\tseekingTransport = ( structure === transports );\n\n\tfunction inspect( dataType ) {\n\t\tvar selected;\n\t\tinspected[ dataType ] = true;\n\t\tjQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n\t\t\tvar dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n\t\t\tif ( typeof dataTypeOrTransport === \"string\" &&\n\t\t\t\t!seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n\n\t\t\t\toptions.dataTypes.unshift( dataTypeOrTransport );\n\t\t\t\tinspect( dataTypeOrTransport );\n\t\t\t\treturn false;\n\t\t\t} else if ( seekingTransport ) {\n\t\t\t\treturn !( selected = dataTypeOrTransport );\n\t\t\t}\n\t\t} );\n\t\treturn selected;\n\t}\n\n\treturn inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n\tvar key, deep,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n\n\treturn target;\n}\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n\tvar ct, type, finalDataType, firstDataType,\n\t\tcontents = s.contents,\n\t\tdataTypes = s.dataTypes;\n\n\t// Remove auto dataType and get content-type in the process\n\twhile ( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader( \"Content-Type\" );\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[ 0 ] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n\tvar conv2, current, conv, tmp, prev,\n\t\tconverters = {},\n\n\t\t// Work with a copy of dataTypes in case we need to modify it for conversion\n\t\tdataTypes = s.dataTypes.slice();\n\n\t// Create converters map with lowercased keys\n\tif ( dataTypes[ 1 ] ) {\n\t\tfor ( conv in s.converters ) {\n\t\t\tconverters[ conv.toLowerCase() ] = s.converters[ conv ];\n\t\t}\n\t}\n\n\tcurrent = dataTypes.shift();\n\n\t// Convert to each sequential dataType\n\twhile ( current ) {\n\n\t\tif ( s.responseFields[ current ] ) {\n\t\t\tjqXHR[ s.responseFields[ current ] ] = response;\n\t\t}\n\n\t\t// Apply the dataFilter if provided\n\t\tif ( !prev && isSuccess && s.dataFilter ) {\n\t\t\tresponse = s.dataFilter( response, s.dataType );\n\t\t}\n\n\t\tprev = current;\n\t\tcurrent = dataTypes.shift();\n\n\t\tif ( current ) {\n\n\t\t\t// There's only work to do if current dataType is non-auto\n\t\t\tif ( current === \"*\" ) {\n\n\t\t\t\tcurrent = prev;\n\n\t\t\t// Convert response if prev dataType is non-auto and differs from current\n\t\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t\t// Seek a direct converter\n\t\t\t\tconv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n\t\t\t\t// If none found, seek a pair\n\t\t\t\tif ( !conv ) {\n\t\t\t\t\tfor ( conv2 in converters ) {\n\n\t\t\t\t\t\t// If conv2 outputs current\n\t\t\t\t\t\ttmp = conv2.split( \" \" );\n\t\t\t\t\t\tif ( tmp[ 1 ] === current ) {\n\n\t\t\t\t\t\t\t// If prev can be converted to accepted input\n\t\t\t\t\t\t\tconv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n\t\t\t\t\t\t\t\tconverters[ \"* \" + tmp[ 0 ] ];\n\t\t\t\t\t\t\tif ( conv ) {\n\n\t\t\t\t\t\t\t\t// Condense equivalence converters\n\t\t\t\t\t\t\t\tif ( conv === true ) {\n\t\t\t\t\t\t\t\t\tconv = converters[ conv2 ];\n\n\t\t\t\t\t\t\t\t// Otherwise, insert the intermediate dataType\n\t\t\t\t\t\t\t\t} else if ( converters[ conv2 ] !== true ) {\n\t\t\t\t\t\t\t\t\tcurrent = tmp[ 0 ];\n\t\t\t\t\t\t\t\t\tdataTypes.unshift( tmp[ 1 ] );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply converter (if not an equivalence)\n\t\t\t\tif ( conv !== true ) {\n\n\t\t\t\t\t// Unless errors are allowed to bubble, catch and return them\n\t\t\t\t\tif ( conv && s.throws ) {\n\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tstate: \"parsererror\",\n\t\t\t\t\t\t\t\terror: conv ? e : \"No conversion from \" + prev + \" to \" + current\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { state: \"success\", data: response };\n}\n\njQuery.extend( {\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {},\n\n\tajaxSettings: {\n\t\turl: location.href,\n\t\ttype: \"GET\",\n\t\tisLocal: rlocalProtocol.test( location.protocol ),\n\t\tglobal: true,\n\t\tprocessData: true,\n\t\tasync: true,\n\t\tcontentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\tthrows: false,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\t\"*\": allTypes,\n\t\t\ttext: \"text/plain\",\n\t\t\thtml: \"text/html\",\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\tjson: \"application/json, text/javascript\"\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /\\bxml\\b/,\n\t\t\thtml: /\\bhtml/,\n\t\t\tjson: /\\bjson\\b/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\",\n\t\t\tjson: \"responseJSON\"\n\t\t},\n\n\t\t// Data converters\n\t\t// Keys separate source (or catchall \"*\") and destination types with a single space\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": JSON.parse,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\turl: true,\n\t\t\tcontext: true\n\t\t}\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\treturn settings ?\n\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n\t\t\t// Extending ajaxSettings\n\t\t\tajaxExtend( jQuery.ajaxSettings, target );\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar transport,\n\n\t\t\t// URL without anti-cache param\n\t\t\tcacheURL,\n\n\t\t\t// Response headers\n\t\t\tresponseHeadersString,\n\t\t\tresponseHeaders,\n\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\n\t\t\t// Url cleanup var\n\t\t\turlAnchor,\n\n\t\t\t// Request state (becomes false upon send and true upon completion)\n\t\t\tcompleted,\n\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\n\t\t\t// Loop variable\n\t\t\ti,\n\n\t\t\t// uncached part of the url\n\t\t\tuncached,\n\n\t\t\t// Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\n\t\t\t// Context for global events is callbackContext if it is a DOM node or jQuery collection\n\t\t\tglobalEventContext = s.context &&\n\t\t\t\t( callbackContext.nodeType || callbackContext.jquery ) ?\n\t\t\t\t\tjQuery( callbackContext ) :\n\t\t\t\t\tjQuery.event,\n\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks( \"once memory\" ),\n\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\n\t\t\t// Default abort message\n\t\t\tstrAbort = \"canceled\",\n\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( completed ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile ( ( match = rheaders.exec( responseHeadersString ) ) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[ 1 ].toLowerCase() + \" \" ] =\n\t\t\t\t\t\t\t\t\t( responseHeaders[ match[ 1 ].toLowerCase() + \" \" ] || [] )\n\t\t\t\t\t\t\t\t\t\t.concat( match[ 2 ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() + \" \" ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match == null ? null : match.join( \", \" );\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn completed ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\tname = requestHeadersNames[ name.toLowerCase() ] =\n\t\t\t\t\t\t\trequestHeadersNames[ name.toLowerCase() ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Status-dependent callbacks\n\t\t\t\tstatusCode: function( map ) {\n\t\t\t\t\tvar code;\n\t\t\t\t\tif ( map ) {\n\t\t\t\t\t\tif ( completed ) {\n\n\t\t\t\t\t\t\t// Execute the appropriate callbacks\n\t\t\t\t\t\t\tjqXHR.always( map[ jqXHR.status ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Lazy-add the new callbacks in a way that preserves old ones\n\t\t\t\t\t\t\tfor ( code in map ) {\n\t\t\t\t\t\t\t\tstatusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tvar finalText = statusText || strAbort;\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( finalText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, finalText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR );\n\n\t\t// Add protocol if not provided (prefilters might expect it)\n\t\t// Handle falsy url in the settings object (#10093: consistency with old signature)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url || location.href ) + \"\" )\n\t\t\t.replace( rprotocol, location.protocol + \"//\" );\n\n\t\t// Alias method option to type as per ticket #12004\n\t\ts.type = options.method || options.type || s.method || s.type;\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = ( s.dataType || \"*\" ).toLowerCase().match( rnothtmlwhite ) || [ \"\" ];\n\n\t\t// A cross-domain request is in order when the origin doesn't match the current origin.\n\t\tif ( s.crossDomain == null ) {\n\t\t\turlAnchor = document.createElement( \"a\" );\n\n\t\t\t// Support: IE <=8 - 11, Edge 12 - 15\n\t\t\t// IE throws exception on accessing the href property if url is malformed,\n\t\t\t// e.g. http://example.com:80x/\n\t\t\ttry {\n\t\t\t\turlAnchor.href = s.url;\n\n\t\t\t\t// Support: IE <=8 - 11 only\n\t\t\t\t// Anchor's host property isn't correctly set when s.url is relative\n\t\t\t\turlAnchor.href = urlAnchor.href;\n\t\t\t\ts.crossDomain = originAnchor.protocol + \"//\" + originAnchor.host !==\n\t\t\t\t\turlAnchor.protocol + \"//\" + urlAnchor.host;\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// If there is an error parsing the URL, assume it is crossDomain,\n\t\t\t\t// it can be rejected by the transport if it is invalid\n\t\t\t\ts.crossDomain = true;\n\t\t\t}\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefilter, stop there\n\t\tif ( completed ) {\n\t\t\treturn jqXHR;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\t// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)\n\t\tfireGlobals = jQuery.event && s.global;\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger( \"ajaxStart\" );\n\t\t}\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Save the URL in case we're toying with the If-Modified-Since\n\t\t// and/or If-None-Match header later on\n\t\t// Remove hash to simplify url manipulation\n\t\tcacheURL = s.url.replace( rhash, \"\" );\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// Remember the hash so we can put it back\n\t\t\tuncached = s.url.slice( cacheURL.length );\n\n\t\t\t// If data is available and should be processed, append data to url\n\t\t\tif ( s.data && ( s.processData || typeof s.data === \"string\" ) ) {\n\t\t\t\tcacheURL += ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data;\n\n\t\t\t\t// #9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Add or update anti-cache param if needed\n\t\t\tif ( s.cache === false ) {\n\t\t\t\tcacheURL = cacheURL.replace( rantiCache, \"$1\" );\n\t\t\t\tuncached = ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + ( nonce++ ) + uncached;\n\t\t\t}\n\n\t\t\t// Put hash and anti-cache on the URL that will be requested (gh-1732)\n\t\t\ts.url = cacheURL + uncached;\n\n\t\t// Change '%20' to '+' if this is encoded form body content (gh-2658)\n\t\t} else if ( s.data && s.processData &&\n\t\t\t( s.contentType || \"\" ).indexOf( \"application/x-www-form-urlencoded\" ) === 0 ) {\n\t\t\ts.data = s.data.replace( r20, \"+\" );\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tif ( jQuery.lastModified[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[ 0 ] ] +\n\t\t\t\t\t( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend &&\n\t\t\t( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {\n\n\t\t\t// Abort if not done already and return\n\t\t\treturn jqXHR.abort();\n\t\t}\n\n\t\t// Aborting is no longer a cancellation\n\t\tstrAbort = \"abort\";\n\n\t\t// Install callbacks on deferreds\n\t\tcompleteDeferred.add( s.complete );\n\t\tjqXHR.done( s.success );\n\t\tjqXHR.fail( s.error );\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\n\t\t\t// If request was aborted inside ajaxSend, stop there\n\t\t\tif ( completed ) {\n\t\t\t\treturn jqXHR;\n\t\t\t}\n\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = window.setTimeout( function() {\n\t\t\t\t\tjqXHR.abort( \"timeout\" );\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tcompleted = false;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// Rethrow post-completion exceptions\n\t\t\t\tif ( completed ) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\n\t\t\t\t// Propagate others as results\n\t\t\t\tdone( -1, e );\n\t\t\t}\n\t\t}\n\n\t\t// Callback for when everything is done\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\t\t\tvar isSuccess, success, error, response, modified,\n\t\t\t\tstatusText = nativeStatusText;\n\n\t\t\t// Ignore repeat invocations\n\t\t\tif ( completed ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcompleted = true;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\twindow.clearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\t// Determine if successful\n\t\t\tisSuccess = status >= 200 && status < 300 || status === 304;\n\n\t\t\t// Get response data\n\t\t\tif ( responses ) {\n\t\t\t\tresponse = ajaxHandleResponses( s, jqXHR, responses );\n\t\t\t}\n\n\t\t\t// Convert no matter what (that way responseXXX fields are always set)\n\t\t\tresponse = ajaxConvert( s, response, jqXHR, isSuccess );\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( isSuccess ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"Last-Modified\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.lastModified[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"etag\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.etag[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// if no content\n\t\t\t\tif ( status === 204 || s.type === \"HEAD\" ) {\n\t\t\t\t\tstatusText = \"nocontent\";\n\n\t\t\t\t// if not modified\n\t\t\t\t} else if ( status === 304 ) {\n\t\t\t\t\tstatusText = \"notmodified\";\n\n\t\t\t\t// If we have data, let's convert it\n\t\t\t\t} else {\n\t\t\t\t\tstatusText = response.state;\n\t\t\t\t\tsuccess = response.data;\n\t\t\t\t\terror = response.error;\n\t\t\t\t\tisSuccess = !error;\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// Extract error from statusText and normalize for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( status || !statusText ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger( \"ajaxStop\" );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t}\n} );\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\n\t\t// Shift arguments if data argument was omitted\n\t\tif ( isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\t// The url can be an options object (which then must have .url)\n\t\treturn jQuery.ajax( jQuery.extend( {\n\t\t\turl: url,\n\t\t\ttype: method,\n\t\t\tdataType: type,\n\t\t\tdata: data,\n\t\t\tsuccess: callback\n\t\t}, jQuery.isPlainObject( url ) && url ) );\n\t};\n} );\n\n\njQuery._evalUrl = function( url, options ) {\n\treturn jQuery.ajax( {\n\t\turl: url,\n\n\t\t// Make this explicit, since user can override this through ajaxSetup (#11264)\n\t\ttype: \"GET\",\n\t\tdataType: \"script\",\n\t\tcache: true,\n\t\tasync: false,\n\t\tglobal: false,\n\n\t\t// Only evaluate the response if it is successful (gh-4126)\n\t\t// dataFilter is not invoked for failure responses, so using it instead\n\t\t// of the default converter is kludgy but it works.\n\t\tconverters: {\n\t\t\t\"text script\": function() {}\n\t\t},\n\t\tdataFilter: function( response ) {\n\t\t\tjQuery.globalEval( response, options );\n\t\t}\n\t} );\n};\n\n\njQuery.fn.extend( {\n\twrapAll: function( html ) {\n\t\tvar wrap;\n\n\t\tif ( this[ 0 ] ) {\n\t\t\tif ( isFunction( html ) ) {\n\t\t\t\thtml = html.call( this[ 0 ] );\n\t\t\t}\n\n\t\t\t// The elements to wrap the target around\n\t\t\twrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n\t\t\tif ( this[ 0 ].parentNode ) {\n\t\t\t\twrap.insertBefore( this[ 0 ] );\n\t\t\t}\n\n\t\t\twrap.map( function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstElementChild ) {\n\t\t\t\t\telem = elem.firstElementChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t} ).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( isFunction( html ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).wrapInner( html.call( this, i ) );\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t} );\n\t},\n\n\twrap: function( html ) {\n\t\tvar htmlIsFunction = isFunction( html );\n\n\t\treturn this.each( function( i ) {\n\t\t\tjQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );\n\t\t} );\n\t},\n\n\tunwrap: function( selector ) {\n\t\tthis.parent( selector ).not( \"body\" ).each( function() {\n\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t} );\n\t\treturn this;\n\t}\n} );\n\n\njQuery.expr.pseudos.hidden = function( elem ) {\n\treturn !jQuery.expr.pseudos.visible( elem );\n};\njQuery.expr.pseudos.visible = function( elem ) {\n\treturn !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );\n};\n\n\n\n\njQuery.ajaxSettings.xhr = function() {\n\ttry {\n\t\treturn new window.XMLHttpRequest();\n\t} catch ( e ) {}\n};\n\nvar xhrSuccessStatus = {\n\n\t\t// File protocol always yields status code 0, assume 200\n\t\t0: 200,\n\n\t\t// Support: IE <=9 only\n\t\t// #1450: sometimes IE returns 1223 when it should be 204\n\t\t1223: 204\n\t},\n\txhrSupported = jQuery.ajaxSettings.xhr();\n\nsupport.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\nsupport.ajax = xhrSupported = !!xhrSupported;\n\njQuery.ajaxTransport( function( options ) {\n\tvar callback, errorCallback;\n\n\t// Cross domain only allowed if supported through XMLHttpRequest\n\tif ( support.cors || xhrSupported && !options.crossDomain ) {\n\t\treturn {\n\t\t\tsend: function( headers, complete ) {\n\t\t\t\tvar i,\n\t\t\t\t\txhr = options.xhr();\n\n\t\t\t\txhr.open(\n\t\t\t\t\toptions.type,\n\t\t\t\t\toptions.url,\n\t\t\t\t\toptions.async,\n\t\t\t\t\toptions.username,\n\t\t\t\t\toptions.password\n\t\t\t\t);\n\n\t\t\t\t// Apply custom fields if provided\n\t\t\t\tif ( options.xhrFields ) {\n\t\t\t\t\tfor ( i in options.xhrFields ) {\n\t\t\t\t\t\txhr[ i ] = options.xhrFields[ i ];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Override mime type if needed\n\t\t\t\tif ( options.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\txhr.overrideMimeType( options.mimeType );\n\t\t\t\t}\n\n\t\t\t\t// X-Requested-With header\n\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\tif ( !options.crossDomain && !headers[ \"X-Requested-With\" ] ) {\n\t\t\t\t\theaders[ \"X-Requested-With\" ] = \"XMLHttpRequest\";\n\t\t\t\t}\n\n\t\t\t\t// Set headers\n\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] );\n\t\t\t\t}\n\n\t\t\t\t// Callback\n\t\t\t\tcallback = function( type ) {\n\t\t\t\t\treturn function() {\n\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\tcallback = errorCallback = xhr.onload =\n\t\t\t\t\t\t\t\txhr.onerror = xhr.onabort = xhr.ontimeout =\n\t\t\t\t\t\t\t\t\txhr.onreadystatechange = null;\n\n\t\t\t\t\t\t\tif ( type === \"abort\" ) {\n\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t} else if ( type === \"error\" ) {\n\n\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t// On a manual native abort, IE9 throws\n\t\t\t\t\t\t\t\t// errors on any property access that is not readyState\n\t\t\t\t\t\t\t\tif ( typeof xhr.status !== \"number\" ) {\n\t\t\t\t\t\t\t\t\tcomplete( 0, \"error\" );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcomplete(\n\n\t\t\t\t\t\t\t\t\t\t// File: protocol always yields status 0; see #8605, #14207\n\t\t\t\t\t\t\t\t\t\txhr.status,\n\t\t\t\t\t\t\t\t\t\txhr.statusText\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcomplete(\n\t\t\t\t\t\t\t\t\txhrSuccessStatus[ xhr.status ] || xhr.status,\n\t\t\t\t\t\t\t\t\txhr.statusText,\n\n\t\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t\t// IE9 has no XHR2 but throws on binary (trac-11426)\n\t\t\t\t\t\t\t\t\t// For XHR2 non-text, let the caller handle it (gh-2498)\n\t\t\t\t\t\t\t\t\t( xhr.responseType || \"text\" ) !== \"text\"  ||\n\t\t\t\t\t\t\t\t\ttypeof xhr.responseText !== \"string\" ?\n\t\t\t\t\t\t\t\t\t\t{ binary: xhr.response } :\n\t\t\t\t\t\t\t\t\t\t{ text: xhr.responseText },\n\t\t\t\t\t\t\t\t\txhr.getAllResponseHeaders()\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t};\n\n\t\t\t\t// Listen to events\n\t\t\t\txhr.onload = callback();\n\t\t\t\terrorCallback = xhr.onerror = xhr.ontimeout = callback( \"error\" );\n\n\t\t\t\t// Support: IE 9 only\n\t\t\t\t// Use onreadystatechange to replace onabort\n\t\t\t\t// to handle uncaught aborts\n\t\t\t\tif ( xhr.onabort !== undefined ) {\n\t\t\t\t\txhr.onabort = errorCallback;\n\t\t\t\t} else {\n\t\t\t\t\txhr.onreadystatechange = function() {\n\n\t\t\t\t\t\t// Check readyState before timeout as it changes\n\t\t\t\t\t\tif ( xhr.readyState === 4 ) {\n\n\t\t\t\t\t\t\t// Allow onerror to be called first,\n\t\t\t\t\t\t\t// but that will not handle a native abort\n\t\t\t\t\t\t\t// Also, save errorCallback to a variable\n\t\t\t\t\t\t\t// as xhr.onerror cannot be accessed\n\t\t\t\t\t\t\twindow.setTimeout( function() {\n\t\t\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\t\t\terrorCallback();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Create the abort callback\n\t\t\t\tcallback = callback( \"abort\" );\n\n\t\t\t\ttry {\n\n\t\t\t\t\t// Do send the request (this may raise an exception)\n\t\t\t\t\txhr.send( options.hasContent && options.data || null );\n\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t// #14683: Only rethrow if this hasn't been notified as an error yet\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\n// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)\njQuery.ajaxPrefilter( function( s ) {\n\tif ( s.crossDomain ) {\n\t\ts.contents.script = false;\n\t}\n} );\n\n// Install script dataType\njQuery.ajaxSetup( {\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, \" +\n\t\t\t\"application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /\\b(?:java|ecma)script\\b/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n} );\n\n// Handle cache's special case and crossDomain\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t}\n} );\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function( s ) {\n\n\t// This transport only deals with cross domain or forced-by-attrs requests\n\tif ( s.crossDomain || s.scriptAttrs ) {\n\t\tvar script, callback;\n\t\treturn {\n\t\t\tsend: function( _, complete ) {\n\t\t\t\tscript = jQuery( \"<script>\" )\n\t\t\t\t\t.attr( s.scriptAttrs || {} )\n\t\t\t\t\t.prop( { charset: s.scriptCharset, src: s.url } )\n\t\t\t\t\t.on( \"load error\", callback = function( evt ) {\n\t\t\t\t\t\tscript.remove();\n\t\t\t\t\t\tcallback = null;\n\t\t\t\t\t\tif ( evt ) {\n\t\t\t\t\t\t\tcomplete( evt.type === \"error\" ? 404 : 200, evt.type );\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\t\t\t\t// Use native DOM manipulation to avoid our domManip AJAX trickery\n\t\t\t\tdocument.head.appendChild( script[ 0 ] );\n\t\t\t},\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\nvar oldCallbacks = [],\n\trjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup( {\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\tvar callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( nonce++ ) );\n\t\tthis[ callback ] = true;\n\t\treturn callback;\n\t}\n} );\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar callbackName, overwritten, responseContainer,\n\t\tjsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n\t\t\t\"url\" :\n\t\t\ttypeof s.data === \"string\" &&\n\t\t\t\t( s.contentType || \"\" )\n\t\t\t\t\t.indexOf( \"application/x-www-form-urlencoded\" ) === 0 &&\n\t\t\t\trjsonp.test( s.data ) && \"data\"\n\t\t);\n\n\t// Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n\tif ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n\t\t// Get callback name, remembering preexisting value associated with it\n\t\tcallbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?\n\t\t\ts.jsonpCallback() :\n\t\t\ts.jsonpCallback;\n\n\t\t// Insert callback into url or form data\n\t\tif ( jsonProp ) {\n\t\t\ts[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n\t\t} else if ( s.jsonp !== false ) {\n\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n\t\t}\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[ \"script json\" ] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( callbackName + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// Force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Install callback\n\t\toverwritten = window[ callbackName ];\n\t\twindow[ callbackName ] = function() {\n\t\t\tresponseContainer = arguments;\n\t\t};\n\n\t\t// Clean-up function (fires after converters)\n\t\tjqXHR.always( function() {\n\n\t\t\t// If previous value didn't exist - remove it\n\t\t\tif ( overwritten === undefined ) {\n\t\t\t\tjQuery( window ).removeProp( callbackName );\n\n\t\t\t// Otherwise restore preexisting value\n\t\t\t} else {\n\t\t\t\twindow[ callbackName ] = overwritten;\n\t\t\t}\n\n\t\t\t// Save back as free\n\t\t\tif ( s[ callbackName ] ) {\n\n\t\t\t\t// Make sure that re-using the options doesn't screw things around\n\t\t\t\ts.jsonpCallback = originalSettings.jsonpCallback;\n\n\t\t\t\t// Save the callback name for future use\n\t\t\t\toldCallbacks.push( callbackName );\n\t\t\t}\n\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && isFunction( overwritten ) ) {\n\t\t\t\toverwritten( responseContainer[ 0 ] );\n\t\t\t}\n\n\t\t\tresponseContainer = overwritten = undefined;\n\t\t} );\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n} );\n\n\n\n\n// Support: Safari 8 only\n// In Safari 8 documents created via document.implementation.createHTMLDocument\n// collapse sibling forms: the second one becomes a child of the first one.\n// Because of that, this security measure has to be disabled in Safari 8.\n// https://bugs.webkit.org/show_bug.cgi?id=137337\nsupport.createHTMLDocument = ( function() {\n\tvar body = document.implementation.createHTMLDocument( \"\" ).body;\n\tbody.innerHTML = \"<form></form><form></form>\";\n\treturn body.childNodes.length === 2;\n} )();\n\n\n// Argument \"data\" should be string of html\n// context (optional): If specified, the fragment will be created in this context,\n// defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( typeof data !== \"string\" ) {\n\t\treturn [];\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\n\tvar base, parsed, scripts;\n\n\tif ( !context ) {\n\n\t\t// Stop scripts or inline event handlers from being executed immediately\n\t\t// by using document.implementation\n\t\tif ( support.createHTMLDocument ) {\n\t\t\tcontext = document.implementation.createHTMLDocument( \"\" );\n\n\t\t\t// Set the base href for the created document\n\t\t\t// so any parsed elements with URLs\n\t\t\t// are based on the document's URL (gh-2965)\n\t\t\tbase = context.createElement( \"base\" );\n\t\t\tbase.href = document.location.href;\n\t\t\tcontext.head.appendChild( base );\n\t\t} else {\n\t\t\tcontext = document;\n\t\t}\n\t}\n\n\tparsed = rsingleTag.exec( data );\n\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[ 1 ] ) ];\n\t}\n\n\tparsed = buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\n\n/**\n * Load a url into a page\n */\njQuery.fn.load = function( url, params, callback ) {\n\tvar selector, type, response,\n\t\tself = this,\n\t\toff = url.indexOf( \" \" );\n\n\tif ( off > -1 ) {\n\t\tselector = stripAndCollapse( url.slice( off ) );\n\t\turl = url.slice( 0, off );\n\t}\n\n\t// If it's a function\n\tif ( isFunction( params ) ) {\n\n\t\t// We assume that it's the callback\n\t\tcallback = params;\n\t\tparams = undefined;\n\n\t// Otherwise, build a param string\n\t} else if ( params && typeof params === \"object\" ) {\n\t\ttype = \"POST\";\n\t}\n\n\t// If we have elements to modify, make the request\n\tif ( self.length > 0 ) {\n\t\tjQuery.ajax( {\n\t\t\turl: url,\n\n\t\t\t// If \"type\" variable is undefined, then \"GET\" method will be used.\n\t\t\t// Make value of this field explicit since\n\t\t\t// user can override it through ajaxSetup method\n\t\t\ttype: type || \"GET\",\n\t\t\tdataType: \"html\",\n\t\t\tdata: params\n\t\t} ).done( function( responseText ) {\n\n\t\t\t// Save response for use in complete callback\n\t\t\tresponse = arguments;\n\n\t\t\tself.html( selector ?\n\n\t\t\t\t// If a selector was specified, locate the right elements in a dummy div\n\t\t\t\t// Exclude scripts to avoid IE 'Permission Denied' errors\n\t\t\t\tjQuery( \"<div>\" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n\t\t\t\t// Otherwise use the full result\n\t\t\t\tresponseText );\n\n\t\t// If the request succeeds, this function gets \"data\", \"status\", \"jqXHR\"\n\t\t// but they are ignored because response was set above.\n\t\t// If it fails, this function gets \"jqXHR\", \"status\", \"error\"\n\t\t} ).always( callback && function( jqXHR, status ) {\n\t\t\tself.each( function() {\n\t\t\t\tcallback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );\n\t\t\t} );\n\t\t} );\n\t}\n\n\treturn this;\n};\n\n\n\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( [\n\t\"ajaxStart\",\n\t\"ajaxStop\",\n\t\"ajaxComplete\",\n\t\"ajaxError\",\n\t\"ajaxSuccess\",\n\t\"ajaxSend\"\n], function( i, type ) {\n\tjQuery.fn[ type ] = function( fn ) {\n\t\treturn this.on( type, fn );\n\t};\n} );\n\n\n\n\njQuery.expr.pseudos.animated = function( elem ) {\n\treturn jQuery.grep( jQuery.timers, function( fn ) {\n\t\treturn elem === fn.elem;\n\t} ).length;\n};\n\n\n\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// Set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\t( curCSSTop + curCSSLeft ).indexOf( \"auto\" ) > -1;\n\n\t\t// Need to be able to calculate position if either\n\t\t// top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( isFunction( options ) ) {\n\n\t\t\t// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)\n\t\t\toptions = options.call( elem, i, jQuery.extend( {}, curOffset ) );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend( {\n\n\t// offset() relates an element's border box to the document origin\n\toffset: function( options ) {\n\n\t\t// Preserve chaining for setter\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each( function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t} );\n\t\t}\n\n\t\tvar rect, win,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !elem ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Return zeros for disconnected and hidden (display: none) elements (gh-2310)\n\t\t// Support: IE <=11 only\n\t\t// Running getBoundingClientRect on a\n\t\t// disconnected node in IE throws an error\n\t\tif ( !elem.getClientRects().length ) {\n\t\t\treturn { top: 0, left: 0 };\n\t\t}\n\n\t\t// Get document-relative position by adding viewport scroll to viewport-relative gBCR\n\t\trect = elem.getBoundingClientRect();\n\t\twin = elem.ownerDocument.defaultView;\n\t\treturn {\n\t\t\ttop: rect.top + win.pageYOffset,\n\t\t\tleft: rect.left + win.pageXOffset\n\t\t};\n\t},\n\n\t// position() relates an element's margin box to its offset parent's padding box\n\t// This corresponds to the behavior of CSS absolute positioning\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset, doc,\n\t\t\telem = this[ 0 ],\n\t\t\tparentOffset = { top: 0, left: 0 };\n\n\t\t// position:fixed elements are offset from the viewport, which itself always has zero offset\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\n\t\t\t// Assume position:fixed implies availability of getBoundingClientRect\n\t\t\toffset = elem.getBoundingClientRect();\n\n\t\t} else {\n\t\t\toffset = this.offset();\n\n\t\t\t// Account for the *real* offset parent, which can be the document or its root element\n\t\t\t// when a statically positioned element is identified\n\t\t\tdoc = elem.ownerDocument;\n\t\t\toffsetParent = elem.offsetParent || doc.documentElement;\n\t\t\twhile ( offsetParent &&\n\t\t\t\t( offsetParent === doc.body || offsetParent === doc.documentElement ) &&\n\t\t\t\tjQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\n\t\t\t\toffsetParent = offsetParent.parentNode;\n\t\t\t}\n\t\t\tif ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {\n\n\t\t\t\t// Incorporate borders into its offset, since they are outside its content origin\n\t\t\t\tparentOffset = jQuery( offsetParent ).offset();\n\t\t\t\tparentOffset.top += jQuery.css( offsetParent, \"borderTopWidth\", true );\n\t\t\t\tparentOffset.left += jQuery.css( offsetParent, \"borderLeftWidth\", true );\n\t\t\t}\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\treturn {\n\t\t\ttop: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n\t\t};\n\t},\n\n\t// This method will return documentElement in the following cases:\n\t// 1) For the element inside the iframe without offsetParent, this method will return\n\t//    documentElement of the parent window\n\t// 2) For the hidden or detached element\n\t// 3) For body or html element, i.e. in case of the html node - it will return itself\n\t//\n\t// but those exceptions were never presented as a real life use-cases\n\t// and might be considered as more preferable results.\n\t//\n\t// This logic, however, is not guaranteed and can change at any point in the future\n\toffsetParent: function() {\n\t\treturn this.map( function() {\n\t\t\tvar offsetParent = this.offsetParent;\n\n\t\t\twhile ( offsetParent && jQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\n\t\t\treturn offsetParent || documentElement;\n\t\t} );\n\t}\n} );\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = \"pageYOffset\" === prop;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\n\t\t\t// Coalesce documents and windows\n\t\t\tvar win;\n\t\t\tif ( isWindow( elem ) ) {\n\t\t\t\twin = elem;\n\t\t\t} else if ( elem.nodeType === 9 ) {\n\t\t\t\twin = elem.defaultView;\n\t\t\t}\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? win[ prop ] : elem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : win.pageXOffset,\n\t\t\t\t\ttop ? val : win.pageYOffset\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length );\n\t};\n} );\n\n// Support: Safari <=7 - 9.1, Chrome <=37 - 49\n// Add the top/left cssHooks using jQuery.fn.position\n// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347\n// getComputedStyle returns percent when specified for top/left/bottom/right;\n// rather than make the css module depend on the offset module, just check for it here\njQuery.each( [ \"top\", \"left\" ], function( i, prop ) {\n\tjQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,\n\t\tfunction( elem, computed ) {\n\t\t\tif ( computed ) {\n\t\t\t\tcomputed = curCSS( elem, prop );\n\n\t\t\t\t// If curCSS returns percentage, fallback to offset\n\t\t\t\treturn rnumnonpx.test( computed ) ?\n\t\t\t\t\tjQuery( elem ).position()[ prop ] + \"px\" :\n\t\t\t\t\tcomputed;\n\t\t\t}\n\t\t}\n\t);\n} );\n\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( { padding: \"inner\" + name, content: type, \"\": \"outer\" + name },\n\t\tfunction( defaultExtra, funcName ) {\n\n\t\t// Margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( isWindow( elem ) ) {\n\n\t\t\t\t\t// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)\n\t\t\t\t\treturn funcName.indexOf( \"outer\" ) === 0 ?\n\t\t\t\t\t\telem[ \"inner\" + name ] :\n\t\t\t\t\t\telem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n\t\t\t\t\t// whichever is greatest\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable );\n\t\t};\n\t} );\n} );\n\n\njQuery.each( ( \"blur focus focusin focusout resize scroll click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup contextmenu\" ).split( \" \" ),\n\tfunction( i, name ) {\n\n\t// Handle event binding\n\tjQuery.fn[ name ] = function( data, fn ) {\n\t\treturn arguments.length > 0 ?\n\t\t\tthis.on( name, null, data, fn ) :\n\t\t\tthis.trigger( name );\n\t};\n} );\n\njQuery.fn.extend( {\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t}\n} );\n\n\n\n\njQuery.fn.extend( {\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ?\n\t\t\tthis.off( selector, \"**\" ) :\n\t\t\tthis.off( types, selector || \"**\", fn );\n\t}\n} );\n\n// Bind a function to a context, optionally partially applying any\n// arguments.\n// jQuery.proxy is deprecated to promote standards (specifically Function#bind)\n// However, it is not slated for removal any time soon\njQuery.proxy = function( fn, context ) {\n\tvar tmp, args, proxy;\n\n\tif ( typeof context === \"string\" ) {\n\t\ttmp = fn[ context ];\n\t\tcontext = fn;\n\t\tfn = tmp;\n\t}\n\n\t// Quick check to determine if target is callable, in the spec\n\t// this throws a TypeError, but we will just return undefined.\n\tif ( !isFunction( fn ) ) {\n\t\treturn undefined;\n\t}\n\n\t// Simulated bind\n\targs = slice.call( arguments, 2 );\n\tproxy = function() {\n\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t};\n\n\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\treturn proxy;\n};\n\njQuery.holdReady = function( hold ) {\n\tif ( hold ) {\n\t\tjQuery.readyWait++;\n\t} else {\n\t\tjQuery.ready( true );\n\t}\n};\njQuery.isArray = Array.isArray;\njQuery.parseJSON = JSON.parse;\njQuery.nodeName = nodeName;\njQuery.isFunction = isFunction;\njQuery.isWindow = isWindow;\njQuery.camelCase = camelCase;\njQuery.type = toType;\n\njQuery.now = Date.now;\n\njQuery.isNumeric = function( obj ) {\n\n\t// As of jQuery 3.0, isNumeric is limited to\n\t// strings and numbers (primitives or objects)\n\t// that can be coerced to finite numbers (gh-2662)\n\tvar type = jQuery.type( obj );\n\treturn ( type === \"number\" || type === \"string\" ) &&\n\n\t\t// parseFloat NaNs numeric-cast false positives (\"\")\n\t\t// ...but misinterprets leading-number strings, particularly hex literals (\"0x...\")\n\t\t// subtraction forces infinities to NaN\n\t\t!isNaN( obj - parseFloat( obj ) );\n};\n\n\n\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t} );\n}\n\n\n\n\nvar\n\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in AMD\n// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)\n// and CommonJS for browser emulators (#13566)\nif ( !noGlobal ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\n\n\n\nreturn jQuery;\n} );","/**!\n * @fileOverview Kickass library to create and place poppers near their reference elements.\n * @version 1.15.0\n * @license\n * Copyright (c) 2016 Federico Zivolo and contributors\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.Popper = factory());\n}(this, (function () { 'use strict';\n\nvar isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';\n\nvar longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox'];\nvar timeoutDuration = 0;\nfor (var i = 0; i < longerTimeoutBrowsers.length; i += 1) {\n  if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) {\n    timeoutDuration = 1;\n    break;\n  }\n}\n\nfunction microtaskDebounce(fn) {\n  var called = false;\n  return function () {\n    if (called) {\n      return;\n    }\n    called = true;\n    window.Promise.resolve().then(function () {\n      called = false;\n      fn();\n    });\n  };\n}\n\nfunction taskDebounce(fn) {\n  var scheduled = false;\n  return function () {\n    if (!scheduled) {\n      scheduled = true;\n      setTimeout(function () {\n        scheduled = false;\n        fn();\n      }, timeoutDuration);\n    }\n  };\n}\n\nvar supportsMicroTasks = isBrowser && window.Promise;\n\n/**\n* Create a debounced version of a method, that's asynchronously deferred\n* but called in the minimum time possible.\n*\n* @method\n* @memberof Popper.Utils\n* @argument {Function} fn\n* @returns {Function}\n*/\nvar debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce;\n\n/**\n * Check if the given variable is a function\n * @method\n * @memberof Popper.Utils\n * @argument {Any} functionToCheck - variable to check\n * @returns {Boolean} answer to: is a function?\n */\nfunction isFunction(functionToCheck) {\n  var getType = {};\n  return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';\n}\n\n/**\n * Get CSS computed property of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Eement} element\n * @argument {String} property\n */\nfunction getStyleComputedProperty(element, property) {\n  if (element.nodeType !== 1) {\n    return [];\n  }\n  // NOTE: 1 DOM access here\n  var window = element.ownerDocument.defaultView;\n  var css = window.getComputedStyle(element, null);\n  return property ? css[property] : css;\n}\n\n/**\n * Returns the parentNode or the host of the element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} parent\n */\nfunction getParentNode(element) {\n  if (element.nodeName === 'HTML') {\n    return element;\n  }\n  return element.parentNode || element.host;\n}\n\n/**\n * Returns the scrolling parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} scroll parent\n */\nfunction getScrollParent(element) {\n  // Return body, `getScroll` will take care to get the correct `scrollTop` from it\n  if (!element) {\n    return document.body;\n  }\n\n  switch (element.nodeName) {\n    case 'HTML':\n    case 'BODY':\n      return element.ownerDocument.body;\n    case '#document':\n      return element.body;\n  }\n\n  // Firefox want us to check `-x` and `-y` variations as well\n\n  var _getStyleComputedProp = getStyleComputedProperty(element),\n      overflow = _getStyleComputedProp.overflow,\n      overflowX = _getStyleComputedProp.overflowX,\n      overflowY = _getStyleComputedProp.overflowY;\n\n  if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {\n    return element;\n  }\n\n  return getScrollParent(getParentNode(element));\n}\n\nvar isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);\nvar isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);\n\n/**\n * Determines if the browser is Internet Explorer\n * @method\n * @memberof Popper.Utils\n * @param {Number} version to check\n * @returns {Boolean} isIE\n */\nfunction isIE(version) {\n  if (version === 11) {\n    return isIE11;\n  }\n  if (version === 10) {\n    return isIE10;\n  }\n  return isIE11 || isIE10;\n}\n\n/**\n * Returns the offset parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} offset parent\n */\nfunction getOffsetParent(element) {\n  if (!element) {\n    return document.documentElement;\n  }\n\n  var noOffsetParent = isIE(10) ? document.body : null;\n\n  // NOTE: 1 DOM access here\n  var offsetParent = element.offsetParent || null;\n  // Skip hidden elements which don't have an offsetParent\n  while (offsetParent === noOffsetParent && element.nextElementSibling) {\n    offsetParent = (element = element.nextElementSibling).offsetParent;\n  }\n\n  var nodeName = offsetParent && offsetParent.nodeName;\n\n  if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {\n    return element ? element.ownerDocument.documentElement : document.documentElement;\n  }\n\n  // .offsetParent will return the closest TH, TD or TABLE in case\n  // no offsetParent is present, I hate this job...\n  if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {\n    return getOffsetParent(offsetParent);\n  }\n\n  return offsetParent;\n}\n\nfunction isOffsetContainer(element) {\n  var nodeName = element.nodeName;\n\n  if (nodeName === 'BODY') {\n    return false;\n  }\n  return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element;\n}\n\n/**\n * Finds the root node (document, shadowDOM root) of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} node\n * @returns {Element} root node\n */\nfunction getRoot(node) {\n  if (node.parentNode !== null) {\n    return getRoot(node.parentNode);\n  }\n\n  return node;\n}\n\n/**\n * Finds the offset parent common to the two provided nodes\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element1\n * @argument {Element} element2\n * @returns {Element} common offset parent\n */\nfunction findCommonOffsetParent(element1, element2) {\n  // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n  if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) {\n    return document.documentElement;\n  }\n\n  // Here we make sure to give as \"start\" the element that comes first in the DOM\n  var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING;\n  var start = order ? element1 : element2;\n  var end = order ? element2 : element1;\n\n  // Get common ancestor container\n  var range = document.createRange();\n  range.setStart(start, 0);\n  range.setEnd(end, 0);\n  var commonAncestorContainer = range.commonAncestorContainer;\n\n  // Both nodes are inside #document\n\n  if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) {\n    if (isOffsetContainer(commonAncestorContainer)) {\n      return commonAncestorContainer;\n    }\n\n    return getOffsetParent(commonAncestorContainer);\n  }\n\n  // one of the nodes is inside shadowDOM, find which one\n  var element1root = getRoot(element1);\n  if (element1root.host) {\n    return findCommonOffsetParent(element1root.host, element2);\n  } else {\n    return findCommonOffsetParent(element1, getRoot(element2).host);\n  }\n}\n\n/**\n * Gets the scroll value of the given element in the given side (top and left)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {String} side `top` or `left`\n * @returns {number} amount of scrolled pixels\n */\nfunction getScroll(element) {\n  var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top';\n\n  var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft';\n  var nodeName = element.nodeName;\n\n  if (nodeName === 'BODY' || nodeName === 'HTML') {\n    var html = element.ownerDocument.documentElement;\n    var scrollingElement = element.ownerDocument.scrollingElement || html;\n    return scrollingElement[upperSide];\n  }\n\n  return element[upperSide];\n}\n\n/*\n * Sum or subtract the element scroll values (left and top) from a given rect object\n * @method\n * @memberof Popper.Utils\n * @param {Object} rect - Rect object you want to change\n * @param {HTMLElement} element - The element from the function reads the scroll values\n * @param {Boolean} subtract - set to true if you want to subtract the scroll values\n * @return {Object} rect - The modifier rect object\n */\nfunction includeScroll(rect, element) {\n  var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;\n\n  var scrollTop = getScroll(element, 'top');\n  var scrollLeft = getScroll(element, 'left');\n  var modifier = subtract ? -1 : 1;\n  rect.top += scrollTop * modifier;\n  rect.bottom += scrollTop * modifier;\n  rect.left += scrollLeft * modifier;\n  rect.right += scrollLeft * modifier;\n  return rect;\n}\n\n/*\n * Helper to detect borders of a given element\n * @method\n * @memberof Popper.Utils\n * @param {CSSStyleDeclaration} styles\n * Result of `getStyleComputedProperty` on the given element\n * @param {String} axis - `x` or `y`\n * @return {number} borders - The borders size of the given axis\n */\n\nfunction getBordersSize(styles, axis) {\n  var sideA = axis === 'x' ? 'Left' : 'Top';\n  var sideB = sideA === 'Left' ? 'Right' : 'Bottom';\n\n  return parseFloat(styles['border' + sideA + 'Width'], 10) + parseFloat(styles['border' + sideB + 'Width'], 10);\n}\n\nfunction getSize(axis, body, html, computedStyle) {\n  return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0);\n}\n\nfunction getWindowSizes(document) {\n  var body = document.body;\n  var html = document.documentElement;\n  var computedStyle = isIE(10) && getComputedStyle(html);\n\n  return {\n    height: getSize('Height', body, html, computedStyle),\n    width: getSize('Width', body, html, computedStyle)\n  };\n}\n\nvar classCallCheck = function (instance, Constructor) {\n  if (!(instance instanceof Constructor)) {\n    throw new TypeError(\"Cannot call a class as a function\");\n  }\n};\n\nvar createClass = function () {\n  function defineProperties(target, props) {\n    for (var i = 0; i < props.length; i++) {\n      var descriptor = props[i];\n      descriptor.enumerable = descriptor.enumerable || false;\n      descriptor.configurable = true;\n      if (\"value\" in descriptor) descriptor.writable = true;\n      Object.defineProperty(target, descriptor.key, descriptor);\n    }\n  }\n\n  return function (Constructor, protoProps, staticProps) {\n    if (protoProps) defineProperties(Constructor.prototype, protoProps);\n    if (staticProps) defineProperties(Constructor, staticProps);\n    return Constructor;\n  };\n}();\n\n\n\n\n\nvar defineProperty = function (obj, key, value) {\n  if (key in obj) {\n    Object.defineProperty(obj, key, {\n      value: value,\n      enumerable: true,\n      configurable: true,\n      writable: true\n    });\n  } else {\n    obj[key] = value;\n  }\n\n  return obj;\n};\n\nvar _extends = Object.assign || function (target) {\n  for (var i = 1; i < arguments.length; i++) {\n    var source = arguments[i];\n\n    for (var key in source) {\n      if (Object.prototype.hasOwnProperty.call(source, key)) {\n        target[key] = source[key];\n      }\n    }\n  }\n\n  return target;\n};\n\n/**\n * Given element offsets, generate an output similar to getBoundingClientRect\n * @method\n * @memberof Popper.Utils\n * @argument {Object} offsets\n * @returns {Object} ClientRect like output\n */\nfunction getClientRect(offsets) {\n  return _extends({}, offsets, {\n    right: offsets.left + offsets.width,\n    bottom: offsets.top + offsets.height\n  });\n}\n\n/**\n * Get bounding client rect of given element\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} element\n * @return {Object} client rect\n */\nfunction getBoundingClientRect(element) {\n  var rect = {};\n\n  // IE10 10 FIX: Please, don't ask, the element isn't\n  // considered in DOM in some circumstances...\n  // This isn't reproducible in IE10 compatibility mode of IE11\n  try {\n    if (isIE(10)) {\n      rect = element.getBoundingClientRect();\n      var scrollTop = getScroll(element, 'top');\n      var scrollLeft = getScroll(element, 'left');\n      rect.top += scrollTop;\n      rect.left += scrollLeft;\n      rect.bottom += scrollTop;\n      rect.right += scrollLeft;\n    } else {\n      rect = element.getBoundingClientRect();\n    }\n  } catch (e) {}\n\n  var result = {\n    left: rect.left,\n    top: rect.top,\n    width: rect.right - rect.left,\n    height: rect.bottom - rect.top\n  };\n\n  // subtract scrollbar size from sizes\n  var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {};\n  var width = sizes.width || element.clientWidth || result.right - result.left;\n  var height = sizes.height || element.clientHeight || result.bottom - result.top;\n\n  var horizScrollbar = element.offsetWidth - width;\n  var vertScrollbar = element.offsetHeight - height;\n\n  // if an hypothetical scrollbar is detected, we must be sure it's not a `border`\n  // we make this check conditional for performance reasons\n  if (horizScrollbar || vertScrollbar) {\n    var styles = getStyleComputedProperty(element);\n    horizScrollbar -= getBordersSize(styles, 'x');\n    vertScrollbar -= getBordersSize(styles, 'y');\n\n    result.width -= horizScrollbar;\n    result.height -= vertScrollbar;\n  }\n\n  return getClientRect(result);\n}\n\nfunction getOffsetRectRelativeToArbitraryNode(children, parent) {\n  var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;\n\n  var isIE10 = isIE(10);\n  var isHTML = parent.nodeName === 'HTML';\n  var childrenRect = getBoundingClientRect(children);\n  var parentRect = getBoundingClientRect(parent);\n  var scrollParent = getScrollParent(children);\n\n  var styles = getStyleComputedProperty(parent);\n  var borderTopWidth = parseFloat(styles.borderTopWidth, 10);\n  var borderLeftWidth = parseFloat(styles.borderLeftWidth, 10);\n\n  // In cases where the parent is fixed, we must ignore negative scroll in offset calc\n  if (fixedPosition && isHTML) {\n    parentRect.top = Math.max(parentRect.top, 0);\n    parentRect.left = Math.max(parentRect.left, 0);\n  }\n  var offsets = getClientRect({\n    top: childrenRect.top - parentRect.top - borderTopWidth,\n    left: childrenRect.left - parentRect.left - borderLeftWidth,\n    width: childrenRect.width,\n    height: childrenRect.height\n  });\n  offsets.marginTop = 0;\n  offsets.marginLeft = 0;\n\n  // Subtract margins of documentElement in case it's being used as parent\n  // we do this only on HTML because it's the only element that behaves\n  // differently when margins are applied to it. The margins are included in\n  // the box of the documentElement, in the other cases not.\n  if (!isIE10 && isHTML) {\n    var marginTop = parseFloat(styles.marginTop, 10);\n    var marginLeft = parseFloat(styles.marginLeft, 10);\n\n    offsets.top -= borderTopWidth - marginTop;\n    offsets.bottom -= borderTopWidth - marginTop;\n    offsets.left -= borderLeftWidth - marginLeft;\n    offsets.right -= borderLeftWidth - marginLeft;\n\n    // Attach marginTop and marginLeft because in some circumstances we may need them\n    offsets.marginTop = marginTop;\n    offsets.marginLeft = marginLeft;\n  }\n\n  if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') {\n    offsets = includeScroll(offsets, parent);\n  }\n\n  return offsets;\n}\n\nfunction getViewportOffsetRectRelativeToArtbitraryNode(element) {\n  var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n  var html = element.ownerDocument.documentElement;\n  var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html);\n  var width = Math.max(html.clientWidth, window.innerWidth || 0);\n  var height = Math.max(html.clientHeight, window.innerHeight || 0);\n\n  var scrollTop = !excludeScroll ? getScroll(html) : 0;\n  var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0;\n\n  var offset = {\n    top: scrollTop - relativeOffset.top + relativeOffset.marginTop,\n    left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft,\n    width: width,\n    height: height\n  };\n\n  return getClientRect(offset);\n}\n\n/**\n * Check if the given element is fixed or is inside a fixed parent\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {Element} customContainer\n * @returns {Boolean} answer to \"isFixed?\"\n */\nfunction isFixed(element) {\n  var nodeName = element.nodeName;\n  if (nodeName === 'BODY' || nodeName === 'HTML') {\n    return false;\n  }\n  if (getStyleComputedProperty(element, 'position') === 'fixed') {\n    return true;\n  }\n  var parentNode = getParentNode(element);\n  if (!parentNode) {\n    return false;\n  }\n  return isFixed(parentNode);\n}\n\n/**\n * Finds the first parent of an element that has a transformed property defined\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} first transformed parent or documentElement\n */\n\nfunction getFixedPositionOffsetParent(element) {\n  // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n  if (!element || !element.parentElement || isIE()) {\n    return document.documentElement;\n  }\n  var el = element.parentElement;\n  while (el && getStyleComputedProperty(el, 'transform') === 'none') {\n    el = el.parentElement;\n  }\n  return el || document.documentElement;\n}\n\n/**\n * Computed the boundaries limits and return them\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} popper\n * @param {HTMLElement} reference\n * @param {number} padding\n * @param {HTMLElement} boundariesElement - Element used to define the boundaries\n * @param {Boolean} fixedPosition - Is in fixed position mode\n * @returns {Object} Coordinates of the boundaries\n */\nfunction getBoundaries(popper, reference, padding, boundariesElement) {\n  var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;\n\n  // NOTE: 1 DOM access here\n\n  var boundaries = { top: 0, left: 0 };\n  var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n\n  // Handle viewport case\n  if (boundariesElement === 'viewport') {\n    boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition);\n  } else {\n    // Handle other cases based on DOM element used as boundaries\n    var boundariesNode = void 0;\n    if (boundariesElement === 'scrollParent') {\n      boundariesNode = getScrollParent(getParentNode(reference));\n      if (boundariesNode.nodeName === 'BODY') {\n        boundariesNode = popper.ownerDocument.documentElement;\n      }\n    } else if (boundariesElement === 'window') {\n      boundariesNode = popper.ownerDocument.documentElement;\n    } else {\n      boundariesNode = boundariesElement;\n    }\n\n    var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition);\n\n    // In case of HTML, we need a different computation\n    if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) {\n      var _getWindowSizes = getWindowSizes(popper.ownerDocument),\n          height = _getWindowSizes.height,\n          width = _getWindowSizes.width;\n\n      boundaries.top += offsets.top - offsets.marginTop;\n      boundaries.bottom = height + offsets.top;\n      boundaries.left += offsets.left - offsets.marginLeft;\n      boundaries.right = width + offsets.left;\n    } else {\n      // for all the other DOM elements, this one is good\n      boundaries = offsets;\n    }\n  }\n\n  // Add paddings\n  padding = padding || 0;\n  var isPaddingNumber = typeof padding === 'number';\n  boundaries.left += isPaddingNumber ? padding : padding.left || 0;\n  boundaries.top += isPaddingNumber ? padding : padding.top || 0;\n  boundaries.right -= isPaddingNumber ? padding : padding.right || 0;\n  boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0;\n\n  return boundaries;\n}\n\nfunction getArea(_ref) {\n  var width = _ref.width,\n      height = _ref.height;\n\n  return width * height;\n}\n\n/**\n * Utility used to transform the `auto` placement to the placement with more\n * available space.\n * @method\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) {\n  var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0;\n\n  if (placement.indexOf('auto') === -1) {\n    return placement;\n  }\n\n  var boundaries = getBoundaries(popper, reference, padding, boundariesElement);\n\n  var rects = {\n    top: {\n      width: boundaries.width,\n      height: refRect.top - boundaries.top\n    },\n    right: {\n      width: boundaries.right - refRect.right,\n      height: boundaries.height\n    },\n    bottom: {\n      width: boundaries.width,\n      height: boundaries.bottom - refRect.bottom\n    },\n    left: {\n      width: refRect.left - boundaries.left,\n      height: boundaries.height\n    }\n  };\n\n  var sortedAreas = Object.keys(rects).map(function (key) {\n    return _extends({\n      key: key\n    }, rects[key], {\n      area: getArea(rects[key])\n    });\n  }).sort(function (a, b) {\n    return b.area - a.area;\n  });\n\n  var filteredAreas = sortedAreas.filter(function (_ref2) {\n    var width = _ref2.width,\n        height = _ref2.height;\n    return width >= popper.clientWidth && height >= popper.clientHeight;\n  });\n\n  var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key;\n\n  var variation = placement.split('-')[1];\n\n  return computedPlacement + (variation ? '-' + variation : '');\n}\n\n/**\n * Get offsets to the reference element\n * @method\n * @memberof Popper.Utils\n * @param {Object} state\n * @param {Element} popper - the popper element\n * @param {Element} reference - the reference element (the popper will be relative to this)\n * @param {Element} fixedPosition - is in fixed position mode\n * @returns {Object} An object containing the offsets which will be applied to the popper\n */\nfunction getReferenceOffsets(state, popper, reference) {\n  var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;\n\n  var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n  return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition);\n}\n\n/**\n * Get the outer sizes of the given element (offset size + margins)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Object} object containing width and height properties\n */\nfunction getOuterSizes(element) {\n  var window = element.ownerDocument.defaultView;\n  var styles = window.getComputedStyle(element);\n  var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0);\n  var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0);\n  var result = {\n    width: element.offsetWidth + y,\n    height: element.offsetHeight + x\n  };\n  return result;\n}\n\n/**\n * Get the opposite placement of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement\n * @returns {String} flipped placement\n */\nfunction getOppositePlacement(placement) {\n  var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' };\n  return placement.replace(/left|right|bottom|top/g, function (matched) {\n    return hash[matched];\n  });\n}\n\n/**\n * Get offsets to the popper\n * @method\n * @memberof Popper.Utils\n * @param {Object} position - CSS position the Popper will get applied\n * @param {HTMLElement} popper - the popper element\n * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this)\n * @param {String} placement - one of the valid placement options\n * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper\n */\nfunction getPopperOffsets(popper, referenceOffsets, placement) {\n  placement = placement.split('-')[0];\n\n  // Get popper node sizes\n  var popperRect = getOuterSizes(popper);\n\n  // Add position, width and height to our offsets object\n  var popperOffsets = {\n    width: popperRect.width,\n    height: popperRect.height\n  };\n\n  // depending by the popper placement we have to compute its offsets slightly differently\n  var isHoriz = ['right', 'left'].indexOf(placement) !== -1;\n  var mainSide = isHoriz ? 'top' : 'left';\n  var secondarySide = isHoriz ? 'left' : 'top';\n  var measurement = isHoriz ? 'height' : 'width';\n  var secondaryMeasurement = !isHoriz ? 'height' : 'width';\n\n  popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2;\n  if (placement === secondarySide) {\n    popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement];\n  } else {\n    popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)];\n  }\n\n  return popperOffsets;\n}\n\n/**\n * Mimics the `find` method of Array\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nfunction find(arr, check) {\n  // use native find if supported\n  if (Array.prototype.find) {\n    return arr.find(check);\n  }\n\n  // use `filter` to obtain the same behavior of `find`\n  return arr.filter(check)[0];\n}\n\n/**\n * Return the index of the matching object\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nfunction findIndex(arr, prop, value) {\n  // use native findIndex if supported\n  if (Array.prototype.findIndex) {\n    return arr.findIndex(function (cur) {\n      return cur[prop] === value;\n    });\n  }\n\n  // use `find` + `indexOf` if `findIndex` isn't supported\n  var match = find(arr, function (obj) {\n    return obj[prop] === value;\n  });\n  return arr.indexOf(match);\n}\n\n/**\n * Loop trough the list of modifiers and run them in order,\n * each of them will then edit the data object.\n * @method\n * @memberof Popper.Utils\n * @param {dataObject} data\n * @param {Array} modifiers\n * @param {String} ends - Optional modifier name used as stopper\n * @returns {dataObject}\n */\nfunction runModifiers(modifiers, data, ends) {\n  var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends));\n\n  modifiersToRun.forEach(function (modifier) {\n    if (modifier['function']) {\n      // eslint-disable-line dot-notation\n      console.warn('`modifier.function` is deprecated, use `modifier.fn`!');\n    }\n    var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation\n    if (modifier.enabled && isFunction(fn)) {\n      // Add properties to offsets to make them a complete clientRect object\n      // we do this before each modifier to make sure the previous one doesn't\n      // mess with these values\n      data.offsets.popper = getClientRect(data.offsets.popper);\n      data.offsets.reference = getClientRect(data.offsets.reference);\n\n      data = fn(data, modifier);\n    }\n  });\n\n  return data;\n}\n\n/**\n * Updates the position of the popper, computing the new offsets and applying\n * the new style.<br />\n * Prefer `scheduleUpdate` over `update` because of performance reasons.\n * @method\n * @memberof Popper\n */\nfunction update() {\n  // if popper is destroyed, don't perform any further update\n  if (this.state.isDestroyed) {\n    return;\n  }\n\n  var data = {\n    instance: this,\n    styles: {},\n    arrowStyles: {},\n    attributes: {},\n    flipped: false,\n    offsets: {}\n  };\n\n  // compute reference element offsets\n  data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed);\n\n  // compute auto placement, store placement inside the data object,\n  // modifiers will be able to edit `placement` if needed\n  // and refer to originalPlacement to know the original value\n  data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding);\n\n  // store the computed placement inside `originalPlacement`\n  data.originalPlacement = data.placement;\n\n  data.positionFixed = this.options.positionFixed;\n\n  // compute the popper offsets\n  data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement);\n\n  data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute';\n\n  // run the modifiers\n  data = runModifiers(this.modifiers, data);\n\n  // the first `update` will call `onCreate` callback\n  // the other ones will call `onUpdate` callback\n  if (!this.state.isCreated) {\n    this.state.isCreated = true;\n    this.options.onCreate(data);\n  } else {\n    this.options.onUpdate(data);\n  }\n}\n\n/**\n * Helper used to know if the given modifier is enabled.\n * @method\n * @memberof Popper.Utils\n * @returns {Boolean}\n */\nfunction isModifierEnabled(modifiers, modifierName) {\n  return modifiers.some(function (_ref) {\n    var name = _ref.name,\n        enabled = _ref.enabled;\n    return enabled && name === modifierName;\n  });\n}\n\n/**\n * Get the prefixed supported property name\n * @method\n * @memberof Popper.Utils\n * @argument {String} property (camelCase)\n * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix)\n */\nfunction getSupportedPropertyName(property) {\n  var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O'];\n  var upperProp = property.charAt(0).toUpperCase() + property.slice(1);\n\n  for (var i = 0; i < prefixes.length; i++) {\n    var prefix = prefixes[i];\n    var toCheck = prefix ? '' + prefix + upperProp : property;\n    if (typeof document.body.style[toCheck] !== 'undefined') {\n      return toCheck;\n    }\n  }\n  return null;\n}\n\n/**\n * Destroys the popper.\n * @method\n * @memberof Popper\n */\nfunction destroy() {\n  this.state.isDestroyed = true;\n\n  // touch DOM only if `applyStyle` modifier is enabled\n  if (isModifierEnabled(this.modifiers, 'applyStyle')) {\n    this.popper.removeAttribute('x-placement');\n    this.popper.style.position = '';\n    this.popper.style.top = '';\n    this.popper.style.left = '';\n    this.popper.style.right = '';\n    this.popper.style.bottom = '';\n    this.popper.style.willChange = '';\n    this.popper.style[getSupportedPropertyName('transform')] = '';\n  }\n\n  this.disableEventListeners();\n\n  // remove the popper if user explicity asked for the deletion on destroy\n  // do not use `remove` because IE11 doesn't support it\n  if (this.options.removeOnDestroy) {\n    this.popper.parentNode.removeChild(this.popper);\n  }\n  return this;\n}\n\n/**\n * Get the window associated with the element\n * @argument {Element} element\n * @returns {Window}\n */\nfunction getWindow(element) {\n  var ownerDocument = element.ownerDocument;\n  return ownerDocument ? ownerDocument.defaultView : window;\n}\n\nfunction attachToScrollParents(scrollParent, event, callback, scrollParents) {\n  var isBody = scrollParent.nodeName === 'BODY';\n  var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent;\n  target.addEventListener(event, callback, { passive: true });\n\n  if (!isBody) {\n    attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents);\n  }\n  scrollParents.push(target);\n}\n\n/**\n * Setup needed event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nfunction setupEventListeners(reference, options, state, updateBound) {\n  // Resize event listener on window\n  state.updateBound = updateBound;\n  getWindow(reference).addEventListener('resize', state.updateBound, { passive: true });\n\n  // Scroll event listener on scroll parents\n  var scrollElement = getScrollParent(reference);\n  attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents);\n  state.scrollElement = scrollElement;\n  state.eventsEnabled = true;\n\n  return state;\n}\n\n/**\n * It will add resize/scroll events and start recalculating\n * position of the popper element when they are triggered.\n * @method\n * @memberof Popper\n */\nfunction enableEventListeners() {\n  if (!this.state.eventsEnabled) {\n    this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate);\n  }\n}\n\n/**\n * Remove event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nfunction removeEventListeners(reference, state) {\n  // Remove resize event listener on window\n  getWindow(reference).removeEventListener('resize', state.updateBound);\n\n  // Remove scroll event listener on scroll parents\n  state.scrollParents.forEach(function (target) {\n    target.removeEventListener('scroll', state.updateBound);\n  });\n\n  // Reset state\n  state.updateBound = null;\n  state.scrollParents = [];\n  state.scrollElement = null;\n  state.eventsEnabled = false;\n  return state;\n}\n\n/**\n * It will remove resize/scroll events and won't recalculate popper position\n * when they are triggered. It also won't trigger `onUpdate` callback anymore,\n * unless you call `update` method manually.\n * @method\n * @memberof Popper\n */\nfunction disableEventListeners() {\n  if (this.state.eventsEnabled) {\n    cancelAnimationFrame(this.scheduleUpdate);\n    this.state = removeEventListeners(this.reference, this.state);\n  }\n}\n\n/**\n * Tells if a given input is a number\n * @method\n * @memberof Popper.Utils\n * @param {*} input to check\n * @return {Boolean}\n */\nfunction isNumeric(n) {\n  return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);\n}\n\n/**\n * Set the style to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the style to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nfunction setStyles(element, styles) {\n  Object.keys(styles).forEach(function (prop) {\n    var unit = '';\n    // add unit if the value is numeric and is one of the following\n    if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) {\n      unit = 'px';\n    }\n    element.style[prop] = styles[prop] + unit;\n  });\n}\n\n/**\n * Set the attributes to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the attributes to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nfunction setAttributes(element, attributes) {\n  Object.keys(attributes).forEach(function (prop) {\n    var value = attributes[prop];\n    if (value !== false) {\n      element.setAttribute(prop, attributes[prop]);\n    } else {\n      element.removeAttribute(prop);\n    }\n  });\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} data.styles - List of style properties - values to apply to popper element\n * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The same data object\n */\nfunction applyStyle(data) {\n  // any property present in `data.styles` will be applied to the popper,\n  // in this way we can make the 3rd party modifiers add custom styles to it\n  // Be aware, modifiers could override the properties defined in the previous\n  // lines of this modifier!\n  setStyles(data.instance.popper, data.styles);\n\n  // any property present in `data.attributes` will be applied to the popper,\n  // they will be set as HTML attributes of the element\n  setAttributes(data.instance.popper, data.attributes);\n\n  // if arrowElement is defined and arrowStyles has some properties\n  if (data.arrowElement && Object.keys(data.arrowStyles).length) {\n    setStyles(data.arrowElement, data.arrowStyles);\n  }\n\n  return data;\n}\n\n/**\n * Set the x-placement attribute before everything else because it could be used\n * to add margins to the popper margins needs to be calculated to get the\n * correct popper offsets.\n * @method\n * @memberof Popper.modifiers\n * @param {HTMLElement} reference - The reference element used to position the popper\n * @param {HTMLElement} popper - The HTML element used as popper\n * @param {Object} options - Popper.js options\n */\nfunction applyStyleOnLoad(reference, popper, options, modifierOptions, state) {\n  // compute reference element offsets\n  var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed);\n\n  // compute auto placement, store placement inside the data object,\n  // modifiers will be able to edit `placement` if needed\n  // and refer to originalPlacement to know the original value\n  var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding);\n\n  popper.setAttribute('x-placement', placement);\n\n  // Apply `position` to popper before anything else because\n  // without the position applied we can't guarantee correct computations\n  setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' });\n\n  return options;\n}\n\n/**\n * @function\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Boolean} shouldRound - If the offsets should be rounded at all\n * @returns {Object} The popper's position offsets rounded\n *\n * The tale of pixel-perfect positioning. It's still not 100% perfect, but as\n * good as it can be within reason.\n * Discussion here: https://github.com/FezVrasta/popper.js/pull/715\n *\n * Low DPI screens cause a popper to be blurry if not using full pixels (Safari\n * as well on High DPI screens).\n *\n * Firefox prefers no rounding for positioning and does not have blurriness on\n * high DPI screens.\n *\n * Only horizontal placement and left/right values need to be considered.\n */\nfunction getRoundedOffsets(data, shouldRound) {\n  var _data$offsets = data.offsets,\n      popper = _data$offsets.popper,\n      reference = _data$offsets.reference;\n  var round = Math.round,\n      floor = Math.floor;\n\n  var noRound = function noRound(v) {\n    return v;\n  };\n\n  var referenceWidth = round(reference.width);\n  var popperWidth = round(popper.width);\n\n  var isVertical = ['left', 'right'].indexOf(data.placement) !== -1;\n  var isVariation = data.placement.indexOf('-') !== -1;\n  var sameWidthParity = referenceWidth % 2 === popperWidth % 2;\n  var bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1;\n\n  var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthParity ? round : floor;\n  var verticalToInteger = !shouldRound ? noRound : round;\n\n  return {\n    left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left),\n    top: verticalToInteger(popper.top),\n    bottom: verticalToInteger(popper.bottom),\n    right: horizontalToInteger(popper.right)\n  };\n}\n\nvar isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent);\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction computeStyle(data, options) {\n  var x = options.x,\n      y = options.y;\n  var popper = data.offsets.popper;\n\n  // Remove this legacy support in Popper.js v2\n\n  var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) {\n    return modifier.name === 'applyStyle';\n  }).gpuAcceleration;\n  if (legacyGpuAccelerationOption !== undefined) {\n    console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');\n  }\n  var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration;\n\n  var offsetParent = getOffsetParent(data.instance.popper);\n  var offsetParentRect = getBoundingClientRect(offsetParent);\n\n  // Styles\n  var styles = {\n    position: popper.position\n  };\n\n  var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox);\n\n  var sideA = x === 'bottom' ? 'top' : 'bottom';\n  var sideB = y === 'right' ? 'left' : 'right';\n\n  // if gpuAcceleration is set to `true` and transform is supported,\n  //  we use `translate3d` to apply the position to the popper we\n  // automatically use the supported prefixed version if needed\n  var prefixedProperty = getSupportedPropertyName('transform');\n\n  // now, let's make a step back and look at this code closely (wtf?)\n  // If the content of the popper grows once it's been positioned, it\n  // may happen that the popper gets misplaced because of the new content\n  // overflowing its reference element\n  // To avoid this problem, we provide two options (x and y), which allow\n  // the consumer to define the offset origin.\n  // If we position a popper on top of a reference element, we can set\n  // `x` to `top` to make the popper grow towards its top instead of\n  // its bottom.\n  var left = void 0,\n      top = void 0;\n  if (sideA === 'bottom') {\n    // when offsetParent is <html> the positioning is relative to the bottom of the screen (excluding the scrollbar)\n    // and not the bottom of the html element\n    if (offsetParent.nodeName === 'HTML') {\n      top = -offsetParent.clientHeight + offsets.bottom;\n    } else {\n      top = -offsetParentRect.height + offsets.bottom;\n    }\n  } else {\n    top = offsets.top;\n  }\n  if (sideB === 'right') {\n    if (offsetParent.nodeName === 'HTML') {\n      left = -offsetParent.clientWidth + offsets.right;\n    } else {\n      left = -offsetParentRect.width + offsets.right;\n    }\n  } else {\n    left = offsets.left;\n  }\n  if (gpuAcceleration && prefixedProperty) {\n    styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';\n    styles[sideA] = 0;\n    styles[sideB] = 0;\n    styles.willChange = 'transform';\n  } else {\n    // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties\n    var invertTop = sideA === 'bottom' ? -1 : 1;\n    var invertLeft = sideB === 'right' ? -1 : 1;\n    styles[sideA] = top * invertTop;\n    styles[sideB] = left * invertLeft;\n    styles.willChange = sideA + ', ' + sideB;\n  }\n\n  // Attributes\n  var attributes = {\n    'x-placement': data.placement\n  };\n\n  // Update `data` attributes, styles and arrowStyles\n  data.attributes = _extends({}, attributes, data.attributes);\n  data.styles = _extends({}, styles, data.styles);\n  data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles);\n\n  return data;\n}\n\n/**\n * Helper used to know if the given modifier depends from another one.<br />\n * It checks if the needed modifier is listed and enabled.\n * @method\n * @memberof Popper.Utils\n * @param {Array} modifiers - list of modifiers\n * @param {String} requestingName - name of requesting modifier\n * @param {String} requestedName - name of requested modifier\n * @returns {Boolean}\n */\nfunction isModifierRequired(modifiers, requestingName, requestedName) {\n  var requesting = find(modifiers, function (_ref) {\n    var name = _ref.name;\n    return name === requestingName;\n  });\n\n  var isRequired = !!requesting && modifiers.some(function (modifier) {\n    return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order;\n  });\n\n  if (!isRequired) {\n    var _requesting = '`' + requestingName + '`';\n    var requested = '`' + requestedName + '`';\n    console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!');\n  }\n  return isRequired;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction arrow(data, options) {\n  var _data$offsets$arrow;\n\n  // arrow depends on keepTogether in order to work\n  if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) {\n    return data;\n  }\n\n  var arrowElement = options.element;\n\n  // if arrowElement is a string, suppose it's a CSS selector\n  if (typeof arrowElement === 'string') {\n    arrowElement = data.instance.popper.querySelector(arrowElement);\n\n    // if arrowElement is not found, don't run the modifier\n    if (!arrowElement) {\n      return data;\n    }\n  } else {\n    // if the arrowElement isn't a query selector we must check that the\n    // provided DOM node is child of its popper node\n    if (!data.instance.popper.contains(arrowElement)) {\n      console.warn('WARNING: `arrow.element` must be child of its popper element!');\n      return data;\n    }\n  }\n\n  var placement = data.placement.split('-')[0];\n  var _data$offsets = data.offsets,\n      popper = _data$offsets.popper,\n      reference = _data$offsets.reference;\n\n  var isVertical = ['left', 'right'].indexOf(placement) !== -1;\n\n  var len = isVertical ? 'height' : 'width';\n  var sideCapitalized = isVertical ? 'Top' : 'Left';\n  var side = sideCapitalized.toLowerCase();\n  var altSide = isVertical ? 'left' : 'top';\n  var opSide = isVertical ? 'bottom' : 'right';\n  var arrowElementSize = getOuterSizes(arrowElement)[len];\n\n  //\n  // extends keepTogether behavior making sure the popper and its\n  // reference have enough pixels in conjunction\n  //\n\n  // top/left side\n  if (reference[opSide] - arrowElementSize < popper[side]) {\n    data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize);\n  }\n  // bottom/right side\n  if (reference[side] + arrowElementSize > popper[opSide]) {\n    data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide];\n  }\n  data.offsets.popper = getClientRect(data.offsets.popper);\n\n  // compute center of the popper\n  var center = reference[side] + reference[len] / 2 - arrowElementSize / 2;\n\n  // Compute the sideValue using the updated popper offsets\n  // take popper margin in account because we don't have this info available\n  var css = getStyleComputedProperty(data.instance.popper);\n  var popperMarginSide = parseFloat(css['margin' + sideCapitalized], 10);\n  var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width'], 10);\n  var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide;\n\n  // prevent arrowElement from being placed not contiguously to its popper\n  sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0);\n\n  data.arrowElement = arrowElement;\n  data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow);\n\n  return data;\n}\n\n/**\n * Get the opposite placement variation of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement variation\n * @returns {String} flipped placement variation\n */\nfunction getOppositeVariation(variation) {\n  if (variation === 'end') {\n    return 'start';\n  } else if (variation === 'start') {\n    return 'end';\n  }\n  return variation;\n}\n\n/**\n * List of accepted placements to use as values of the `placement` option.<br />\n * Valid placements are:\n * - `auto`\n * - `top`\n * - `right`\n * - `bottom`\n * - `left`\n *\n * Each placement can have a variation from this list:\n * - `-start`\n * - `-end`\n *\n * Variations are interpreted easily if you think of them as the left to right\n * written languages. Horizontally (`top` and `bottom`), `start` is left and `end`\n * is right.<br />\n * Vertically (`left` and `right`), `start` is top and `end` is bottom.\n *\n * Some valid examples are:\n * - `top-end` (on top of reference, right aligned)\n * - `right-start` (on right of reference, top aligned)\n * - `bottom` (on bottom, centered)\n * - `auto-end` (on the side with more space available, alignment depends by placement)\n *\n * @static\n * @type {Array}\n * @enum {String}\n * @readonly\n * @method placements\n * @memberof Popper\n */\nvar placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start'];\n\n// Get rid of `auto` `auto-start` and `auto-end`\nvar validPlacements = placements.slice(3);\n\n/**\n * Given an initial placement, returns all the subsequent placements\n * clockwise (or counter-clockwise).\n *\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement - A valid placement (it accepts variations)\n * @argument {Boolean} counter - Set to true to walk the placements counterclockwise\n * @returns {Array} placements including their variations\n */\nfunction clockwise(placement) {\n  var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n  var index = validPlacements.indexOf(placement);\n  var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index));\n  return counter ? arr.reverse() : arr;\n}\n\nvar BEHAVIORS = {\n  FLIP: 'flip',\n  CLOCKWISE: 'clockwise',\n  COUNTERCLOCKWISE: 'counterclockwise'\n};\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction flip(data, options) {\n  // if `inner` modifier is enabled, we can't use the `flip` modifier\n  if (isModifierEnabled(data.instance.modifiers, 'inner')) {\n    return data;\n  }\n\n  if (data.flipped && data.placement === data.originalPlacement) {\n    // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides\n    return data;\n  }\n\n  var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed);\n\n  var placement = data.placement.split('-')[0];\n  var placementOpposite = getOppositePlacement(placement);\n  var variation = data.placement.split('-')[1] || '';\n\n  var flipOrder = [];\n\n  switch (options.behavior) {\n    case BEHAVIORS.FLIP:\n      flipOrder = [placement, placementOpposite];\n      break;\n    case BEHAVIORS.CLOCKWISE:\n      flipOrder = clockwise(placement);\n      break;\n    case BEHAVIORS.COUNTERCLOCKWISE:\n      flipOrder = clockwise(placement, true);\n      break;\n    default:\n      flipOrder = options.behavior;\n  }\n\n  flipOrder.forEach(function (step, index) {\n    if (placement !== step || flipOrder.length === index + 1) {\n      return data;\n    }\n\n    placement = data.placement.split('-')[0];\n    placementOpposite = getOppositePlacement(placement);\n\n    var popperOffsets = data.offsets.popper;\n    var refOffsets = data.offsets.reference;\n\n    // using floor because the reference offsets may contain decimals we are not going to consider here\n    var floor = Math.floor;\n    var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom);\n\n    var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left);\n    var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right);\n    var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top);\n    var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom);\n\n    var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom;\n\n    // flip the variation if required\n    var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n\n    // flips variation if reference element overflows boundaries\n    var flippedVariationByRef = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom);\n\n    // flips variation if popper content overflows boundaries\n    var flippedVariationByContent = !!options.flipVariationsByContent && (isVertical && variation === 'start' && overflowsRight || isVertical && variation === 'end' && overflowsLeft || !isVertical && variation === 'start' && overflowsBottom || !isVertical && variation === 'end' && overflowsTop);\n\n    var flippedVariation = flippedVariationByRef || flippedVariationByContent;\n\n    if (overlapsRef || overflowsBoundaries || flippedVariation) {\n      // this boolean to detect any flip loop\n      data.flipped = true;\n\n      if (overlapsRef || overflowsBoundaries) {\n        placement = flipOrder[index + 1];\n      }\n\n      if (flippedVariation) {\n        variation = getOppositeVariation(variation);\n      }\n\n      data.placement = placement + (variation ? '-' + variation : '');\n\n      // this object contains `position`, we want to preserve it along with\n      // any additional property we may add in the future\n      data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement));\n\n      data = runModifiers(data.instance.modifiers, data, 'flip');\n    }\n  });\n  return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction keepTogether(data) {\n  var _data$offsets = data.offsets,\n      popper = _data$offsets.popper,\n      reference = _data$offsets.reference;\n\n  var placement = data.placement.split('-')[0];\n  var floor = Math.floor;\n  var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n  var side = isVertical ? 'right' : 'bottom';\n  var opSide = isVertical ? 'left' : 'top';\n  var measurement = isVertical ? 'width' : 'height';\n\n  if (popper[side] < floor(reference[opSide])) {\n    data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement];\n  }\n  if (popper[opSide] > floor(reference[side])) {\n    data.offsets.popper[opSide] = floor(reference[side]);\n  }\n\n  return data;\n}\n\n/**\n * Converts a string containing value + unit into a px value number\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} str - Value + unit string\n * @argument {String} measurement - `height` or `width`\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @returns {Number|String}\n * Value in pixels, or original string if no values were extracted\n */\nfunction toValue(str, measurement, popperOffsets, referenceOffsets) {\n  // separate value from unit\n  var split = str.match(/((?:\\-|\\+)?\\d*\\.?\\d*)(.*)/);\n  var value = +split[1];\n  var unit = split[2];\n\n  // If it's not a number it's an operator, I guess\n  if (!value) {\n    return str;\n  }\n\n  if (unit.indexOf('%') === 0) {\n    var element = void 0;\n    switch (unit) {\n      case '%p':\n        element = popperOffsets;\n        break;\n      case '%':\n      case '%r':\n      default:\n        element = referenceOffsets;\n    }\n\n    var rect = getClientRect(element);\n    return rect[measurement] / 100 * value;\n  } else if (unit === 'vh' || unit === 'vw') {\n    // if is a vh or vw, we calculate the size based on the viewport\n    var size = void 0;\n    if (unit === 'vh') {\n      size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);\n    } else {\n      size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);\n    }\n    return size / 100 * value;\n  } else {\n    // if is an explicit pixel unit, we get rid of the unit and keep the value\n    // if is an implicit unit, it's px, and we return just the value\n    return value;\n  }\n}\n\n/**\n * Parse an `offset` string to extrapolate `x` and `y` numeric offsets.\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} offset\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @argument {String} basePlacement\n * @returns {Array} a two cells array with x and y offsets in numbers\n */\nfunction parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) {\n  var offsets = [0, 0];\n\n  // Use height if placement is left or right and index is 0 otherwise use width\n  // in this way the first offset will use an axis and the second one\n  // will use the other one\n  var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1;\n\n  // Split the offset string to obtain a list of values and operands\n  // The regex addresses values with the plus or minus sign in front (+10, -20, etc)\n  var fragments = offset.split(/(\\+|\\-)/).map(function (frag) {\n    return frag.trim();\n  });\n\n  // Detect if the offset string contains a pair of values or a single one\n  // they could be separated by comma or space\n  var divider = fragments.indexOf(find(fragments, function (frag) {\n    return frag.search(/,|\\s/) !== -1;\n  }));\n\n  if (fragments[divider] && fragments[divider].indexOf(',') === -1) {\n    console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');\n  }\n\n  // If divider is found, we divide the list of values and operands to divide\n  // them by ofset X and Y.\n  var splitRegex = /\\s*,\\s*|\\s+/;\n  var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments];\n\n  // Convert the values with units to absolute pixels to allow our computations\n  ops = ops.map(function (op, index) {\n    // Most of the units rely on the orientation of the popper\n    var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width';\n    var mergeWithPrevious = false;\n    return op\n    // This aggregates any `+` or `-` sign that aren't considered operators\n    // e.g.: 10 + +5 => [10, +, +5]\n    .reduce(function (a, b) {\n      if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) {\n        a[a.length - 1] = b;\n        mergeWithPrevious = true;\n        return a;\n      } else if (mergeWithPrevious) {\n        a[a.length - 1] += b;\n        mergeWithPrevious = false;\n        return a;\n      } else {\n        return a.concat(b);\n      }\n    }, [])\n    // Here we convert the string values into number values (in px)\n    .map(function (str) {\n      return toValue(str, measurement, popperOffsets, referenceOffsets);\n    });\n  });\n\n  // Loop trough the offsets arrays and execute the operations\n  ops.forEach(function (op, index) {\n    op.forEach(function (frag, index2) {\n      if (isNumeric(frag)) {\n        offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1);\n      }\n    });\n  });\n  return offsets;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @argument {Number|String} options.offset=0\n * The offset value as described in the modifier description\n * @returns {Object} The data object, properly modified\n */\nfunction offset(data, _ref) {\n  var offset = _ref.offset;\n  var placement = data.placement,\n      _data$offsets = data.offsets,\n      popper = _data$offsets.popper,\n      reference = _data$offsets.reference;\n\n  var basePlacement = placement.split('-')[0];\n\n  var offsets = void 0;\n  if (isNumeric(+offset)) {\n    offsets = [+offset, 0];\n  } else {\n    offsets = parseOffset(offset, popper, reference, basePlacement);\n  }\n\n  if (basePlacement === 'left') {\n    popper.top += offsets[0];\n    popper.left -= offsets[1];\n  } else if (basePlacement === 'right') {\n    popper.top += offsets[0];\n    popper.left += offsets[1];\n  } else if (basePlacement === 'top') {\n    popper.left += offsets[0];\n    popper.top -= offsets[1];\n  } else if (basePlacement === 'bottom') {\n    popper.left += offsets[0];\n    popper.top += offsets[1];\n  }\n\n  data.popper = popper;\n  return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction preventOverflow(data, options) {\n  var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper);\n\n  // If offsetParent is the reference element, we really want to\n  // go one step up and use the next offsetParent as reference to\n  // avoid to make this modifier completely useless and look like broken\n  if (data.instance.reference === boundariesElement) {\n    boundariesElement = getOffsetParent(boundariesElement);\n  }\n\n  // NOTE: DOM access here\n  // resets the popper's position so that the document size can be calculated excluding\n  // the size of the popper element itself\n  var transformProp = getSupportedPropertyName('transform');\n  var popperStyles = data.instance.popper.style; // assignment to help minification\n  var top = popperStyles.top,\n      left = popperStyles.left,\n      transform = popperStyles[transformProp];\n\n  popperStyles.top = '';\n  popperStyles.left = '';\n  popperStyles[transformProp] = '';\n\n  var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed);\n\n  // NOTE: DOM access here\n  // restores the original style properties after the offsets have been computed\n  popperStyles.top = top;\n  popperStyles.left = left;\n  popperStyles[transformProp] = transform;\n\n  options.boundaries = boundaries;\n\n  var order = options.priority;\n  var popper = data.offsets.popper;\n\n  var check = {\n    primary: function primary(placement) {\n      var value = popper[placement];\n      if (popper[placement] < boundaries[placement] && !options.escapeWithReference) {\n        value = Math.max(popper[placement], boundaries[placement]);\n      }\n      return defineProperty({}, placement, value);\n    },\n    secondary: function secondary(placement) {\n      var mainSide = placement === 'right' ? 'left' : 'top';\n      var value = popper[mainSide];\n      if (popper[placement] > boundaries[placement] && !options.escapeWithReference) {\n        value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height));\n      }\n      return defineProperty({}, mainSide, value);\n    }\n  };\n\n  order.forEach(function (placement) {\n    var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';\n    popper = _extends({}, popper, check[side](placement));\n  });\n\n  data.offsets.popper = popper;\n\n  return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction shift(data) {\n  var placement = data.placement;\n  var basePlacement = placement.split('-')[0];\n  var shiftvariation = placement.split('-')[1];\n\n  // if shift shiftvariation is specified, run the modifier\n  if (shiftvariation) {\n    var _data$offsets = data.offsets,\n        reference = _data$offsets.reference,\n        popper = _data$offsets.popper;\n\n    var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1;\n    var side = isVertical ? 'left' : 'top';\n    var measurement = isVertical ? 'width' : 'height';\n\n    var shiftOffsets = {\n      start: defineProperty({}, side, reference[side]),\n      end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement])\n    };\n\n    data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]);\n  }\n\n  return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction hide(data) {\n  if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) {\n    return data;\n  }\n\n  var refRect = data.offsets.reference;\n  var bound = find(data.instance.modifiers, function (modifier) {\n    return modifier.name === 'preventOverflow';\n  }).boundaries;\n\n  if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) {\n    // Avoid unnecessary DOM access if visibility hasn't changed\n    if (data.hide === true) {\n      return data;\n    }\n\n    data.hide = true;\n    data.attributes['x-out-of-boundaries'] = '';\n  } else {\n    // Avoid unnecessary DOM access if visibility hasn't changed\n    if (data.hide === false) {\n      return data;\n    }\n\n    data.hide = false;\n    data.attributes['x-out-of-boundaries'] = false;\n  }\n\n  return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction inner(data) {\n  var placement = data.placement;\n  var basePlacement = placement.split('-')[0];\n  var _data$offsets = data.offsets,\n      popper = _data$offsets.popper,\n      reference = _data$offsets.reference;\n\n  var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1;\n\n  var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1;\n\n  popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0);\n\n  data.placement = getOppositePlacement(placement);\n  data.offsets.popper = getClientRect(popper);\n\n  return data;\n}\n\n/**\n * Modifier function, each modifier can have a function of this type assigned\n * to its `fn` property.<br />\n * These functions will be called on each update, this means that you must\n * make sure they are performant enough to avoid performance bottlenecks.\n *\n * @function ModifierFn\n * @argument {dataObject} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {dataObject} The data object, properly modified\n */\n\n/**\n * Modifiers are plugins used to alter the behavior of your poppers.<br />\n * Popper.js uses a set of 9 modifiers to provide all the basic functionalities\n * needed by the library.\n *\n * Usually you don't want to override the `order`, `fn` and `onLoad` props.\n * All the other properties are configurations that could be tweaked.\n * @namespace modifiers\n */\nvar modifiers = {\n  /**\n   * Modifier used to shift the popper on the start or end of its reference\n   * element.<br />\n   * It will read the variation of the `placement` property.<br />\n   * It can be one either `-end` or `-start`.\n   * @memberof modifiers\n   * @inner\n   */\n  shift: {\n    /** @prop {number} order=100 - Index used to define the order of execution */\n    order: 100,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: shift\n  },\n\n  /**\n   * The `offset` modifier can shift your popper on both its axis.\n   *\n   * It accepts the following units:\n   * - `px` or unit-less, interpreted as pixels\n   * - `%` or `%r`, percentage relative to the length of the reference element\n   * - `%p`, percentage relative to the length of the popper element\n   * - `vw`, CSS viewport width unit\n   * - `vh`, CSS viewport height unit\n   *\n   * For length is intended the main axis relative to the placement of the popper.<br />\n   * This means that if the placement is `top` or `bottom`, the length will be the\n   * `width`. In case of `left` or `right`, it will be the `height`.\n   *\n   * You can provide a single value (as `Number` or `String`), or a pair of values\n   * as `String` divided by a comma or one (or more) white spaces.<br />\n   * The latter is a deprecated method because it leads to confusion and will be\n   * removed in v2.<br />\n   * Additionally, it accepts additions and subtractions between different units.\n   * Note that multiplications and divisions aren't supported.\n   *\n   * Valid examples are:\n   * ```\n   * 10\n   * '10%'\n   * '10, 10'\n   * '10%, 10'\n   * '10 + 10%'\n   * '10 - 5vh + 3%'\n   * '-10px + 5vh, 5px - 6%'\n   * ```\n   * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap\n   * > with their reference element, unfortunately, you will have to disable the `flip` modifier.\n   * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373).\n   *\n   * @memberof modifiers\n   * @inner\n   */\n  offset: {\n    /** @prop {number} order=200 - Index used to define the order of execution */\n    order: 200,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: offset,\n    /** @prop {Number|String} offset=0\n     * The offset value as described in the modifier description\n     */\n    offset: 0\n  },\n\n  /**\n   * Modifier used to prevent the popper from being positioned outside the boundary.\n   *\n   * A scenario exists where the reference itself is not within the boundaries.<br />\n   * We can say it has \"escaped the boundaries\" — or just \"escaped\".<br />\n   * In this case we need to decide whether the popper should either:\n   *\n   * - detach from the reference and remain \"trapped\" in the boundaries, or\n   * - if it should ignore the boundary and \"escape with its reference\"\n   *\n   * When `escapeWithReference` is set to`true` and reference is completely\n   * outside its boundaries, the popper will overflow (or completely leave)\n   * the boundaries in order to remain attached to the edge of the reference.\n   *\n   * @memberof modifiers\n   * @inner\n   */\n  preventOverflow: {\n    /** @prop {number} order=300 - Index used to define the order of execution */\n    order: 300,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: preventOverflow,\n    /**\n     * @prop {Array} [priority=['left','right','top','bottom']]\n     * Popper will try to prevent overflow following these priorities by default,\n     * then, it could overflow on the left and on top of the `boundariesElement`\n     */\n    priority: ['left', 'right', 'top', 'bottom'],\n    /**\n     * @prop {number} padding=5\n     * Amount of pixel used to define a minimum distance between the boundaries\n     * and the popper. This makes sure the popper always has a little padding\n     * between the edges of its container\n     */\n    padding: 5,\n    /**\n     * @prop {String|HTMLElement} boundariesElement='scrollParent'\n     * Boundaries used by the modifier. Can be `scrollParent`, `window`,\n     * `viewport` or any DOM element.\n     */\n    boundariesElement: 'scrollParent'\n  },\n\n  /**\n   * Modifier used to make sure the reference and its popper stay near each other\n   * without leaving any gap between the two. Especially useful when the arrow is\n   * enabled and you want to ensure that it points to its reference element.\n   * It cares only about the first axis. You can still have poppers with margin\n   * between the popper and its reference element.\n   * @memberof modifiers\n   * @inner\n   */\n  keepTogether: {\n    /** @prop {number} order=400 - Index used to define the order of execution */\n    order: 400,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: keepTogether\n  },\n\n  /**\n   * This modifier is used to move the `arrowElement` of the popper to make\n   * sure it is positioned between the reference element and its popper element.\n   * It will read the outer size of the `arrowElement` node to detect how many\n   * pixels of conjunction are needed.\n   *\n   * It has no effect if no `arrowElement` is provided.\n   * @memberof modifiers\n   * @inner\n   */\n  arrow: {\n    /** @prop {number} order=500 - Index used to define the order of execution */\n    order: 500,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: arrow,\n    /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */\n    element: '[x-arrow]'\n  },\n\n  /**\n   * Modifier used to flip the popper's placement when it starts to overlap its\n   * reference element.\n   *\n   * Requires the `preventOverflow` modifier before it in order to work.\n   *\n   * **NOTE:** this modifier will interrupt the current update cycle and will\n   * restart it if it detects the need to flip the placement.\n   * @memberof modifiers\n   * @inner\n   */\n  flip: {\n    /** @prop {number} order=600 - Index used to define the order of execution */\n    order: 600,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: flip,\n    /**\n     * @prop {String|Array} behavior='flip'\n     * The behavior used to change the popper's placement. It can be one of\n     * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid\n     * placements (with optional variations)\n     */\n    behavior: 'flip',\n    /**\n     * @prop {number} padding=5\n     * The popper will flip if it hits the edges of the `boundariesElement`\n     */\n    padding: 5,\n    /**\n     * @prop {String|HTMLElement} boundariesElement='viewport'\n     * The element which will define the boundaries of the popper position.\n     * The popper will never be placed outside of the defined boundaries\n     * (except if `keepTogether` is enabled)\n     */\n    boundariesElement: 'viewport',\n    /**\n     * @prop {Boolean} flipVariations=false\n     * The popper will switch placement variation between `-start` and `-end` when\n     * the reference element overlaps its boundaries.\n     *\n     * The original placement should have a set variation.\n     */\n    flipVariations: false,\n    /**\n     * @prop {Boolean} flipVariationsByContent=false\n     * The popper will switch placement variation between `-start` and `-end` when\n     * the popper element overlaps its reference boundaries.\n     *\n     * The original placement should have a set variation.\n     */\n    flipVariationsByContent: false\n  },\n\n  /**\n   * Modifier used to make the popper flow toward the inner of the reference element.\n   * By default, when this modifier is disabled, the popper will be placed outside\n   * the reference element.\n   * @memberof modifiers\n   * @inner\n   */\n  inner: {\n    /** @prop {number} order=700 - Index used to define the order of execution */\n    order: 700,\n    /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */\n    enabled: false,\n    /** @prop {ModifierFn} */\n    fn: inner\n  },\n\n  /**\n   * Modifier used to hide the popper when its reference element is outside of the\n   * popper boundaries. It will set a `x-out-of-boundaries` attribute which can\n   * be used to hide with a CSS selector the popper when its reference is\n   * out of boundaries.\n   *\n   * Requires the `preventOverflow` modifier before it in order to work.\n   * @memberof modifiers\n   * @inner\n   */\n  hide: {\n    /** @prop {number} order=800 - Index used to define the order of execution */\n    order: 800,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: hide\n  },\n\n  /**\n   * Computes the style that will be applied to the popper element to gets\n   * properly positioned.\n   *\n   * Note that this modifier will not touch the DOM, it just prepares the styles\n   * so that `applyStyle` modifier can apply it. This separation is useful\n   * in case you need to replace `applyStyle` with a custom implementation.\n   *\n   * This modifier has `850` as `order` value to maintain backward compatibility\n   * with previous versions of Popper.js. Expect the modifiers ordering method\n   * to change in future major versions of the library.\n   *\n   * @memberof modifiers\n   * @inner\n   */\n  computeStyle: {\n    /** @prop {number} order=850 - Index used to define the order of execution */\n    order: 850,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: computeStyle,\n    /**\n     * @prop {Boolean} gpuAcceleration=true\n     * If true, it uses the CSS 3D transformation to position the popper.\n     * Otherwise, it will use the `top` and `left` properties\n     */\n    gpuAcceleration: true,\n    /**\n     * @prop {string} [x='bottom']\n     * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin.\n     * Change this if your popper should grow in a direction different from `bottom`\n     */\n    x: 'bottom',\n    /**\n     * @prop {string} [x='left']\n     * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin.\n     * Change this if your popper should grow in a direction different from `right`\n     */\n    y: 'right'\n  },\n\n  /**\n   * Applies the computed styles to the popper element.\n   *\n   * All the DOM manipulations are limited to this modifier. This is useful in case\n   * you want to integrate Popper.js inside a framework or view library and you\n   * want to delegate all the DOM manipulations to it.\n   *\n   * Note that if you disable this modifier, you must make sure the popper element\n   * has its position set to `absolute` before Popper.js can do its work!\n   *\n   * Just disable this modifier and define your own to achieve the desired effect.\n   *\n   * @memberof modifiers\n   * @inner\n   */\n  applyStyle: {\n    /** @prop {number} order=900 - Index used to define the order of execution */\n    order: 900,\n    /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n    enabled: true,\n    /** @prop {ModifierFn} */\n    fn: applyStyle,\n    /** @prop {Function} */\n    onLoad: applyStyleOnLoad,\n    /**\n     * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier\n     * @prop {Boolean} gpuAcceleration=true\n     * If true, it uses the CSS 3D transformation to position the popper.\n     * Otherwise, it will use the `top` and `left` properties\n     */\n    gpuAcceleration: undefined\n  }\n};\n\n/**\n * The `dataObject` is an object containing all the information used by Popper.js.\n * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks.\n * @name dataObject\n * @property {Object} data.instance The Popper.js instance\n * @property {String} data.placement Placement applied to popper\n * @property {String} data.originalPlacement Placement originally defined on init\n * @property {Boolean} data.flipped True if popper has been flipped by flip modifier\n * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper\n * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier\n * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.boundaries Offsets of the popper boundaries\n * @property {Object} data.offsets The measurements of popper, reference and arrow elements\n * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0\n */\n\n/**\n * Default options provided to Popper.js constructor.<br />\n * These can be overridden using the `options` argument of Popper.js.<br />\n * To override an option, simply pass an object with the same\n * structure of the `options` object, as the 3rd argument. For example:\n * ```\n * new Popper(ref, pop, {\n *   modifiers: {\n *     preventOverflow: { enabled: false }\n *   }\n * })\n * ```\n * @type {Object}\n * @static\n * @memberof Popper\n */\nvar Defaults = {\n  /**\n   * Popper's placement.\n   * @prop {Popper.placements} placement='bottom'\n   */\n  placement: 'bottom',\n\n  /**\n   * Set this to true if you want popper to position it self in 'fixed' mode\n   * @prop {Boolean} positionFixed=false\n   */\n  positionFixed: false,\n\n  /**\n   * Whether events (resize, scroll) are initially enabled.\n   * @prop {Boolean} eventsEnabled=true\n   */\n  eventsEnabled: true,\n\n  /**\n   * Set to true if you want to automatically remove the popper when\n   * you call the `destroy` method.\n   * @prop {Boolean} removeOnDestroy=false\n   */\n  removeOnDestroy: false,\n\n  /**\n   * Callback called when the popper is created.<br />\n   * By default, it is set to no-op.<br />\n   * Access Popper.js instance with `data.instance`.\n   * @prop {onCreate}\n   */\n  onCreate: function onCreate() {},\n\n  /**\n   * Callback called when the popper is updated. This callback is not called\n   * on the initialization/creation of the popper, but only on subsequent\n   * updates.<br />\n   * By default, it is set to no-op.<br />\n   * Access Popper.js instance with `data.instance`.\n   * @prop {onUpdate}\n   */\n  onUpdate: function onUpdate() {},\n\n  /**\n   * List of modifiers used to modify the offsets before they are applied to the popper.\n   * They provide most of the functionalities of Popper.js.\n   * @prop {modifiers}\n   */\n  modifiers: modifiers\n};\n\n/**\n * @callback onCreate\n * @param {dataObject} data\n */\n\n/**\n * @callback onUpdate\n * @param {dataObject} data\n */\n\n// Utils\n// Methods\nvar Popper = function () {\n  /**\n   * Creates a new Popper.js instance.\n   * @class Popper\n   * @param {Element|referenceObject} reference - The reference element used to position the popper\n   * @param {Element} popper - The HTML / XML element used as the popper\n   * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults)\n   * @return {Object} instance - The generated Popper.js instance\n   */\n  function Popper(reference, popper) {\n    var _this = this;\n\n    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n    classCallCheck(this, Popper);\n\n    this.scheduleUpdate = function () {\n      return requestAnimationFrame(_this.update);\n    };\n\n    // make update() debounced, so that it only runs at most once-per-tick\n    this.update = debounce(this.update.bind(this));\n\n    // with {} we create a new object with the options inside it\n    this.options = _extends({}, Popper.Defaults, options);\n\n    // init state\n    this.state = {\n      isDestroyed: false,\n      isCreated: false,\n      scrollParents: []\n    };\n\n    // get reference and popper elements (allow jQuery wrappers)\n    this.reference = reference && reference.jquery ? reference[0] : reference;\n    this.popper = popper && popper.jquery ? popper[0] : popper;\n\n    // Deep merge modifiers options\n    this.options.modifiers = {};\n    Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) {\n      _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {});\n    });\n\n    // Refactoring modifiers' list (Object => Array)\n    this.modifiers = Object.keys(this.options.modifiers).map(function (name) {\n      return _extends({\n        name: name\n      }, _this.options.modifiers[name]);\n    })\n    // sort the modifiers by order\n    .sort(function (a, b) {\n      return a.order - b.order;\n    });\n\n    // modifiers have the ability to execute arbitrary code when Popper.js get inited\n    // such code is executed in the same order of its modifier\n    // they could add new properties to their options configuration\n    // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`!\n    this.modifiers.forEach(function (modifierOptions) {\n      if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) {\n        modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state);\n      }\n    });\n\n    // fire the first update to position the popper in the right place\n    this.update();\n\n    var eventsEnabled = this.options.eventsEnabled;\n    if (eventsEnabled) {\n      // setup event listeners, they will take care of update the position in specific situations\n      this.enableEventListeners();\n    }\n\n    this.state.eventsEnabled = eventsEnabled;\n  }\n\n  // We can't use class properties because they don't get listed in the\n  // class prototype and break stuff like Sinon stubs\n\n\n  createClass(Popper, [{\n    key: 'update',\n    value: function update$$1() {\n      return update.call(this);\n    }\n  }, {\n    key: 'destroy',\n    value: function destroy$$1() {\n      return destroy.call(this);\n    }\n  }, {\n    key: 'enableEventListeners',\n    value: function enableEventListeners$$1() {\n      return enableEventListeners.call(this);\n    }\n  }, {\n    key: 'disableEventListeners',\n    value: function disableEventListeners$$1() {\n      return disableEventListeners.call(this);\n    }\n\n    /**\n     * Schedules an update. It will run on the next UI update available.\n     * @method scheduleUpdate\n     * @memberof Popper\n     */\n\n\n    /**\n     * Collection of utilities useful when writing custom modifiers.\n     * Starting from version 1.7, this method is available only if you\n     * include `popper-utils.js` before `popper.js`.\n     *\n     * **DEPRECATION**: This way to access PopperUtils is deprecated\n     * and will be removed in v2! Use the PopperUtils module directly instead.\n     * Due to the high instability of the methods contained in Utils, we can't\n     * guarantee them to follow semver. Use them at your own risk!\n     * @static\n     * @private\n     * @type {Object}\n     * @deprecated since version 1.8\n     * @member Utils\n     * @memberof Popper\n     */\n\n  }]);\n  return Popper;\n}();\n\n/**\n * The `referenceObject` is an object that provides an interface compatible with Popper.js\n * and lets you use it as replacement of a real DOM node.<br />\n * You can use this method to position a popper relatively to a set of coordinates\n * in case you don't have a DOM node to use as reference.\n *\n * ```\n * new Popper(referenceObject, popperNode);\n * ```\n *\n * NB: This feature isn't supported in Internet Explorer 10.\n * @name referenceObject\n * @property {Function} data.getBoundingClientRect\n * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method.\n * @property {number} data.clientWidth\n * An ES6 getter that will return the width of the virtual reference element.\n * @property {number} data.clientHeight\n * An ES6 getter that will return the height of the virtual reference element.\n */\n\n\nPopper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils;\nPopper.placements = placements;\nPopper.Defaults = Defaults;\n\nreturn Popper;\n\n})));\n//# sourceMappingURL=popper.js.map","/*!\n  * Bootstrap v4.3.1 (https://getbootstrap.com/)\n  * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n  */\n (function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery'), require('popper.js')) :\n  typeof define === 'function' && define.amd ? define(['exports', 'jquery', 'popper.js'], factory) :\n  (global = global || self, factory(global.bootstrap = {}, global.jQuery, global.Popper));\n}(this, function (exports, $, Popper) { 'use strict';\n\n  $ = $ && $.hasOwnProperty('default') ? $['default'] : $;\n  Popper = Popper && Popper.hasOwnProperty('default') ? Popper['default'] : Popper;\n\n  function _defineProperties(target, props) {\n    for (var i = 0; i < props.length; i++) {\n      var descriptor = props[i];\n      descriptor.enumerable = descriptor.enumerable || false;\n      descriptor.configurable = true;\n      if (\"value\" in descriptor) descriptor.writable = true;\n      Object.defineProperty(target, descriptor.key, descriptor);\n    }\n  }\n\n  function _createClass(Constructor, protoProps, staticProps) {\n    if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n    if (staticProps) _defineProperties(Constructor, staticProps);\n    return Constructor;\n  }\n\n  function _defineProperty(obj, key, value) {\n    if (key in obj) {\n      Object.defineProperty(obj, key, {\n        value: value,\n        enumerable: true,\n        configurable: true,\n        writable: true\n      });\n    } else {\n      obj[key] = value;\n    }\n\n    return obj;\n  }\n\n  function _objectSpread(target) {\n    for (var i = 1; i < arguments.length; i++) {\n      var source = arguments[i] != null ? arguments[i] : {};\n      var ownKeys = Object.keys(source);\n\n      if (typeof Object.getOwnPropertySymbols === 'function') {\n        ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {\n          return Object.getOwnPropertyDescriptor(source, sym).enumerable;\n        }));\n      }\n\n      ownKeys.forEach(function (key) {\n        _defineProperty(target, key, source[key]);\n      });\n    }\n\n    return target;\n  }\n\n  function _inheritsLoose(subClass, superClass) {\n    subClass.prototype = Object.create(superClass.prototype);\n    subClass.prototype.constructor = subClass;\n    subClass.__proto__ = superClass;\n  }\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap (v4.3.1): util.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n  /**\n   * ------------------------------------------------------------------------\n   * Private TransitionEnd Helpers\n   * ------------------------------------------------------------------------\n   */\n\n  var TRANSITION_END = 'transitionend';\n  var MAX_UID = 1000000;\n  var MILLISECONDS_MULTIPLIER = 1000; // Shoutout AngusCroll (https://goo.gl/pxwQGp)\n\n  function toType(obj) {\n    return {}.toString.call(obj).match(/\\s([a-z]+)/i)[1].toLowerCase();\n  }\n\n  function getSpecialTransitionEndEvent() {\n    return {\n      bindType: TRANSITION_END,\n      delegateType: TRANSITION_END,\n      handle: function handle(event) {\n        if ($(event.target).is(this)) {\n          return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params\n        }\n\n        return undefined; // eslint-disable-line no-undefined\n      }\n    };\n  }\n\n  function transitionEndEmulator(duration) {\n    var _this = this;\n\n    var called = false;\n    $(this).one(Util.TRANSITION_END, function () {\n      called = true;\n    });\n    setTimeout(function () {\n      if (!called) {\n        Util.triggerTransitionEnd(_this);\n      }\n    }, duration);\n    return this;\n  }\n\n  function setTransitionEndSupport() {\n    $.fn.emulateTransitionEnd = transitionEndEmulator;\n    $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent();\n  }\n  /**\n   * --------------------------------------------------------------------------\n   * Public Util Api\n   * --------------------------------------------------------------------------\n   */\n\n\n  var Util = {\n    TRANSITION_END: 'bsTransitionEnd',\n    getUID: function getUID(prefix) {\n      do {\n        // eslint-disable-next-line no-bitwise\n        prefix += ~~(Math.random() * MAX_UID); // \"~~\" acts like a faster Math.floor() here\n      } while (document.getElementById(prefix));\n\n      return prefix;\n    },\n    getSelectorFromElement: function getSelectorFromElement(element) {\n      var selector = element.getAttribute('data-target');\n\n      if (!selector || selector === '#') {\n        var hrefAttr = element.getAttribute('href');\n        selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : '';\n      }\n\n      try {\n        return document.querySelector(selector) ? selector : null;\n      } catch (err) {\n        return null;\n      }\n    },\n    getTransitionDurationFromElement: function getTransitionDurationFromElement(element) {\n      if (!element) {\n        return 0;\n      } // Get transition-duration of the element\n\n\n      var transitionDuration = $(element).css('transition-duration');\n      var transitionDelay = $(element).css('transition-delay');\n      var floatTransitionDuration = parseFloat(transitionDuration);\n      var floatTransitionDelay = parseFloat(transitionDelay); // Return 0 if element or transition duration is not found\n\n      if (!floatTransitionDuration && !floatTransitionDelay) {\n        return 0;\n      } // If multiple durations are defined, take the first\n\n\n      transitionDuration = transitionDuration.split(',')[0];\n      transitionDelay = transitionDelay.split(',')[0];\n      return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n    },\n    reflow: function reflow(element) {\n      return element.offsetHeight;\n    },\n    triggerTransitionEnd: function triggerTransitionEnd(element) {\n      $(element).trigger(TRANSITION_END);\n    },\n    // TODO: Remove in v5\n    supportsTransitionEnd: function supportsTransitionEnd() {\n      return Boolean(TRANSITION_END);\n    },\n    isElement: function isElement(obj) {\n      return (obj[0] || obj).nodeType;\n    },\n    typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) {\n      for (var property in configTypes) {\n        if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n          var expectedTypes = configTypes[property];\n          var value = config[property];\n          var valueType = value && Util.isElement(value) ? 'element' : toType(value);\n\n          if (!new RegExp(expectedTypes).test(valueType)) {\n            throw new Error(componentName.toUpperCase() + \": \" + (\"Option \\\"\" + property + \"\\\" provided type \\\"\" + valueType + \"\\\" \") + (\"but expected type \\\"\" + expectedTypes + \"\\\".\"));\n          }\n        }\n      }\n    },\n    findShadowRoot: function findShadowRoot(element) {\n      if (!document.documentElement.attachShadow) {\n        return null;\n      } // Can find the shadow root otherwise it'll return the document\n\n\n      if (typeof element.getRootNode === 'function') {\n        var root = element.getRootNode();\n        return root instanceof ShadowRoot ? root : null;\n      }\n\n      if (element instanceof ShadowRoot) {\n        return element;\n      } // when we don't find a shadow root\n\n\n      if (!element.parentNode) {\n        return null;\n      }\n\n      return Util.findShadowRoot(element.parentNode);\n    }\n  };\n  setTransitionEndSupport();\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME = 'alert';\n  var VERSION = '4.3.1';\n  var DATA_KEY = 'bs.alert';\n  var EVENT_KEY = \".\" + DATA_KEY;\n  var DATA_API_KEY = '.data-api';\n  var JQUERY_NO_CONFLICT = $.fn[NAME];\n  var Selector = {\n    DISMISS: '[data-dismiss=\"alert\"]'\n  };\n  var Event = {\n    CLOSE: \"close\" + EVENT_KEY,\n    CLOSED: \"closed\" + EVENT_KEY,\n    CLICK_DATA_API: \"click\" + EVENT_KEY + DATA_API_KEY\n  };\n  var ClassName = {\n    ALERT: 'alert',\n    FADE: 'fade',\n    SHOW: 'show'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Alert =\n  /*#__PURE__*/\n  function () {\n    function Alert(element) {\n      this._element = element;\n    } // Getters\n\n\n    var _proto = Alert.prototype;\n\n    // Public\n    _proto.close = function close(element) {\n      var rootElement = this._element;\n\n      if (element) {\n        rootElement = this._getRootElement(element);\n      }\n\n      var customEvent = this._triggerCloseEvent(rootElement);\n\n      if (customEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      this._removeElement(rootElement);\n    };\n\n    _proto.dispose = function dispose() {\n      $.removeData(this._element, DATA_KEY);\n      this._element = null;\n    } // Private\n    ;\n\n    _proto._getRootElement = function _getRootElement(element) {\n      var selector = Util.getSelectorFromElement(element);\n      var parent = false;\n\n      if (selector) {\n        parent = document.querySelector(selector);\n      }\n\n      if (!parent) {\n        parent = $(element).closest(\".\" + ClassName.ALERT)[0];\n      }\n\n      return parent;\n    };\n\n    _proto._triggerCloseEvent = function _triggerCloseEvent(element) {\n      var closeEvent = $.Event(Event.CLOSE);\n      $(element).trigger(closeEvent);\n      return closeEvent;\n    };\n\n    _proto._removeElement = function _removeElement(element) {\n      var _this = this;\n\n      $(element).removeClass(ClassName.SHOW);\n\n      if (!$(element).hasClass(ClassName.FADE)) {\n        this._destroyElement(element);\n\n        return;\n      }\n\n      var transitionDuration = Util.getTransitionDurationFromElement(element);\n      $(element).one(Util.TRANSITION_END, function (event) {\n        return _this._destroyElement(element, event);\n      }).emulateTransitionEnd(transitionDuration);\n    };\n\n    _proto._destroyElement = function _destroyElement(element) {\n      $(element).detach().trigger(Event.CLOSED).remove();\n    } // Static\n    ;\n\n    Alert._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var $element = $(this);\n        var data = $element.data(DATA_KEY);\n\n        if (!data) {\n          data = new Alert(this);\n          $element.data(DATA_KEY, data);\n        }\n\n        if (config === 'close') {\n          data[config](this);\n        }\n      });\n    };\n\n    Alert._handleDismiss = function _handleDismiss(alertInstance) {\n      return function (event) {\n        if (event) {\n          event.preventDefault();\n        }\n\n        alertInstance.close(this);\n      };\n    };\n\n    _createClass(Alert, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION;\n      }\n    }]);\n\n    return Alert;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert()));\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME] = Alert._jQueryInterface;\n  $.fn[NAME].Constructor = Alert;\n\n  $.fn[NAME].noConflict = function () {\n    $.fn[NAME] = JQUERY_NO_CONFLICT;\n    return Alert._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$1 = 'button';\n  var VERSION$1 = '4.3.1';\n  var DATA_KEY$1 = 'bs.button';\n  var EVENT_KEY$1 = \".\" + DATA_KEY$1;\n  var DATA_API_KEY$1 = '.data-api';\n  var JQUERY_NO_CONFLICT$1 = $.fn[NAME$1];\n  var ClassName$1 = {\n    ACTIVE: 'active',\n    BUTTON: 'btn',\n    FOCUS: 'focus'\n  };\n  var Selector$1 = {\n    DATA_TOGGLE_CARROT: '[data-toggle^=\"button\"]',\n    DATA_TOGGLE: '[data-toggle=\"buttons\"]',\n    INPUT: 'input:not([type=\"hidden\"])',\n    ACTIVE: '.active',\n    BUTTON: '.btn'\n  };\n  var Event$1 = {\n    CLICK_DATA_API: \"click\" + EVENT_KEY$1 + DATA_API_KEY$1,\n    FOCUS_BLUR_DATA_API: \"focus\" + EVENT_KEY$1 + DATA_API_KEY$1 + \" \" + (\"blur\" + EVENT_KEY$1 + DATA_API_KEY$1)\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Button =\n  /*#__PURE__*/\n  function () {\n    function Button(element) {\n      this._element = element;\n    } // Getters\n\n\n    var _proto = Button.prototype;\n\n    // Public\n    _proto.toggle = function toggle() {\n      var triggerChangeEvent = true;\n      var addAriaPressed = true;\n      var rootElement = $(this._element).closest(Selector$1.DATA_TOGGLE)[0];\n\n      if (rootElement) {\n        var input = this._element.querySelector(Selector$1.INPUT);\n\n        if (input) {\n          if (input.type === 'radio') {\n            if (input.checked && this._element.classList.contains(ClassName$1.ACTIVE)) {\n              triggerChangeEvent = false;\n            } else {\n              var activeElement = rootElement.querySelector(Selector$1.ACTIVE);\n\n              if (activeElement) {\n                $(activeElement).removeClass(ClassName$1.ACTIVE);\n              }\n            }\n          }\n\n          if (triggerChangeEvent) {\n            if (input.hasAttribute('disabled') || rootElement.hasAttribute('disabled') || input.classList.contains('disabled') || rootElement.classList.contains('disabled')) {\n              return;\n            }\n\n            input.checked = !this._element.classList.contains(ClassName$1.ACTIVE);\n            $(input).trigger('change');\n          }\n\n          input.focus();\n          addAriaPressed = false;\n        }\n      }\n\n      if (addAriaPressed) {\n        this._element.setAttribute('aria-pressed', !this._element.classList.contains(ClassName$1.ACTIVE));\n      }\n\n      if (triggerChangeEvent) {\n        $(this._element).toggleClass(ClassName$1.ACTIVE);\n      }\n    };\n\n    _proto.dispose = function dispose() {\n      $.removeData(this._element, DATA_KEY$1);\n      this._element = null;\n    } // Static\n    ;\n\n    Button._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var data = $(this).data(DATA_KEY$1);\n\n        if (!data) {\n          data = new Button(this);\n          $(this).data(DATA_KEY$1, data);\n        }\n\n        if (config === 'toggle') {\n          data[config]();\n        }\n      });\n    };\n\n    _createClass(Button, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$1;\n      }\n    }]);\n\n    return Button;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(document).on(Event$1.CLICK_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {\n    event.preventDefault();\n    var button = event.target;\n\n    if (!$(button).hasClass(ClassName$1.BUTTON)) {\n      button = $(button).closest(Selector$1.BUTTON);\n    }\n\n    Button._jQueryInterface.call($(button), 'toggle');\n  }).on(Event$1.FOCUS_BLUR_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {\n    var button = $(event.target).closest(Selector$1.BUTTON)[0];\n    $(button).toggleClass(ClassName$1.FOCUS, /^focus(in)?$/.test(event.type));\n  });\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME$1] = Button._jQueryInterface;\n  $.fn[NAME$1].Constructor = Button;\n\n  $.fn[NAME$1].noConflict = function () {\n    $.fn[NAME$1] = JQUERY_NO_CONFLICT$1;\n    return Button._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$2 = 'carousel';\n  var VERSION$2 = '4.3.1';\n  var DATA_KEY$2 = 'bs.carousel';\n  var EVENT_KEY$2 = \".\" + DATA_KEY$2;\n  var DATA_API_KEY$2 = '.data-api';\n  var JQUERY_NO_CONFLICT$2 = $.fn[NAME$2];\n  var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key\n\n  var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key\n\n  var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\n  var SWIPE_THRESHOLD = 40;\n  var Default = {\n    interval: 5000,\n    keyboard: true,\n    slide: false,\n    pause: 'hover',\n    wrap: true,\n    touch: true\n  };\n  var DefaultType = {\n    interval: '(number|boolean)',\n    keyboard: 'boolean',\n    slide: '(boolean|string)',\n    pause: '(string|boolean)',\n    wrap: 'boolean',\n    touch: 'boolean'\n  };\n  var Direction = {\n    NEXT: 'next',\n    PREV: 'prev',\n    LEFT: 'left',\n    RIGHT: 'right'\n  };\n  var Event$2 = {\n    SLIDE: \"slide\" + EVENT_KEY$2,\n    SLID: \"slid\" + EVENT_KEY$2,\n    KEYDOWN: \"keydown\" + EVENT_KEY$2,\n    MOUSEENTER: \"mouseenter\" + EVENT_KEY$2,\n    MOUSELEAVE: \"mouseleave\" + EVENT_KEY$2,\n    TOUCHSTART: \"touchstart\" + EVENT_KEY$2,\n    TOUCHMOVE: \"touchmove\" + EVENT_KEY$2,\n    TOUCHEND: \"touchend\" + EVENT_KEY$2,\n    POINTERDOWN: \"pointerdown\" + EVENT_KEY$2,\n    POINTERUP: \"pointerup\" + EVENT_KEY$2,\n    DRAG_START: \"dragstart\" + EVENT_KEY$2,\n    LOAD_DATA_API: \"load\" + EVENT_KEY$2 + DATA_API_KEY$2,\n    CLICK_DATA_API: \"click\" + EVENT_KEY$2 + DATA_API_KEY$2\n  };\n  var ClassName$2 = {\n    CAROUSEL: 'carousel',\n    ACTIVE: 'active',\n    SLIDE: 'slide',\n    RIGHT: 'carousel-item-right',\n    LEFT: 'carousel-item-left',\n    NEXT: 'carousel-item-next',\n    PREV: 'carousel-item-prev',\n    ITEM: 'carousel-item',\n    POINTER_EVENT: 'pointer-event'\n  };\n  var Selector$2 = {\n    ACTIVE: '.active',\n    ACTIVE_ITEM: '.active.carousel-item',\n    ITEM: '.carousel-item',\n    ITEM_IMG: '.carousel-item img',\n    NEXT_PREV: '.carousel-item-next, .carousel-item-prev',\n    INDICATORS: '.carousel-indicators',\n    DATA_SLIDE: '[data-slide], [data-slide-to]',\n    DATA_RIDE: '[data-ride=\"carousel\"]'\n  };\n  var PointerType = {\n    TOUCH: 'touch',\n    PEN: 'pen'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Carousel =\n  /*#__PURE__*/\n  function () {\n    function Carousel(element, config) {\n      this._items = null;\n      this._interval = null;\n      this._activeElement = null;\n      this._isPaused = false;\n      this._isSliding = false;\n      this.touchTimeout = null;\n      this.touchStartX = 0;\n      this.touchDeltaX = 0;\n      this._config = this._getConfig(config);\n      this._element = element;\n      this._indicatorsElement = this._element.querySelector(Selector$2.INDICATORS);\n      this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n      this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent);\n\n      this._addEventListeners();\n    } // Getters\n\n\n    var _proto = Carousel.prototype;\n\n    // Public\n    _proto.next = function next() {\n      if (!this._isSliding) {\n        this._slide(Direction.NEXT);\n      }\n    };\n\n    _proto.nextWhenVisible = function nextWhenVisible() {\n      // Don't call next when the page isn't visible\n      // or the carousel or its parent isn't visible\n      if (!document.hidden && $(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden') {\n        this.next();\n      }\n    };\n\n    _proto.prev = function prev() {\n      if (!this._isSliding) {\n        this._slide(Direction.PREV);\n      }\n    };\n\n    _proto.pause = function pause(event) {\n      if (!event) {\n        this._isPaused = true;\n      }\n\n      if (this._element.querySelector(Selector$2.NEXT_PREV)) {\n        Util.triggerTransitionEnd(this._element);\n        this.cycle(true);\n      }\n\n      clearInterval(this._interval);\n      this._interval = null;\n    };\n\n    _proto.cycle = function cycle(event) {\n      if (!event) {\n        this._isPaused = false;\n      }\n\n      if (this._interval) {\n        clearInterval(this._interval);\n        this._interval = null;\n      }\n\n      if (this._config.interval && !this._isPaused) {\n        this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval);\n      }\n    };\n\n    _proto.to = function to(index) {\n      var _this = this;\n\n      this._activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);\n\n      var activeIndex = this._getItemIndex(this._activeElement);\n\n      if (index > this._items.length - 1 || index < 0) {\n        return;\n      }\n\n      if (this._isSliding) {\n        $(this._element).one(Event$2.SLID, function () {\n          return _this.to(index);\n        });\n        return;\n      }\n\n      if (activeIndex === index) {\n        this.pause();\n        this.cycle();\n        return;\n      }\n\n      var direction = index > activeIndex ? Direction.NEXT : Direction.PREV;\n\n      this._slide(direction, this._items[index]);\n    };\n\n    _proto.dispose = function dispose() {\n      $(this._element).off(EVENT_KEY$2);\n      $.removeData(this._element, DATA_KEY$2);\n      this._items = null;\n      this._config = null;\n      this._element = null;\n      this._interval = null;\n      this._isPaused = null;\n      this._isSliding = null;\n      this._activeElement = null;\n      this._indicatorsElement = null;\n    } // Private\n    ;\n\n    _proto._getConfig = function _getConfig(config) {\n      config = _objectSpread({}, Default, config);\n      Util.typeCheckConfig(NAME$2, config, DefaultType);\n      return config;\n    };\n\n    _proto._handleSwipe = function _handleSwipe() {\n      var absDeltax = Math.abs(this.touchDeltaX);\n\n      if (absDeltax <= SWIPE_THRESHOLD) {\n        return;\n      }\n\n      var direction = absDeltax / this.touchDeltaX; // swipe left\n\n      if (direction > 0) {\n        this.prev();\n      } // swipe right\n\n\n      if (direction < 0) {\n        this.next();\n      }\n    };\n\n    _proto._addEventListeners = function _addEventListeners() {\n      var _this2 = this;\n\n      if (this._config.keyboard) {\n        $(this._element).on(Event$2.KEYDOWN, function (event) {\n          return _this2._keydown(event);\n        });\n      }\n\n      if (this._config.pause === 'hover') {\n        $(this._element).on(Event$2.MOUSEENTER, function (event) {\n          return _this2.pause(event);\n        }).on(Event$2.MOUSELEAVE, function (event) {\n          return _this2.cycle(event);\n        });\n      }\n\n      if (this._config.touch) {\n        this._addTouchEventListeners();\n      }\n    };\n\n    _proto._addTouchEventListeners = function _addTouchEventListeners() {\n      var _this3 = this;\n\n      if (!this._touchSupported) {\n        return;\n      }\n\n      var start = function start(event) {\n        if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n          _this3.touchStartX = event.originalEvent.clientX;\n        } else if (!_this3._pointerEvent) {\n          _this3.touchStartX = event.originalEvent.touches[0].clientX;\n        }\n      };\n\n      var move = function move(event) {\n        // ensure swiping with one touch and not pinching\n        if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n          _this3.touchDeltaX = 0;\n        } else {\n          _this3.touchDeltaX = event.originalEvent.touches[0].clientX - _this3.touchStartX;\n        }\n      };\n\n      var end = function end(event) {\n        if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n          _this3.touchDeltaX = event.originalEvent.clientX - _this3.touchStartX;\n        }\n\n        _this3._handleSwipe();\n\n        if (_this3._config.pause === 'hover') {\n          // If it's a touch-enabled device, mouseenter/leave are fired as\n          // part of the mouse compatibility events on first tap - the carousel\n          // would stop cycling until user tapped out of it;\n          // here, we listen for touchend, explicitly pause the carousel\n          // (as if it's the second time we tap on it, mouseenter compat event\n          // is NOT fired) and after a timeout (to allow for mouse compatibility\n          // events to fire) we explicitly restart cycling\n          _this3.pause();\n\n          if (_this3.touchTimeout) {\n            clearTimeout(_this3.touchTimeout);\n          }\n\n          _this3.touchTimeout = setTimeout(function (event) {\n            return _this3.cycle(event);\n          }, TOUCHEVENT_COMPAT_WAIT + _this3._config.interval);\n        }\n      };\n\n      $(this._element.querySelectorAll(Selector$2.ITEM_IMG)).on(Event$2.DRAG_START, function (e) {\n        return e.preventDefault();\n      });\n\n      if (this._pointerEvent) {\n        $(this._element).on(Event$2.POINTERDOWN, function (event) {\n          return start(event);\n        });\n        $(this._element).on(Event$2.POINTERUP, function (event) {\n          return end(event);\n        });\n\n        this._element.classList.add(ClassName$2.POINTER_EVENT);\n      } else {\n        $(this._element).on(Event$2.TOUCHSTART, function (event) {\n          return start(event);\n        });\n        $(this._element).on(Event$2.TOUCHMOVE, function (event) {\n          return move(event);\n        });\n        $(this._element).on(Event$2.TOUCHEND, function (event) {\n          return end(event);\n        });\n      }\n    };\n\n    _proto._keydown = function _keydown(event) {\n      if (/input|textarea/i.test(event.target.tagName)) {\n        return;\n      }\n\n      switch (event.which) {\n        case ARROW_LEFT_KEYCODE:\n          event.preventDefault();\n          this.prev();\n          break;\n\n        case ARROW_RIGHT_KEYCODE:\n          event.preventDefault();\n          this.next();\n          break;\n\n        default:\n      }\n    };\n\n    _proto._getItemIndex = function _getItemIndex(element) {\n      this._items = element && element.parentNode ? [].slice.call(element.parentNode.querySelectorAll(Selector$2.ITEM)) : [];\n      return this._items.indexOf(element);\n    };\n\n    _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) {\n      var isNextDirection = direction === Direction.NEXT;\n      var isPrevDirection = direction === Direction.PREV;\n\n      var activeIndex = this._getItemIndex(activeElement);\n\n      var lastItemIndex = this._items.length - 1;\n      var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex;\n\n      if (isGoingToWrap && !this._config.wrap) {\n        return activeElement;\n      }\n\n      var delta = direction === Direction.PREV ? -1 : 1;\n      var itemIndex = (activeIndex + delta) % this._items.length;\n      return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex];\n    };\n\n    _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) {\n      var targetIndex = this._getItemIndex(relatedTarget);\n\n      var fromIndex = this._getItemIndex(this._element.querySelector(Selector$2.ACTIVE_ITEM));\n\n      var slideEvent = $.Event(Event$2.SLIDE, {\n        relatedTarget: relatedTarget,\n        direction: eventDirectionName,\n        from: fromIndex,\n        to: targetIndex\n      });\n      $(this._element).trigger(slideEvent);\n      return slideEvent;\n    };\n\n    _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) {\n      if (this._indicatorsElement) {\n        var indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector$2.ACTIVE));\n        $(indicators).removeClass(ClassName$2.ACTIVE);\n\n        var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)];\n\n        if (nextIndicator) {\n          $(nextIndicator).addClass(ClassName$2.ACTIVE);\n        }\n      }\n    };\n\n    _proto._slide = function _slide(direction, element) {\n      var _this4 = this;\n\n      var activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);\n\n      var activeElementIndex = this._getItemIndex(activeElement);\n\n      var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement);\n\n      var nextElementIndex = this._getItemIndex(nextElement);\n\n      var isCycling = Boolean(this._interval);\n      var directionalClassName;\n      var orderClassName;\n      var eventDirectionName;\n\n      if (direction === Direction.NEXT) {\n        directionalClassName = ClassName$2.LEFT;\n        orderClassName = ClassName$2.NEXT;\n        eventDirectionName = Direction.LEFT;\n      } else {\n        directionalClassName = ClassName$2.RIGHT;\n        orderClassName = ClassName$2.PREV;\n        eventDirectionName = Direction.RIGHT;\n      }\n\n      if (nextElement && $(nextElement).hasClass(ClassName$2.ACTIVE)) {\n        this._isSliding = false;\n        return;\n      }\n\n      var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName);\n\n      if (slideEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      if (!activeElement || !nextElement) {\n        // Some weirdness is happening, so we bail\n        return;\n      }\n\n      this._isSliding = true;\n\n      if (isCycling) {\n        this.pause();\n      }\n\n      this._setActiveIndicatorElement(nextElement);\n\n      var slidEvent = $.Event(Event$2.SLID, {\n        relatedTarget: nextElement,\n        direction: eventDirectionName,\n        from: activeElementIndex,\n        to: nextElementIndex\n      });\n\n      if ($(this._element).hasClass(ClassName$2.SLIDE)) {\n        $(nextElement).addClass(orderClassName);\n        Util.reflow(nextElement);\n        $(activeElement).addClass(directionalClassName);\n        $(nextElement).addClass(directionalClassName);\n        var nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10);\n\n        if (nextElementInterval) {\n          this._config.defaultInterval = this._config.defaultInterval || this._config.interval;\n          this._config.interval = nextElementInterval;\n        } else {\n          this._config.interval = this._config.defaultInterval || this._config.interval;\n        }\n\n        var transitionDuration = Util.getTransitionDurationFromElement(activeElement);\n        $(activeElement).one(Util.TRANSITION_END, function () {\n          $(nextElement).removeClass(directionalClassName + \" \" + orderClassName).addClass(ClassName$2.ACTIVE);\n          $(activeElement).removeClass(ClassName$2.ACTIVE + \" \" + orderClassName + \" \" + directionalClassName);\n          _this4._isSliding = false;\n          setTimeout(function () {\n            return $(_this4._element).trigger(slidEvent);\n          }, 0);\n        }).emulateTransitionEnd(transitionDuration);\n      } else {\n        $(activeElement).removeClass(ClassName$2.ACTIVE);\n        $(nextElement).addClass(ClassName$2.ACTIVE);\n        this._isSliding = false;\n        $(this._element).trigger(slidEvent);\n      }\n\n      if (isCycling) {\n        this.cycle();\n      }\n    } // Static\n    ;\n\n    Carousel._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var data = $(this).data(DATA_KEY$2);\n\n        var _config = _objectSpread({}, Default, $(this).data());\n\n        if (typeof config === 'object') {\n          _config = _objectSpread({}, _config, config);\n        }\n\n        var action = typeof config === 'string' ? config : _config.slide;\n\n        if (!data) {\n          data = new Carousel(this, _config);\n          $(this).data(DATA_KEY$2, data);\n        }\n\n        if (typeof config === 'number') {\n          data.to(config);\n        } else if (typeof action === 'string') {\n          if (typeof data[action] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + action + \"\\\"\");\n          }\n\n          data[action]();\n        } else if (_config.interval && _config.ride) {\n          data.pause();\n          data.cycle();\n        }\n      });\n    };\n\n    Carousel._dataApiClickHandler = function _dataApiClickHandler(event) {\n      var selector = Util.getSelectorFromElement(this);\n\n      if (!selector) {\n        return;\n      }\n\n      var target = $(selector)[0];\n\n      if (!target || !$(target).hasClass(ClassName$2.CAROUSEL)) {\n        return;\n      }\n\n      var config = _objectSpread({}, $(target).data(), $(this).data());\n\n      var slideIndex = this.getAttribute('data-slide-to');\n\n      if (slideIndex) {\n        config.interval = false;\n      }\n\n      Carousel._jQueryInterface.call($(target), config);\n\n      if (slideIndex) {\n        $(target).data(DATA_KEY$2).to(slideIndex);\n      }\n\n      event.preventDefault();\n    };\n\n    _createClass(Carousel, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$2;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default;\n      }\n    }]);\n\n    return Carousel;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(document).on(Event$2.CLICK_DATA_API, Selector$2.DATA_SLIDE, Carousel._dataApiClickHandler);\n  $(window).on(Event$2.LOAD_DATA_API, function () {\n    var carousels = [].slice.call(document.querySelectorAll(Selector$2.DATA_RIDE));\n\n    for (var i = 0, len = carousels.length; i < len; i++) {\n      var $carousel = $(carousels[i]);\n\n      Carousel._jQueryInterface.call($carousel, $carousel.data());\n    }\n  });\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME$2] = Carousel._jQueryInterface;\n  $.fn[NAME$2].Constructor = Carousel;\n\n  $.fn[NAME$2].noConflict = function () {\n    $.fn[NAME$2] = JQUERY_NO_CONFLICT$2;\n    return Carousel._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$3 = 'collapse';\n  var VERSION$3 = '4.3.1';\n  var DATA_KEY$3 = 'bs.collapse';\n  var EVENT_KEY$3 = \".\" + DATA_KEY$3;\n  var DATA_API_KEY$3 = '.data-api';\n  var JQUERY_NO_CONFLICT$3 = $.fn[NAME$3];\n  var Default$1 = {\n    toggle: true,\n    parent: ''\n  };\n  var DefaultType$1 = {\n    toggle: 'boolean',\n    parent: '(string|element)'\n  };\n  var Event$3 = {\n    SHOW: \"show\" + EVENT_KEY$3,\n    SHOWN: \"shown\" + EVENT_KEY$3,\n    HIDE: \"hide\" + EVENT_KEY$3,\n    HIDDEN: \"hidden\" + EVENT_KEY$3,\n    CLICK_DATA_API: \"click\" + EVENT_KEY$3 + DATA_API_KEY$3\n  };\n  var ClassName$3 = {\n    SHOW: 'show',\n    COLLAPSE: 'collapse',\n    COLLAPSING: 'collapsing',\n    COLLAPSED: 'collapsed'\n  };\n  var Dimension = {\n    WIDTH: 'width',\n    HEIGHT: 'height'\n  };\n  var Selector$3 = {\n    ACTIVES: '.show, .collapsing',\n    DATA_TOGGLE: '[data-toggle=\"collapse\"]'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Collapse =\n  /*#__PURE__*/\n  function () {\n    function Collapse(element, config) {\n      this._isTransitioning = false;\n      this._element = element;\n      this._config = this._getConfig(config);\n      this._triggerArray = [].slice.call(document.querySelectorAll(\"[data-toggle=\\\"collapse\\\"][href=\\\"#\" + element.id + \"\\\"],\" + (\"[data-toggle=\\\"collapse\\\"][data-target=\\\"#\" + element.id + \"\\\"]\")));\n      var toggleList = [].slice.call(document.querySelectorAll(Selector$3.DATA_TOGGLE));\n\n      for (var i = 0, len = toggleList.length; i < len; i++) {\n        var elem = toggleList[i];\n        var selector = Util.getSelectorFromElement(elem);\n        var filterElement = [].slice.call(document.querySelectorAll(selector)).filter(function (foundElem) {\n          return foundElem === element;\n        });\n\n        if (selector !== null && filterElement.length > 0) {\n          this._selector = selector;\n\n          this._triggerArray.push(elem);\n        }\n      }\n\n      this._parent = this._config.parent ? this._getParent() : null;\n\n      if (!this._config.parent) {\n        this._addAriaAndCollapsedClass(this._element, this._triggerArray);\n      }\n\n      if (this._config.toggle) {\n        this.toggle();\n      }\n    } // Getters\n\n\n    var _proto = Collapse.prototype;\n\n    // Public\n    _proto.toggle = function toggle() {\n      if ($(this._element).hasClass(ClassName$3.SHOW)) {\n        this.hide();\n      } else {\n        this.show();\n      }\n    };\n\n    _proto.show = function show() {\n      var _this = this;\n\n      if (this._isTransitioning || $(this._element).hasClass(ClassName$3.SHOW)) {\n        return;\n      }\n\n      var actives;\n      var activesData;\n\n      if (this._parent) {\n        actives = [].slice.call(this._parent.querySelectorAll(Selector$3.ACTIVES)).filter(function (elem) {\n          if (typeof _this._config.parent === 'string') {\n            return elem.getAttribute('data-parent') === _this._config.parent;\n          }\n\n          return elem.classList.contains(ClassName$3.COLLAPSE);\n        });\n\n        if (actives.length === 0) {\n          actives = null;\n        }\n      }\n\n      if (actives) {\n        activesData = $(actives).not(this._selector).data(DATA_KEY$3);\n\n        if (activesData && activesData._isTransitioning) {\n          return;\n        }\n      }\n\n      var startEvent = $.Event(Event$3.SHOW);\n      $(this._element).trigger(startEvent);\n\n      if (startEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      if (actives) {\n        Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide');\n\n        if (!activesData) {\n          $(actives).data(DATA_KEY$3, null);\n        }\n      }\n\n      var dimension = this._getDimension();\n\n      $(this._element).removeClass(ClassName$3.COLLAPSE).addClass(ClassName$3.COLLAPSING);\n      this._element.style[dimension] = 0;\n\n      if (this._triggerArray.length) {\n        $(this._triggerArray).removeClass(ClassName$3.COLLAPSED).attr('aria-expanded', true);\n      }\n\n      this.setTransitioning(true);\n\n      var complete = function complete() {\n        $(_this._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).addClass(ClassName$3.SHOW);\n        _this._element.style[dimension] = '';\n\n        _this.setTransitioning(false);\n\n        $(_this._element).trigger(Event$3.SHOWN);\n      };\n\n      var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n      var scrollSize = \"scroll\" + capitalizedDimension;\n      var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n      $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n      this._element.style[dimension] = this._element[scrollSize] + \"px\";\n    };\n\n    _proto.hide = function hide() {\n      var _this2 = this;\n\n      if (this._isTransitioning || !$(this._element).hasClass(ClassName$3.SHOW)) {\n        return;\n      }\n\n      var startEvent = $.Event(Event$3.HIDE);\n      $(this._element).trigger(startEvent);\n\n      if (startEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      var dimension = this._getDimension();\n\n      this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + \"px\";\n      Util.reflow(this._element);\n      $(this._element).addClass(ClassName$3.COLLAPSING).removeClass(ClassName$3.COLLAPSE).removeClass(ClassName$3.SHOW);\n      var triggerArrayLength = this._triggerArray.length;\n\n      if (triggerArrayLength > 0) {\n        for (var i = 0; i < triggerArrayLength; i++) {\n          var trigger = this._triggerArray[i];\n          var selector = Util.getSelectorFromElement(trigger);\n\n          if (selector !== null) {\n            var $elem = $([].slice.call(document.querySelectorAll(selector)));\n\n            if (!$elem.hasClass(ClassName$3.SHOW)) {\n              $(trigger).addClass(ClassName$3.COLLAPSED).attr('aria-expanded', false);\n            }\n          }\n        }\n      }\n\n      this.setTransitioning(true);\n\n      var complete = function complete() {\n        _this2.setTransitioning(false);\n\n        $(_this2._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).trigger(Event$3.HIDDEN);\n      };\n\n      this._element.style[dimension] = '';\n      var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n      $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n    };\n\n    _proto.setTransitioning = function setTransitioning(isTransitioning) {\n      this._isTransitioning = isTransitioning;\n    };\n\n    _proto.dispose = function dispose() {\n      $.removeData(this._element, DATA_KEY$3);\n      this._config = null;\n      this._parent = null;\n      this._element = null;\n      this._triggerArray = null;\n      this._isTransitioning = null;\n    } // Private\n    ;\n\n    _proto._getConfig = function _getConfig(config) {\n      config = _objectSpread({}, Default$1, config);\n      config.toggle = Boolean(config.toggle); // Coerce string values\n\n      Util.typeCheckConfig(NAME$3, config, DefaultType$1);\n      return config;\n    };\n\n    _proto._getDimension = function _getDimension() {\n      var hasWidth = $(this._element).hasClass(Dimension.WIDTH);\n      return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT;\n    };\n\n    _proto._getParent = function _getParent() {\n      var _this3 = this;\n\n      var parent;\n\n      if (Util.isElement(this._config.parent)) {\n        parent = this._config.parent; // It's a jQuery object\n\n        if (typeof this._config.parent.jquery !== 'undefined') {\n          parent = this._config.parent[0];\n        }\n      } else {\n        parent = document.querySelector(this._config.parent);\n      }\n\n      var selector = \"[data-toggle=\\\"collapse\\\"][data-parent=\\\"\" + this._config.parent + \"\\\"]\";\n      var children = [].slice.call(parent.querySelectorAll(selector));\n      $(children).each(function (i, element) {\n        _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]);\n      });\n      return parent;\n    };\n\n    _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) {\n      var isOpen = $(element).hasClass(ClassName$3.SHOW);\n\n      if (triggerArray.length) {\n        $(triggerArray).toggleClass(ClassName$3.COLLAPSED, !isOpen).attr('aria-expanded', isOpen);\n      }\n    } // Static\n    ;\n\n    Collapse._getTargetFromElement = function _getTargetFromElement(element) {\n      var selector = Util.getSelectorFromElement(element);\n      return selector ? document.querySelector(selector) : null;\n    };\n\n    Collapse._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var $this = $(this);\n        var data = $this.data(DATA_KEY$3);\n\n        var _config = _objectSpread({}, Default$1, $this.data(), typeof config === 'object' && config ? config : {});\n\n        if (!data && _config.toggle && /show|hide/.test(config)) {\n          _config.toggle = false;\n        }\n\n        if (!data) {\n          data = new Collapse(this, _config);\n          $this.data(DATA_KEY$3, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config]();\n        }\n      });\n    };\n\n    _createClass(Collapse, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$3;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default$1;\n      }\n    }]);\n\n    return Collapse;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(document).on(Event$3.CLICK_DATA_API, Selector$3.DATA_TOGGLE, function (event) {\n    // preventDefault only for <a> elements (which change the URL) not inside the collapsible element\n    if (event.currentTarget.tagName === 'A') {\n      event.preventDefault();\n    }\n\n    var $trigger = $(this);\n    var selector = Util.getSelectorFromElement(this);\n    var selectors = [].slice.call(document.querySelectorAll(selector));\n    $(selectors).each(function () {\n      var $target = $(this);\n      var data = $target.data(DATA_KEY$3);\n      var config = data ? 'toggle' : $trigger.data();\n\n      Collapse._jQueryInterface.call($target, config);\n    });\n  });\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME$3] = Collapse._jQueryInterface;\n  $.fn[NAME$3].Constructor = Collapse;\n\n  $.fn[NAME$3].noConflict = function () {\n    $.fn[NAME$3] = JQUERY_NO_CONFLICT$3;\n    return Collapse._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$4 = 'dropdown';\n  var VERSION$4 = '4.3.1';\n  var DATA_KEY$4 = 'bs.dropdown';\n  var EVENT_KEY$4 = \".\" + DATA_KEY$4;\n  var DATA_API_KEY$4 = '.data-api';\n  var JQUERY_NO_CONFLICT$4 = $.fn[NAME$4];\n  var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key\n\n  var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key\n\n  var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key\n\n  var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key\n\n  var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key\n\n  var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse)\n\n  var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + \"|\" + ARROW_DOWN_KEYCODE + \"|\" + ESCAPE_KEYCODE);\n  var Event$4 = {\n    HIDE: \"hide\" + EVENT_KEY$4,\n    HIDDEN: \"hidden\" + EVENT_KEY$4,\n    SHOW: \"show\" + EVENT_KEY$4,\n    SHOWN: \"shown\" + EVENT_KEY$4,\n    CLICK: \"click\" + EVENT_KEY$4,\n    CLICK_DATA_API: \"click\" + EVENT_KEY$4 + DATA_API_KEY$4,\n    KEYDOWN_DATA_API: \"keydown\" + EVENT_KEY$4 + DATA_API_KEY$4,\n    KEYUP_DATA_API: \"keyup\" + EVENT_KEY$4 + DATA_API_KEY$4\n  };\n  var ClassName$4 = {\n    DISABLED: 'disabled',\n    SHOW: 'show',\n    DROPUP: 'dropup',\n    DROPRIGHT: 'dropright',\n    DROPLEFT: 'dropleft',\n    MENURIGHT: 'dropdown-menu-right',\n    MENULEFT: 'dropdown-menu-left',\n    POSITION_STATIC: 'position-static'\n  };\n  var Selector$4 = {\n    DATA_TOGGLE: '[data-toggle=\"dropdown\"]',\n    FORM_CHILD: '.dropdown form',\n    MENU: '.dropdown-menu',\n    NAVBAR_NAV: '.navbar-nav',\n    VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n  };\n  var AttachmentMap = {\n    TOP: 'top-start',\n    TOPEND: 'top-end',\n    BOTTOM: 'bottom-start',\n    BOTTOMEND: 'bottom-end',\n    RIGHT: 'right-start',\n    RIGHTEND: 'right-end',\n    LEFT: 'left-start',\n    LEFTEND: 'left-end'\n  };\n  var Default$2 = {\n    offset: 0,\n    flip: true,\n    boundary: 'scrollParent',\n    reference: 'toggle',\n    display: 'dynamic'\n  };\n  var DefaultType$2 = {\n    offset: '(number|string|function)',\n    flip: 'boolean',\n    boundary: '(string|element)',\n    reference: '(string|element)',\n    display: 'string'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Dropdown =\n  /*#__PURE__*/\n  function () {\n    function Dropdown(element, config) {\n      this._element = element;\n      this._popper = null;\n      this._config = this._getConfig(config);\n      this._menu = this._getMenuElement();\n      this._inNavbar = this._detectNavbar();\n\n      this._addEventListeners();\n    } // Getters\n\n\n    var _proto = Dropdown.prototype;\n\n    // Public\n    _proto.toggle = function toggle() {\n      if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED)) {\n        return;\n      }\n\n      var parent = Dropdown._getParentFromElement(this._element);\n\n      var isActive = $(this._menu).hasClass(ClassName$4.SHOW);\n\n      Dropdown._clearMenus();\n\n      if (isActive) {\n        return;\n      }\n\n      var relatedTarget = {\n        relatedTarget: this._element\n      };\n      var showEvent = $.Event(Event$4.SHOW, relatedTarget);\n      $(parent).trigger(showEvent);\n\n      if (showEvent.isDefaultPrevented()) {\n        return;\n      } // Disable totally Popper.js for Dropdown in Navbar\n\n\n      if (!this._inNavbar) {\n        /**\n         * Check for Popper dependency\n         * Popper - https://popper.js.org\n         */\n        if (typeof Popper === 'undefined') {\n          throw new TypeError('Bootstrap\\'s dropdowns require Popper.js (https://popper.js.org/)');\n        }\n\n        var referenceElement = this._element;\n\n        if (this._config.reference === 'parent') {\n          referenceElement = parent;\n        } else if (Util.isElement(this._config.reference)) {\n          referenceElement = this._config.reference; // Check if it's jQuery element\n\n          if (typeof this._config.reference.jquery !== 'undefined') {\n            referenceElement = this._config.reference[0];\n          }\n        } // If boundary is not `scrollParent`, then set position to `static`\n        // to allow the menu to \"escape\" the scroll parent's boundaries\n        // https://github.com/twbs/bootstrap/issues/24251\n\n\n        if (this._config.boundary !== 'scrollParent') {\n          $(parent).addClass(ClassName$4.POSITION_STATIC);\n        }\n\n        this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig());\n      } // If this is a touch-enabled device we add extra\n      // empty mouseover listeners to the body's immediate children;\n      // only needed because of broken event delegation on iOS\n      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n\n\n      if ('ontouchstart' in document.documentElement && $(parent).closest(Selector$4.NAVBAR_NAV).length === 0) {\n        $(document.body).children().on('mouseover', null, $.noop);\n      }\n\n      this._element.focus();\n\n      this._element.setAttribute('aria-expanded', true);\n\n      $(this._menu).toggleClass(ClassName$4.SHOW);\n      $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));\n    };\n\n    _proto.show = function show() {\n      if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || $(this._menu).hasClass(ClassName$4.SHOW)) {\n        return;\n      }\n\n      var relatedTarget = {\n        relatedTarget: this._element\n      };\n      var showEvent = $.Event(Event$4.SHOW, relatedTarget);\n\n      var parent = Dropdown._getParentFromElement(this._element);\n\n      $(parent).trigger(showEvent);\n\n      if (showEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      $(this._menu).toggleClass(ClassName$4.SHOW);\n      $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));\n    };\n\n    _proto.hide = function hide() {\n      if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || !$(this._menu).hasClass(ClassName$4.SHOW)) {\n        return;\n      }\n\n      var relatedTarget = {\n        relatedTarget: this._element\n      };\n      var hideEvent = $.Event(Event$4.HIDE, relatedTarget);\n\n      var parent = Dropdown._getParentFromElement(this._element);\n\n      $(parent).trigger(hideEvent);\n\n      if (hideEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      $(this._menu).toggleClass(ClassName$4.SHOW);\n      $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));\n    };\n\n    _proto.dispose = function dispose() {\n      $.removeData(this._element, DATA_KEY$4);\n      $(this._element).off(EVENT_KEY$4);\n      this._element = null;\n      this._menu = null;\n\n      if (this._popper !== null) {\n        this._popper.destroy();\n\n        this._popper = null;\n      }\n    };\n\n    _proto.update = function update() {\n      this._inNavbar = this._detectNavbar();\n\n      if (this._popper !== null) {\n        this._popper.scheduleUpdate();\n      }\n    } // Private\n    ;\n\n    _proto._addEventListeners = function _addEventListeners() {\n      var _this = this;\n\n      $(this._element).on(Event$4.CLICK, function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        _this.toggle();\n      });\n    };\n\n    _proto._getConfig = function _getConfig(config) {\n      config = _objectSpread({}, this.constructor.Default, $(this._element).data(), config);\n      Util.typeCheckConfig(NAME$4, config, this.constructor.DefaultType);\n      return config;\n    };\n\n    _proto._getMenuElement = function _getMenuElement() {\n      if (!this._menu) {\n        var parent = Dropdown._getParentFromElement(this._element);\n\n        if (parent) {\n          this._menu = parent.querySelector(Selector$4.MENU);\n        }\n      }\n\n      return this._menu;\n    };\n\n    _proto._getPlacement = function _getPlacement() {\n      var $parentDropdown = $(this._element.parentNode);\n      var placement = AttachmentMap.BOTTOM; // Handle dropup\n\n      if ($parentDropdown.hasClass(ClassName$4.DROPUP)) {\n        placement = AttachmentMap.TOP;\n\n        if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {\n          placement = AttachmentMap.TOPEND;\n        }\n      } else if ($parentDropdown.hasClass(ClassName$4.DROPRIGHT)) {\n        placement = AttachmentMap.RIGHT;\n      } else if ($parentDropdown.hasClass(ClassName$4.DROPLEFT)) {\n        placement = AttachmentMap.LEFT;\n      } else if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {\n        placement = AttachmentMap.BOTTOMEND;\n      }\n\n      return placement;\n    };\n\n    _proto._detectNavbar = function _detectNavbar() {\n      return $(this._element).closest('.navbar').length > 0;\n    };\n\n    _proto._getOffset = function _getOffset() {\n      var _this2 = this;\n\n      var offset = {};\n\n      if (typeof this._config.offset === 'function') {\n        offset.fn = function (data) {\n          data.offsets = _objectSpread({}, data.offsets, _this2._config.offset(data.offsets, _this2._element) || {});\n          return data;\n        };\n      } else {\n        offset.offset = this._config.offset;\n      }\n\n      return offset;\n    };\n\n    _proto._getPopperConfig = function _getPopperConfig() {\n      var popperConfig = {\n        placement: this._getPlacement(),\n        modifiers: {\n          offset: this._getOffset(),\n          flip: {\n            enabled: this._config.flip\n          },\n          preventOverflow: {\n            boundariesElement: this._config.boundary\n          }\n        } // Disable Popper.js if we have a static display\n\n      };\n\n      if (this._config.display === 'static') {\n        popperConfig.modifiers.applyStyle = {\n          enabled: false\n        };\n      }\n\n      return popperConfig;\n    } // Static\n    ;\n\n    Dropdown._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var data = $(this).data(DATA_KEY$4);\n\n        var _config = typeof config === 'object' ? config : null;\n\n        if (!data) {\n          data = new Dropdown(this, _config);\n          $(this).data(DATA_KEY$4, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config]();\n        }\n      });\n    };\n\n    Dropdown._clearMenus = function _clearMenus(event) {\n      if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n        return;\n      }\n\n      var toggles = [].slice.call(document.querySelectorAll(Selector$4.DATA_TOGGLE));\n\n      for (var i = 0, len = toggles.length; i < len; i++) {\n        var parent = Dropdown._getParentFromElement(toggles[i]);\n\n        var context = $(toggles[i]).data(DATA_KEY$4);\n        var relatedTarget = {\n          relatedTarget: toggles[i]\n        };\n\n        if (event && event.type === 'click') {\n          relatedTarget.clickEvent = event;\n        }\n\n        if (!context) {\n          continue;\n        }\n\n        var dropdownMenu = context._menu;\n\n        if (!$(parent).hasClass(ClassName$4.SHOW)) {\n          continue;\n        }\n\n        if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $.contains(parent, event.target)) {\n          continue;\n        }\n\n        var hideEvent = $.Event(Event$4.HIDE, relatedTarget);\n        $(parent).trigger(hideEvent);\n\n        if (hideEvent.isDefaultPrevented()) {\n          continue;\n        } // If this is a touch-enabled device we remove the extra\n        // empty mouseover listeners we added for iOS support\n\n\n        if ('ontouchstart' in document.documentElement) {\n          $(document.body).children().off('mouseover', null, $.noop);\n        }\n\n        toggles[i].setAttribute('aria-expanded', 'false');\n        $(dropdownMenu).removeClass(ClassName$4.SHOW);\n        $(parent).removeClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));\n      }\n    };\n\n    Dropdown._getParentFromElement = function _getParentFromElement(element) {\n      var parent;\n      var selector = Util.getSelectorFromElement(element);\n\n      if (selector) {\n        parent = document.querySelector(selector);\n      }\n\n      return parent || element.parentNode;\n    } // eslint-disable-next-line complexity\n    ;\n\n    Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) {\n      // If not input/textarea:\n      //  - And not a key in REGEXP_KEYDOWN => not a dropdown command\n      // If input/textarea:\n      //  - If space key => not a dropdown command\n      //  - If key is other than escape\n      //    - If key is not up or down => not a dropdown command\n      //    - If trigger inside the menu => not a dropdown command\n      if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $(event.target).closest(Selector$4.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n        return;\n      }\n\n      event.preventDefault();\n      event.stopPropagation();\n\n      if (this.disabled || $(this).hasClass(ClassName$4.DISABLED)) {\n        return;\n      }\n\n      var parent = Dropdown._getParentFromElement(this);\n\n      var isActive = $(parent).hasClass(ClassName$4.SHOW);\n\n      if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n        if (event.which === ESCAPE_KEYCODE) {\n          var toggle = parent.querySelector(Selector$4.DATA_TOGGLE);\n          $(toggle).trigger('focus');\n        }\n\n        $(this).trigger('click');\n        return;\n      }\n\n      var items = [].slice.call(parent.querySelectorAll(Selector$4.VISIBLE_ITEMS));\n\n      if (items.length === 0) {\n        return;\n      }\n\n      var index = items.indexOf(event.target);\n\n      if (event.which === ARROW_UP_KEYCODE && index > 0) {\n        // Up\n        index--;\n      }\n\n      if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) {\n        // Down\n        index++;\n      }\n\n      if (index < 0) {\n        index = 0;\n      }\n\n      items[index].focus();\n    };\n\n    _createClass(Dropdown, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$4;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default$2;\n      }\n    }, {\n      key: \"DefaultType\",\n      get: function get() {\n        return DefaultType$2;\n      }\n    }]);\n\n    return Dropdown;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(document).on(Event$4.KEYDOWN_DATA_API, Selector$4.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event$4.KEYDOWN_DATA_API, Selector$4.MENU, Dropdown._dataApiKeydownHandler).on(Event$4.CLICK_DATA_API + \" \" + Event$4.KEYUP_DATA_API, Dropdown._clearMenus).on(Event$4.CLICK_DATA_API, Selector$4.DATA_TOGGLE, function (event) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    Dropdown._jQueryInterface.call($(this), 'toggle');\n  }).on(Event$4.CLICK_DATA_API, Selector$4.FORM_CHILD, function (e) {\n    e.stopPropagation();\n  });\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME$4] = Dropdown._jQueryInterface;\n  $.fn[NAME$4].Constructor = Dropdown;\n\n  $.fn[NAME$4].noConflict = function () {\n    $.fn[NAME$4] = JQUERY_NO_CONFLICT$4;\n    return Dropdown._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$5 = 'modal';\n  var VERSION$5 = '4.3.1';\n  var DATA_KEY$5 = 'bs.modal';\n  var EVENT_KEY$5 = \".\" + DATA_KEY$5;\n  var DATA_API_KEY$5 = '.data-api';\n  var JQUERY_NO_CONFLICT$5 = $.fn[NAME$5];\n  var ESCAPE_KEYCODE$1 = 27; // KeyboardEvent.which value for Escape (Esc) key\n\n  var Default$3 = {\n    backdrop: true,\n    keyboard: true,\n    focus: true,\n    show: true\n  };\n  var DefaultType$3 = {\n    backdrop: '(boolean|string)',\n    keyboard: 'boolean',\n    focus: 'boolean',\n    show: 'boolean'\n  };\n  var Event$5 = {\n    HIDE: \"hide\" + EVENT_KEY$5,\n    HIDDEN: \"hidden\" + EVENT_KEY$5,\n    SHOW: \"show\" + EVENT_KEY$5,\n    SHOWN: \"shown\" + EVENT_KEY$5,\n    FOCUSIN: \"focusin\" + EVENT_KEY$5,\n    RESIZE: \"resize\" + EVENT_KEY$5,\n    CLICK_DISMISS: \"click.dismiss\" + EVENT_KEY$5,\n    KEYDOWN_DISMISS: \"keydown.dismiss\" + EVENT_KEY$5,\n    MOUSEUP_DISMISS: \"mouseup.dismiss\" + EVENT_KEY$5,\n    MOUSEDOWN_DISMISS: \"mousedown.dismiss\" + EVENT_KEY$5,\n    CLICK_DATA_API: \"click\" + EVENT_KEY$5 + DATA_API_KEY$5\n  };\n  var ClassName$5 = {\n    SCROLLABLE: 'modal-dialog-scrollable',\n    SCROLLBAR_MEASURER: 'modal-scrollbar-measure',\n    BACKDROP: 'modal-backdrop',\n    OPEN: 'modal-open',\n    FADE: 'fade',\n    SHOW: 'show'\n  };\n  var Selector$5 = {\n    DIALOG: '.modal-dialog',\n    MODAL_BODY: '.modal-body',\n    DATA_TOGGLE: '[data-toggle=\"modal\"]',\n    DATA_DISMISS: '[data-dismiss=\"modal\"]',\n    FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n    STICKY_CONTENT: '.sticky-top'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Modal =\n  /*#__PURE__*/\n  function () {\n    function Modal(element, config) {\n      this._config = this._getConfig(config);\n      this._element = element;\n      this._dialog = element.querySelector(Selector$5.DIALOG);\n      this._backdrop = null;\n      this._isShown = false;\n      this._isBodyOverflowing = false;\n      this._ignoreBackdropClick = false;\n      this._isTransitioning = false;\n      this._scrollbarWidth = 0;\n    } // Getters\n\n\n    var _proto = Modal.prototype;\n\n    // Public\n    _proto.toggle = function toggle(relatedTarget) {\n      return this._isShown ? this.hide() : this.show(relatedTarget);\n    };\n\n    _proto.show = function show(relatedTarget) {\n      var _this = this;\n\n      if (this._isShown || this._isTransitioning) {\n        return;\n      }\n\n      if ($(this._element).hasClass(ClassName$5.FADE)) {\n        this._isTransitioning = true;\n      }\n\n      var showEvent = $.Event(Event$5.SHOW, {\n        relatedTarget: relatedTarget\n      });\n      $(this._element).trigger(showEvent);\n\n      if (this._isShown || showEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      this._isShown = true;\n\n      this._checkScrollbar();\n\n      this._setScrollbar();\n\n      this._adjustDialog();\n\n      this._setEscapeEvent();\n\n      this._setResizeEvent();\n\n      $(this._element).on(Event$5.CLICK_DISMISS, Selector$5.DATA_DISMISS, function (event) {\n        return _this.hide(event);\n      });\n      $(this._dialog).on(Event$5.MOUSEDOWN_DISMISS, function () {\n        $(_this._element).one(Event$5.MOUSEUP_DISMISS, function (event) {\n          if ($(event.target).is(_this._element)) {\n            _this._ignoreBackdropClick = true;\n          }\n        });\n      });\n\n      this._showBackdrop(function () {\n        return _this._showElement(relatedTarget);\n      });\n    };\n\n    _proto.hide = function hide(event) {\n      var _this2 = this;\n\n      if (event) {\n        event.preventDefault();\n      }\n\n      if (!this._isShown || this._isTransitioning) {\n        return;\n      }\n\n      var hideEvent = $.Event(Event$5.HIDE);\n      $(this._element).trigger(hideEvent);\n\n      if (!this._isShown || hideEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      this._isShown = false;\n      var transition = $(this._element).hasClass(ClassName$5.FADE);\n\n      if (transition) {\n        this._isTransitioning = true;\n      }\n\n      this._setEscapeEvent();\n\n      this._setResizeEvent();\n\n      $(document).off(Event$5.FOCUSIN);\n      $(this._element).removeClass(ClassName$5.SHOW);\n      $(this._element).off(Event$5.CLICK_DISMISS);\n      $(this._dialog).off(Event$5.MOUSEDOWN_DISMISS);\n\n      if (transition) {\n        var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n        $(this._element).one(Util.TRANSITION_END, function (event) {\n          return _this2._hideModal(event);\n        }).emulateTransitionEnd(transitionDuration);\n      } else {\n        this._hideModal();\n      }\n    };\n\n    _proto.dispose = function dispose() {\n      [window, this._element, this._dialog].forEach(function (htmlElement) {\n        return $(htmlElement).off(EVENT_KEY$5);\n      });\n      /**\n       * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`\n       * Do not move `document` in `htmlElements` array\n       * It will remove `Event.CLICK_DATA_API` event that should remain\n       */\n\n      $(document).off(Event$5.FOCUSIN);\n      $.removeData(this._element, DATA_KEY$5);\n      this._config = null;\n      this._element = null;\n      this._dialog = null;\n      this._backdrop = null;\n      this._isShown = null;\n      this._isBodyOverflowing = null;\n      this._ignoreBackdropClick = null;\n      this._isTransitioning = null;\n      this._scrollbarWidth = null;\n    };\n\n    _proto.handleUpdate = function handleUpdate() {\n      this._adjustDialog();\n    } // Private\n    ;\n\n    _proto._getConfig = function _getConfig(config) {\n      config = _objectSpread({}, Default$3, config);\n      Util.typeCheckConfig(NAME$5, config, DefaultType$3);\n      return config;\n    };\n\n    _proto._showElement = function _showElement(relatedTarget) {\n      var _this3 = this;\n\n      var transition = $(this._element).hasClass(ClassName$5.FADE);\n\n      if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n        // Don't move modal's DOM position\n        document.body.appendChild(this._element);\n      }\n\n      this._element.style.display = 'block';\n\n      this._element.removeAttribute('aria-hidden');\n\n      this._element.setAttribute('aria-modal', true);\n\n      if ($(this._dialog).hasClass(ClassName$5.SCROLLABLE)) {\n        this._dialog.querySelector(Selector$5.MODAL_BODY).scrollTop = 0;\n      } else {\n        this._element.scrollTop = 0;\n      }\n\n      if (transition) {\n        Util.reflow(this._element);\n      }\n\n      $(this._element).addClass(ClassName$5.SHOW);\n\n      if (this._config.focus) {\n        this._enforceFocus();\n      }\n\n      var shownEvent = $.Event(Event$5.SHOWN, {\n        relatedTarget: relatedTarget\n      });\n\n      var transitionComplete = function transitionComplete() {\n        if (_this3._config.focus) {\n          _this3._element.focus();\n        }\n\n        _this3._isTransitioning = false;\n        $(_this3._element).trigger(shownEvent);\n      };\n\n      if (transition) {\n        var transitionDuration = Util.getTransitionDurationFromElement(this._dialog);\n        $(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(transitionDuration);\n      } else {\n        transitionComplete();\n      }\n    };\n\n    _proto._enforceFocus = function _enforceFocus() {\n      var _this4 = this;\n\n      $(document).off(Event$5.FOCUSIN) // Guard against infinite focus loop\n      .on(Event$5.FOCUSIN, function (event) {\n        if (document !== event.target && _this4._element !== event.target && $(_this4._element).has(event.target).length === 0) {\n          _this4._element.focus();\n        }\n      });\n    };\n\n    _proto._setEscapeEvent = function _setEscapeEvent() {\n      var _this5 = this;\n\n      if (this._isShown && this._config.keyboard) {\n        $(this._element).on(Event$5.KEYDOWN_DISMISS, function (event) {\n          if (event.which === ESCAPE_KEYCODE$1) {\n            event.preventDefault();\n\n            _this5.hide();\n          }\n        });\n      } else if (!this._isShown) {\n        $(this._element).off(Event$5.KEYDOWN_DISMISS);\n      }\n    };\n\n    _proto._setResizeEvent = function _setResizeEvent() {\n      var _this6 = this;\n\n      if (this._isShown) {\n        $(window).on(Event$5.RESIZE, function (event) {\n          return _this6.handleUpdate(event);\n        });\n      } else {\n        $(window).off(Event$5.RESIZE);\n      }\n    };\n\n    _proto._hideModal = function _hideModal() {\n      var _this7 = this;\n\n      this._element.style.display = 'none';\n\n      this._element.setAttribute('aria-hidden', true);\n\n      this._element.removeAttribute('aria-modal');\n\n      this._isTransitioning = false;\n\n      this._showBackdrop(function () {\n        $(document.body).removeClass(ClassName$5.OPEN);\n\n        _this7._resetAdjustments();\n\n        _this7._resetScrollbar();\n\n        $(_this7._element).trigger(Event$5.HIDDEN);\n      });\n    };\n\n    _proto._removeBackdrop = function _removeBackdrop() {\n      if (this._backdrop) {\n        $(this._backdrop).remove();\n        this._backdrop = null;\n      }\n    };\n\n    _proto._showBackdrop = function _showBackdrop(callback) {\n      var _this8 = this;\n\n      var animate = $(this._element).hasClass(ClassName$5.FADE) ? ClassName$5.FADE : '';\n\n      if (this._isShown && this._config.backdrop) {\n        this._backdrop = document.createElement('div');\n        this._backdrop.className = ClassName$5.BACKDROP;\n\n        if (animate) {\n          this._backdrop.classList.add(animate);\n        }\n\n        $(this._backdrop).appendTo(document.body);\n        $(this._element).on(Event$5.CLICK_DISMISS, function (event) {\n          if (_this8._ignoreBackdropClick) {\n            _this8._ignoreBackdropClick = false;\n            return;\n          }\n\n          if (event.target !== event.currentTarget) {\n            return;\n          }\n\n          if (_this8._config.backdrop === 'static') {\n            _this8._element.focus();\n          } else {\n            _this8.hide();\n          }\n        });\n\n        if (animate) {\n          Util.reflow(this._backdrop);\n        }\n\n        $(this._backdrop).addClass(ClassName$5.SHOW);\n\n        if (!callback) {\n          return;\n        }\n\n        if (!animate) {\n          callback();\n          return;\n        }\n\n        var backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);\n        $(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(backdropTransitionDuration);\n      } else if (!this._isShown && this._backdrop) {\n        $(this._backdrop).removeClass(ClassName$5.SHOW);\n\n        var callbackRemove = function callbackRemove() {\n          _this8._removeBackdrop();\n\n          if (callback) {\n            callback();\n          }\n        };\n\n        if ($(this._element).hasClass(ClassName$5.FADE)) {\n          var _backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);\n\n          $(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(_backdropTransitionDuration);\n        } else {\n          callbackRemove();\n        }\n      } else if (callback) {\n        callback();\n      }\n    } // ----------------------------------------------------------------------\n    // the following methods are used to handle overflowing modals\n    // todo (fat): these should probably be refactored out of modal.js\n    // ----------------------------------------------------------------------\n    ;\n\n    _proto._adjustDialog = function _adjustDialog() {\n      var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n\n      if (!this._isBodyOverflowing && isModalOverflowing) {\n        this._element.style.paddingLeft = this._scrollbarWidth + \"px\";\n      }\n\n      if (this._isBodyOverflowing && !isModalOverflowing) {\n        this._element.style.paddingRight = this._scrollbarWidth + \"px\";\n      }\n    };\n\n    _proto._resetAdjustments = function _resetAdjustments() {\n      this._element.style.paddingLeft = '';\n      this._element.style.paddingRight = '';\n    };\n\n    _proto._checkScrollbar = function _checkScrollbar() {\n      var rect = document.body.getBoundingClientRect();\n      this._isBodyOverflowing = rect.left + rect.right < window.innerWidth;\n      this._scrollbarWidth = this._getScrollbarWidth();\n    };\n\n    _proto._setScrollbar = function _setScrollbar() {\n      var _this9 = this;\n\n      if (this._isBodyOverflowing) {\n        // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n        //   while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n        var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));\n        var stickyContent = [].slice.call(document.querySelectorAll(Selector$5.STICKY_CONTENT)); // Adjust fixed content padding\n\n        $(fixedContent).each(function (index, element) {\n          var actualPadding = element.style.paddingRight;\n          var calculatedPadding = $(element).css('padding-right');\n          $(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this9._scrollbarWidth + \"px\");\n        }); // Adjust sticky content margin\n\n        $(stickyContent).each(function (index, element) {\n          var actualMargin = element.style.marginRight;\n          var calculatedMargin = $(element).css('margin-right');\n          $(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this9._scrollbarWidth + \"px\");\n        }); // Adjust body padding\n\n        var actualPadding = document.body.style.paddingRight;\n        var calculatedPadding = $(document.body).css('padding-right');\n        $(document.body).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + \"px\");\n      }\n\n      $(document.body).addClass(ClassName$5.OPEN);\n    };\n\n    _proto._resetScrollbar = function _resetScrollbar() {\n      // Restore fixed content padding\n      var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));\n      $(fixedContent).each(function (index, element) {\n        var padding = $(element).data('padding-right');\n        $(element).removeData('padding-right');\n        element.style.paddingRight = padding ? padding : '';\n      }); // Restore sticky content\n\n      var elements = [].slice.call(document.querySelectorAll(\"\" + Selector$5.STICKY_CONTENT));\n      $(elements).each(function (index, element) {\n        var margin = $(element).data('margin-right');\n\n        if (typeof margin !== 'undefined') {\n          $(element).css('margin-right', margin).removeData('margin-right');\n        }\n      }); // Restore body padding\n\n      var padding = $(document.body).data('padding-right');\n      $(document.body).removeData('padding-right');\n      document.body.style.paddingRight = padding ? padding : '';\n    };\n\n    _proto._getScrollbarWidth = function _getScrollbarWidth() {\n      // thx d.walsh\n      var scrollDiv = document.createElement('div');\n      scrollDiv.className = ClassName$5.SCROLLBAR_MEASURER;\n      document.body.appendChild(scrollDiv);\n      var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;\n      document.body.removeChild(scrollDiv);\n      return scrollbarWidth;\n    } // Static\n    ;\n\n    Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) {\n      return this.each(function () {\n        var data = $(this).data(DATA_KEY$5);\n\n        var _config = _objectSpread({}, Default$3, $(this).data(), typeof config === 'object' && config ? config : {});\n\n        if (!data) {\n          data = new Modal(this, _config);\n          $(this).data(DATA_KEY$5, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config](relatedTarget);\n        } else if (_config.show) {\n          data.show(relatedTarget);\n        }\n      });\n    };\n\n    _createClass(Modal, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$5;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default$3;\n      }\n    }]);\n\n    return Modal;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(document).on(Event$5.CLICK_DATA_API, Selector$5.DATA_TOGGLE, function (event) {\n    var _this10 = this;\n\n    var target;\n    var selector = Util.getSelectorFromElement(this);\n\n    if (selector) {\n      target = document.querySelector(selector);\n    }\n\n    var config = $(target).data(DATA_KEY$5) ? 'toggle' : _objectSpread({}, $(target).data(), $(this).data());\n\n    if (this.tagName === 'A' || this.tagName === 'AREA') {\n      event.preventDefault();\n    }\n\n    var $target = $(target).one(Event$5.SHOW, function (showEvent) {\n      if (showEvent.isDefaultPrevented()) {\n        // Only register focus restorer if modal will actually get shown\n        return;\n      }\n\n      $target.one(Event$5.HIDDEN, function () {\n        if ($(_this10).is(':visible')) {\n          _this10.focus();\n        }\n      });\n    });\n\n    Modal._jQueryInterface.call($(target), config, this);\n  });\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME$5] = Modal._jQueryInterface;\n  $.fn[NAME$5].Constructor = Modal;\n\n  $.fn[NAME$5].noConflict = function () {\n    $.fn[NAME$5] = JQUERY_NO_CONFLICT$5;\n    return Modal._jQueryInterface;\n  };\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap (v4.3.1): tools/sanitizer.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n  var uriAttrs = ['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href'];\n  var ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\n  var DefaultWhitelist = {\n    // Global attributes allowed on any supplied element below.\n    '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n    a: ['target', 'href', 'title', 'rel'],\n    area: [],\n    b: [],\n    br: [],\n    col: [],\n    code: [],\n    div: [],\n    em: [],\n    hr: [],\n    h1: [],\n    h2: [],\n    h3: [],\n    h4: [],\n    h5: [],\n    h6: [],\n    i: [],\n    img: ['src', 'alt', 'title', 'width', 'height'],\n    li: [],\n    ol: [],\n    p: [],\n    pre: [],\n    s: [],\n    small: [],\n    span: [],\n    sub: [],\n    sup: [],\n    strong: [],\n    u: [],\n    ul: []\n    /**\n     * A pattern that recognizes a commonly useful subset of URLs that are safe.\n     *\n     * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n     */\n\n  };\n  var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;\n  /**\n   * A pattern that matches safe data URLs. Only matches image, video and audio types.\n   *\n   * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n   */\n\n  var DATA_URL_PATTERN = /^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;\n\n  function allowedAttribute(attr, allowedAttributeList) {\n    var attrName = attr.nodeName.toLowerCase();\n\n    if (allowedAttributeList.indexOf(attrName) !== -1) {\n      if (uriAttrs.indexOf(attrName) !== -1) {\n        return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN));\n      }\n\n      return true;\n    }\n\n    var regExp = allowedAttributeList.filter(function (attrRegex) {\n      return attrRegex instanceof RegExp;\n    }); // Check if a regular expression validates the attribute.\n\n    for (var i = 0, l = regExp.length; i < l; i++) {\n      if (attrName.match(regExp[i])) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {\n    if (unsafeHtml.length === 0) {\n      return unsafeHtml;\n    }\n\n    if (sanitizeFn && typeof sanitizeFn === 'function') {\n      return sanitizeFn(unsafeHtml);\n    }\n\n    var domParser = new window.DOMParser();\n    var createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n    var whitelistKeys = Object.keys(whiteList);\n    var elements = [].slice.call(createdDocument.body.querySelectorAll('*'));\n\n    var _loop = function _loop(i, len) {\n      var el = elements[i];\n      var elName = el.nodeName.toLowerCase();\n\n      if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {\n        el.parentNode.removeChild(el);\n        return \"continue\";\n      }\n\n      var attributeList = [].slice.call(el.attributes);\n      var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []);\n      attributeList.forEach(function (attr) {\n        if (!allowedAttribute(attr, whitelistedAttributes)) {\n          el.removeAttribute(attr.nodeName);\n        }\n      });\n    };\n\n    for (var i = 0, len = elements.length; i < len; i++) {\n      var _ret = _loop(i, len);\n\n      if (_ret === \"continue\") continue;\n    }\n\n    return createdDocument.body.innerHTML;\n  }\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$6 = 'tooltip';\n  var VERSION$6 = '4.3.1';\n  var DATA_KEY$6 = 'bs.tooltip';\n  var EVENT_KEY$6 = \".\" + DATA_KEY$6;\n  var JQUERY_NO_CONFLICT$6 = $.fn[NAME$6];\n  var CLASS_PREFIX = 'bs-tooltip';\n  var BSCLS_PREFIX_REGEX = new RegExp(\"(^|\\\\s)\" + CLASS_PREFIX + \"\\\\S+\", 'g');\n  var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn'];\n  var DefaultType$4 = {\n    animation: 'boolean',\n    template: 'string',\n    title: '(string|element|function)',\n    trigger: 'string',\n    delay: '(number|object)',\n    html: 'boolean',\n    selector: '(string|boolean)',\n    placement: '(string|function)',\n    offset: '(number|string|function)',\n    container: '(string|element|boolean)',\n    fallbackPlacement: '(string|array)',\n    boundary: '(string|element)',\n    sanitize: 'boolean',\n    sanitizeFn: '(null|function)',\n    whiteList: 'object'\n  };\n  var AttachmentMap$1 = {\n    AUTO: 'auto',\n    TOP: 'top',\n    RIGHT: 'right',\n    BOTTOM: 'bottom',\n    LEFT: 'left'\n  };\n  var Default$4 = {\n    animation: true,\n    template: '<div class=\"tooltip\" role=\"tooltip\">' + '<div class=\"arrow\"></div>' + '<div class=\"tooltip-inner\"></div></div>',\n    trigger: 'hover focus',\n    title: '',\n    delay: 0,\n    html: false,\n    selector: false,\n    placement: 'top',\n    offset: 0,\n    container: false,\n    fallbackPlacement: 'flip',\n    boundary: 'scrollParent',\n    sanitize: true,\n    sanitizeFn: null,\n    whiteList: DefaultWhitelist\n  };\n  var HoverState = {\n    SHOW: 'show',\n    OUT: 'out'\n  };\n  var Event$6 = {\n    HIDE: \"hide\" + EVENT_KEY$6,\n    HIDDEN: \"hidden\" + EVENT_KEY$6,\n    SHOW: \"show\" + EVENT_KEY$6,\n    SHOWN: \"shown\" + EVENT_KEY$6,\n    INSERTED: \"inserted\" + EVENT_KEY$6,\n    CLICK: \"click\" + EVENT_KEY$6,\n    FOCUSIN: \"focusin\" + EVENT_KEY$6,\n    FOCUSOUT: \"focusout\" + EVENT_KEY$6,\n    MOUSEENTER: \"mouseenter\" + EVENT_KEY$6,\n    MOUSELEAVE: \"mouseleave\" + EVENT_KEY$6\n  };\n  var ClassName$6 = {\n    FADE: 'fade',\n    SHOW: 'show'\n  };\n  var Selector$6 = {\n    TOOLTIP: '.tooltip',\n    TOOLTIP_INNER: '.tooltip-inner',\n    ARROW: '.arrow'\n  };\n  var Trigger = {\n    HOVER: 'hover',\n    FOCUS: 'focus',\n    CLICK: 'click',\n    MANUAL: 'manual'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Tooltip =\n  /*#__PURE__*/\n  function () {\n    function Tooltip(element, config) {\n      /**\n       * Check for Popper dependency\n       * Popper - https://popper.js.org\n       */\n      if (typeof Popper === 'undefined') {\n        throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)');\n      } // private\n\n\n      this._isEnabled = true;\n      this._timeout = 0;\n      this._hoverState = '';\n      this._activeTrigger = {};\n      this._popper = null; // Protected\n\n      this.element = element;\n      this.config = this._getConfig(config);\n      this.tip = null;\n\n      this._setListeners();\n    } // Getters\n\n\n    var _proto = Tooltip.prototype;\n\n    // Public\n    _proto.enable = function enable() {\n      this._isEnabled = true;\n    };\n\n    _proto.disable = function disable() {\n      this._isEnabled = false;\n    };\n\n    _proto.toggleEnabled = function toggleEnabled() {\n      this._isEnabled = !this._isEnabled;\n    };\n\n    _proto.toggle = function toggle(event) {\n      if (!this._isEnabled) {\n        return;\n      }\n\n      if (event) {\n        var dataKey = this.constructor.DATA_KEY;\n        var context = $(event.currentTarget).data(dataKey);\n\n        if (!context) {\n          context = new this.constructor(event.currentTarget, this._getDelegateConfig());\n          $(event.currentTarget).data(dataKey, context);\n        }\n\n        context._activeTrigger.click = !context._activeTrigger.click;\n\n        if (context._isWithActiveTrigger()) {\n          context._enter(null, context);\n        } else {\n          context._leave(null, context);\n        }\n      } else {\n        if ($(this.getTipElement()).hasClass(ClassName$6.SHOW)) {\n          this._leave(null, this);\n\n          return;\n        }\n\n        this._enter(null, this);\n      }\n    };\n\n    _proto.dispose = function dispose() {\n      clearTimeout(this._timeout);\n      $.removeData(this.element, this.constructor.DATA_KEY);\n      $(this.element).off(this.constructor.EVENT_KEY);\n      $(this.element).closest('.modal').off('hide.bs.modal');\n\n      if (this.tip) {\n        $(this.tip).remove();\n      }\n\n      this._isEnabled = null;\n      this._timeout = null;\n      this._hoverState = null;\n      this._activeTrigger = null;\n\n      if (this._popper !== null) {\n        this._popper.destroy();\n      }\n\n      this._popper = null;\n      this.element = null;\n      this.config = null;\n      this.tip = null;\n    };\n\n    _proto.show = function show() {\n      var _this = this;\n\n      if ($(this.element).css('display') === 'none') {\n        throw new Error('Please use show on visible elements');\n      }\n\n      var showEvent = $.Event(this.constructor.Event.SHOW);\n\n      if (this.isWithContent() && this._isEnabled) {\n        $(this.element).trigger(showEvent);\n        var shadowRoot = Util.findShadowRoot(this.element);\n        var isInTheDom = $.contains(shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement, this.element);\n\n        if (showEvent.isDefaultPrevented() || !isInTheDom) {\n          return;\n        }\n\n        var tip = this.getTipElement();\n        var tipId = Util.getUID(this.constructor.NAME);\n        tip.setAttribute('id', tipId);\n        this.element.setAttribute('aria-describedby', tipId);\n        this.setContent();\n\n        if (this.config.animation) {\n          $(tip).addClass(ClassName$6.FADE);\n        }\n\n        var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement;\n\n        var attachment = this._getAttachment(placement);\n\n        this.addAttachmentClass(attachment);\n\n        var container = this._getContainer();\n\n        $(tip).data(this.constructor.DATA_KEY, this);\n\n        if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n          $(tip).appendTo(container);\n        }\n\n        $(this.element).trigger(this.constructor.Event.INSERTED);\n        this._popper = new Popper(this.element, tip, {\n          placement: attachment,\n          modifiers: {\n            offset: this._getOffset(),\n            flip: {\n              behavior: this.config.fallbackPlacement\n            },\n            arrow: {\n              element: Selector$6.ARROW\n            },\n            preventOverflow: {\n              boundariesElement: this.config.boundary\n            }\n          },\n          onCreate: function onCreate(data) {\n            if (data.originalPlacement !== data.placement) {\n              _this._handlePopperPlacementChange(data);\n            }\n          },\n          onUpdate: function onUpdate(data) {\n            return _this._handlePopperPlacementChange(data);\n          }\n        });\n        $(tip).addClass(ClassName$6.SHOW); // If this is a touch-enabled device we add extra\n        // empty mouseover listeners to the body's immediate children;\n        // only needed because of broken event delegation on iOS\n        // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n\n        if ('ontouchstart' in document.documentElement) {\n          $(document.body).children().on('mouseover', null, $.noop);\n        }\n\n        var complete = function complete() {\n          if (_this.config.animation) {\n            _this._fixTransition();\n          }\n\n          var prevHoverState = _this._hoverState;\n          _this._hoverState = null;\n          $(_this.element).trigger(_this.constructor.Event.SHOWN);\n\n          if (prevHoverState === HoverState.OUT) {\n            _this._leave(null, _this);\n          }\n        };\n\n        if ($(this.tip).hasClass(ClassName$6.FADE)) {\n          var transitionDuration = Util.getTransitionDurationFromElement(this.tip);\n          $(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n        } else {\n          complete();\n        }\n      }\n    };\n\n    _proto.hide = function hide(callback) {\n      var _this2 = this;\n\n      var tip = this.getTipElement();\n      var hideEvent = $.Event(this.constructor.Event.HIDE);\n\n      var complete = function complete() {\n        if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) {\n          tip.parentNode.removeChild(tip);\n        }\n\n        _this2._cleanTipClass();\n\n        _this2.element.removeAttribute('aria-describedby');\n\n        $(_this2.element).trigger(_this2.constructor.Event.HIDDEN);\n\n        if (_this2._popper !== null) {\n          _this2._popper.destroy();\n        }\n\n        if (callback) {\n          callback();\n        }\n      };\n\n      $(this.element).trigger(hideEvent);\n\n      if (hideEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      $(tip).removeClass(ClassName$6.SHOW); // If this is a touch-enabled device we remove the extra\n      // empty mouseover listeners we added for iOS support\n\n      if ('ontouchstart' in document.documentElement) {\n        $(document.body).children().off('mouseover', null, $.noop);\n      }\n\n      this._activeTrigger[Trigger.CLICK] = false;\n      this._activeTrigger[Trigger.FOCUS] = false;\n      this._activeTrigger[Trigger.HOVER] = false;\n\n      if ($(this.tip).hasClass(ClassName$6.FADE)) {\n        var transitionDuration = Util.getTransitionDurationFromElement(tip);\n        $(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n      } else {\n        complete();\n      }\n\n      this._hoverState = '';\n    };\n\n    _proto.update = function update() {\n      if (this._popper !== null) {\n        this._popper.scheduleUpdate();\n      }\n    } // Protected\n    ;\n\n    _proto.isWithContent = function isWithContent() {\n      return Boolean(this.getTitle());\n    };\n\n    _proto.addAttachmentClass = function addAttachmentClass(attachment) {\n      $(this.getTipElement()).addClass(CLASS_PREFIX + \"-\" + attachment);\n    };\n\n    _proto.getTipElement = function getTipElement() {\n      this.tip = this.tip || $(this.config.template)[0];\n      return this.tip;\n    };\n\n    _proto.setContent = function setContent() {\n      var tip = this.getTipElement();\n      this.setElementContent($(tip.querySelectorAll(Selector$6.TOOLTIP_INNER)), this.getTitle());\n      $(tip).removeClass(ClassName$6.FADE + \" \" + ClassName$6.SHOW);\n    };\n\n    _proto.setElementContent = function setElementContent($element, content) {\n      if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n        // Content is a DOM node or a jQuery\n        if (this.config.html) {\n          if (!$(content).parent().is($element)) {\n            $element.empty().append(content);\n          }\n        } else {\n          $element.text($(content).text());\n        }\n\n        return;\n      }\n\n      if (this.config.html) {\n        if (this.config.sanitize) {\n          content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn);\n        }\n\n        $element.html(content);\n      } else {\n        $element.text(content);\n      }\n    };\n\n    _proto.getTitle = function getTitle() {\n      var title = this.element.getAttribute('data-original-title');\n\n      if (!title) {\n        title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title;\n      }\n\n      return title;\n    } // Private\n    ;\n\n    _proto._getOffset = function _getOffset() {\n      var _this3 = this;\n\n      var offset = {};\n\n      if (typeof this.config.offset === 'function') {\n        offset.fn = function (data) {\n          data.offsets = _objectSpread({}, data.offsets, _this3.config.offset(data.offsets, _this3.element) || {});\n          return data;\n        };\n      } else {\n        offset.offset = this.config.offset;\n      }\n\n      return offset;\n    };\n\n    _proto._getContainer = function _getContainer() {\n      if (this.config.container === false) {\n        return document.body;\n      }\n\n      if (Util.isElement(this.config.container)) {\n        return $(this.config.container);\n      }\n\n      return $(document).find(this.config.container);\n    };\n\n    _proto._getAttachment = function _getAttachment(placement) {\n      return AttachmentMap$1[placement.toUpperCase()];\n    };\n\n    _proto._setListeners = function _setListeners() {\n      var _this4 = this;\n\n      var triggers = this.config.trigger.split(' ');\n      triggers.forEach(function (trigger) {\n        if (trigger === 'click') {\n          $(_this4.element).on(_this4.constructor.Event.CLICK, _this4.config.selector, function (event) {\n            return _this4.toggle(event);\n          });\n        } else if (trigger !== Trigger.MANUAL) {\n          var eventIn = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSEENTER : _this4.constructor.Event.FOCUSIN;\n          var eventOut = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSELEAVE : _this4.constructor.Event.FOCUSOUT;\n          $(_this4.element).on(eventIn, _this4.config.selector, function (event) {\n            return _this4._enter(event);\n          }).on(eventOut, _this4.config.selector, function (event) {\n            return _this4._leave(event);\n          });\n        }\n      });\n      $(this.element).closest('.modal').on('hide.bs.modal', function () {\n        if (_this4.element) {\n          _this4.hide();\n        }\n      });\n\n      if (this.config.selector) {\n        this.config = _objectSpread({}, this.config, {\n          trigger: 'manual',\n          selector: ''\n        });\n      } else {\n        this._fixTitle();\n      }\n    };\n\n    _proto._fixTitle = function _fixTitle() {\n      var titleType = typeof this.element.getAttribute('data-original-title');\n\n      if (this.element.getAttribute('title') || titleType !== 'string') {\n        this.element.setAttribute('data-original-title', this.element.getAttribute('title') || '');\n        this.element.setAttribute('title', '');\n      }\n    };\n\n    _proto._enter = function _enter(event, context) {\n      var dataKey = this.constructor.DATA_KEY;\n      context = context || $(event.currentTarget).data(dataKey);\n\n      if (!context) {\n        context = new this.constructor(event.currentTarget, this._getDelegateConfig());\n        $(event.currentTarget).data(dataKey, context);\n      }\n\n      if (event) {\n        context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true;\n      }\n\n      if ($(context.getTipElement()).hasClass(ClassName$6.SHOW) || context._hoverState === HoverState.SHOW) {\n        context._hoverState = HoverState.SHOW;\n        return;\n      }\n\n      clearTimeout(context._timeout);\n      context._hoverState = HoverState.SHOW;\n\n      if (!context.config.delay || !context.config.delay.show) {\n        context.show();\n        return;\n      }\n\n      context._timeout = setTimeout(function () {\n        if (context._hoverState === HoverState.SHOW) {\n          context.show();\n        }\n      }, context.config.delay.show);\n    };\n\n    _proto._leave = function _leave(event, context) {\n      var dataKey = this.constructor.DATA_KEY;\n      context = context || $(event.currentTarget).data(dataKey);\n\n      if (!context) {\n        context = new this.constructor(event.currentTarget, this._getDelegateConfig());\n        $(event.currentTarget).data(dataKey, context);\n      }\n\n      if (event) {\n        context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false;\n      }\n\n      if (context._isWithActiveTrigger()) {\n        return;\n      }\n\n      clearTimeout(context._timeout);\n      context._hoverState = HoverState.OUT;\n\n      if (!context.config.delay || !context.config.delay.hide) {\n        context.hide();\n        return;\n      }\n\n      context._timeout = setTimeout(function () {\n        if (context._hoverState === HoverState.OUT) {\n          context.hide();\n        }\n      }, context.config.delay.hide);\n    };\n\n    _proto._isWithActiveTrigger = function _isWithActiveTrigger() {\n      for (var trigger in this._activeTrigger) {\n        if (this._activeTrigger[trigger]) {\n          return true;\n        }\n      }\n\n      return false;\n    };\n\n    _proto._getConfig = function _getConfig(config) {\n      var dataAttributes = $(this.element).data();\n      Object.keys(dataAttributes).forEach(function (dataAttr) {\n        if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {\n          delete dataAttributes[dataAttr];\n        }\n      });\n      config = _objectSpread({}, this.constructor.Default, dataAttributes, typeof config === 'object' && config ? config : {});\n\n      if (typeof config.delay === 'number') {\n        config.delay = {\n          show: config.delay,\n          hide: config.delay\n        };\n      }\n\n      if (typeof config.title === 'number') {\n        config.title = config.title.toString();\n      }\n\n      if (typeof config.content === 'number') {\n        config.content = config.content.toString();\n      }\n\n      Util.typeCheckConfig(NAME$6, config, this.constructor.DefaultType);\n\n      if (config.sanitize) {\n        config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn);\n      }\n\n      return config;\n    };\n\n    _proto._getDelegateConfig = function _getDelegateConfig() {\n      var config = {};\n\n      if (this.config) {\n        for (var key in this.config) {\n          if (this.constructor.Default[key] !== this.config[key]) {\n            config[key] = this.config[key];\n          }\n        }\n      }\n\n      return config;\n    };\n\n    _proto._cleanTipClass = function _cleanTipClass() {\n      var $tip = $(this.getTipElement());\n      var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX);\n\n      if (tabClass !== null && tabClass.length) {\n        $tip.removeClass(tabClass.join(''));\n      }\n    };\n\n    _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(popperData) {\n      var popperInstance = popperData.instance;\n      this.tip = popperInstance.popper;\n\n      this._cleanTipClass();\n\n      this.addAttachmentClass(this._getAttachment(popperData.placement));\n    };\n\n    _proto._fixTransition = function _fixTransition() {\n      var tip = this.getTipElement();\n      var initConfigAnimation = this.config.animation;\n\n      if (tip.getAttribute('x-placement') !== null) {\n        return;\n      }\n\n      $(tip).removeClass(ClassName$6.FADE);\n      this.config.animation = false;\n      this.hide();\n      this.show();\n      this.config.animation = initConfigAnimation;\n    } // Static\n    ;\n\n    Tooltip._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var data = $(this).data(DATA_KEY$6);\n\n        var _config = typeof config === 'object' && config;\n\n        if (!data && /dispose|hide/.test(config)) {\n          return;\n        }\n\n        if (!data) {\n          data = new Tooltip(this, _config);\n          $(this).data(DATA_KEY$6, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config]();\n        }\n      });\n    };\n\n    _createClass(Tooltip, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$6;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default$4;\n      }\n    }, {\n      key: \"NAME\",\n      get: function get() {\n        return NAME$6;\n      }\n    }, {\n      key: \"DATA_KEY\",\n      get: function get() {\n        return DATA_KEY$6;\n      }\n    }, {\n      key: \"Event\",\n      get: function get() {\n        return Event$6;\n      }\n    }, {\n      key: \"EVENT_KEY\",\n      get: function get() {\n        return EVENT_KEY$6;\n      }\n    }, {\n      key: \"DefaultType\",\n      get: function get() {\n        return DefaultType$4;\n      }\n    }]);\n\n    return Tooltip;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n\n  $.fn[NAME$6] = Tooltip._jQueryInterface;\n  $.fn[NAME$6].Constructor = Tooltip;\n\n  $.fn[NAME$6].noConflict = function () {\n    $.fn[NAME$6] = JQUERY_NO_CONFLICT$6;\n    return Tooltip._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$7 = 'popover';\n  var VERSION$7 = '4.3.1';\n  var DATA_KEY$7 = 'bs.popover';\n  var EVENT_KEY$7 = \".\" + DATA_KEY$7;\n  var JQUERY_NO_CONFLICT$7 = $.fn[NAME$7];\n  var CLASS_PREFIX$1 = 'bs-popover';\n  var BSCLS_PREFIX_REGEX$1 = new RegExp(\"(^|\\\\s)\" + CLASS_PREFIX$1 + \"\\\\S+\", 'g');\n\n  var Default$5 = _objectSpread({}, Tooltip.Default, {\n    placement: 'right',\n    trigger: 'click',\n    content: '',\n    template: '<div class=\"popover\" role=\"tooltip\">' + '<div class=\"arrow\"></div>' + '<h3 class=\"popover-header\"></h3>' + '<div class=\"popover-body\"></div></div>'\n  });\n\n  var DefaultType$5 = _objectSpread({}, Tooltip.DefaultType, {\n    content: '(string|element|function)'\n  });\n\n  var ClassName$7 = {\n    FADE: 'fade',\n    SHOW: 'show'\n  };\n  var Selector$7 = {\n    TITLE: '.popover-header',\n    CONTENT: '.popover-body'\n  };\n  var Event$7 = {\n    HIDE: \"hide\" + EVENT_KEY$7,\n    HIDDEN: \"hidden\" + EVENT_KEY$7,\n    SHOW: \"show\" + EVENT_KEY$7,\n    SHOWN: \"shown\" + EVENT_KEY$7,\n    INSERTED: \"inserted\" + EVENT_KEY$7,\n    CLICK: \"click\" + EVENT_KEY$7,\n    FOCUSIN: \"focusin\" + EVENT_KEY$7,\n    FOCUSOUT: \"focusout\" + EVENT_KEY$7,\n    MOUSEENTER: \"mouseenter\" + EVENT_KEY$7,\n    MOUSELEAVE: \"mouseleave\" + EVENT_KEY$7\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Popover =\n  /*#__PURE__*/\n  function (_Tooltip) {\n    _inheritsLoose(Popover, _Tooltip);\n\n    function Popover() {\n      return _Tooltip.apply(this, arguments) || this;\n    }\n\n    var _proto = Popover.prototype;\n\n    // Overrides\n    _proto.isWithContent = function isWithContent() {\n      return this.getTitle() || this._getContent();\n    };\n\n    _proto.addAttachmentClass = function addAttachmentClass(attachment) {\n      $(this.getTipElement()).addClass(CLASS_PREFIX$1 + \"-\" + attachment);\n    };\n\n    _proto.getTipElement = function getTipElement() {\n      this.tip = this.tip || $(this.config.template)[0];\n      return this.tip;\n    };\n\n    _proto.setContent = function setContent() {\n      var $tip = $(this.getTipElement()); // We use append for html objects to maintain js events\n\n      this.setElementContent($tip.find(Selector$7.TITLE), this.getTitle());\n\n      var content = this._getContent();\n\n      if (typeof content === 'function') {\n        content = content.call(this.element);\n      }\n\n      this.setElementContent($tip.find(Selector$7.CONTENT), content);\n      $tip.removeClass(ClassName$7.FADE + \" \" + ClassName$7.SHOW);\n    } // Private\n    ;\n\n    _proto._getContent = function _getContent() {\n      return this.element.getAttribute('data-content') || this.config.content;\n    };\n\n    _proto._cleanTipClass = function _cleanTipClass() {\n      var $tip = $(this.getTipElement());\n      var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX$1);\n\n      if (tabClass !== null && tabClass.length > 0) {\n        $tip.removeClass(tabClass.join(''));\n      }\n    } // Static\n    ;\n\n    Popover._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var data = $(this).data(DATA_KEY$7);\n\n        var _config = typeof config === 'object' ? config : null;\n\n        if (!data && /dispose|hide/.test(config)) {\n          return;\n        }\n\n        if (!data) {\n          data = new Popover(this, _config);\n          $(this).data(DATA_KEY$7, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config]();\n        }\n      });\n    };\n\n    _createClass(Popover, null, [{\n      key: \"VERSION\",\n      // Getters\n      get: function get() {\n        return VERSION$7;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default$5;\n      }\n    }, {\n      key: \"NAME\",\n      get: function get() {\n        return NAME$7;\n      }\n    }, {\n      key: \"DATA_KEY\",\n      get: function get() {\n        return DATA_KEY$7;\n      }\n    }, {\n      key: \"Event\",\n      get: function get() {\n        return Event$7;\n      }\n    }, {\n      key: \"EVENT_KEY\",\n      get: function get() {\n        return EVENT_KEY$7;\n      }\n    }, {\n      key: \"DefaultType\",\n      get: function get() {\n        return DefaultType$5;\n      }\n    }]);\n\n    return Popover;\n  }(Tooltip);\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n\n  $.fn[NAME$7] = Popover._jQueryInterface;\n  $.fn[NAME$7].Constructor = Popover;\n\n  $.fn[NAME$7].noConflict = function () {\n    $.fn[NAME$7] = JQUERY_NO_CONFLICT$7;\n    return Popover._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$8 = 'scrollspy';\n  var VERSION$8 = '4.3.1';\n  var DATA_KEY$8 = 'bs.scrollspy';\n  var EVENT_KEY$8 = \".\" + DATA_KEY$8;\n  var DATA_API_KEY$6 = '.data-api';\n  var JQUERY_NO_CONFLICT$8 = $.fn[NAME$8];\n  var Default$6 = {\n    offset: 10,\n    method: 'auto',\n    target: ''\n  };\n  var DefaultType$6 = {\n    offset: 'number',\n    method: 'string',\n    target: '(string|element)'\n  };\n  var Event$8 = {\n    ACTIVATE: \"activate\" + EVENT_KEY$8,\n    SCROLL: \"scroll\" + EVENT_KEY$8,\n    LOAD_DATA_API: \"load\" + EVENT_KEY$8 + DATA_API_KEY$6\n  };\n  var ClassName$8 = {\n    DROPDOWN_ITEM: 'dropdown-item',\n    DROPDOWN_MENU: 'dropdown-menu',\n    ACTIVE: 'active'\n  };\n  var Selector$8 = {\n    DATA_SPY: '[data-spy=\"scroll\"]',\n    ACTIVE: '.active',\n    NAV_LIST_GROUP: '.nav, .list-group',\n    NAV_LINKS: '.nav-link',\n    NAV_ITEMS: '.nav-item',\n    LIST_ITEMS: '.list-group-item',\n    DROPDOWN: '.dropdown',\n    DROPDOWN_ITEMS: '.dropdown-item',\n    DROPDOWN_TOGGLE: '.dropdown-toggle'\n  };\n  var OffsetMethod = {\n    OFFSET: 'offset',\n    POSITION: 'position'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var ScrollSpy =\n  /*#__PURE__*/\n  function () {\n    function ScrollSpy(element, config) {\n      var _this = this;\n\n      this._element = element;\n      this._scrollElement = element.tagName === 'BODY' ? window : element;\n      this._config = this._getConfig(config);\n      this._selector = this._config.target + \" \" + Selector$8.NAV_LINKS + \",\" + (this._config.target + \" \" + Selector$8.LIST_ITEMS + \",\") + (this._config.target + \" \" + Selector$8.DROPDOWN_ITEMS);\n      this._offsets = [];\n      this._targets = [];\n      this._activeTarget = null;\n      this._scrollHeight = 0;\n      $(this._scrollElement).on(Event$8.SCROLL, function (event) {\n        return _this._process(event);\n      });\n      this.refresh();\n\n      this._process();\n    } // Getters\n\n\n    var _proto = ScrollSpy.prototype;\n\n    // Public\n    _proto.refresh = function refresh() {\n      var _this2 = this;\n\n      var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION;\n      var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method;\n      var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0;\n      this._offsets = [];\n      this._targets = [];\n      this._scrollHeight = this._getScrollHeight();\n      var targets = [].slice.call(document.querySelectorAll(this._selector));\n      targets.map(function (element) {\n        var target;\n        var targetSelector = Util.getSelectorFromElement(element);\n\n        if (targetSelector) {\n          target = document.querySelector(targetSelector);\n        }\n\n        if (target) {\n          var targetBCR = target.getBoundingClientRect();\n\n          if (targetBCR.width || targetBCR.height) {\n            // TODO (fat): remove sketch reliance on jQuery position/offset\n            return [$(target)[offsetMethod]().top + offsetBase, targetSelector];\n          }\n        }\n\n        return null;\n      }).filter(function (item) {\n        return item;\n      }).sort(function (a, b) {\n        return a[0] - b[0];\n      }).forEach(function (item) {\n        _this2._offsets.push(item[0]);\n\n        _this2._targets.push(item[1]);\n      });\n    };\n\n    _proto.dispose = function dispose() {\n      $.removeData(this._element, DATA_KEY$8);\n      $(this._scrollElement).off(EVENT_KEY$8);\n      this._element = null;\n      this._scrollElement = null;\n      this._config = null;\n      this._selector = null;\n      this._offsets = null;\n      this._targets = null;\n      this._activeTarget = null;\n      this._scrollHeight = null;\n    } // Private\n    ;\n\n    _proto._getConfig = function _getConfig(config) {\n      config = _objectSpread({}, Default$6, typeof config === 'object' && config ? config : {});\n\n      if (typeof config.target !== 'string') {\n        var id = $(config.target).attr('id');\n\n        if (!id) {\n          id = Util.getUID(NAME$8);\n          $(config.target).attr('id', id);\n        }\n\n        config.target = \"#\" + id;\n      }\n\n      Util.typeCheckConfig(NAME$8, config, DefaultType$6);\n      return config;\n    };\n\n    _proto._getScrollTop = function _getScrollTop() {\n      return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop;\n    };\n\n    _proto._getScrollHeight = function _getScrollHeight() {\n      return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);\n    };\n\n    _proto._getOffsetHeight = function _getOffsetHeight() {\n      return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height;\n    };\n\n    _proto._process = function _process() {\n      var scrollTop = this._getScrollTop() + this._config.offset;\n\n      var scrollHeight = this._getScrollHeight();\n\n      var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight();\n\n      if (this._scrollHeight !== scrollHeight) {\n        this.refresh();\n      }\n\n      if (scrollTop >= maxScroll) {\n        var target = this._targets[this._targets.length - 1];\n\n        if (this._activeTarget !== target) {\n          this._activate(target);\n        }\n\n        return;\n      }\n\n      if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n        this._activeTarget = null;\n\n        this._clear();\n\n        return;\n      }\n\n      var offsetLength = this._offsets.length;\n\n      for (var i = offsetLength; i--;) {\n        var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]);\n\n        if (isActiveTarget) {\n          this._activate(this._targets[i]);\n        }\n      }\n    };\n\n    _proto._activate = function _activate(target) {\n      this._activeTarget = target;\n\n      this._clear();\n\n      var queries = this._selector.split(',').map(function (selector) {\n        return selector + \"[data-target=\\\"\" + target + \"\\\"],\" + selector + \"[href=\\\"\" + target + \"\\\"]\";\n      });\n\n      var $link = $([].slice.call(document.querySelectorAll(queries.join(','))));\n\n      if ($link.hasClass(ClassName$8.DROPDOWN_ITEM)) {\n        $link.closest(Selector$8.DROPDOWN).find(Selector$8.DROPDOWN_TOGGLE).addClass(ClassName$8.ACTIVE);\n        $link.addClass(ClassName$8.ACTIVE);\n      } else {\n        // Set triggered link as active\n        $link.addClass(ClassName$8.ACTIVE); // Set triggered links parents as active\n        // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor\n\n        $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_LINKS + \", \" + Selector$8.LIST_ITEMS).addClass(ClassName$8.ACTIVE); // Handle special case when .nav-link is inside .nav-item\n\n        $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_ITEMS).children(Selector$8.NAV_LINKS).addClass(ClassName$8.ACTIVE);\n      }\n\n      $(this._scrollElement).trigger(Event$8.ACTIVATE, {\n        relatedTarget: target\n      });\n    };\n\n    _proto._clear = function _clear() {\n      [].slice.call(document.querySelectorAll(this._selector)).filter(function (node) {\n        return node.classList.contains(ClassName$8.ACTIVE);\n      }).forEach(function (node) {\n        return node.classList.remove(ClassName$8.ACTIVE);\n      });\n    } // Static\n    ;\n\n    ScrollSpy._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var data = $(this).data(DATA_KEY$8);\n\n        var _config = typeof config === 'object' && config;\n\n        if (!data) {\n          data = new ScrollSpy(this, _config);\n          $(this).data(DATA_KEY$8, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config]();\n        }\n      });\n    };\n\n    _createClass(ScrollSpy, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$8;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default$6;\n      }\n    }]);\n\n    return ScrollSpy;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(window).on(Event$8.LOAD_DATA_API, function () {\n    var scrollSpys = [].slice.call(document.querySelectorAll(Selector$8.DATA_SPY));\n    var scrollSpysLength = scrollSpys.length;\n\n    for (var i = scrollSpysLength; i--;) {\n      var $spy = $(scrollSpys[i]);\n\n      ScrollSpy._jQueryInterface.call($spy, $spy.data());\n    }\n  });\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME$8] = ScrollSpy._jQueryInterface;\n  $.fn[NAME$8].Constructor = ScrollSpy;\n\n  $.fn[NAME$8].noConflict = function () {\n    $.fn[NAME$8] = JQUERY_NO_CONFLICT$8;\n    return ScrollSpy._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$9 = 'tab';\n  var VERSION$9 = '4.3.1';\n  var DATA_KEY$9 = 'bs.tab';\n  var EVENT_KEY$9 = \".\" + DATA_KEY$9;\n  var DATA_API_KEY$7 = '.data-api';\n  var JQUERY_NO_CONFLICT$9 = $.fn[NAME$9];\n  var Event$9 = {\n    HIDE: \"hide\" + EVENT_KEY$9,\n    HIDDEN: \"hidden\" + EVENT_KEY$9,\n    SHOW: \"show\" + EVENT_KEY$9,\n    SHOWN: \"shown\" + EVENT_KEY$9,\n    CLICK_DATA_API: \"click\" + EVENT_KEY$9 + DATA_API_KEY$7\n  };\n  var ClassName$9 = {\n    DROPDOWN_MENU: 'dropdown-menu',\n    ACTIVE: 'active',\n    DISABLED: 'disabled',\n    FADE: 'fade',\n    SHOW: 'show'\n  };\n  var Selector$9 = {\n    DROPDOWN: '.dropdown',\n    NAV_LIST_GROUP: '.nav, .list-group',\n    ACTIVE: '.active',\n    ACTIVE_UL: '> li > .active',\n    DATA_TOGGLE: '[data-toggle=\"tab\"], [data-toggle=\"pill\"], [data-toggle=\"list\"]',\n    DROPDOWN_TOGGLE: '.dropdown-toggle',\n    DROPDOWN_ACTIVE_CHILD: '> .dropdown-menu .active'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Tab =\n  /*#__PURE__*/\n  function () {\n    function Tab(element) {\n      this._element = element;\n    } // Getters\n\n\n    var _proto = Tab.prototype;\n\n    // Public\n    _proto.show = function show() {\n      var _this = this;\n\n      if (this._element.parentNode && this._element.parentNode.nodeType === Node.ELEMENT_NODE && $(this._element).hasClass(ClassName$9.ACTIVE) || $(this._element).hasClass(ClassName$9.DISABLED)) {\n        return;\n      }\n\n      var target;\n      var previous;\n      var listElement = $(this._element).closest(Selector$9.NAV_LIST_GROUP)[0];\n      var selector = Util.getSelectorFromElement(this._element);\n\n      if (listElement) {\n        var itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector$9.ACTIVE_UL : Selector$9.ACTIVE;\n        previous = $.makeArray($(listElement).find(itemSelector));\n        previous = previous[previous.length - 1];\n      }\n\n      var hideEvent = $.Event(Event$9.HIDE, {\n        relatedTarget: this._element\n      });\n      var showEvent = $.Event(Event$9.SHOW, {\n        relatedTarget: previous\n      });\n\n      if (previous) {\n        $(previous).trigger(hideEvent);\n      }\n\n      $(this._element).trigger(showEvent);\n\n      if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) {\n        return;\n      }\n\n      if (selector) {\n        target = document.querySelector(selector);\n      }\n\n      this._activate(this._element, listElement);\n\n      var complete = function complete() {\n        var hiddenEvent = $.Event(Event$9.HIDDEN, {\n          relatedTarget: _this._element\n        });\n        var shownEvent = $.Event(Event$9.SHOWN, {\n          relatedTarget: previous\n        });\n        $(previous).trigger(hiddenEvent);\n        $(_this._element).trigger(shownEvent);\n      };\n\n      if (target) {\n        this._activate(target, target.parentNode, complete);\n      } else {\n        complete();\n      }\n    };\n\n    _proto.dispose = function dispose() {\n      $.removeData(this._element, DATA_KEY$9);\n      this._element = null;\n    } // Private\n    ;\n\n    _proto._activate = function _activate(element, container, callback) {\n      var _this2 = this;\n\n      var activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') ? $(container).find(Selector$9.ACTIVE_UL) : $(container).children(Selector$9.ACTIVE);\n      var active = activeElements[0];\n      var isTransitioning = callback && active && $(active).hasClass(ClassName$9.FADE);\n\n      var complete = function complete() {\n        return _this2._transitionComplete(element, active, callback);\n      };\n\n      if (active && isTransitioning) {\n        var transitionDuration = Util.getTransitionDurationFromElement(active);\n        $(active).removeClass(ClassName$9.SHOW).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n      } else {\n        complete();\n      }\n    };\n\n    _proto._transitionComplete = function _transitionComplete(element, active, callback) {\n      if (active) {\n        $(active).removeClass(ClassName$9.ACTIVE);\n        var dropdownChild = $(active.parentNode).find(Selector$9.DROPDOWN_ACTIVE_CHILD)[0];\n\n        if (dropdownChild) {\n          $(dropdownChild).removeClass(ClassName$9.ACTIVE);\n        }\n\n        if (active.getAttribute('role') === 'tab') {\n          active.setAttribute('aria-selected', false);\n        }\n      }\n\n      $(element).addClass(ClassName$9.ACTIVE);\n\n      if (element.getAttribute('role') === 'tab') {\n        element.setAttribute('aria-selected', true);\n      }\n\n      Util.reflow(element);\n\n      if (element.classList.contains(ClassName$9.FADE)) {\n        element.classList.add(ClassName$9.SHOW);\n      }\n\n      if (element.parentNode && $(element.parentNode).hasClass(ClassName$9.DROPDOWN_MENU)) {\n        var dropdownElement = $(element).closest(Selector$9.DROPDOWN)[0];\n\n        if (dropdownElement) {\n          var dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(Selector$9.DROPDOWN_TOGGLE));\n          $(dropdownToggleList).addClass(ClassName$9.ACTIVE);\n        }\n\n        element.setAttribute('aria-expanded', true);\n      }\n\n      if (callback) {\n        callback();\n      }\n    } // Static\n    ;\n\n    Tab._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var $this = $(this);\n        var data = $this.data(DATA_KEY$9);\n\n        if (!data) {\n          data = new Tab(this);\n          $this.data(DATA_KEY$9, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config]();\n        }\n      });\n    };\n\n    _createClass(Tab, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$9;\n      }\n    }]);\n\n    return Tab;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * Data Api implementation\n   * ------------------------------------------------------------------------\n   */\n\n\n  $(document).on(Event$9.CLICK_DATA_API, Selector$9.DATA_TOGGLE, function (event) {\n    event.preventDefault();\n\n    Tab._jQueryInterface.call($(this), 'show');\n  });\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n  $.fn[NAME$9] = Tab._jQueryInterface;\n  $.fn[NAME$9].Constructor = Tab;\n\n  $.fn[NAME$9].noConflict = function () {\n    $.fn[NAME$9] = JQUERY_NO_CONFLICT$9;\n    return Tab._jQueryInterface;\n  };\n\n  /**\n   * ------------------------------------------------------------------------\n   * Constants\n   * ------------------------------------------------------------------------\n   */\n\n  var NAME$a = 'toast';\n  var VERSION$a = '4.3.1';\n  var DATA_KEY$a = 'bs.toast';\n  var EVENT_KEY$a = \".\" + DATA_KEY$a;\n  var JQUERY_NO_CONFLICT$a = $.fn[NAME$a];\n  var Event$a = {\n    CLICK_DISMISS: \"click.dismiss\" + EVENT_KEY$a,\n    HIDE: \"hide\" + EVENT_KEY$a,\n    HIDDEN: \"hidden\" + EVENT_KEY$a,\n    SHOW: \"show\" + EVENT_KEY$a,\n    SHOWN: \"shown\" + EVENT_KEY$a\n  };\n  var ClassName$a = {\n    FADE: 'fade',\n    HIDE: 'hide',\n    SHOW: 'show',\n    SHOWING: 'showing'\n  };\n  var DefaultType$7 = {\n    animation: 'boolean',\n    autohide: 'boolean',\n    delay: 'number'\n  };\n  var Default$7 = {\n    animation: true,\n    autohide: true,\n    delay: 500\n  };\n  var Selector$a = {\n    DATA_DISMISS: '[data-dismiss=\"toast\"]'\n    /**\n     * ------------------------------------------------------------------------\n     * Class Definition\n     * ------------------------------------------------------------------------\n     */\n\n  };\n\n  var Toast =\n  /*#__PURE__*/\n  function () {\n    function Toast(element, config) {\n      this._element = element;\n      this._config = this._getConfig(config);\n      this._timeout = null;\n\n      this._setListeners();\n    } // Getters\n\n\n    var _proto = Toast.prototype;\n\n    // Public\n    _proto.show = function show() {\n      var _this = this;\n\n      $(this._element).trigger(Event$a.SHOW);\n\n      if (this._config.animation) {\n        this._element.classList.add(ClassName$a.FADE);\n      }\n\n      var complete = function complete() {\n        _this._element.classList.remove(ClassName$a.SHOWING);\n\n        _this._element.classList.add(ClassName$a.SHOW);\n\n        $(_this._element).trigger(Event$a.SHOWN);\n\n        if (_this._config.autohide) {\n          _this.hide();\n        }\n      };\n\n      this._element.classList.remove(ClassName$a.HIDE);\n\n      this._element.classList.add(ClassName$a.SHOWING);\n\n      if (this._config.animation) {\n        var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n        $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n      } else {\n        complete();\n      }\n    };\n\n    _proto.hide = function hide(withoutTimeout) {\n      var _this2 = this;\n\n      if (!this._element.classList.contains(ClassName$a.SHOW)) {\n        return;\n      }\n\n      $(this._element).trigger(Event$a.HIDE);\n\n      if (withoutTimeout) {\n        this._close();\n      } else {\n        this._timeout = setTimeout(function () {\n          _this2._close();\n        }, this._config.delay);\n      }\n    };\n\n    _proto.dispose = function dispose() {\n      clearTimeout(this._timeout);\n      this._timeout = null;\n\n      if (this._element.classList.contains(ClassName$a.SHOW)) {\n        this._element.classList.remove(ClassName$a.SHOW);\n      }\n\n      $(this._element).off(Event$a.CLICK_DISMISS);\n      $.removeData(this._element, DATA_KEY$a);\n      this._element = null;\n      this._config = null;\n    } // Private\n    ;\n\n    _proto._getConfig = function _getConfig(config) {\n      config = _objectSpread({}, Default$7, $(this._element).data(), typeof config === 'object' && config ? config : {});\n      Util.typeCheckConfig(NAME$a, config, this.constructor.DefaultType);\n      return config;\n    };\n\n    _proto._setListeners = function _setListeners() {\n      var _this3 = this;\n\n      $(this._element).on(Event$a.CLICK_DISMISS, Selector$a.DATA_DISMISS, function () {\n        return _this3.hide(true);\n      });\n    };\n\n    _proto._close = function _close() {\n      var _this4 = this;\n\n      var complete = function complete() {\n        _this4._element.classList.add(ClassName$a.HIDE);\n\n        $(_this4._element).trigger(Event$a.HIDDEN);\n      };\n\n      this._element.classList.remove(ClassName$a.SHOW);\n\n      if (this._config.animation) {\n        var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n        $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n      } else {\n        complete();\n      }\n    } // Static\n    ;\n\n    Toast._jQueryInterface = function _jQueryInterface(config) {\n      return this.each(function () {\n        var $element = $(this);\n        var data = $element.data(DATA_KEY$a);\n\n        var _config = typeof config === 'object' && config;\n\n        if (!data) {\n          data = new Toast(this, _config);\n          $element.data(DATA_KEY$a, data);\n        }\n\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n          }\n\n          data[config](this);\n        }\n      });\n    };\n\n    _createClass(Toast, null, [{\n      key: \"VERSION\",\n      get: function get() {\n        return VERSION$a;\n      }\n    }, {\n      key: \"DefaultType\",\n      get: function get() {\n        return DefaultType$7;\n      }\n    }, {\n      key: \"Default\",\n      get: function get() {\n        return Default$7;\n      }\n    }]);\n\n    return Toast;\n  }();\n  /**\n   * ------------------------------------------------------------------------\n   * jQuery\n   * ------------------------------------------------------------------------\n   */\n\n\n  $.fn[NAME$a] = Toast._jQueryInterface;\n  $.fn[NAME$a].Constructor = Toast;\n\n  $.fn[NAME$a].noConflict = function () {\n    $.fn[NAME$a] = JQUERY_NO_CONFLICT$a;\n    return Toast._jQueryInterface;\n  };\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap (v4.3.1): index.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  (function () {\n    if (typeof $ === 'undefined') {\n      throw new TypeError('Bootstrap\\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\\'s JavaScript.');\n    }\n\n    var version = $.fn.jquery.split(' ')[0].split('.');\n    var minMajor = 1;\n    var ltMajor = 2;\n    var minMinor = 9;\n    var minPatch = 1;\n    var maxMajor = 4;\n\n    if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {\n      throw new Error('Bootstrap\\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0');\n    }\n  })();\n\n  exports.Util = Util;\n  exports.Alert = Alert;\n  exports.Button = Button;\n  exports.Carousel = Carousel;\n  exports.Collapse = Collapse;\n  exports.Dropdown = Dropdown;\n  exports.Modal = Modal;\n  exports.Popover = Popover;\n  exports.Scrollspy = ScrollSpy;\n  exports.Tab = Tab;\n  exports.Toast = Toast;\n  exports.Tooltip = Tooltip;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n}));\n//# sourceMappingURL=bootstrap.js.map","\"use strict\";\nvar Platform = {};\n\n(function () {\n\n  Platform.detectDevice = function () {\n    var body = document.body;\n    var ua = navigator.userAgent;\n    var checker = {\n      // OS\n      Windows: ua.match(/Windows/),\n      MacOS: ua.match(/Mac/),\n      Android: ua.match(/Android/),\n\n      // Browser\n      Msie: ua.match(/Trident/),\n      Edge: ua.match(/Edge/),\n      Chrome: ua.match(/Chrome/),\n      Firefox: ua.match(/Firefox/),\n      Safari: ua.match(/Safari/),\n\n      // Device\n      isApple: ua.match(/(iPhone|iPod|iPad)/),\n      iPhone: ua.match(/iPhone/),\n      iPad: ua.match(/iPad/),\n      iPod: ua.match(/iPod/),\n    };\n\n    if (checker.isApple) {\n      // Apple\n      body.classList.add('isApple');\n\n      if (checker.iPhone) {\n        // Apple iPhone\n        body.classList.add('iphone');\n      } else if (checker.iPad) {\n        // Apple iPad\n        body.classList.add('ipad');\n      } else if (checker.iPod) {\n        // Apple iPod\n        body.classList.add('ipod');\n      }\n\n    } else  if (checker.Windows){\n      // Windows OS\n      body.classList.add('windowsOS');\n\n      if (checker.Edge){\n        // Edge Browser\n        body.classList.add('edge');\n      } else if (checker.Chrome){\n        // Chrome Browser\n        body.classList.add('chrome');\n      } else if(checker.Safari){\n        // Safari Browser\n        body.classList.add('safari');\n      } else if(checker.Firefox){\n        // Firefox Browser\n        body.classList.add('firefox');\n      } else if(checker.Msie){\n        // IE Browser\n        body.classList.add('msie');\n      }\n\n    } else if (checker.MacOS){\n      // Mac OS\n      body.classList.add('macOS');\n\n      if (checker.Chrome){\n        // Chrome Browser\n        body.classList.add('chrome');\n      } else if(checker.Safari){\n        // Safari Browser\n        body.classList.add('safari');\n      } else if(checker.Firefox){\n        // Firefox Browser\n        body.classList.add('firefox');\n      }\n\n    } else if (checker.Android){\n      // Android OS\n      body.classList.add('AndroidOS');\n    }\n\n  }\n\n  Platform.detectDevice();\n\n})($);\n","\"use strict\";\n\n\njQuery(document).ready(function() {\n    //removeIf(production)\n    console.log(\"document ready\");\n    //endRemoveIf(production)\n});\n\nfunction copyCodeToClipboard(event, element) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    const textInput = element.nextSibling.nextSibling;\n    textInput.select();\n\n\ttry {\n\t\tif (document.execCommand('copy')) {\n            element.innerHTML = 'Copied';\n            \n            setTimeout(function() {\n                element.innerHTML = 'Copy';\n            }, 3000);\n\t\t}\n\t} catch (err) {\n\t\talert('Please use CTRL/CMD + C to copy.');\n\t\tconsole.log('Oops, unable to copy', err);\n\t}\n\n    return false;\n}\n"]}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/failure.html b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/failure.html
new file mode 100644 (file)
index 0000000..81afa1e
--- /dev/null
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>openHAB - Miele Cloud Binding Configuration - Status</title>
+    <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+    <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+    <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+    <div class="container">
+        <div class="logo-container">
+            <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+            <img src="/mielecloud/assets/img/miele.png" width="190" />
+        </div>
+
+
+        <h2>Cloud Binding Configuration</h2>
+
+        <ul class="statusbar">
+            <li>Overview</li>
+            <li>Pairing</li>
+            <li class="active">Status</li>
+        </ul>
+        <main role="main">
+            <section class="mt-4 mb-5">
+                <div id="success-body">
+                    <h3>Pairing failed!</h3>
+                    <p>
+                        <!-- ERROR MESSAGE TEXT -->
+                    </p>
+                </div>
+
+                <a href="/mielecloud" class="btn btn-danger btn-lg">Go back to account overview</a>
+            </section>
+
+        </main>
+        <script src="/mielecloud/assets/js/main.js"></script>
+    </div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/index.html b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/index.html
new file mode 100644 (file)
index 0000000..a546c79
--- /dev/null
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>openHAB - Miele Cloud Binding Configuration - Home</title>
+    <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+    <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+    <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+    <div class="container">
+        <div class="logo-container">
+            <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+            <img src="/mielecloud/assets/img/miele.png" width="190" />
+        </div>
+
+
+        <h2>Cloud Binding Configuration</h2>
+
+        <ul class="statusbar">
+            <li class="active">Overview</li>
+            <li>Pairing</li>
+            <li>Status</li>
+        </ul>
+        <main role="main">
+            <section class="mt-4 mb-5">
+                <h3><!-- BRIDGES TITLE --></h3>
+
+                <ul class="accounts">
+                    <!-- BRIDGES -->
+                </ul>
+
+                <!-- NO SSL WARNING -->
+
+                <div class="controls">
+                    <a href="/mielecloud/pair" class="btn btn-danger btn-lg">Pair Account</a>
+                </div>
+            </section>
+
+        </main>
+        <script src="/mielecloud/assets/js/main.js"></script>
+    </div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/pairing.html b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/pairing.html
new file mode 100644 (file)
index 0000000..8c6bb6c
--- /dev/null
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>openHAB - Miele Cloud Binding Configuration - Pairing</title>
+    <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+    <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+    <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+    <div class="container">
+        <div class="logo-container">
+            <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+            <img src="/mielecloud/assets/img/miele.png" width="190" />
+        </div>
+
+
+        <h2>Cloud Binding Configuration</h2>
+
+        <ul class="statusbar">
+            <li>Overview</li>
+            <li class="active">Pairing</li>
+            <li>Status</li>
+        </ul>
+        <main role="main">
+            <section class="mt-4 mb-5">
+                <div id="pair-body">
+                    <p>
+                        Go to <a href="https://www.miele.com/f/com/en/register_api.aspx">the Miele developer portal</a> to obtain your
+                        client ID and client secret.
+                    </p>
+
+                    <!-- ERROR MESSAGE -->
+
+                    <form action="/mielecloud/forwardToLogin">
+                        <div class="form-group">
+                            <label for="clientId">Client ID:</label>
+                            <input type="text" class="form-control" id="clientId" name="clientId" placeholder="Enter your client ID" value="<!-- CLIENT ID -->" required />
+                        </div>
+                        <div class="form-group">
+                            <label for="clientSecret">Client Secret:</label>
+                            <input type="text" class="form-control" id="clientSecret" name="clientSecret" placeholder="Enter your client secret" value="<!-- CLIENT SECRET -->" required />
+                        </div>
+                        <div class="form-group">
+                            <label for="bridgeId">Bridge ID:</label>
+                            <input type="text" class="form-control" id="bridgeId" name="bridgeId" placeholder="Enter the bridge ID to use for pairing" required pattern="[A-Za-z0-9_-]*" />
+                        </div>
+                        <div class="form-group">
+                            <label for="email">E-mail address:</label>
+                            <input type="text" class="form-control" id="email" name="email" placeholder="Enter the e-mail address associated with you Miele Cloud Account" required pattern="[a-z0-9._%+-]{3,}@[a-z]{3,}([.]{1}[a-z]{2,}|[.]{1}[a-z]{2,}[.]{1}[a-z]{2,})" />
+                        </div>
+                        <button type="submit" class="btn btn-danger btn-lg">Pair Account</button>
+                    </form>
+                </div>
+            </section>
+
+        </main>
+        <script src="/mielecloud/assets/js/main.js"></script>
+    </div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/success.html b/bundles/org.openhab.binding.mielecloud/src/main/resources/org/openhab/binding/mielecloud/internal/config/success.html
new file mode 100644 (file)
index 0000000..0a94114
--- /dev/null
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>openHAB - Miele Cloud Binding Configuration - Status</title>
+    <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+    <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+    <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+    <div class="container">
+        <div class="logo-container">
+            <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+            <img src="/mielecloud/assets/img/miele.png" width="190" />
+        </div>
+
+
+        <h2>Cloud Binding Configuration</h2>
+
+        <ul class="statusbar">
+            <li>Overview</li>
+            <li>Pairing</li>
+            <li class="active">Status</li>
+        </ul>
+        <main role="main">
+            <section class="mt-4 mb-5">
+                <script type="text/javascript">
+                    window.onload = function() {
+                        var locale = document.getElementById("locale").value
+                        updateLocale(locale)
+                    }
+
+                    function onLocaleChanged(event) {
+                        var locale = event.target.value
+                        updateLocale(locale)
+                    }
+
+                    function updateLocale(locale) {
+                        var thingsTemplate = document.getElementById("things-template")
+                        thingsTemplate.innerHTML = thingsTemplate.innerHTML.replace(/locale=".."/g, "locale=\"" + locale + "\"")
+                    }
+                </script>
+                <div id="success-body">
+                    <div>
+                        <h3>Pairing successful!</h3>
+                        <!-- ERROR MESSAGE TEXT -->
+                        <p>You can now either let us automatically create and configure a bridge thing for you or configure it via a things-file.</p>
+                        <p>Please choose a locale to use for display purposes.</p>
+
+                        <form action="/mielecloud/createBridgeThing">
+                            <div class="form-group">
+                                <input type="hidden" name="bridgeUid" value="<!-- BRIDGE UID -->" />
+                                <input type="hidden" name="email" value="<!-- EMAIL -->" />
+                                <label for="locale">Locale:</label>
+                                <select class="form-control" id="locale" name="locale" onchange="onLocaleChanged(event);">
+                                    <!-- LOCALE OPTIONS -->
+                                </select>
+                            </div>
+                            <button type="submit" class="btn btn-danger btn-lg">Create and Configure</button>
+                        </form>
+                    </div>
+
+                    <div class="things">
+                        <span class="legend">
+                            or use this .things-file template:
+                          </span>
+                        <div class="code-container">
+                            <a href="#" onclick="copyCodeToClipboard(event, this);" class="btn btn-outline-info btn-sm copy">Copy</a>
+                            <textarea id="things-template" readonly><!-- THINGS TEMPLATE CODE --></textarea>
+                        </div>
+                    </div>
+
+                    <div class="controls">
+                        <a href="/mielecloud" class="btn btn-info btn-lg">Back to overview</a>
+                    </div>
+                </div>
+            </section>
+
+        </main>
+        <script src="/mielecloud/assets/js/main.js"></script>
+    </div>
+</body>
+
+</html>
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingTestConstants.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingTestConstants.java
new file mode 100644 (file)
index 0000000..446cbbc
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MieleCloudBindingTestConstants {
+    private MieleCloudBindingTestConstants() {
+        throw new IllegalStateException("MieleCloudTestConstants must not be instantiated");
+    }
+
+    public static final String BRIDGE_ID = "genesis";
+
+    public static final String SERVICE_HANDLE = MieleCloudBindingConstants.THING_TYPE_BRIDGE.getAsString() + ":"
+            + BRIDGE_ID;
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresherTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresherTest.java
new file mode 100644 (file)
index 0000000..672ba33
--- /dev/null
@@ -0,0 +1,334 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
+import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class OpenHabOAuthTokenRefresherTest {
+    private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
+
+    private boolean hasAccessTokenRefreshListenerForServiceHandle(OpenHabOAuthTokenRefresher refresher,
+            String serviceHandle)
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        return ReflectionUtil
+                .<Map<String, @Nullable AccessTokenRefreshListener>> getPrivate(refresher, "listenerByServiceHandle")
+                .get(MieleCloudBindingTestConstants.SERVICE_HANDLE) != null;
+    }
+
+    private AccessTokenRefreshListener getAccessTokenRefreshListenerByServiceHandle(
+            OpenHabOAuthTokenRefresher refresher, String serviceHandle)
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        AccessTokenRefreshListener listener = ReflectionUtil
+                .<Map<String, @Nullable AccessTokenRefreshListener>> getPrivate(refresher, "listenerByServiceHandle")
+                .get(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+        assertNotNull(listener);
+        return Objects.requireNonNull(listener);
+    }
+
+    @Test
+    public void whenTheAccountWasNotConfiguredPriorToTheThingInitializingThenNoRefreshListenerCanBeRegistered() {
+        // given:
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+        });
+    }
+
+    @Test
+    public void whenARefreshListenerIsRegisteredThenAListenerIsRegisteredAtTheClientService()
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        // given:
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+        // when:
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // then:
+        verify(oauthClientService).addAccessTokenRefreshListener(any());
+        assertNotNull(
+                getAccessTokenRefreshListenerByServiceHandle(refresher, MieleCloudBindingTestConstants.SERVICE_HANDLE));
+    }
+
+    @Test
+    public void whenTokenIsRefreshedThenTheListenerIsCalledWithTheNewAccessToken()
+            throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+        // given:
+        AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+        accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+        when(oauthClientService.refreshToken()).thenAnswer(new Answer<@Nullable AccessTokenResponse>() {
+            @Override
+            @Nullable
+            public AccessTokenResponse answer(@Nullable InvocationOnMock invocation) throws Throwable {
+                getAccessTokenRefreshListenerByServiceHandle(refresher, MieleCloudBindingTestConstants.SERVICE_HANDLE)
+                        .onAccessTokenResponse(accessTokenResponse);
+                return accessTokenResponse;
+            }
+        });
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // then:
+        verify(listener).onNewAccessToken(ACCESS_TOKEN);
+    }
+
+    @Test
+    public void whenTokenIsRefreshedAndNoAccessTokenIsProvidedThenTheListenerIsNotNotified()
+            throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+        // given:
+        AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+        when(oauthClientService.refreshToken()).thenAnswer(new Answer<@Nullable AccessTokenResponse>() {
+            @Override
+            @Nullable
+            public AccessTokenResponse answer(@Nullable InvocationOnMock invocation) throws Throwable {
+                getAccessTokenRefreshListenerByServiceHandle(refresher, MieleCloudBindingTestConstants.SERVICE_HANDLE)
+                        .onAccessTokenResponse(accessTokenResponse);
+                return accessTokenResponse;
+            }
+        });
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+            } catch (OAuthException e) {
+                verifyNoInteractions(listener);
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenTokenRefreshFailsWithOAuthExceptionThenTheListenerIsNotNotified()
+            throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+        // given:
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+        when(oauthClientService.refreshToken()).thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+            } catch (OAuthException e) {
+                verifyNoInteractions(listener);
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenTokenRefreshFailsDueToNetworkErrorThenTheListenerIsNotNotified()
+            throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+        // given:
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+        when(oauthClientService.refreshToken()).thenThrow(new IOException());
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+            } catch (OAuthException e) {
+                verifyNoInteractions(listener);
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenTokenRefreshFailsDueToAnIllegalResponseThenTheListenerIsNotNotified()
+            throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+        // given:
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+        when(oauthClientService.refreshToken()).thenThrow(new OAuthResponseException());
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+            } catch (OAuthException e) {
+                verifyNoInteractions(listener);
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenTheRefreshListenerIsUnsetAndWasNotRegisteredBeforeThenNothingHappens()
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        // given:
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        // when:
+        refresher.unsetRefreshListener(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // then:
+        assertFalse(hasAccessTokenRefreshListenerForServiceHandle(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE));
+    }
+
+    @Test
+    public void whenTheRefreshListenerIsUnsetAndTheClientServiceIsNotAvailableThenTheListenerIsCleared()
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        // given:
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        refresher.unsetRefreshListener(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // then:
+        assertFalse(hasAccessTokenRefreshListenerForServiceHandle(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE));
+    }
+
+    @Test
+    public void whenTheRefreshListenerIsUnsetThenTheListenerIsClearedAndRemovedFromTheClientService()
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        // given:
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        refresher.unsetRefreshListener(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // then:
+        verify(oauthClientService).removeAccessTokenRefreshListener(any());
+        assertFalse(hasAccessTokenRefreshListenerForServiceHandle(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE));
+    }
+
+    @Test
+    public void whenTokensAreRemovedThenTheRuntimeIsRequestedToDeleteServiceAndAccessToken()
+            throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+        // given:
+        OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+                .thenReturn(oauthClientService);
+
+        OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+        OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+        refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        refresher.removeTokensFromStorage(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // then:
+        verify(oauthFactory).deleteServiceAndAccessToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImplTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImplTest.java
new file mode 100644 (file)
index 0000000..e05e6b4
--- /dev/null
@@ -0,0 +1,358 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.getPrivate;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class OAuthAuthorizationHandlerImplTest {
+    private static final String CLIENT_ID = "01234567-890a-bcde-f012-34567890abcd";
+    private static final String CLIENT_SECRET = "0123456789abcdefghijklmnopqrstiu";
+    private static final String REDIRECT_URL = "http://127.0.0.1:8080/mielecloud/result";
+    private static final String AUTH_CODE = "abcdef";
+    private static final ThingUID BRIDGE_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+            MieleCloudBindingTestConstants.BRIDGE_ID);
+    private static final String EMAIL = "openhab@openhab.org";
+
+    @Nullable
+    private OAuthClientService clientService;
+    @Nullable
+    private ScheduledFuture<?> timer;
+    @Nullable
+    private Runnable scheduledRunnable;
+    @Nullable
+    private OAuthAuthorizationHandler authorizationHandler;
+
+    private OAuthClientService getClientService() {
+        final OAuthClientService clientService = this.clientService;
+        assertNotNull(clientService);
+        return Objects.requireNonNull(clientService);
+    }
+
+    private ScheduledFuture<?> getTimer() {
+        final ScheduledFuture<?> timer = this.timer;
+        assertNotNull(timer);
+        return Objects.requireNonNull(timer);
+    }
+
+    private Runnable getScheduledRunnable() {
+        final Runnable scheduledRunnable = this.scheduledRunnable;
+        assertNotNull(scheduledRunnable);
+        return Objects.requireNonNull(scheduledRunnable);
+    }
+
+    private OAuthAuthorizationHandler getAuthorizationHandler() {
+        final OAuthAuthorizationHandler authorizationHandler = this.authorizationHandler;
+        assertNotNull(authorizationHandler);
+        return Objects.requireNonNull(authorizationHandler);
+    }
+
+    @BeforeEach
+    public void setUp() {
+        OAuthClientService clientService = mock(OAuthClientService.class);
+
+        OAuthFactory oauthFactory = mock(OAuthFactory.class);
+        when(oauthFactory.createOAuthClientService(anyString(), anyString(), anyString(), anyString(), anyString(),
+                isNull(), any())).thenReturn(clientService);
+
+        ScheduledFuture<?> timer = mock(ScheduledFuture.class);
+        when(timer.isDone()).thenReturn(false);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+        when(scheduler.schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any())).thenAnswer(invocation -> {
+            scheduledRunnable = invocation.getArgument(0);
+            return timer;
+        });
+
+        OAuthAuthorizationHandler authorizationHandler = new OAuthAuthorizationHandlerImpl(oauthFactory, scheduler);
+
+        this.clientService = clientService;
+        this.timer = timer;
+        this.scheduledRunnable = null;
+        this.authorizationHandler = authorizationHandler;
+    }
+
+    @Test
+    public void whenTheAuthorizationIsCompletedInTimeThenTheTimerIsCancelledAndAllResourcesAreCleanedUp()
+            throws Exception {
+        // given:
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+        // when:
+        getAuthorizationHandler().completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+
+        // then:
+        assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+        verify(getTimer()).cancel(false);
+
+        assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+        assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+        assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+        assertNull(getPrivate(getAuthorizationHandler(), "email"));
+
+        verify(getClientService()).extractAuthCodeFromAuthResponse(anyString());
+        verify(getClientService()).getAccessTokenResponseByAuthorizationCode(isNull(), anyString());
+    }
+
+    @Test
+    public void whenTheAuthorizationTimesOutThenTheOngoingAuthorizationIsCancelled() throws Exception {
+        // given:
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+        // when:
+        getScheduledRunnable().run();
+
+        // then:
+        assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+        assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+        assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+        assertNull(getPrivate(getAuthorizationHandler(), "email"));
+        assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+        verify(getTimer()).cancel(false);
+    }
+
+    @Test
+    public void whenTheAuthorizationCompletesAfterItTimedOutThenAnNoOngoingAuthorizationExceptionIsThrown()
+            throws Exception {
+        // given:
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+        getScheduledRunnable().run();
+
+        // when:
+        assertThrows(NoOngoingAuthorizationException.class, () -> {
+            getAuthorizationHandler()
+                    .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+        });
+    }
+
+    @Test
+    public void whenASecondAuthorizationIsBegunWhileAnotherIsStillOngoingThenAnOngoingAuthorizationExceptionIsThrown() {
+        // given:
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+        // when:
+        assertThrows(OngoingAuthorizationException.class, () -> {
+            getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        });
+    }
+
+    @Test
+    public void whenNoAuthorizationIsOngoingAndTheAuthorizationUrlIsRequestedThenAnNoOngoingAuthorizationExceptionIsThrown() {
+        // when:
+        assertThrows(NoOngoingAuthorizationException.class, () -> {
+            getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+        });
+    }
+
+    @Test
+    public void whenGetAuthorizationUrlFromTheFrameworkFailsThenTheOngoingAuthorizationIsAborted()
+            throws org.openhab.core.auth.client.oauth2.OAuthException, IllegalArgumentException, IllegalAccessException,
+            NoSuchFieldException, SecurityException {
+        // given:
+        when(getClientService().getAuthorizationUrl(anyString(), isNull(), isNull()))
+                .thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+            } catch (OAuthException e) {
+                assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+                assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+                assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+                assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+                assertNull(getPrivate(getAuthorizationHandler(), "email"));
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenExtractingTheAuthCodeFromTheResponseFailsThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+            throws Exception {
+        // given:
+        when(getClientService().extractAuthCodeFromAuthResponse(anyString()))
+                .thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                getAuthorizationHandler()
+                        .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+            } catch (OAuthException e) {
+                assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+                assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+                assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+                assertNull(getPrivate(getAuthorizationHandler(), "email"));
+                assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenRetrievingTheAccessTokenFailsDueToANetworkErrorThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+            throws Exception {
+        // given:
+        when(getClientService().extractAuthCodeFromAuthResponse(anyString())).thenReturn(AUTH_CODE);
+        when(getClientService().getAccessTokenResponseByAuthorizationCode(anyString(), anyString()))
+                .thenThrow(new IOException());
+
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                getAuthorizationHandler()
+                        .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+            } catch (OAuthException e) {
+                assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+                assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+                assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+                assertNull(getPrivate(getAuthorizationHandler(), "email"));
+                assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenRetrievingTheAccessTokenFailsDueToAnIllegalAnswerFromTheMieleServiceThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+            throws Exception {
+        // given:
+        when(getClientService().extractAuthCodeFromAuthResponse(anyString())).thenReturn(AUTH_CODE);
+        when(getClientService().getAccessTokenResponseByAuthorizationCode(anyString(), anyString()))
+                .thenThrow(new OAuthResponseException());
+
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                getAuthorizationHandler()
+                        .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+            } catch (OAuthException e) {
+                assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+                assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+                assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+                assertNull(getPrivate(getAuthorizationHandler(), "email"));
+                assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenRetrievingTheAccessTokenFailsWhileProcessingTheResponseThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+            throws Exception {
+        // given:
+        when(getClientService().extractAuthCodeFromAuthResponse(anyString())).thenReturn(AUTH_CODE);
+        when(getClientService().getAccessTokenResponseByAuthorizationCode(anyString(), anyString()))
+                .thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+        getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+        // when:
+        assertThrows(OAuthException.class, () -> {
+            try {
+                getAuthorizationHandler()
+                        .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+            } catch (OAuthException e) {
+                assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+                assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+                assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+                assertNull(getPrivate(getAuthorizationHandler(), "email"));
+                assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void whenNoAuthorizationIsOngoingThenGetBridgeUidThrowsNoOngoingAuthorizationException() {
+        // when:
+        assertThrows(NoOngoingAuthorizationException.class, () -> {
+            getAuthorizationHandler().getBridgeUid();
+        });
+    }
+
+    @Test
+    public void whenNoAuthorizationIsOngoingThenGetEmailThrowsNoOngoingAuthorizationException() {
+        // when:
+        assertThrows(NoOngoingAuthorizationException.class, () -> {
+            getAuthorizationHandler().getEmail();
+        });
+    }
+
+    @Test
+    public void whenAnAuthorizationIsOngoingThenGetBridgeUidReturnsTheUidOfTheBridgeBeingAuthorized() {
+        // given:
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+        // when:
+        ThingUID bridgeUid = getAuthorizationHandler().getBridgeUid();
+
+        // then:
+        assertEquals(BRIDGE_UID, bridgeUid);
+    }
+
+    @Test
+    public void whenAnAuthorizationIsOngoingThenGetEmailReturnsTheEmailBeingAuthorized() {
+        // given:
+        getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+        // when:
+        String email = getAuthorizationHandler().getEmail();
+
+        // then:
+        assertEquals(EMAIL, email);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGeneratorTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGeneratorTest.java
new file mode 100644 (file)
index 0000000..a626498
--- /dev/null
@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ThingsTemplateGeneratorTest {
+    private static final String BRIDGE_ID = "genesis";
+    private static final String ALTERNATIVE_BRIDGE_ID = "mielebridge";
+
+    private static final String LOCALE = "en";
+    private static final String ALTERNATIVE_LOCALE = "de";
+
+    private static final String EMAIL = "openhab@openhab.org";
+    private static final String ALTERNATIVE_EMAIL = "everyone@openhab.org";
+
+    @Test
+    public void whenBridgeIdAndAccessTokenAndLocaleAreProvidedThenAValidBridgeConfigurationTemplateIsGenerated() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        // when:
+        String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, EMAIL, LOCALE);
+
+        // then:
+        assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ]", template);
+    }
+
+    @Test
+    public void whenAnAlternativeBridgeIdIsProvidedThenAValidBridgeConfigurationTemplateWithThatIdIsGenerated() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        // when:
+        String template = templateGenerator.createBridgeConfigurationTemplate(ALTERNATIVE_BRIDGE_ID, EMAIL, LOCALE);
+
+        // then:
+        assertEquals("Bridge mielecloud:account:mielebridge [ email=\"openhab@openhab.org\", locale=\"en\" ]",
+                template);
+    }
+
+    @Test
+    public void whenAnAlternativeAccessTokenIsProvidedThenAValidBridgeConfigurationTemplateWithThatAccessTokenIsGenerated() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        // when:
+        String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, EMAIL, LOCALE);
+
+        // then:
+        assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ]", template);
+    }
+
+    @Test
+    public void whenAnAlternativeLocaleIsProvidedThenAValidBridgeConfigurationTemplateWithThatLocaleIsGenerated() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        // when:
+        String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, EMAIL, ALTERNATIVE_LOCALE);
+
+        // then:
+        assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"de\" ]", template);
+    }
+
+    @Test
+    public void whenAnAlternativeEmailIsProvidedThenAValidBridgeConfigurationTemplateWithThatEmailIsGenerated() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        // when:
+        String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, ALTERNATIVE_EMAIL, LOCALE);
+
+        // then:
+        assertEquals("Bridge mielecloud:account:genesis [ email=\"everyone@openhab.org\", locale=\"en\" ]", template);
+    }
+
+    private Bridge createBridgeMock(String id, String locale, String email) {
+        Configuration configuration = mock(Configuration.class);
+        when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn(locale);
+        when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn(email);
+
+        Bridge bridge = mock(Bridge.class);
+        when(bridge.getUID()).thenReturn(new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, id));
+        when(bridge.getConfiguration()).thenReturn(configuration);
+
+        return bridge;
+    }
+
+    private Thing createThingMock(ThingTypeUID thingTypeUid, String deviceIdentifier, @Nullable String label,
+            String bridgeId) {
+        Configuration configuration = mock(Configuration.class);
+        when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER)).thenReturn(deviceIdentifier);
+
+        Thing thing = mock(Thing.class);
+        when(thing.getThingTypeUID()).thenReturn(thingTypeUid);
+        when(thing.getUID()).thenReturn(new ThingUID(thingTypeUid, deviceIdentifier, bridgeId));
+        when(thing.getLabel()).thenReturn(label);
+        when(thing.getConfiguration()).thenReturn(configuration);
+        return thing;
+    }
+
+    private DiscoveryResult createDiscoveryResultMock(ThingTypeUID thingTypeUid, String id, String label,
+            String bridgeId) {
+        DiscoveryResult discoveryResult = mock(DiscoveryResult.class);
+        when(discoveryResult.getLabel()).thenReturn(label);
+        when(discoveryResult.getThingTypeUID()).thenReturn(thingTypeUid);
+        when(discoveryResult.getThingUID()).thenReturn(new ThingUID(thingTypeUid, id, bridgeId));
+        when(discoveryResult.getProperties())
+                .thenReturn(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, id));
+        return discoveryResult;
+    }
+
+    @Test
+    public void whenNoThingsArePairedAndNoInboxEntriesAreAvailableThenAnEmptyConfigurationTemplateIsGenerated() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        Bridge bridge = createBridgeMock(MieleCloudBindingTestConstants.BRIDGE_ID, LOCALE, EMAIL);
+
+        // when:
+        String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, Collections.emptyList(),
+                Collections.emptyList());
+
+        // then:
+        assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ] {\n}",
+                template);
+    }
+
+    @Test
+    public void whenPairedThingsArePresentThenTheyArePresentInTheConfigurationTemplate() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        Bridge bridge = createBridgeMock(ALTERNATIVE_BRIDGE_ID, ALTERNATIVE_LOCALE, ALTERNATIVE_EMAIL);
+
+        Thing thing1 = createThingMock(MieleCloudBindingConstants.THING_TYPE_OVEN, "000137439123", "Oven H7860XY",
+                ALTERNATIVE_BRIDGE_ID);
+        Thing thing2 = createThingMock(MieleCloudBindingConstants.THING_TYPE_HOB, "000160106123", null,
+                ALTERNATIVE_BRIDGE_ID);
+
+        List<Thing> pairedThings = Arrays.asList(thing1, thing2);
+
+        // when:
+        String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings,
+                Collections.emptyList());
+
+        // then:
+        assertEquals("Bridge mielecloud:account:mielebridge [ email=\"everyone@openhab.org\", locale=\"de\" ] {\n"
+                + "    Thing oven 000137439123 \"Oven H7860XY\" [ deviceIdentifier=\"000137439123\" ]\n"
+                + "    Thing hob 000160106123 [ deviceIdentifier=\"000160106123\" ]\n}", template);
+    }
+
+    @Test
+    public void whenDiscoveryResultsAreInTheInboxThenTheyArePresentInTheConfigurationTemplate() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        Bridge bridge = createBridgeMock(ALTERNATIVE_BRIDGE_ID, ALTERNATIVE_LOCALE, ALTERNATIVE_EMAIL);
+
+        DiscoveryResult discoveryResult1 = createDiscoveryResultMock(
+                MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, "000154106123", "Fridge-Freezer Kitchen",
+                ALTERNATIVE_BRIDGE_ID);
+        DiscoveryResult discoveryResult2 = createDiscoveryResultMock(
+                MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, "000189106123", "Washing Machine",
+                ALTERNATIVE_BRIDGE_ID);
+
+        List<DiscoveryResult> discoveredThings = Arrays.asList(discoveryResult1, discoveryResult2);
+
+        // when:
+        String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, Collections.emptyList(),
+                discoveredThings);
+
+        // then:
+        assertEquals("Bridge mielecloud:account:mielebridge [ email=\"everyone@openhab.org\", locale=\"de\" ] {\n"
+                + "    Thing fridge_freezer 000154106123 \"Fridge-Freezer Kitchen\" [ deviceIdentifier=\"000154106123\" ]\n"
+                + "    Thing washing_machine 000189106123 \"Washing Machine\" [ deviceIdentifier=\"000189106123\" ]\n}",
+                template);
+    }
+
+    @Test
+    public void whenThingsArePresentAndDiscoveryResultsAreInTheInboxThenTheyArePresentInTheConfigurationTemplate() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        Bridge bridge = createBridgeMock(ALTERNATIVE_BRIDGE_ID, ALTERNATIVE_LOCALE, EMAIL);
+
+        Thing thing1 = createThingMock(MieleCloudBindingConstants.THING_TYPE_OVEN, "000137439123", "Oven H7860XY",
+                ALTERNATIVE_BRIDGE_ID);
+        Thing thing2 = createThingMock(MieleCloudBindingConstants.THING_TYPE_HOB, "000160106123", null,
+                ALTERNATIVE_BRIDGE_ID);
+
+        List<Thing> pairedThings = Arrays.asList(thing1, thing2);
+
+        DiscoveryResult discoveryResult1 = createDiscoveryResultMock(
+                MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, "000154106123", "Fridge-Freezer Kitchen",
+                ALTERNATIVE_BRIDGE_ID);
+        DiscoveryResult discoveryResult2 = createDiscoveryResultMock(
+                MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, "000189106123", "Washing Machine",
+                ALTERNATIVE_BRIDGE_ID);
+
+        List<DiscoveryResult> discoveredThings = Arrays.asList(discoveryResult1, discoveryResult2);
+
+        // when:
+        String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings,
+                discoveredThings);
+
+        // then:
+        assertEquals("Bridge mielecloud:account:mielebridge [ email=\"openhab@openhab.org\", locale=\"de\" ] {\n"
+                + "    Thing oven 000137439123 \"Oven H7860XY\" [ deviceIdentifier=\"000137439123\" ]\n"
+                + "    Thing hob 000160106123 [ deviceIdentifier=\"000160106123\" ]\n"
+                + "    Thing fridge_freezer 000154106123 \"Fridge-Freezer Kitchen\" [ deviceIdentifier=\"000154106123\" ]\n"
+                + "    Thing washing_machine 000189106123 \"Washing Machine\" [ deviceIdentifier=\"000189106123\" ]\n}",
+                template);
+    }
+
+    @Test
+    public void whenNoLocaleIsConfiguredThenTheDefaultIsUsed() {
+        // given:
+        ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+        Configuration configuration = mock(Configuration.class);
+        when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn(null);
+        when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn(EMAIL);
+
+        Bridge bridge = mock(Bridge.class);
+        when(bridge.getUID()).thenReturn(
+                new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, MieleCloudBindingTestConstants.BRIDGE_ID));
+        when(bridge.getConfiguration()).thenReturn(configuration);
+
+        // when:
+        String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, Collections.emptyList(),
+                Collections.emptyList());
+
+        // then:
+        assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ] {\n}",
+                template);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/discovery/ThingInformationExtractorTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/discovery/ThingInformationExtractorTest.java
new file mode 100644 (file)
index 0000000..50f107e
--- /dev/null
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ThingInformationExtractorTest {
+    private static Stream<Arguments> extractedPropertiesContainSerialNumberAndModelIdParameterSource() {
+        return Stream.of(
+                Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+                        Optional.of("000124430017"), Optional.of("Ventilation Hood"), Optional.of("DA-6996"),
+                        "000124430017", "Ventilation Hood DA-6996", "000124430018"),
+                Arguments.of(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, DeviceType.COFFEE_SYSTEM,
+                        "000124431235", Optional.of("000124431234"), Optional.of("Coffee Machine"),
+                        Optional.of("CM-1234"), "000124431234", "Coffee Machine CM-1234", "000124431235"),
+                Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+                        Optional.empty(), Optional.of("Ventilation Hood"), Optional.of("DA-6996"), "000124430018",
+                        "Ventilation Hood DA-6996", "000124430018"),
+                Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+                        Optional.empty(), Optional.empty(), Optional.of("DA-6996"), "000124430018", "DA-6996",
+                        "000124430018"),
+                Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+                        Optional.empty(), Optional.of("Ventilation Hood"), Optional.empty(), "000124430018",
+                        "Ventilation Hood", "000124430018"),
+                Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+                        Optional.empty(), Optional.empty(), Optional.empty(), "000124430018", "Unknown",
+                        "000124430018"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("extractedPropertiesContainSerialNumberAndModelIdParameterSource")
+    void extractedPropertiesContainSerialNumberAndModelId(ThingTypeUID thingTypeUid, DeviceType deviceType,
+            String deviceIdentifier, Optional<String> fabNumber, Optional<String> type, Optional<String> techType,
+            String expectedSerialNumber, String expectedModelId, String expectedDeviceIdentifier) {
+        // given:
+        var deviceState = mock(DeviceState.class);
+        when(deviceState.getRawType()).thenReturn(deviceType);
+        when(deviceState.getDeviceIdentifier()).thenReturn(deviceIdentifier);
+        when(deviceState.getFabNumber()).thenReturn(fabNumber);
+        when(deviceState.getType()).thenReturn(type);
+        when(deviceState.getTechType()).thenReturn(techType);
+
+        // when:
+        var properties = ThingInformationExtractor.extractProperties(thingTypeUid, deviceState);
+
+        // then:
+        assertEquals(3, properties.size());
+        assertEquals(expectedSerialNumber, properties.get(Thing.PROPERTY_SERIAL_NUMBER));
+        assertEquals(expectedModelId, properties.get(Thing.PROPERTY_MODEL_ID));
+        assertEquals(expectedDeviceIdentifier,
+                properties.get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER));
+    }
+
+    @ParameterizedTest
+    @CsvSource({ "2,2", "4,4" })
+    void propertiesForHobContainPlateCount(int plateCount, String expectedPlateCountPropertyValue) {
+        // given:
+        var deviceState = mock(DeviceState.class);
+        when(deviceState.getRawType()).thenReturn(DeviceType.HOB_INDUCTION);
+        when(deviceState.getDeviceIdentifier()).thenReturn("000124430019");
+        when(deviceState.getFabNumber()).thenReturn(Optional.of("000124430019"));
+        when(deviceState.getType()).thenReturn(Optional.of("Induction Hob"));
+        when(deviceState.getTechType()).thenReturn(Optional.of("IH-7890"));
+        when(deviceState.getPlateStepCount()).thenReturn(Optional.of(plateCount));
+
+        // when:
+        var properties = ThingInformationExtractor.extractProperties(MieleCloudBindingConstants.THING_TYPE_HOB,
+                deviceState);
+
+        // then:
+        assertEquals(4, properties.size());
+        assertEquals("000124430019", properties.get(Thing.PROPERTY_SERIAL_NUMBER));
+        assertEquals("Induction Hob IH-7890", properties.get(Thing.PROPERTY_MODEL_ID));
+        assertEquals("000124430019", properties.get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER));
+        assertEquals(expectedPlateCountPropertyValue, properties.get(MieleCloudBindingConstants.PROPERTY_PLATE_COUNT));
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/LocaleValidatorTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/LocaleValidatorTest.java
new file mode 100644 (file)
index 0000000..4987f9e
--- /dev/null
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class LocaleValidatorTest {
+    @Test
+    public void enIsAValidLanguage() {
+        // given:
+        String language = "en";
+
+        // when:
+        boolean valid = LocaleValidator.isValidLanguage(language);
+
+        // then:
+        assertTrue(valid);
+    }
+
+    @Test
+    public void deIsAValidLanguage() {
+        // given:
+        String language = "de";
+
+        // when:
+        boolean valid = LocaleValidator.isValidLanguage(language);
+
+        // then:
+        assertTrue(valid);
+    }
+
+    @Test
+    public void aFullLocaleIsNotAValidLanguage() {
+        // given:
+        String language = "en_us";
+
+        // when:
+        boolean valid = LocaleValidator.isValidLanguage(language);
+
+        // then:
+        assertFalse(valid);
+    }
+
+    @Test
+    public void textIsNotAValidLanguage() {
+        // given:
+        String language = "Hello World!";
+
+        // when:
+        boolean valid = LocaleValidator.isValidLanguage(language);
+
+        // then:
+        assertFalse(valid);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/MockUtil.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/MockUtil.java
new file mode 100644 (file)
index 0000000..7253ba1
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+
+/**
+ * Utility class for creating common mocks.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MockUtil {
+    private MockUtil() {
+    }
+
+    public static Device mockDevice(String fabNumber) {
+        DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+        when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(fabNumber));
+
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+
+        return device;
+    }
+
+    public static <T> T requireNonNull(@Nullable T obj) {
+        if (obj == null) {
+            throw new IllegalArgumentException("Object must not be null");
+        }
+        return obj;
+    }
+
+    /**
+     * Creates a mock for {@link HttpClient} circumventing the problem that {@link HttpClient#start()} is {@code final}
+     * and {@link HttpClient#doStart()} {@code protected} and unaccessible when mocking with Mockito.
+     */
+    public static HttpClient mockHttpClient() {
+        return new HttpClient() {
+            @Override
+            protected void doStart() throws Exception {
+            }
+        };
+    }
+
+    /**
+     * Creates a mock for {@link HttpClient} circumventing the problem that {@link HttpClient#start()} is {@code final}
+     * and {@link HttpClient#doStart()} {@code protected} and unaccessible when mocking with Mockito.
+     *
+     * @param newRequestUri {@code uri} parameter of {@link HttpClient#newRequest(String)} to mock.
+     * @param newRequestReturnValue Return value of {@link HttpClient#newRequest(String)} to mock.
+     */
+    public static HttpClient mockHttpClient(String newRequestUri, Request newRequestReturnValue) {
+        return new HttpClient() {
+            @Override
+            protected void doStart() throws Exception {
+            }
+
+            @Override
+            public Request newRequest(@Nullable String uri) {
+                if (newRequestUri.equals(uri)) {
+                    return newRequestReturnValue;
+                } else {
+                    fail();
+                    throw new IllegalStateException();
+                }
+            }
+        };
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/ReflectionUtil.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/ReflectionUtil.java
new file mode 100644 (file)
index 0000000..035b99b
--- /dev/null
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility class for reflection operations such as accessing private fields or methods.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ReflectionUtil {
+    private ReflectionUtil() {
+    }
+
+    /**
+     * Gets a private attribute.
+     *
+     * @param object The object to get the attribute from.
+     * @param fieldName The name of the field to get.
+     * @return The obtained value.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws NoSuchFieldException if no field with the given name exists.
+     * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T getPrivate(Object object, String fieldName)
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        Field field = getFieldFromClassHierarchy(object.getClass(), fieldName);
+        field.setAccessible(true);
+        return (T) field.get(object);
+    }
+
+    private static Field getFieldFromClassHierarchy(Class<?> clazz, String fieldName)
+            throws NoSuchFieldException, SecurityException {
+        Class<?> iteratedClass = clazz;
+        do {
+            try {
+                return iteratedClass.getDeclaredField(fieldName);
+            } catch (NoSuchFieldException e) {
+            }
+            iteratedClass = iteratedClass.getSuperclass();
+        } while (iteratedClass != null);
+        throw new NoSuchFieldException();
+    }
+
+    /**
+     * Sets a private attribute.
+     *
+     * @param object The object to set the attribute on.
+     * @param fieldName The name of the field to set.
+     * @param value The value to set.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws NoSuchFieldException if no field with the given name exists.
+     * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     */
+    public static void setPrivate(Object object, String fieldName, Object value)
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        Field field = object.getClass().getDeclaredField(fieldName);
+        field.setAccessible(true);
+        field.set(object, value);
+    }
+
+    /**
+     * Invokes a private method on an object.
+     *
+     * @param object The object to invoke the method on.
+     * @param methodName The name of the method to invoke.
+     * @param parameters The parameters of the method invocation.
+     * @return The method call's return value.
+     * @throws NoSuchMethodException if no method with the given parameters or name exists.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     * @throws InvocationTargetException if the invoked method throws an exception.
+     */
+    public static <T> T invokePrivate(Object object, String methodName, Object... parameters)
+            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+        Class<?>[] parameterTypes = new Class[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            parameterTypes[i] = parameters[i].getClass();
+        }
+
+        return invokePrivate(object, methodName, parameterTypes, parameters);
+    }
+
+    /**
+     * Invokes a private method on an object.
+     *
+     * @param object The object to invoke the method on.
+     * @param methodName The name of the method to invoke.
+     * @param parameterTypes The types of the parameters.
+     * @param parameters The parameters of the method invocation.
+     * @return The method call's return value.
+     * @throws NoSuchMethodException if no method with the given parameters or name exists.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     * @throws InvocationTargetException if the invoked method throws an exception.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T invokePrivate(Object object, String methodName, Class<?>[] parameterTypes, Object... parameters)
+            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+        Method method = getMethodFromClassHierarchy(object.getClass(), methodName, parameterTypes);
+        method.setAccessible(true);
+        try {
+            return (T) method.invoke(object, parameters);
+        } catch (InvocationTargetException e) {
+            throw new IllegalStateException(e.getCause());
+        }
+    }
+
+    private static Method getMethodFromClassHierarchy(Class<?> clazz, String methodName, Class<?>[] parameterTypes)
+            throws NoSuchMethodException {
+        Class<?> iteratedClass = clazz;
+        do {
+            try {
+                return iteratedClass.getDeclaredMethod(methodName, parameterTypes);
+            } catch (NoSuchMethodException e) {
+            }
+            iteratedClass = iteratedClass.getSuperclass();
+        } while (iteratedClass != null);
+        throw new NoSuchMethodException();
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/ResourceUtil.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/util/ResourceUtil.java
new file mode 100644 (file)
index 0000000..6f4a391
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility class for handling test resources.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ResourceUtil {
+    private ResourceUtil() {
+    }
+
+    /**
+     * Gets the contents of a resource file as {@link String}.
+     *
+     * @param resourceName The resource name (path inside the resources source folder).
+     * @return The file contents.
+     * @throws IOException if reading the resource fails or it cannot be found.
+     */
+    public static String getResourceAsString(String resourceName) throws IOException {
+        InputStream inputStream = ResourceUtil.class.getResourceAsStream(resourceName);
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+            return reader.lines().collect(Collectors.joining("\n"));
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/ActionStateFetcherTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/ActionStateFetcherTest.java
new file mode 100644 (file)
index 0000000..cf71cb5
--- /dev/null
@@ -0,0 +1,178 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ActionStateFetcherTest {
+    private ScheduledExecutorService mockImmediatelyExecutingExecutorService() {
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+        when(scheduler.submit(ArgumentMatchers.<Runnable> any()))
+                .thenAnswer(new Answer<@Nullable ScheduledFuture<?>>() {
+                    @Override
+                    @Nullable
+                    public ScheduledFuture<?> answer(@Nullable InvocationOnMock invocation) throws Throwable {
+                        ((Runnable) MockUtil.requireNonNull(invocation).getArgument(0)).run();
+                        return null;
+                    }
+                });
+        return scheduler;
+    }
+
+    @Test
+    public void testFetchActionsIsInvokedWhenInitialDeviceStateIsSet() {
+        // given:
+        ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+        MieleWebservice webservice = mock(MieleWebservice.class);
+        DeviceState deviceState = mock(DeviceState.class);
+        DeviceState newDeviceState = mock(DeviceState.class);
+        ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+
+        // when:
+        actionsfetcher.onDeviceStateUpdated(deviceState);
+
+        // then:
+        verify(webservice).fetchActions(any());
+    }
+
+    @Test
+    public void testFetchActionsIsInvokedOnStateTransition() {
+        // given:
+        ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+        MieleWebservice webservice = mock(MieleWebservice.class);
+        DeviceState deviceState = mock(DeviceState.class);
+        DeviceState newDeviceState = mock(DeviceState.class);
+        ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+
+        actionsfetcher.onDeviceStateUpdated(deviceState);
+
+        // when:
+        actionsfetcher.onDeviceStateUpdated(newDeviceState);
+
+        // then:
+        verify(webservice, times(2)).fetchActions(any());
+    }
+
+    @Test
+    public void testFetchActionsIsNotInvokedWhenNoStateTransitionOccurrs() {
+        // given:
+        ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+        MieleWebservice webservice = mock(MieleWebservice.class);
+        DeviceState deviceState = mock(DeviceState.class);
+        DeviceState newDeviceState = mock(DeviceState.class);
+        ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+        actionsfetcher.onDeviceStateUpdated(deviceState);
+
+        // when:
+        actionsfetcher.onDeviceStateUpdated(newDeviceState);
+
+        // then:
+        verify(webservice, times(1)).fetchActions(any());
+    }
+
+    @Test
+    public void whenFetchActionsFailsWithAMieleWebserviceExceptionThenNoExceptionIsThrown() {
+        // given:
+        ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+        MieleWebservice webservice = mock(MieleWebservice.class);
+        doThrow(new MieleWebserviceException("It went wrong", ConnectionError.REQUEST_EXECUTION_FAILED))
+                .when(webservice).fetchActions(any());
+
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+        ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+        // when:
+        actionsfetcher.onDeviceStateUpdated(deviceState);
+
+        // then:
+        verify(webservice, times(1)).fetchActions(any());
+    }
+
+    @Test
+    public void whenFetchActionsFailsWithAnAuthorizationFailedExceptionThenNoExceptionIsThrown() {
+        // given:
+        ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+        MieleWebservice webservice = mock(MieleWebservice.class);
+        doThrow(new AuthorizationFailedException("Authorization failed")).when(webservice).fetchActions(any());
+
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+        ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+        // when:
+        actionsfetcher.onDeviceStateUpdated(deviceState);
+
+        // then:
+        verify(webservice, times(1)).fetchActions(any());
+    }
+
+    @Test
+    public void whenFetchActionsFailsWithATooManyRequestsExceptionThenNoExceptionIsThrown() {
+        // given:
+        ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+        MieleWebservice webservice = mock(MieleWebservice.class);
+        doThrow(new TooManyRequestsException("Too many requests", null)).when(webservice).fetchActions(any());
+
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+        ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+        // when:
+        actionsfetcher.onDeviceStateUpdated(deviceState);
+
+        // then:
+        verify(webservice, times(1)).fetchActions(any());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebserviceTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DefaultMieleWebserviceTest.java
new file mode 100644 (file)
index 0000000..628c87c
--- /dev/null
@@ -0,0 +1,742 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.getPrivate;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpFields;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactory;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DefaultMieleWebserviceTest {
+    private static final String MESSAGE_INTERNAL_SERVER_ERROR = "{\"message\": \"Internal Server Error\"}";
+    private static final String MESSAGE_SERVICE_UNAVAILABLE = "{\"message\": \"unavailable\"}";
+    private static final String MESSAGE_INVALID_JSON = "{\"abc123: \"äfgh\"}";
+
+    private static final String DEVICE_IDENTIFIER = "000124430016";
+
+    private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
+    private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
+    private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + DEVICE_IDENTIFIER + "/actions";
+    private static final String ENDPOINT_LOGOUT = SERVER_ADDRESS + "/thirdparty/logout";
+
+    private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
+
+    private final RetryStrategy retryStrategy = new UncatchedRetryStrategy();
+    private final Request request = mock(Request.class);
+
+    @Test
+    public void testDefaultRetryStrategyIsCombinationOfOneTimeRetryStrategyAndAuthorizationFailedStrategy()
+            throws Exception {
+        // given:
+        HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
+        when(httpClientFactory.createHttpClient(anyString())).thenReturn(MockUtil.mockHttpClient());
+        LanguageProvider languageProvider = mock(LanguageProvider.class);
+        OAuthTokenRefresher tokenRefresher = mock(OAuthTokenRefresher.class);
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        // when:
+        DefaultMieleWebservice webservice = new DefaultMieleWebservice(MieleWebserviceConfiguration.builder()
+                .withHttpClientFactory(httpClientFactory).withLanguageProvider(languageProvider)
+                .withTokenRefresher(tokenRefresher).withServiceHandle(MieleCloudBindingTestConstants.SERVICE_HANDLE)
+                .withScheduler(scheduler).build());
+
+        // then:
+        RetryStrategy retryStrategy = getPrivate(webservice, "retryStrategy");
+        assertTrue(retryStrategy instanceof RetryStrategyCombiner);
+
+        RetryStrategy first = getPrivate(retryStrategy, "first");
+        assertTrue(first instanceof NTimesRetryStrategy);
+        int numberOfRetries = getPrivate(first, "numberOfRetries");
+        assertEquals(1, numberOfRetries);
+
+        RetryStrategy second = getPrivate(retryStrategy, "second");
+        assertTrue(second instanceof AuthorizationFailedRetryStrategy);
+        OAuthTokenRefresher internalTokenRefresher = getPrivate(second, "tokenRefresher");
+        assertEquals(tokenRefresher, internalTokenRefresher);
+    }
+
+    private ContentResponse createContentResponseMock(int errorCode, String content) {
+        ContentResponse response = mock(ContentResponse.class);
+        when(response.getStatus()).thenReturn(errorCode);
+        when(response.getContentAsString()).thenReturn(content);
+        return response;
+    }
+
+    private void performFetchActions() throws Exception {
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            webservice.fetchActions(DEVICE_IDENTIFIER);
+        }
+    }
+
+    private void performFetchActionsExpectingFailure(ConnectionError expectedError) throws Exception {
+        try {
+            performFetchActions();
+        } catch (MieleWebserviceException e) {
+            assertEquals(expectedError, e.getConnectionError());
+            throw e;
+        } catch (MieleWebserviceTransientException e) {
+            assertEquals(expectedError, e.getConnectionError());
+            throw e;
+        }
+    }
+
+    @Test
+    public void testTimeoutExceptionWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        when(request.send()).thenThrow(TimeoutException.class);
+
+        // when:
+        assertThrows(MieleWebserviceTransientException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.TIMEOUT);
+        });
+    }
+
+    @Test
+    public void test500InternalServerErrorWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse contentResponse = createContentResponseMock(500, MESSAGE_INTERNAL_SERVER_ERROR);
+        when(request.send()).thenReturn(contentResponse);
+
+        // when:
+        assertThrows(MieleWebserviceTransientException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.SERVER_ERROR);
+        });
+    }
+
+    @Test
+    public void test503ServiceUnavailableWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse contentResponse = createContentResponseMock(503, MESSAGE_SERVICE_UNAVAILABLE);
+        when(request.send()).thenReturn(contentResponse);
+
+        // when:
+        assertThrows(MieleWebserviceTransientException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.SERVICE_UNAVAILABLE);
+        });
+    }
+
+    @Test
+    public void testInvalidJsonWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse contentResponse = createContentResponseMock(200, MESSAGE_INVALID_JSON);
+        when(request.send()).thenReturn(contentResponse);
+
+        // when:
+        assertThrows(MieleWebserviceTransientException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.RESPONSE_MALFORMED);
+        });
+    }
+
+    @Test
+    public void testInterruptedExceptionWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        when(request.send()).thenThrow(InterruptedException.class);
+
+        // when:
+        assertThrows(MieleWebserviceException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.REQUEST_INTERRUPTED);
+        });
+    }
+
+    @Test
+    public void testExecutionExceptionWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        when(request.send()).thenThrow(ExecutionException.class);
+
+        // when:
+        assertThrows(MieleWebserviceException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.REQUEST_EXECUTION_FAILED);
+        });
+    }
+
+    @Test
+    public void test400BadRequestWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(400, "{\"message\": \"grant_type is invalid\"}");
+        when(request.send()).thenReturn(response);
+
+        // when:
+        assertThrows(MieleWebserviceException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+        });
+    }
+
+    @Test
+    public void test401UnauthorizedWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
+        when(request.send()).thenReturn(response);
+
+        // when:
+        assertThrows(AuthorizationFailedException.class, () -> {
+            performFetchActions();
+        });
+    }
+
+    @Test
+    public void test404NotFoundWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(404, "{\"message\": \"Not found\"}");
+        when(request.send()).thenReturn(response);
+
+        // when:
+        assertThrows(MieleWebserviceException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+        });
+    }
+
+    @Test
+    public void test405MethodNotAllowedWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
+        when(request.send()).thenReturn(response);
+
+        // when:
+        assertThrows(MieleWebserviceException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+        });
+    }
+
+    @Test
+    public void test429TooManyRequestsWhilePerformingFetchActionsRequest() throws Exception {
+        // given:
+        HttpFields headerFields = mock(HttpFields.class);
+        when(headerFields.containsKey(anyString())).thenReturn(false);
+
+        ContentResponse response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
+        when(response.getHeaders()).thenReturn(headerFields);
+
+        when(request.send()).thenReturn(response);
+
+        // when:
+        assertThrows(TooManyRequestsException.class, () -> {
+            performFetchActions();
+        });
+    }
+
+    @Test
+    public void test502BadGatewayWhilePerforminggFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(502, "{\"message\": \"Bad Gateway\"}");
+        when(request.send()).thenReturn(response);
+
+        // when:
+        assertThrows(MieleWebserviceException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+        });
+    }
+
+    @Test
+    public void testMalformatedBodyWhilePerforminggFetchActionsRequest() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(502, "{\"message \"Bad Gateway\"}");
+        when(request.send()).thenReturn(response);
+
+        // when:
+        assertThrows(MieleWebserviceException.class, () -> {
+            performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+        });
+    }
+
+    private void fillRequestMockWithDefaultContent() throws InterruptedException, TimeoutException, ExecutionException {
+        ContentResponse response = createContentResponseMock(200,
+                "{\"000124430016\":{\"ident\": {\"deviceName\": \"MyFancyHood\", \"deviceIdentLabel\": {\"fabNumber\": \"000124430016\"}}}}");
+        when(request.send()).thenReturn(response);
+    }
+
+    @Test
+    public void testAddDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
+        // given:
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                dispatcher, scheduler)) {
+            DeviceStateListener listener = mock(DeviceStateListener.class);
+
+            // when:
+            webservice.addDeviceStateListener(listener);
+
+            // then:
+            verify(dispatcher).addListener(listener);
+            verifyNoMoreInteractions(dispatcher);
+        }
+    }
+
+    @Test
+    public void testFetchActionsDelegatesDeviceStateListenerDispatchingToDeviceStateDispatcher() throws Exception {
+        // given:
+        fillRequestMockWithDefaultContent();
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
+
+        DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy, dispatcher,
+                scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.fetchActions(DEVICE_IDENTIFIER);
+
+            // then:
+            verify(dispatcher).dispatchActionStateUpdates(any(), any());
+            verifyNoMoreInteractions(dispatcher);
+        }
+    }
+
+    @Test
+    public void testFetchActionsThrowsMieleWebserviceTransientExceptionWhenRequestContentIsMalformatted()
+            throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(200, "{\"}");
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            assertThrows(MieleWebserviceTransientException.class, () -> {
+                webservice.fetchActions(DEVICE_IDENTIFIER);
+            });
+        }
+    }
+
+    @Test
+    public void testPutProcessActionSendsRequestWithCorrectJsonContent() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(204, "");
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
+                .thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
+
+            // then:
+            verify(request).send();
+            verifyNoMoreInteractions(request);
+        }
+    }
+
+    @Test
+    public void testPutProcessActionThrowsIllegalArgumentExceptionWhenProcessActionIsUnknown() throws Exception {
+        // given:
+        RequestFactory requestFactory = mock(RequestFactory.class);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+                new DeviceStateDispatcher(), scheduler)) {
+
+            // when:
+            assertThrows(IllegalArgumentException.class, () -> {
+                webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.UNKNOWN);
+            });
+        }
+    }
+
+    @Test
+    public void testPutProcessActionThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
+        // given:
+        HttpFields responseHeaders = mock(HttpFields.class);
+        when(responseHeaders.containsKey(anyString())).thenReturn(false);
+
+        ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
+        when(response.getHeaders()).thenReturn(responseHeaders);
+
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
+                .thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            assertThrows(TooManyRequestsException.class, () -> {
+                webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
+            });
+        }
+    }
+
+    @Test
+    public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOn() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(204, "");
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":1}")).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.putLight(DEVICE_IDENTIFIER, true);
+
+            // then:
+            verify(request).send();
+            verifyNoMoreInteractions(request);
+        }
+    }
+
+    @Test
+    public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOff() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(204, "");
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.putLight(DEVICE_IDENTIFIER, false);
+
+            // then:
+            verify(request).send();
+            verifyNoMoreInteractions(request);
+        }
+    }
+
+    @Test
+    public void testPutLightThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
+        // given:
+        HttpFields responseHeaders = mock(HttpFields.class);
+        when(responseHeaders.containsKey(anyString())).thenReturn(false);
+
+        ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
+        when(response.getHeaders()).thenReturn(responseHeaders);
+
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            assertThrows(TooManyRequestsException.class, () -> {
+                webservice.putLight(DEVICE_IDENTIFIER, false);
+            });
+        }
+    }
+
+    @Test
+    public void testLogoutInvalidatesAccessTokenOnSuccess() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(204, "");
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.logout();
+
+            // then:
+            assertFalse(webservice.hasAccessToken());
+            verify(request).send();
+            verifyNoMoreInteractions(request);
+        }
+    }
+
+    @Test
+    public void testLogoutThrowsMieleWebserviceExceptionWhenMieleWebserviceTransientExceptionIsThrownInternally()
+            throws Exception {
+        // given:
+        when(request.send()).thenThrow(TimeoutException.class);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            assertThrows(MieleWebserviceException.class, () -> {
+                webservice.logout();
+            });
+        }
+    }
+
+    @Test
+    public void testLogoutInvalidatesAccessTokenWhenOperationFails() throws Exception {
+        // given:
+        when(request.send()).thenThrow(TimeoutException.class);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            try {
+                webservice.logout();
+            } catch (MieleWebserviceException e) {
+            }
+
+            // then:
+            assertFalse(webservice.hasAccessToken());
+        }
+    }
+
+    @Test
+    public void testRemoveDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
+        // given:
+        RequestFactory requestFactory = mock(RequestFactory.class);
+
+        DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                dispatcher, scheduler)) {
+            DeviceStateListener listener = mock(DeviceStateListener.class);
+            webservice.addDeviceStateListener(listener);
+
+            // when:
+            webservice.removeDeviceStateListener(listener);
+
+            // then:
+            verify(dispatcher).addListener(listener);
+            verify(dispatcher).removeListener(listener);
+            verifyNoMoreInteractions(dispatcher);
+        }
+    }
+
+    @Test
+    public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenSwitchingTheDeviceOn() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(204, "");
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOn\":true}")).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.putPowerState(DEVICE_IDENTIFIER, true);
+
+            // then:
+            verify(request).send();
+            verifyNoMoreInteractions(request);
+        }
+    }
+
+    @Test
+    public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenDeviceIsSwitchedOff() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(204, "");
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
+                .thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.putPowerState(DEVICE_IDENTIFIER, false);
+
+            // then:
+            verify(request).send();
+            verifyNoMoreInteractions(request);
+        }
+    }
+
+    @Test
+    public void testPutPowerStateThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
+        // given:
+        HttpFields responseHeaders = mock(HttpFields.class);
+        when(responseHeaders.containsKey(anyString())).thenReturn(false);
+
+        ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
+        when(response.getHeaders()).thenReturn(responseHeaders);
+
+        when(request.send()).thenReturn(response);
+
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
+                .thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            assertThrows(TooManyRequestsException.class, () -> {
+                webservice.putPowerState(DEVICE_IDENTIFIER, false);
+            });
+        }
+    }
+
+    @Test
+    public void testPutProgramResultsInARequestWithCorrectJson() throws Exception {
+        // given:
+        ContentResponse response = createContentResponseMock(204, "");
+        when(request.send()).thenReturn(response);
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"programId\":1}")).thenReturn(request);
+
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                new DeviceStateDispatcher(), scheduler)) {
+            webservice.setAccessToken(ACCESS_TOKEN);
+
+            // when:
+            webservice.putProgram(DEVICE_IDENTIFIER, 1);
+
+            // then:
+            verify(request).send();
+            verifyNoMoreInteractions(request);
+        }
+    }
+
+    @Test
+    public void testDispatchDeviceStateIsDelegatedToDeviceStateDispatcher() throws Exception {
+        // given:
+        RequestFactory requestFactory = mock(RequestFactory.class);
+        DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+        ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+        try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+                dispatcher, scheduler)) {
+            // when:
+            webservice.dispatchDeviceState(DEVICE_IDENTIFIER);
+
+            // then:
+            verify(dispatcher).dispatchDeviceState(DEVICE_IDENTIFIER);
+            verifyNoMoreInteractions(dispatcher);
+        }
+    }
+
+    /**
+     * {@link RetryStrategy} for testing purposes. No exceptions will be catched.
+     *
+     * @author Roland Edelhoff - Initial contribution.
+     */
+    private static class UncatchedRetryStrategy implements RetryStrategy {
+
+        @Override
+        public <@Nullable T> T performRetryableOperation(Supplier<T> operation,
+                Consumer<Exception> onTransientException) {
+            return operation.get();
+        }
+
+        @Override
+        public void performRetryableOperation(Runnable operation, Consumer<Exception> onTransientException) {
+            operation.run();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DeviceCacheTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DeviceCacheTest.java
new file mode 100644 (file)
index 0000000..6736243
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceCacheTest {
+    private static final String FIRST_DEVICE_IDENTIFIER = "000124430016";
+    private static final String SECOND_DEVICE_IDENTIFIER = "000124430017";
+    private static final String THIRD_DEVICE_IDENTIFIER = "400124430017";
+
+    private final Device firstDevice = mock(Device.class);
+    private final Device secondDevice = mock(Device.class);
+    private final Device thirdDevice = mock(Device.class);
+
+    private final DeviceCache deviceCache = new DeviceCache();
+
+    @Test
+    public void testCacheIsEmptyAfterConstruction() {
+        // then:
+        assertEquals(0, deviceCache.getDeviceIds().size());
+    }
+
+    @Test
+    public void testReplaceAllDevicesClearsTheCacheAndPutsAllNewDevicesIntoTheCache() {
+        // given:
+        DeviceCollection deviceCollection = mock(DeviceCollection.class);
+        when(deviceCollection.getDeviceIdentifiers())
+                .thenReturn(new HashSet<>(Arrays.asList(FIRST_DEVICE_IDENTIFIER, SECOND_DEVICE_IDENTIFIER)));
+        when(deviceCollection.getDevice(FIRST_DEVICE_IDENTIFIER)).thenReturn(firstDevice);
+        when(deviceCollection.getDevice(SECOND_DEVICE_IDENTIFIER)).thenReturn(secondDevice);
+
+        // when:
+        deviceCache.replaceAllDevices(deviceCollection);
+
+        // then:
+        assertEquals(new HashSet<>(Arrays.asList(FIRST_DEVICE_IDENTIFIER, SECOND_DEVICE_IDENTIFIER)),
+                deviceCache.getDeviceIds());
+        assertEquals(firstDevice, deviceCache.getDevice(FIRST_DEVICE_IDENTIFIER).get());
+        assertEquals(secondDevice, deviceCache.getDevice(SECOND_DEVICE_IDENTIFIER).get());
+    }
+
+    @Test
+    public void testReplaceAllDevicesClearsTheCachePriorToCachingThePassedDevices() {
+        // given:
+        testReplaceAllDevicesClearsTheCacheAndPutsAllNewDevicesIntoTheCache();
+
+        DeviceCollection deviceCollection = mock(DeviceCollection.class);
+        when(deviceCollection.getDeviceIdentifiers()).thenReturn(new HashSet<>(Arrays.asList(THIRD_DEVICE_IDENTIFIER)));
+        when(deviceCollection.getDevice(THIRD_DEVICE_IDENTIFIER)).thenReturn(thirdDevice);
+
+        // when:
+        deviceCache.replaceAllDevices(deviceCollection);
+
+        // then:
+        assertEquals(new HashSet<>(Arrays.asList(THIRD_DEVICE_IDENTIFIER)), deviceCache.getDeviceIds());
+        assertEquals(thirdDevice, deviceCache.getDevice(THIRD_DEVICE_IDENTIFIER).get());
+    }
+
+    @Test
+    public void testClearClearsTheCachedDevices() {
+        // given:
+        testReplaceAllDevicesClearsTheCacheAndPutsAllNewDevicesIntoTheCache();
+
+        // when:
+        deviceCache.clear();
+
+        // then:
+        assertEquals(0, deviceCache.getDeviceIds().size());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateDispatcherTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/DeviceStateDispatcherTest.java
new file mode 100644 (file)
index 0000000..a7bf6f3
--- /dev/null
@@ -0,0 +1,257 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MockUtil.mockDevice;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceStateDispatcherTest {
+    private static final String FIRST_DEVICE_IDENTIFIER = "000124430016";
+    private static final String SECOND_DEVICE_IDENTIFIER = "000124430017";
+    private static final String UNKNOWN_DEVICE_IDENTIFIER = "100124430016";
+
+    @Nullable
+    private Device firstDevice;
+    @Nullable
+    private Device secondDevice;
+    @Nullable
+    private DeviceCollection devices;
+
+    private Device getFirstDevice() {
+        assertNotNull(firstDevice);
+        return Objects.requireNonNull(firstDevice);
+    }
+
+    private Device getSecondDevice() {
+        assertNotNull(secondDevice);
+        return Objects.requireNonNull(secondDevice);
+    }
+
+    private DeviceCollection getDevices() {
+        assertNotNull(devices);
+        return Objects.requireNonNull(devices);
+    }
+
+    @BeforeEach
+    public void setUp() {
+        firstDevice = mockDevice(FIRST_DEVICE_IDENTIFIER);
+        secondDevice = mockDevice(SECOND_DEVICE_IDENTIFIER);
+
+        devices = mock(DeviceCollection.class);
+        when(getDevices().getDeviceIdentifiers())
+                .thenReturn(new HashSet<String>(Arrays.asList(FIRST_DEVICE_IDENTIFIER, SECOND_DEVICE_IDENTIFIER)));
+        when(getDevices().getDevice(FIRST_DEVICE_IDENTIFIER)).thenReturn(getFirstDevice());
+        when(getDevices().getDevice(SECOND_DEVICE_IDENTIFIER)).thenReturn(getSecondDevice());
+    }
+
+    @Test
+    public void testAddListenerDispatchesStateUpdatesToPassedListenerForCachedDevices()
+            throws InterruptedException, TimeoutException, ExecutionException {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+        // when:
+        dispatcher.addListener(listener);
+
+        // then:
+        verify(listener).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+        verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testDeviceStateUpdatesAreNotDispatchedToRemovedListeners() {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.addListener(listener);
+
+        // when:
+        dispatcher.removeListener(listener);
+        dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+        // then:
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testClearCachePreventsDeviceStateUpdateDispatchingOnListenerRegistration() {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+        // when:
+        dispatcher.clearCache();
+        dispatcher.addListener(listener);
+
+        // then:
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testDeviceStateUpdatesAreDispatchedToSubscribedListeners() {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.addListener(listener);
+
+        // when:
+        dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+        // then:
+        verify(listener).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+        verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testRemovalEventsAreDispatchedToSubscribedListeners()
+            throws InterruptedException, TimeoutException, ExecutionException {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+
+        Device deviceWithUnknownIdentifier = mockDevice(UNKNOWN_DEVICE_IDENTIFIER);
+        DeviceCollection devicesWithUnknownDevice = mock(DeviceCollection.class);
+        when(devicesWithUnknownDevice.getDeviceIdentifiers())
+                .thenReturn(new HashSet<String>(Arrays.asList(UNKNOWN_DEVICE_IDENTIFIER)));
+        when(devicesWithUnknownDevice.getDevice(UNKNOWN_DEVICE_IDENTIFIER)).thenReturn(deviceWithUnknownIdentifier);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.dispatchDeviceStateUpdates(devicesWithUnknownDevice);
+        dispatcher.clearCache();
+        dispatcher.addListener(listener);
+
+        // when:
+        dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+        // then:
+        verify(listener).onDeviceRemoved(UNKNOWN_DEVICE_IDENTIFIER);
+        verify(listener, times(2)).onDeviceStateUpdated(any());
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testRemovalEventsAreDispatchedToSubscribedListenersMatchingAllDeviceIds()
+            throws InterruptedException, TimeoutException, ExecutionException {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+
+        Device deviceWithUnknownIdentifier = mockDevice(UNKNOWN_DEVICE_IDENTIFIER);
+        DeviceCollection devicesWithUnknownDevice = mock(DeviceCollection.class);
+        when(devicesWithUnknownDevice.getDeviceIdentifiers())
+                .thenReturn(new HashSet<String>(Arrays.asList(UNKNOWN_DEVICE_IDENTIFIER)));
+        when(devicesWithUnknownDevice.getDevice(UNKNOWN_DEVICE_IDENTIFIER)).thenReturn(deviceWithUnknownIdentifier);
+
+        DeviceCollection emptyDevices = mock(DeviceCollection.class);
+        when(emptyDevices.getDeviceIdentifiers()).thenReturn(new HashSet<String>());
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.dispatchDeviceStateUpdates(devicesWithUnknownDevice);
+        dispatcher.clearCache();
+        dispatcher.addListener(listener);
+
+        // when:
+        dispatcher.dispatchDeviceStateUpdates(emptyDevices);
+
+        // then:
+        verify(listener).onDeviceRemoved(UNKNOWN_DEVICE_IDENTIFIER);
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testDeviceEventDispatchingForSubscribedListenersWithAnyDeviceIdFilter()
+            throws InterruptedException, TimeoutException, ExecutionException {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.addListener(listener);
+
+        // when:
+        dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+        // then:
+        verify(listener).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+        verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testActionsEventDispatchingForSubscribedListeners()
+            throws InterruptedException, TimeoutException, ExecutionException {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+        Actions actions = mock(Actions.class);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.addListener(listener);
+
+        // when:
+        dispatcher.dispatchActionStateUpdates(FIRST_DEVICE_IDENTIFIER, actions);
+
+        // then:
+        verify(listener).onProcessActionUpdated(new ActionsState(FIRST_DEVICE_IDENTIFIER, actions));
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testDeviceStateDispatcherDispatchesDeviceStatesAndActions() {
+        // given:
+        DeviceStateListener listener = mock(DeviceStateListener.class);
+        Actions actions = mock(Actions.class);
+
+        DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+        dispatcher.addListener(listener);
+
+        dispatcher.dispatchDeviceStateUpdates(getDevices());
+        dispatcher.dispatchActionStateUpdates(FIRST_DEVICE_IDENTIFIER, actions);
+
+        // when:
+        dispatcher.dispatchDeviceState(FIRST_DEVICE_IDENTIFIER);
+
+        // then:
+        verify(listener, times(2)).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+        verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+        verify(listener).onProcessActionUpdated(new ActionsState(FIRST_DEVICE_IDENTIFIER, actions));
+        verifyNoMoreInteractions(listener);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/HttpUtilTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/HttpUtilTest.java
new file mode 100644 (file)
index 0000000..8f6b2f0
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.http.HttpFields;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class HttpUtilTest {
+    @Test
+    public void whenTheResponseHasARetryAfterHeaderThenItIsParsedAndPassedWithTheException() {
+        // given:
+        HttpFields httpFields = mock(HttpFields.class);
+        when(httpFields.containsKey("Retry-After")).thenReturn(true);
+        when(httpFields.get("Retry-After")).thenReturn("100");
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(429);
+        when(response.getReason()).thenReturn("Too many requests!");
+        when(response.getHeaders()).thenReturn(httpFields);
+
+        // when:
+        try {
+            HttpUtil.checkHttpSuccess(response);
+            fail();
+        } catch (TooManyRequestsException e) {
+            // then:
+            assertEquals(100L, e.getSecondsUntilRetry());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/RequestFactoryImplTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/RequestFactoryImplTest.java
new file mode 100644 (file)
index 0000000..1d1591e
--- /dev/null
@@ -0,0 +1,241 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactoryImpl;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RequestFactoryImplTest {
+    private static final String URL = "https://www.openhab.org/";
+    private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
+    private static final String JSON_CONTENT = "{ \"update\": 1 }";
+
+    private static final String LANGUAGE = "de";
+
+    private static final long REQUEST_TIMEOUT = 5;
+    private static final long EXTENDED_REQUEST_TIMEOUT = 10;
+    private static final TimeUnit REQUEST_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+    @Nullable
+    private String contentString;
+    @Nullable
+    private String contentType;
+
+    private final LanguageProvider defaultLanguageProvider = new LanguageProvider() {
+        @Override
+        public Optional<String> getLanguage() {
+            return Optional.of(LANGUAGE);
+        }
+    };
+    private final LanguageProvider emptyStringLanguageProvider = new LanguageProvider() {
+        @Override
+        public Optional<String> getLanguage() {
+            return Optional.of("");
+        }
+    };
+
+    private Request getRequestMock() {
+        Request requestMock = mock(Request.class);
+        when(requestMock.header(anyString(), anyString())).thenReturn(requestMock);
+        when(requestMock.timeout(anyLong(), any())).thenReturn(requestMock);
+        when(requestMock.method(any(HttpMethod.class))).thenReturn(requestMock);
+        when(requestMock.param(anyString(), anyString())).thenReturn(requestMock);
+        when(requestMock.content(any())).thenAnswer(i -> {
+            StringContentProvider provider = i.getArgument(0);
+            List<Byte> rawData = new ArrayList<Byte>();
+            provider.forEach(b -> {
+                b.rewind();
+                while (b.hasRemaining()) {
+                    rawData.add(b.get());
+                }
+            });
+            byte[] data = new byte[rawData.size()];
+            for (int j = 0; j < data.length; j++) {
+                data[j] = rawData.get(j);
+            }
+            contentString = new String(data, StandardCharsets.UTF_8);
+            contentType = provider.getContentType();
+            return requestMock;
+        });
+        return requestMock;
+    }
+
+    private RequestFactoryImpl createRequestFactoryImpl(Request requestMock, LanguageProvider languageProvider) {
+        HttpClient httpClient = MockUtil.mockHttpClient(URL, requestMock);
+
+        HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
+        when(httpClientFactory.createHttpClient(anyString())).thenReturn(httpClient);
+
+        return new RequestFactoryImpl(httpClientFactory, languageProvider);
+    }
+
+    @Test
+    public void testCreateGetRequestReturnsRequestWithExpectedHeaders() {
+        // given:
+        Request requestMock = getRequestMock();
+        RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+        // when:
+        Request request = requestFactory.createGetRequest(URL, ACCESS_TOKEN);
+
+        // then:
+        assertEquals(requestMock, request);
+        verify(request).header("Content-type", "application/json");
+        verify(request).header("Accept", "*/*");
+        verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+        verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+        verify(request).method(HttpMethod.GET);
+        verify(request).param("language", LANGUAGE);
+        verifyNoMoreInteractions(request);
+    }
+
+    @Test
+    public void testCreatePutRequestReturnsRequestWithExpectedHeadersAndContent() {
+        Request requestMock = getRequestMock();
+        RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+        // when:
+        Request request = requestFactory.createPutRequest(URL, ACCESS_TOKEN, JSON_CONTENT);
+
+        // then:
+        assertEquals(requestMock, request);
+        verify(request).header("Content-type", "application/json");
+        verify(request).header("Accept", "*/*");
+        verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+        verify(request).timeout(EXTENDED_REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+        verify(request).method(HttpMethod.PUT);
+        verify(request).content(any());
+        verify(request).param("language", LANGUAGE);
+        assertEquals(JSON_CONTENT, contentString);
+        assertEquals("application/json", contentType);
+        verifyNoMoreInteractions(request);
+    }
+
+    @Test
+    public void testCreatePostRequestReturnsRequestWithExpectedHeaders() {
+        Request requestMock = getRequestMock();
+        RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+        // when:
+        Request request = requestFactory.createPostRequest(URL, ACCESS_TOKEN);
+
+        // then:
+        assertEquals(requestMock, request);
+        verify(request).header("Content-type", "application/json");
+        verify(request).header("Accept", "*/*");
+        verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+        verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+        verify(request).method(HttpMethod.POST);
+        verify(request).param("language", LANGUAGE);
+        verifyNoMoreInteractions(request);
+    }
+
+    @Test
+    public void testCreateRequestWithoutSuppliedLangugeCreatesNoLanguageParameter() {
+        // given:
+        Request requestMock = getRequestMock();
+        RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, new LanguageProvider() {
+            @Override
+            public Optional<String> getLanguage() {
+                return Optional.empty();
+            }
+        });
+
+        // when:
+        Request request = requestFactory.createGetRequest(URL, ACCESS_TOKEN);
+
+        // then:
+        assertEquals(requestMock, request);
+        verify(request).header("Content-type", "application/json");
+        verify(request).header("Accept", "*/*");
+        verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+        verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+        verify(request).method(HttpMethod.GET);
+        verifyNoMoreInteractions(request);
+    }
+
+    @Test
+    public void testCreateRequestWithEmptyLanguageCreatesNoLanguageParameter() {
+        // given:
+        Request requestMock = getRequestMock();
+        RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, emptyStringLanguageProvider);
+
+        // when:
+        Request request = requestFactory.createGetRequest(URL, ACCESS_TOKEN);
+
+        // then:
+        assertEquals(requestMock, request);
+        verify(request).header("Content-type", "application/json");
+        verify(request).header("Accept", "*/*");
+        verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+        verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+        verify(request).method(HttpMethod.GET);
+        verifyNoMoreInteractions(request);
+    }
+
+    @Test
+    public void whenAnSseRequestIsCreatedWithoutLanguageThenTheRequiredParametersAreSet() {
+        Request requestMock = getRequestMock();
+        RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, emptyStringLanguageProvider);
+
+        // when:
+        Request request = requestFactory.createSseRequest(URL, ACCESS_TOKEN);
+
+        // then:
+        assertEquals(requestMock, request);
+        verify(request).header("Content-type", "application/json");
+        verify(request).header("Accept", "text/event-stream");
+        verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+        verifyNoMoreInteractions(request);
+    }
+
+    @Test
+    public void whenAnSseRequestIsCreatedWithLanguageThenTheAcceptLanguageHeaderIsSet() {
+        Request requestMock = getRequestMock();
+        RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+        // when:
+        Request request = requestFactory.createSseRequest(URL, ACCESS_TOKEN);
+
+        // then:
+        assertEquals(requestMock, request);
+        verify(request).header("Content-type", "application/json");
+        verify(request).header("Accept", "text/event-stream");
+        verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+        verify(request).header("Accept-Language", LANGUAGE);
+        verifyNoMoreInteractions(request);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/ActionsStateTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/ActionsStateTest.java
new file mode 100644 (file)
index 0000000..5f8ec9d
--- /dev/null
@@ -0,0 +1,299 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ActionsStateTest {
+    private static final String DEVICE_IDENTIFIER = "003458276345";
+
+    @Test
+    public void testGetDeviceIdentifierReturnsDeviceIdentifier() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionsState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+        // when:
+        String deviceId = actionsState.getDeviceIdentifier();
+
+        // then:
+        assertEquals(DEVICE_IDENTIFIER, deviceId);
+    }
+
+    @Test
+    public void testReturnValuesWhenActionsIsNull() {
+        // given:
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, null);
+
+        // when:
+        boolean canBeStarted = actionState.canBeStarted();
+        boolean canBeStopped = actionState.canBeStopped();
+        boolean canBePaused = actionState.canBePaused();
+        boolean canStartSupercooling = actionState.canStartSupercooling();
+        boolean canStopSupercooling = actionState.canStopSupercooling();
+        boolean canContolSupercooling = actionState.canContolSupercooling();
+        boolean canStartSuperfreezing = actionState.canStartSuperfreezing();
+        boolean canStopSuperfreezing = actionState.canStopSuperfreezing();
+        boolean canControlSuperfreezing = actionState.canControlSuperfreezing();
+        boolean canEnableLight = actionState.canEnableLight();
+        boolean canDisableLight = actionState.canDisableLight();
+
+        // then:
+        assertFalse(canBeStarted);
+        assertFalse(canBeStopped);
+        assertFalse(canBePaused);
+        assertFalse(canStartSupercooling);
+        assertFalse(canStopSupercooling);
+        assertFalse(canContolSupercooling);
+        assertFalse(canStartSuperfreezing);
+        assertFalse(canStopSuperfreezing);
+        assertFalse(canControlSuperfreezing);
+        assertFalse(canEnableLight);
+        assertFalse(canDisableLight);
+    }
+
+    @Test
+    public void testReturnValuesWhenProcessActionIsEmpty() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, null);
+        when(actions.getProcessAction()).thenReturn(Collections.emptyList());
+
+        // when:
+        boolean canBeStarted = actionState.canBeStarted();
+        boolean canBeStopped = actionState.canBeStopped();
+        boolean canBePaused = actionState.canBePaused();
+        boolean canStartSupercooling = actionState.canStartSupercooling();
+        boolean canStopSupercooling = actionState.canStopSupercooling();
+        boolean canStartSuperfreezing = actionState.canStartSuperfreezing();
+        boolean canStopSuperfreezing = actionState.canStopSuperfreezing();
+
+        // then:
+        assertFalse(canBeStarted);
+        assertFalse(canBeStopped);
+        assertFalse(canBePaused);
+        assertFalse(canStartSupercooling);
+        assertFalse(canStopSupercooling);
+        assertFalse(canStartSuperfreezing);
+        assertFalse(canStopSuperfreezing);
+    }
+
+    @Test
+    public void testReturnValuesWhenLightIsEmpty() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, null);
+        when(actions.getLight()).thenReturn(Collections.emptyList());
+
+        // when:
+        boolean canEnableLight = actionState.canEnableLight();
+        boolean canDisableLight = actionState.canDisableLight();
+
+        // then:
+        assertFalse(canEnableLight);
+        assertFalse(canDisableLight);
+    }
+
+    @Test
+    public void testReturnValueWhenProcessActionStartIsAvailable() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+        when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.START));
+
+        // when:
+        boolean canBeStarted = actionState.canBeStarted();
+
+        // then:
+        assertTrue(canBeStarted);
+    }
+
+    @Test
+    public void testReturnValueWhenProcessActionStopIsAvailable() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+        when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.STOP));
+
+        // when:
+        boolean canBeStopped = actionState.canBeStopped();
+
+        // then:
+        assertTrue(canBeStopped);
+    }
+
+    @Test
+    public void testReturnValueWhenProcessActionStartSupercoolIsAvailable() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+        when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.START_SUPERCOOLING));
+
+        // when:
+        boolean canStartSupercooling = actionState.canStartSupercooling();
+        boolean canContolSupercooling = actionState.canContolSupercooling();
+
+        // then:
+        assertTrue(canStartSupercooling);
+        assertTrue(canContolSupercooling);
+    }
+
+    @Test
+    public void testReturnValueWhenProcessActionStartSuperfreezeIsAvailable() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+        when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.START_SUPERFREEZING));
+
+        // when:
+        boolean canStartSuperfreezing = actionState.canStartSuperfreezing();
+        boolean canControlSuperfreezing = actionState.canControlSuperfreezing();
+
+        // then:
+        assertTrue(canStartSuperfreezing);
+        assertTrue(canControlSuperfreezing);
+    }
+
+    @Test
+    public void testReturnValueWhenLightEnableIsAvailable() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+        when(actions.getLight()).thenReturn(Collections.singletonList(Light.ENABLE));
+
+        // when:
+        boolean canEnableLight = actionState.canEnableLight();
+
+        // then:
+        assertTrue(canEnableLight);
+    }
+
+    @Test
+    public void testReturnValueWhenLightDisableIsAvailable() {
+        // given:
+        Actions actions = mock(Actions.class);
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+        when(actions.getLight()).thenReturn(Collections.singletonList(Light.DISABLE));
+
+        // when:
+        boolean canDisableLight = actionState.canDisableLight();
+
+        // then:
+        assertTrue(canDisableLight);
+    }
+
+    @Test
+    public void testCanControlLightReturnsTrueWhenLightCanBeEnabled() {
+        // given:
+        Actions actions = mock(Actions.class);
+        when(actions.getLight()).thenReturn(Collections.singletonList(Light.ENABLE));
+
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+        // when:
+        boolean canControlLight = actionState.canControlLight();
+
+        // then:
+        assertTrue(canControlLight);
+    }
+
+    @Test
+    public void testCanControlLightReturnsTrueWhenLightCanBeDisabled() {
+        // given:
+        Actions actions = mock(Actions.class);
+        when(actions.getLight()).thenReturn(Collections.singletonList(Light.DISABLE));
+
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+        // when:
+        boolean canControlLight = actionState.canControlLight();
+
+        // then:
+        assertTrue(canControlLight);
+    }
+
+    @Test
+    public void testCanControlLightReturnsTrueWhenLightCanBeEnabledAndDisabled() {
+        // given:
+        Actions actions = mock(Actions.class);
+        when(actions.getLight()).thenReturn(Arrays.asList(Light.ENABLE, Light.DISABLE));
+
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+        // when:
+        boolean canControlLight = actionState.canControlLight();
+
+        // then:
+        assertTrue(canControlLight);
+    }
+
+    @Test
+    public void testCanControlLightReturnsFalseWhenNoLightOptionIsAvailable() {
+        // given:
+        Actions actions = mock(Actions.class);
+        when(actions.getLight()).thenReturn(new LinkedList<Light>());
+
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+        // when:
+        boolean canControlLight = actionState.canControlLight();
+
+        // then:
+        assertFalse(canControlLight);
+    }
+
+    @Test
+    public void testNoProgramCanBeSetWhenNoProgramIdIsPresent() {
+        // given:
+        Actions actions = mock(Actions.class);
+        when(actions.getProgramId()).thenReturn(Collections.emptyList());
+
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+        // when:
+        boolean canSetActiveProgram = actionState.canSetActiveProgramId();
+
+        // then:
+        assertFalse(canSetActiveProgram);
+    }
+
+    @Test
+    public void testProgramIdCanBeSetWhenProgramIdIsPresent() {
+        // given:
+        Actions actions = mock(Actions.class);
+        when(actions.getProgramId()).thenReturn(Collections.singletonList(1));
+
+        ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+        // when:
+        boolean canSetActiveProgram = actionState.canSetActiveProgramId();
+
+        // then:
+        assertTrue(canSetActiveProgram);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/CoolingDeviceTemperatureStateTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/CoolingDeviceTemperatureStateTest.java
new file mode 100644 (file)
index 0000000..3df8b6f
--- /dev/null
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CoolingDeviceTemperatureStateTest {
+    private static final Integer TEMPERATURE_0 = 8;
+    private static final Integer TEMPERATURE_1 = -10;
+
+    private static final Integer TARGET_TEMPERATURE_0 = 5;
+    private static final Integer TARGET_TEMPERATURE_1 = -18;
+
+    @Nullable
+    private DeviceState deviceState;
+
+    private DeviceState getDeviceState() {
+        assertNotNull(deviceState);
+        return Objects.requireNonNull(deviceState);
+    }
+
+    @BeforeEach
+    public void setUp() {
+        deviceState = mock(DeviceState.class);
+        when(getDeviceState().getTemperature(0)).thenReturn(Optional.of(TEMPERATURE_0));
+        when(getDeviceState().getTemperature(1)).thenReturn(Optional.of(TEMPERATURE_1));
+        when(getDeviceState().getTargetTemperature(0)).thenReturn(Optional.of(TARGET_TEMPERATURE_0));
+        when(getDeviceState().getTargetTemperature(1)).thenReturn(Optional.of(TARGET_TEMPERATURE_1));
+    }
+
+    @Test
+    public void testGetFridgeTemperaturesForFridge() {
+        // given:
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE);
+        CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer current = state.getFridgeTemperature().get();
+        Integer target = state.getFridgeTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_0, current);
+        assertEquals(TARGET_TEMPERATURE_0, target);
+    }
+
+    @Test
+    public void testGetFridgeTemperaturesForFridgeFreezerCombination() {
+        // given:
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer current = state.getFridgeTemperature().get();
+        Integer target = state.getFridgeTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_0, current);
+        assertEquals(TARGET_TEMPERATURE_0, target);
+    }
+
+    @Test
+    public void testGetFridgeTemperaturesForFreezer() {
+        // given:
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.FREEZER);
+        CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> current = state.getFridgeTemperature();
+        Optional<Integer> target = state.getFridgeTargetTemperature();
+
+        // then:
+        assertFalse(current.isPresent());
+        assertFalse(target.isPresent());
+    }
+
+    @Test
+    public void testGetFreezerTemperaturesForFridge() {
+        // given:
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE);
+        CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> current = state.getFreezerTemperature();
+        Optional<Integer> target = state.getFreezerTargetTemperature();
+
+        // then:
+        assertFalse(current.isPresent());
+        assertFalse(target.isPresent());
+    }
+
+    @Test
+    public void testGetFreezerTemperaturesForFridgeFreezerCombination() {
+        // given:
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer current = state.getFreezerTemperature().get();
+        Integer target = state.getFreezerTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_1, current);
+        assertEquals(TARGET_TEMPERATURE_1, target);
+    }
+
+    @Test
+    public void testGetFreezerTemperaturesForFreezer() {
+        // given:
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.FREEZER);
+        CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer current = state.getFreezerTemperature().get();
+        Integer target = state.getFreezerTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_0, current);
+        assertEquals(TARGET_TEMPERATURE_0, target);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/DeviceStateTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/DeviceStateTest.java
new file mode 100644 (file)
index 0000000..e98df6b
--- /dev/null
@@ -0,0 +1,2130 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DryingStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.PlateStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramId;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramPhase;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.RemoteEnable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.SpinningSpeed;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.State;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Status;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Temperature;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.VentilationStep;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm and info state channels and map
+ *         signal flags from API
+ * @author Björn Lange - Add elapsed time channel, robotic vacuum cleaner
+ */
+@NonNullByDefault
+public class DeviceStateTest {
+    private static final String DEVICE_IDENTIFIER = "mac-f83001f37d45ffff";
+
+    @Test
+    public void testGetDeviceIdentifierReturnsDeviceIdentifier() {
+        // given:
+        Device device = mock(Device.class);
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String deviceId = deviceState.getDeviceIdentifier();
+
+        // then:
+        assertEquals(DEVICE_IDENTIFIER, deviceId);
+    }
+
+    @Test
+    public void testReturnValuesWhenDeviceIsNull() {
+        // given:
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, null);
+
+        // when:
+        Optional<String> status = deviceState.getStatus();
+        Optional<Integer> statusRaw = deviceState.getStatusRaw();
+        Optional<StateType> stateType = deviceState.getStateType();
+        Optional<String> selectedProgram = deviceState.getSelectedProgram();
+        Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+        Optional<String> programPhase = deviceState.getProgramPhase();
+        Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+        Optional<String> dryingTarget = deviceState.getDryingTarget();
+        Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+        Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+        Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+        Optional<Integer> temperature = deviceState.getTemperature(0);
+        Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+        Optional<String> ventilationStep = deviceState.getVentilationStep();
+        Optional<Integer> ventilationStepRaw = deviceState.getVentilationStepRaw();
+        Optional<Integer> plateStepCount = deviceState.getPlateStepCount();
+        Optional<String> plateStep = deviceState.getPlateStep(0);
+        Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+        boolean hasError = deviceState.hasError();
+        boolean hasInfo = deviceState.hasInfo();
+        Optional<Boolean> doorState = deviceState.getDoorState();
+        Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+        Optional<String> type = deviceState.getType();
+        DeviceType rawType = deviceState.getRawType();
+        Optional<Integer> batteryLevel = deviceState.getBatteryLevel();
+
+        Optional<String> deviceName = deviceState.getDeviceName();
+        Optional<String> fabNumber = deviceState.getFabNumber();
+        Optional<String> techType = deviceState.getTechType();
+        Optional<Integer> progress = deviceState.getProgress();
+
+        // then:
+        assertFalse(status.isPresent());
+        assertFalse(statusRaw.isPresent());
+        assertFalse(stateType.isPresent());
+        assertFalse(selectedProgram.isPresent());
+        assertFalse(selectedProgramId.isPresent());
+        assertFalse(programPhase.isPresent());
+        assertFalse(programPhaseRaw.isPresent());
+        assertFalse(dryingTarget.isPresent());
+        assertFalse(dryingTargetRaw.isPresent());
+        assertFalse(hasPreHeatFinished.isPresent());
+        assertFalse(targetTemperature.isPresent());
+        assertFalse(temperature.isPresent());
+        assertFalse(remoteControlEnabled.isPresent());
+        assertFalse(ventilationStep.isPresent());
+        assertFalse(ventilationStepRaw.isPresent());
+        assertFalse(plateStepCount.isPresent());
+        assertFalse(plateStep.isPresent());
+        assertFalse(plateStepRaw.isPresent());
+        assertFalse(hasError);
+        assertFalse(hasInfo);
+        assertFalse(doorState.isPresent());
+        assertFalse(doorAlarm.isPresent());
+        assertFalse(type.isPresent());
+        assertEquals(DeviceType.UNKNOWN, rawType);
+        assertFalse(deviceName.isPresent());
+        assertFalse(fabNumber.isPresent());
+        assertFalse(techType.isPresent());
+        assertFalse(progress.isPresent());
+        assertFalse(batteryLevel.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenStateIsNull() {
+        // given:
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.empty());
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> status = deviceState.getStatus();
+        Optional<Integer> statusRaw = deviceState.getStatusRaw();
+        Optional<StateType> stateType = deviceState.getStateType();
+        Optional<String> selectedProgram = deviceState.getSelectedProgram();
+        Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+        Optional<String> programPhase = deviceState.getProgramPhase();
+        Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+        Optional<String> dryingTarget = deviceState.getDryingTarget();
+        Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+        Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+        Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+        Optional<Integer> temperature = deviceState.getTemperature(0);
+        Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+        Optional<Integer> progress = deviceState.getProgress();
+        Optional<String> ventilationStep = deviceState.getVentilationStep();
+        Optional<Integer> ventilationStepRaw = deviceState.getVentilationStepRaw();
+        Optional<Integer> plateStepCount = deviceState.getPlateStepCount();
+        Optional<String> plateStep = deviceState.getPlateStep(0);
+        Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+        Boolean hasError = deviceState.hasError();
+        Optional<Boolean> doorState = deviceState.getDoorState();
+        Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+        Optional<Integer> batteryLevel = deviceState.getBatteryLevel();
+
+        // then:
+        assertFalse(status.isPresent());
+        assertFalse(statusRaw.isPresent());
+        assertFalse(stateType.isPresent());
+        assertFalse(selectedProgram.isPresent());
+        assertFalse(selectedProgramId.isPresent());
+        assertFalse(programPhase.isPresent());
+        assertFalse(programPhaseRaw.isPresent());
+        assertFalse(dryingTarget.isPresent());
+        assertFalse(dryingTargetRaw.isPresent());
+        assertFalse(hasPreHeatFinished.isPresent());
+        assertFalse(targetTemperature.isPresent());
+        assertFalse(temperature.isPresent());
+        assertFalse(remoteControlEnabled.isPresent());
+        assertFalse(progress.isPresent());
+        assertFalse(ventilationStep.isPresent());
+        assertFalse(ventilationStepRaw.isPresent());
+        assertFalse(plateStepCount.isPresent());
+        assertFalse(plateStep.isPresent());
+        assertFalse(plateStepRaw.isPresent());
+        assertFalse(hasError);
+        assertFalse(doorState.isPresent());
+        assertFalse(doorAlarm.isPresent());
+        assertFalse(batteryLevel.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenStatusIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> status = deviceState.getStatus();
+        Optional<Integer> statusRaw = deviceState.getStatusRaw();
+        Optional<StateType> stateType = deviceState.getStateType();
+
+        // then:
+        assertFalse(status.isPresent());
+        assertFalse(statusRaw.isPresent());
+        assertFalse(stateType.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenStatusValuesAreEmpty() {
+        // given:
+        Status statusMock = mock(Status.class);
+        when(statusMock.getValueLocalized()).thenReturn(Optional.empty());
+        when(statusMock.getValueRaw()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(statusMock));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> status = deviceState.getStatus();
+        Optional<Integer> statusRaw = deviceState.getStatusRaw();
+        Optional<StateType> stateType = deviceState.getStateType();
+
+        // then:
+        assertFalse(status.isPresent());
+        assertFalse(statusRaw.isPresent());
+        assertFalse(stateType.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenStatusValueLocalizedIsNotNull() {
+        // given:
+        Status statusMock = mock(Status.class);
+        when(statusMock.getValueLocalized()).thenReturn(Optional.of("Not connected"));
+        when(statusMock.getValueRaw()).thenReturn(Optional.of(StateType.NOT_CONNECTED.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(statusMock));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String status = deviceState.getStatus().get();
+        int statusRaw = deviceState.getStatusRaw().get();
+        StateType stateType = deviceState.getStateType().get();
+
+        // then:
+        assertEquals("Not connected", status);
+        assertEquals(StateType.NOT_CONNECTED.getCode(), statusRaw);
+        assertEquals(StateType.NOT_CONNECTED, stateType);
+    }
+
+    @Test
+    public void testReturnValuesWhenStatusValueRawIsNotNull() {
+        // given:
+        Status statusMock = mock(Status.class);
+        when(statusMock.getValueRaw()).thenReturn(Optional.of(StateType.END_PROGRAMMED.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(statusMock));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        StateType stateType = deviceState.getStateType().get();
+
+        // then:
+        assertEquals(StateType.END_PROGRAMMED, stateType);
+    }
+
+    @Test
+    public void testReturnValuesWhenProgramTypeIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getProgramType()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> selectedProgram = deviceState.getSelectedProgram();
+        Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+
+        // then:
+        assertFalse(selectedProgram.isPresent());
+        assertFalse(selectedProgramId.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenProgramTypeValueLocalizedIsEmpty() {
+        // given:
+        ProgramType programType = mock(ProgramType.class);
+        when(programType.getValueLocalized()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getProgramType()).thenReturn(Optional.of(programType));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> selectedProgram = deviceState.getSelectedProgram();
+        Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+
+        // then:
+        assertFalse(selectedProgram.isPresent());
+        assertFalse(selectedProgramId.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenProgramTypeValueLocalizedIsNotNull() {
+        // given:
+        ProgramId programId = mock(ProgramId.class);
+        when(programId.getValueRaw()).thenReturn(Optional.of(3L));
+        when(programId.getValueLocalized()).thenReturn(Optional.of("Washing"));
+
+        State state = mock(State.class);
+        Status status = mock(Status.class);
+        Device device = mock(Device.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getProgramId()).thenReturn(Optional.of(programId));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String selectedProgram = deviceState.getSelectedProgram().get();
+        long selectedProgramId = deviceState.getSelectedProgramId().get();
+
+        // then:
+        assertEquals("Washing", selectedProgram);
+        assertEquals(3L, selectedProgramId);
+    }
+
+    @Test
+    public void testReturnValuesWhenProgramPhaseIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getProgramPhase()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> programPhase = deviceState.getProgramPhase();
+        Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+
+        // then:
+        assertFalse(programPhase.isPresent());
+        assertFalse(programPhaseRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenProgramPhaseValueLocalizedIsEmpty() {
+        // given:
+        ProgramPhase programPhaseMock = mock(ProgramPhase.class);
+        when(programPhaseMock.getValueLocalized()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getProgramPhase()).thenReturn(Optional.of(programPhaseMock));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> programPhase = deviceState.getProgramPhase();
+        Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+
+        // then:
+        assertFalse(programPhase.isPresent());
+        assertFalse(programPhaseRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenProgramPhaseValueLocalizedIsNotNull() {
+        // given:
+        ProgramPhase programPhaseMock = mock(ProgramPhase.class);
+        when(programPhaseMock.getValueLocalized()).thenReturn(Optional.of("Spülen"));
+        when(programPhaseMock.getValueRaw()).thenReturn(Optional.of(4));
+
+        State state = mock(State.class);
+        Status status = mock(Status.class);
+        Device device = mock(Device.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getProgramPhase()).thenReturn(Optional.of(programPhaseMock));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String programPhase = deviceState.getProgramPhase().get();
+        int programPhaseRaw = deviceState.getProgramPhaseRaw().get();
+
+        // then:
+        assertEquals("Spülen", programPhase);
+        assertEquals(4, programPhaseRaw);
+    }
+
+    @Test
+    public void testReturnValuesWhenDryingStepIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getDryingStep()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> dryingTarget = deviceState.getDryingTarget();
+        Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+
+        // then:
+        assertFalse(dryingTarget.isPresent());
+        assertFalse(dryingTargetRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenDryingStepValueLocalizedIsEmpty() {
+        // given:
+        DryingStep dryingStep = mock(DryingStep.class);
+        when(dryingStep.getValueLocalized()).thenReturn(Optional.empty());
+        when(dryingStep.getValueRaw()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getDryingStep()).thenReturn(Optional.of(dryingStep));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> dryingTarget = deviceState.getDryingTarget();
+        Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+
+        // then:
+        assertFalse(dryingTarget.isPresent());
+        assertFalse(dryingTargetRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenDryingStepValueLocalizedIsNotNull() {
+        // given:
+        DryingStep dryingStep = mock(DryingStep.class);
+        when(dryingStep.getValueLocalized()).thenReturn(Optional.of("Hot"));
+        when(dryingStep.getValueRaw()).thenReturn(Optional.of(5));
+
+        State state = mock(State.class);
+        Device device = mock(Device.class);
+        Status status = mock(Status.class);
+        when(state.getDryingStep()).thenReturn(Optional.of(dryingStep));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String dryingTarget = deviceState.getDryingTarget().get();
+        int dryingTargetRaw = deviceState.getDryingTargetRaw().get();
+
+        // then:
+        assertEquals("Hot", dryingTarget);
+        assertEquals(5, dryingTargetRaw);
+    }
+
+    @Test
+    public void testReturnValuesPreHeatFinishedWhenStateIsNotRunning() {
+        // given:
+        Temperature targetTemperature = mock(Temperature.class);
+        when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(0));
+        Temperature currentTemperature = mock(Temperature.class);
+        when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(0));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+        when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+        // then:
+        assertFalse(hasPreHeatFinished.get());
+    }
+
+    @Test
+    public void testReturnValuesPreHeatFinishedWhenTargetTemperatureIsEmpty() {
+        // given:
+        Temperature targetTemperature = mock(Temperature.class);
+        when(targetTemperature.getValueLocalized()).thenReturn(Optional.empty());
+        Temperature currentTemperature = mock(Temperature.class);
+        when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+        when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+        // then:
+        assertFalse(hasPreHeatFinished.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesPreHeatFinishedWhenCurrentTemperatureIsEmpty() {
+        // given:
+        Temperature targetTemperature = mock(Temperature.class);
+        when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+        Temperature currentTemperature = mock(Temperature.class);
+        when(currentTemperature.getValueLocalized()).thenReturn(Optional.empty());
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+        when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+        // then:
+        assertFalse(hasPreHeatFinished.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesPreHeatFinishedWhenPreHeatingHasFinished() {
+        // given:
+        Temperature targetTemperature = mock(Temperature.class);
+        when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+        Temperature currentTemperature = mock(Temperature.class);
+        when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+        when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+        // then:
+        assertTrue(hasPreHeatFinished.get());
+    }
+
+    @Test
+    public void testReturnValuesPreHeatFinishedWhenPreHeatingHasNotFinished() {
+        // given:
+        Temperature targetTemperature = mock(Temperature.class);
+        when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+        Temperature currentTemperature = mock(Temperature.class);
+        when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(179));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+        when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+        // then:
+        assertFalse(hasPreHeatFinished.get());
+    }
+
+    @Test
+    public void testReturnValuesWhenTargetTemperatureIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(Collections.emptyList());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+
+        // then:
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTargetTemperatureIndexIsOutOfRange() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(new LinkedList<>());
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+
+        // then:
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTargetTemperatureValueLocalizedIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        Temperature temperature = mock(Temperature.class);
+        when(temperature.getValueLocalized()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getTargetTemperature()).thenReturn(Arrays.asList(temperature));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+
+        // then:
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTargetTemperatureValueLocalizedIsValid() {
+        // given:
+        Temperature temperature = mock(Temperature.class);
+        when(temperature.getValueLocalized()).thenReturn(Optional.of(20));
+
+        State state = mock(State.class);
+        Status status = mock(Status.class);
+        Device device = mock(Device.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getTargetTemperature()).thenReturn(Arrays.asList(temperature));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Integer targetTemperature = deviceState.getTargetTemperature(0).get();
+
+        // then:
+        assertEquals(Integer.valueOf(20), targetTemperature);
+    }
+
+    @Test
+    public void testReturnValuesWhenTemperatureIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTemperature()).thenReturn(Collections.emptyList());
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> temperature = deviceState.getTemperature(0);
+
+        // then:
+        assertFalse(temperature.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenVentilationStepIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getVentilationStep()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> ventilationStep = deviceState.getVentilationStep();
+        Optional<Integer> ventilationStepRaw = deviceState.getVentilationStepRaw();
+
+        // then:
+        assertFalse(ventilationStep.isPresent());
+        assertFalse(ventilationStepRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTemperatureIndexIsOutOfRange() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getTemperature()).thenReturn(new LinkedList<>());
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> temperature = deviceState.getTemperature(-1);
+
+        // then:
+        assertFalse(temperature.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTemperatureValueLocalizedIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        Temperature temperatureMock = mock(Temperature.class);
+        when(temperatureMock.getValueLocalized()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getTemperature()).thenReturn(Arrays.asList(temperatureMock));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> temperature = deviceState.getTemperature(0);
+
+        // then:
+        assertFalse(temperature.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTemperatureValueLocalizedIsValid() {
+        // given:
+        Temperature temperatureMock = mock(Temperature.class);
+        when(temperatureMock.getValueLocalized()).thenReturn(Optional.of(10));
+
+        State state = mock(State.class);
+        Device device = mock(Device.class);
+        Status status = mock(Status.class);
+        when(state.getTemperature()).thenReturn(Arrays.asList(temperatureMock));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Integer temperature = deviceState.getTemperature(0).get();
+
+        // then:
+        assertEquals(Integer.valueOf(10), temperature);
+    }
+
+    @Test
+    public void testReturnValuesWhenPlatStepIndexIsOutOfRange() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getPlateStep()).thenReturn(Collections.emptyList());
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        int plateStepCount = deviceState.getPlateStepCount().get();
+        Optional<String> plateStep = deviceState.getPlateStep(0);
+        Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+
+        // then:
+        assertEquals(0, plateStepCount);
+        assertFalse(plateStep.isPresent());
+        assertFalse(plateStepRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenPlateStepValueIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        PlateStep plateStepMock = mock(PlateStep.class);
+        when(plateStepMock.getValueRaw()).thenReturn(Optional.empty());
+        when(plateStepMock.getValueLocalized()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getPlateStep()).thenReturn(Collections.singletonList(plateStepMock));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        int plateStepCount = deviceState.getPlateStepCount().get();
+        Optional<String> plateStep = deviceState.getPlateStep(0);
+        Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+
+        // then:
+        assertEquals(1, plateStepCount);
+        assertFalse(plateStep.isPresent());
+        assertFalse(plateStepRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenPlateStepValueIsValid() {
+        // given:
+        PlateStep plateStepMock = mock(PlateStep.class);
+        when(plateStepMock.getValueRaw()).thenReturn(Optional.of(2));
+        when(plateStepMock.getValueLocalized()).thenReturn(Optional.of("1."));
+
+        State state = mock(State.class);
+        Status status = mock(Status.class);
+        Device device = mock(Device.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getPlateStep()).thenReturn(Arrays.asList(plateStepMock));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        int plateStepCount = deviceState.getPlateStepCount().get();
+        String plateStep = deviceState.getPlateStep(0).get();
+        int plateStepRaw = deviceState.getPlateStepRaw(0).get();
+
+        // then:
+        assertEquals(1, plateStepCount);
+        assertEquals("1.", plateStep);
+        assertEquals(2, plateStepRaw);
+    }
+
+    @Test
+    public void testReturnValuesWhenRemainingTimeIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getRemainingTime()).thenReturn(Optional.empty());
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> progress = deviceState.getProgress();
+
+        // then:
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenRemainingTimeSizeIsNotTwo() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(2)));
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> progress = deviceState.getProgress();
+
+        // then:
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenRemoteEnableIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getRemoteEnable()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+
+        // then:
+        assertFalse(remoteControlEnabled.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenFullRemoteControlIsEmpty() {
+        // given:
+        RemoteEnable remoteEnable = mock(RemoteEnable.class);
+        when(remoteEnable.getFullRemoteControl()).thenReturn(Optional.empty());
+
+        State state = mock(State.class);
+        when(state.getRemoteEnable()).thenReturn(Optional.of(remoteEnable));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+
+        // then:
+        assertFalse(remoteControlEnabled.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenFullRemoteControlIsNotNull() {
+        // given:
+        RemoteEnable remoteEnable = mock(RemoteEnable.class);
+        when(remoteEnable.getFullRemoteControl()).thenReturn(Optional.of(true));
+
+        State state = mock(State.class);
+        when(state.getRemoteEnable()).thenReturn(Optional.of(remoteEnable));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Boolean remoteControlEnabled = deviceState.isRemoteControlEnabled().get();
+
+        // then:
+        assertTrue(remoteControlEnabled);
+    }
+
+    @Test
+    public void testReturnValuesWhenElapsedTimeIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getElapsedTime()).thenReturn(Optional.empty());
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> progress = deviceState.getProgress();
+
+        // then:
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenElapsedTimeSizeIsNotTwo() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0)));
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> progress = deviceState.getProgress();
+
+        // then:
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenElapsedTimeAndRemainingTimeIsZero() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> progress = deviceState.getProgress();
+
+        // then:
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void whenElapsedTimeIsNotPresentThenEmptyIsReturned() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getElapsedTime()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+        // then:
+        assertFalse(elapsedTime.isPresent());
+    }
+
+    @Test
+    public void whenElapsedTimeIsAnEmptyListThenEmptyIsReturned() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getElapsedTime()).thenReturn(Optional.of(Collections.emptyList()));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+        // then:
+        assertFalse(elapsedTime.isPresent());
+    }
+
+    @Test
+    public void whenElapsedTimeHasOnlyOneElementThenEmptyIsReturned() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getElapsedTime()).thenReturn(Optional.of(Collections.singletonList(2)));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+        // then:
+        assertFalse(elapsedTime.isPresent());
+    }
+
+    @Test
+    public void whenElapsedTimeHasThreeElementsThenEmptyIsReturned() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(1, 2, 3)));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+        // then:
+        assertFalse(elapsedTime.isPresent());
+    }
+
+    @Test
+    public void whenElapsedTimeHasTwoElementsThenTheTotalNumberOfSecondsIsReturned() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(1, 2)));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+        // then:
+        assertTrue(elapsedTime.isPresent());
+        assertEquals(Integer.valueOf((60 + 2) * 60), elapsedTime.get());
+    }
+
+    @Test
+    public void whenDeviceIsInOffStateThenElapsedTimeIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(1, 2)));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+        // then:
+        assertFalse(elapsedTime.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenProgressIs50Percent() {
+        // given:
+        State state = mock(State.class);
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+
+        Device device = mock(Device.class);
+        Status status = mock(Status.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Integer progress = deviceState.getProgress().get();
+
+        // then:
+        assertEquals(Integer.valueOf(50), progress);
+    }
+
+    @Test
+    public void testReturnValuesWhenProgressIs25Percent() {
+        // given:
+        State state = mock(State.class);
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 15)));
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+
+        Device device = mock(Device.class);
+        Status status = mock(Status.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Integer progress = deviceState.getProgress().get();
+
+        // then:
+        assertEquals(Integer.valueOf(25), progress);
+    }
+
+    @Test
+    public void testReturnValuesWhenProgressIs0Percent() {
+        // given:
+        State state = mock(State.class);
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+
+        Device device = mock(Device.class);
+        Status status = mock(Status.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Integer progress = deviceState.getProgress().get();
+
+        // then:
+        assertEquals(Integer.valueOf(0), progress);
+    }
+
+    @Test
+    public void testReturnValuesWhenSignalDoorIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalDoor()).thenReturn(Optional.empty());
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> doorState = deviceState.getDoorState();
+        Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+        // then:
+        assertFalse(doorState.isPresent());
+        assertFalse(doorAlarm.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenSignalDoorIsTrue() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalDoor()).thenReturn(Optional.of(true));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> doorState = deviceState.getDoorState();
+
+        // then:
+        assertTrue(doorState.get());
+    }
+
+    @Test
+    public void testReturnValuesWhenSignalDoorIsFalse() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalDoor()).thenReturn(Optional.of(false));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> doorState = deviceState.getDoorState();
+
+        // then:
+        assertFalse(doorState.get());
+    }
+
+    @Test
+    public void testReturnValuesWhenSignalFailureIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalDoor()).thenReturn(Optional.of(true));
+        when(state.getSignalFailure()).thenReturn(Optional.empty());
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+        // then:
+        assertFalse(doorAlarm.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenDoorAlarmIsActive() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalDoor()).thenReturn(Optional.of(true));
+        when(state.getSignalFailure()).thenReturn(Optional.of(true));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+        // then:
+        assertTrue(doorAlarm.get());
+    }
+
+    @Test
+    public void testReturnValuesWhenDoorAlarmIsNotActiveBecauseOfNoDoorSignal() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalDoor()).thenReturn(Optional.of(false));
+        when(state.getSignalFailure()).thenReturn(Optional.of(true));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+        // then:
+        assertFalse(doorAlarm.get());
+    }
+
+    @Test
+    public void testReturnValuesWhenDoorAlarmIsNotActiveBecauseOfNoFailureSignal() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalDoor()).thenReturn(Optional.of(true));
+        when(state.getSignalFailure()).thenReturn(Optional.of(false));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+        // then:
+        assertFalse(doorAlarm.get());
+    }
+
+    @Test
+    public void testReturnValuesWhenIdentIsEmpty() {
+        // given:
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.empty());
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> type = deviceState.getType();
+        DeviceType rawType = deviceState.getRawType();
+        Optional<String> deviceName = deviceState.getDeviceName();
+        Optional<String> fabNumber = deviceState.getFabNumber();
+        Optional<String> techType = deviceState.getTechType();
+
+        // then:
+        assertFalse(type.isPresent());
+        assertEquals(DeviceType.UNKNOWN, rawType);
+        assertFalse(deviceName.isPresent());
+        assertFalse(fabNumber.isPresent());
+        assertFalse(techType.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTypeIsEmpty() {
+        // given:
+        Ident ident = mock(Ident.class);
+        when(ident.getType()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> type = deviceState.getType();
+        DeviceType rawType = deviceState.getRawType();
+
+        // then:
+        assertFalse(type.isPresent());
+        assertEquals(DeviceType.UNKNOWN, rawType);
+    }
+
+    @Test
+    public void testReturnValuesWhenTypeValueLocalizedIsEmpty() {
+        // given:
+        Type typeMock = mock(Type.class);
+        when(typeMock.getValueLocalized()).thenReturn(Optional.empty());
+
+        Ident ident = mock(Ident.class);
+        when(ident.getType()).thenReturn(Optional.of(typeMock));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> type = deviceState.getType();
+
+        // then:
+        assertFalse(type.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenTypeValueLocalizedIsNotNull() {
+        // given:
+        Type typeMock = mock(Type.class);
+        when(typeMock.getValueLocalized()).thenReturn(Optional.of("Hood"));
+
+        Ident ident = mock(Ident.class);
+        when(ident.getType()).thenReturn(Optional.of(typeMock));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String type = deviceState.getType().get();
+
+        // then:
+        assertEquals("Hood", type);
+    }
+
+    @Test
+    public void testReturnValuesWhenTypeValueRawIsNotNull() {
+        // given:
+        Type typeMock = mock(Type.class);
+        when(typeMock.getValueRaw()).thenReturn(DeviceType.COFFEE_SYSTEM);
+
+        Ident ident = mock(Ident.class);
+        when(ident.getType()).thenReturn(Optional.of(typeMock));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        DeviceType rawType = deviceState.getRawType();
+
+        // then:
+        assertEquals(DeviceType.COFFEE_SYSTEM, rawType);
+    }
+
+    @Test
+    public void testReturnValuesWhenDeviceNameIsEmpty() {
+        // given:
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceName()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> deviceName = deviceState.getDeviceName();
+
+        // then:
+        assertFalse(deviceName.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenDeviceNameIsEmptyString() {
+        // given:
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceName()).thenReturn(Optional.of(""));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> deviceName = deviceState.getDeviceName();
+
+        // then:
+        assertFalse(deviceName.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenDeviceNameIsValid() {
+        // given:
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceName()).thenReturn(Optional.of("MyWashingMachine"));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> deviceName = deviceState.getDeviceName();
+
+        // then:
+        assertEquals(Optional.of("MyWashingMachine"), deviceName);
+    }
+
+    @Test
+    public void testReturnValuesWhenFabNumberIsNotNull() {
+        // given:
+        DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+        when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of("000061431659"));
+
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String fabNumber = deviceState.getFabNumber().get();
+
+        // then:
+        assertEquals("000061431659", fabNumber);
+    }
+
+    @Test
+    public void testReturnValuesWhenTechTypeIsNotNull() {
+        // given:
+        DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+        when(deviceIdentLabel.getTechType()).thenReturn(Optional.of("XKM3100WEC"));
+
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String techType = deviceState.getTechType().get();
+
+        // then:
+        assertEquals("XKM3100WEC", techType);
+    }
+
+    @Test
+    public void whenDeviceIsInFailureStateThenItHasAnError() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.FAILURE.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        boolean hasError = deviceState.hasError();
+
+        // then:
+        assertTrue(hasError);
+    }
+
+    @Test
+    public void whenDeviceIsInRunningStateAndDoesNotSignalAFailureThenItHasNoError() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        boolean hasError = deviceState.hasError();
+
+        // then:
+        assertFalse(hasError);
+    }
+
+    @Test
+    public void whenDeviceSignalsAFailureThenItHasAnError() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getSignalFailure()).thenReturn(Optional.of(true));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        boolean hasError = deviceState.hasError();
+
+        // then:
+        assertTrue(hasError);
+    }
+
+    @Test
+    public void testReturnValuesForHasInfoWhenSignalInfoIsEmpty() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalInfo()).thenReturn(Optional.empty());
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        boolean hasInfo = deviceState.hasInfo();
+
+        // then:
+        assertFalse(hasInfo);
+    }
+
+    @Test
+    public void whenDeviceSignalsAnInfoThenItHasAnInfo() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalInfo()).thenReturn(Optional.of(true));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        boolean hasInfo = deviceState.hasInfo();
+
+        // then:
+        assertTrue(hasInfo);
+    }
+
+    @Test
+    public void whenDeviceSignalsNoInfoThenItHasNoInfo() {
+        // given:
+        State state = mock(State.class);
+        when(state.getSignalInfo()).thenReturn(Optional.of(false));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        boolean hasInfo = deviceState.hasInfo();
+
+        // then:
+        assertFalse(hasInfo);
+    }
+
+    @Test
+    public void testReturnValuesForVentilationStep() {
+        // given:
+        VentilationStep ventilationStepMock = mock(VentilationStep.class);
+        when(ventilationStepMock.getValueLocalized()).thenReturn(Optional.of("Step 1"));
+        when(ventilationStepMock.getValueRaw()).thenReturn(Optional.of(1));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getVentilationStep()).thenReturn(Optional.of(ventilationStepMock));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String ventilationStep = deviceState.getVentilationStep().get();
+        int ventilationStepRaw = deviceState.getVentilationStepRaw().get();
+
+        // then:
+        assertEquals("Step 1", ventilationStep);
+        assertEquals(1, ventilationStepRaw);
+    }
+
+    @Test
+    public void testProgramPhaseWhenDeviceIsInOffState() {
+        // given:
+        ProgramPhase programPhase = mock(ProgramPhase.class);
+        when(programPhase.getValueLocalized()).thenReturn(Optional.of("Washing"));
+        when(programPhase.getValueRaw()).thenReturn(Optional.of(3));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+        State state = mock(State.class);
+        when(state.getProgramPhase()).thenReturn(Optional.of(programPhase));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> phase = deviceState.getProgramPhase();
+        Optional<Integer> phaseRaw = deviceState.getProgramPhaseRaw();
+
+        // then:
+        assertFalse(phase.isPresent());
+        assertFalse(phaseRaw.isPresent());
+    }
+
+    @Test
+    public void testDryingTargetWhenDeviceIsInOffState() {
+        // given:
+        DryingStep dryingStep = mock(DryingStep.class);
+        when(dryingStep.getValueLocalized()).thenReturn(Optional.of("Schranktrocken"));
+        when(dryingStep.getValueRaw()).thenReturn(Optional.of(3));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+        State state = mock(State.class);
+        when(state.getDryingStep()).thenReturn(Optional.of(dryingStep));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> dryingTarget = deviceState.getDryingTarget();
+        Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+
+        // then:
+        assertFalse(dryingTarget.isPresent());
+        assertFalse(dryingTargetRaw.isPresent());
+    }
+
+    @Test
+    public void testVentilationStepWhenDeviceIsInOffState() {
+        // given:
+        VentilationStep ventilationStep = mock(VentilationStep.class);
+        when(ventilationStep.getValueLocalized()).thenReturn(Optional.of("Stufe 1"));
+        when(ventilationStep.getValueRaw()).thenReturn(Optional.of(1));
+
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+        State state = mock(State.class);
+        when(state.getVentilationStep()).thenReturn(Optional.of(ventilationStep));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> step = deviceState.getVentilationStep();
+        Optional<Integer> stepRaw = deviceState.getVentilationStepRaw();
+
+        // then:
+        assertFalse(step.isPresent());
+        assertFalse(stepRaw.isPresent());
+    }
+
+    @Test
+    public void testReturnValuesWhenDeviceIsInOffState() {
+        // given:
+        Device device = mock(Device.class);
+        State state = mock(State.class);
+        Status status = mock(Status.class);
+
+        when(device.getState()).thenReturn(Optional.of(state));
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // Test SelectedProgram:
+        ProgramId programId = mock(ProgramId.class);
+        when(state.getProgramId()).thenReturn(Optional.of(programId));
+        when(programId.getValueLocalized()).thenReturn(Optional.of("Washing"));
+        // when:
+        Optional<String> selectedProgram = deviceState.getSelectedProgram();
+        // then:
+        assertFalse(selectedProgram.isPresent());
+
+        // Test TargetTemperature:
+        Temperature targetTemperatureMock = mock(Temperature.class);
+        when(state.getTargetTemperature()).thenReturn(Collections.singletonList(targetTemperatureMock));
+        when(targetTemperatureMock.getValueLocalized()).thenReturn(Optional.of(200));
+        // when:
+        Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+        // then:
+        assertFalse(targetTemperature.isPresent());
+
+        // Test Temperature:
+        Temperature temperature = mock(Temperature.class);
+        when(state.getTemperature()).thenReturn(Collections.singletonList(temperature));
+        when(temperature.getValueLocalized()).thenReturn(Optional.of(200));
+        // when:
+        Optional<Integer> t = deviceState.getTemperature(0);
+        // then:
+        assertFalse(t.isPresent());
+
+        // Test Progress:
+        when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 5)));
+        when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(1, 5)));
+        // when:
+        Optional<Integer> progress = deviceState.getProgress();
+        // then:
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testWhenDeviceIsInOffStateThenGetSpinningSpeedReturnsNull() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+        SpinningSpeed spinningSpeed = mock(SpinningSpeed.class);
+        when(spinningSpeed.getValueRaw()).thenReturn(Optional.of(800));
+        when(spinningSpeed.getValueLocalized()).thenReturn(Optional.of("800"));
+        when(spinningSpeed.getUnit()).thenReturn(Optional.of("rpm"));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getSpinningSpeed()).thenReturn(Optional.of(spinningSpeed));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> speed = deviceState.getSpinningSpeed();
+        Optional<Integer> speedRaw = deviceState.getSpinningSpeedRaw();
+
+        // then:
+        assertFalse(speed.isPresent());
+        assertFalse(speedRaw.isPresent());
+    }
+
+    @Test
+    public void testGetSpinningSpeedReturnsNullWhenSpinningSpeedIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getSpinningSpeed()).thenReturn(Optional.empty());
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> spinningSpeed = deviceState.getSpinningSpeed();
+        Optional<Integer> spinningSpeedRaw = deviceState.getSpinningSpeedRaw();
+
+        // then:
+        assertFalse(spinningSpeed.isPresent());
+        assertFalse(spinningSpeedRaw.isPresent());
+    }
+
+    @Test
+    public void testGetSpinningSpeedReturnsNullWhenSpinningSpeedRawValueIsEmpty() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        SpinningSpeed spinningSpeedMock = mock(SpinningSpeed.class);
+        when(spinningSpeedMock.getValueRaw()).thenReturn(Optional.empty());
+        when(spinningSpeedMock.getValueLocalized()).thenReturn(Optional.of("1200"));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getSpinningSpeed()).thenReturn(Optional.of(spinningSpeedMock));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<String> spinningSpeed = deviceState.getSpinningSpeed();
+        Optional<Integer> spinningSpeedRaw = deviceState.getSpinningSpeedRaw();
+
+        // then:
+        assertFalse(spinningSpeed.isPresent());
+        assertFalse(spinningSpeedRaw.isPresent());
+    }
+
+    @Test
+    public void testGetSpinningSpeedReturnsValidValueWhenSpinningSpeedRawValueIsNotNull() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        SpinningSpeed spinningSpeedMock = mock(SpinningSpeed.class);
+        when(spinningSpeedMock.getValueRaw()).thenReturn(Optional.of(1200));
+        when(spinningSpeedMock.getValueLocalized()).thenReturn(Optional.of("1200"));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getSpinningSpeed()).thenReturn(Optional.of(spinningSpeedMock));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        String spinningSpeed = deviceState.getSpinningSpeed().get();
+        int spinningSpeedRaw = deviceState.getSpinningSpeedRaw().get();
+
+        // then:
+        assertEquals("1200", spinningSpeed);
+        assertEquals(1200, spinningSpeedRaw);
+    }
+
+    @Test
+    public void testGetLightStateWhenDeviceIsOff() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> lightState = deviceState.getLightState();
+
+        // then:
+        assertFalse(lightState.isPresent());
+    }
+
+    @Test
+    public void testGetLightStateWhenLightIsUnknown() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getLight()).thenReturn(Light.UNKNOWN);
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> lightState = deviceState.getLightState();
+
+        // then:
+        assertFalse(lightState.isPresent());
+    }
+
+    @Test
+    public void testGetLightStateWhenLightIsEnabled() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getLight()).thenReturn(Light.ENABLE);
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Boolean lightState = deviceState.getLightState().get();
+
+        // then:
+        assertEquals(Boolean.valueOf(true), lightState);
+    }
+
+    @Test
+    public void testGetLightStateWhenLightIsDisabled() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getLight()).thenReturn(Light.DISABLE);
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Boolean lightState = deviceState.getLightState().get();
+
+        // then:
+        assertEquals(Boolean.valueOf(false), lightState);
+    }
+
+    @Test
+    public void testGetLightStateWhenLightIsNotSupported() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getLight()).thenReturn(Light.NOT_SUPPORTED);
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Optional<Boolean> lightState = deviceState.getLightState();
+
+        // then:
+        assertFalse(lightState.isPresent());
+    }
+
+    @Test
+    public void testGetBatteryLevel() {
+        // given:
+        Status status = mock(Status.class);
+        when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+        State state = mock(State.class);
+        when(state.getStatus()).thenReturn(Optional.of(status));
+        when(state.getBatteryLevel()).thenReturn(Optional.of(4));
+
+        Device device = mock(Device.class);
+        when(device.getState()).thenReturn(Optional.of(state));
+
+        DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+        // when:
+        Integer batteryLevel = deviceState.getBatteryLevel().get();
+
+        // then:
+        assertEquals(Integer.valueOf(4), batteryLevel);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/TransitionStateTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/TransitionStateTest.java
new file mode 100644 (file)
index 0000000..eae90b4
--- /dev/null
@@ -0,0 +1,576 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TransitionStateTest {
+    private final DeviceState historic = mock(DeviceState.class);
+    private final DeviceState previous = mock(DeviceState.class);
+    private final DeviceState next = mock(DeviceState.class);
+
+    @Test
+    public void testHasFinishedChangedReturnsTrueWhenPreviousStateIsNull() {
+        // given:
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+        TransitionState transitionState = new TransitionState(null, next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertTrue(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsTrueWhenPreviousStateIsUnknown() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.empty());
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertTrue(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsFalseWhenNoStateTransitionOccurred() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertFalse(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsTrueWhenStateChangedFromRunningToEndProgrammed() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertTrue(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsTrueWhenStateChangedFromRunningToProgrammed() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertTrue(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsFalseWhenStateChangedFromRunningToPause() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertFalse(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsTrueWhenStateChangedFromProgrammedWaitingToStartToRunning() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertTrue(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsFalseWhenStateRemainsProgrammedWaitingToStart() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertFalse(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsFalseWhenStateChangedFromPauseToRunning() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertFalse(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsTrueWhenStateChangedFromEndProgrammedToOff() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.OFF));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertTrue(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsFalseWhenStateChangedFromRunningToFailure() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertFalse(hasFinishedChanged);
+    }
+
+    @Test
+    public void testHasFinishedChangedReturnsFalseWhenStateChangedFromPauseToFailure() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+        // then:
+        assertFalse(hasFinishedChanged);
+    }
+
+    @Test
+    public void testIsFinishedReturnsTrueWhenStateChangedFromRunningToEndProgrammed() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Boolean isFinished = transitionState.isFinished().get();
+
+        // then:
+        assertTrue(isFinished);
+    }
+
+    @Test
+    public void testIsFinishedReturnsTrueWhenStateChangedFromRunningToProgrammed() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Boolean isFinished = transitionState.isFinished().get();
+
+        // then:
+        assertTrue(isFinished);
+    }
+
+    @Test
+    public void testIsFinishedReturnsFalseWhenStateChangedFromProgrammedWaitingToStartToRunning() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Boolean isFinished = transitionState.isFinished().get();
+
+        // then:
+        assertFalse(isFinished);
+    }
+
+    @Test
+    public void testIsFinishedReturnsFalseWhenStateChangedFromRunningToFailure() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Boolean isFinished = transitionState.isFinished().get();
+
+        // then:
+        assertFalse(isFinished);
+    }
+
+    @Test
+    public void testIsFinishedReturnsFalseWhenStateChangedFromPauseToFailure() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Boolean isFinished = transitionState.isFinished().get();
+
+        // then:
+        assertFalse(isFinished);
+    }
+
+    @Test
+    public void testIsFinishedReturnsTrueWhenStateChangedFromEndProgrammedToOff() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.OFF));
+        when(next.isInState(any())).thenCallRealMethod();
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Boolean isFinished = transitionState.isFinished().get();
+
+        // then:
+        assertFalse(isFinished);
+    }
+
+    @Test
+    public void testIsFinishedReturnsNullWhenPreviousStateIsNull() {
+        // given:
+        when(next.getStateType()).thenReturn(Optional.of(StateType.IDLE));
+
+        TransitionState transitionState = new TransitionState(null, next);
+
+        // when:
+        Optional<Boolean> isFinished = transitionState.isFinished();
+
+        // then:
+        assertFalse(isFinished.isPresent());
+    }
+
+    @Test
+    public void testIsFinishedReturnsNullWhenPreviousStateIsUnknown() {
+        // given:
+        when(previous.getStateType()).thenReturn(Optional.empty());
+        when(next.getStateType()).thenReturn(Optional.of(StateType.IDLE));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Optional<Boolean> isFinished = transitionState.isFinished();
+
+        // then:
+        assertFalse(isFinished.isPresent());
+    }
+
+    @Test
+    public void testProgramStartedWithZeroRemainingTimeShowsNoRemainingTimeAndProgress() {
+        // given:
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(0));
+        when(next.getProgress()).thenReturn(Optional.of(100));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        Optional<Integer> remainingTime = transitionState.getRemainingTime();
+        Optional<Integer> progress = transitionState.getProgress();
+
+        // then:
+        assertFalse(remainingTime.isPresent());
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testProgramStartetdWithRemainingTimeShowsRemainingTimeAndProgress() {
+        // given:
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(2));
+        when(next.getProgress()).thenReturn(Optional.of(50));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        int remainingTime = transitionState.getRemainingTime().get();
+        int progress = transitionState.getProgress().get();
+
+        // then:
+        assertEquals(2, remainingTime);
+        assertEquals(50, progress);
+    }
+
+    @Test
+    public void testProgramCountingDownRemainingTimeToZeroShowsRemainingTimeAndProgress() {
+        // given:
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(previous.getRemainingTime()).thenReturn(Optional.of(1));
+
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(0));
+        when(next.getProgress()).thenReturn(Optional.of(100));
+
+        TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+        // when:
+        int remainingTime = transitionState.getRemainingTime().get();
+        int progress = transitionState.getProgress().get();
+
+        // then:
+        assertEquals(0, remainingTime);
+        assertEquals(100, progress);
+    }
+
+    @Test
+    public void testDevicePairedWhileRunningWithZeroRemainingTimeShowsNoRemainingTimeAndProgress() {
+        // given:
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(0));
+        when(next.getProgress()).thenReturn(Optional.of(100));
+
+        TransitionState transitionState = new TransitionState(null, next);
+
+        // when:
+        Optional<Integer> remainingTime = transitionState.getRemainingTime();
+        Optional<Integer> progress = transitionState.getProgress();
+
+        // then:
+        assertFalse(remainingTime.isPresent());
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testDevicePairedWhileRunningWithRemainingTimeShowsRemainingTimeAndProgress() {
+        // given:
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(3));
+        when(next.getProgress()).thenReturn(Optional.of(80));
+
+        TransitionState transitionState = new TransitionState(null, next);
+
+        // when:
+        int remainingTime = transitionState.getRemainingTime().get();
+        int progress = transitionState.getProgress().get();
+
+        // then:
+        assertEquals(3, remainingTime);
+        assertEquals(80, progress);
+    }
+
+    @Test
+    public void testWhenNoRemainingTimeIsSetWhileProgramIsRunningThenNoRemainingTimeAndProgressIsShown() {
+        // given:
+        when(historic.isInState(any())).thenCallRealMethod();
+        when(historic.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(0));
+        when(next.getProgress()).thenReturn(Optional.of(100));
+
+        TransitionState transitionState = new TransitionState(
+                new TransitionState(new TransitionState(null, historic), previous), next);
+
+        // when:
+        Optional<Integer> remainingTime = transitionState.getRemainingTime();
+        Optional<Integer> progress = transitionState.getProgress();
+
+        // then:
+        assertFalse(remainingTime.isPresent());
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testRemainingTimeIsSetWhileRunningShowsRemainingTimeAndProgress() {
+        // given:
+        when(historic.isInState(any())).thenCallRealMethod();
+        when(historic.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(100));
+        when(next.getProgress()).thenReturn(Optional.of(10));
+
+        TransitionState transitionState = new TransitionState(
+                new TransitionState(new TransitionState(null, historic), previous), next);
+
+        // when:
+        int remainingTime = transitionState.getRemainingTime().get();
+        int progress = transitionState.getProgress().get();
+
+        // then:
+        assertEquals(100, remainingTime);
+        assertEquals(10, progress);
+    }
+
+    @Test
+    public void testPreviousProgramDoesNotAffectHandlingOfRemainingTimeAndProgressForNextProgramCase1() {
+        // given:
+        DeviceState beforeHistoric = mock(DeviceState.class);
+        when(beforeHistoric.isInState(any())).thenCallRealMethod();
+        when(beforeHistoric.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(beforeHistoric.getRemainingTime()).thenReturn(Optional.of(1));
+
+        when(historic.isInState(any())).thenCallRealMethod();
+        when(historic.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(historic.getRemainingTime()).thenReturn(Optional.of(0));
+
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+        when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(0));
+        when(next.getProgress()).thenReturn(Optional.of(100));
+
+        TransitionState transitionState = new TransitionState(
+                new TransitionState(new TransitionState(new TransitionState(null, beforeHistoric), historic), previous),
+                next);
+
+        // when:
+        Optional<Integer> remainingTime = transitionState.getRemainingTime();
+        Optional<Integer> progress = transitionState.getProgress();
+
+        // then:
+        assertFalse(remainingTime.isPresent());
+        assertFalse(progress.isPresent());
+    }
+
+    @Test
+    public void testPreviousProgramDoesNotAffectHandlingOfRemainingTimeAndProgressForNextProgramCase2() {
+        // given:
+        DeviceState beforeHistoric = mock(DeviceState.class);
+        when(beforeHistoric.isInState(any())).thenCallRealMethod();
+        when(beforeHistoric.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(beforeHistoric.getRemainingTime()).thenReturn(Optional.of(1));
+
+        when(historic.isInState(any())).thenCallRealMethod();
+        when(historic.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(historic.getRemainingTime()).thenReturn(Optional.of(0));
+
+        when(previous.isInState(any())).thenCallRealMethod();
+        when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+        when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+        when(next.isInState(any())).thenCallRealMethod();
+        when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(next.getRemainingTime()).thenReturn(Optional.of(10));
+        when(next.getProgress()).thenReturn(Optional.of(60));
+
+        TransitionState transitionState = new TransitionState(
+                new TransitionState(new TransitionState(new TransitionState(null, beforeHistoric), historic), previous),
+                next);
+
+        // when:
+        int remainingTime = transitionState.getRemainingTime().get();
+        int progress = transitionState.getProgress().get();
+
+        // then:
+        assertEquals(10, remainingTime);
+        assertEquals(60, progress);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/WineStorageDeviceTemperatureStateTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/WineStorageDeviceTemperatureStateTest.java
new file mode 100644 (file)
index 0000000..d4edcff
--- /dev/null
@@ -0,0 +1,455 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class WineStorageDeviceTemperatureStateTest {
+    private static final Integer TEMPERATURE_0 = 8;
+    private static final Integer TEMPERATURE_1 = 10;
+    private static final Integer TEMPERATURE_2 = 12;
+
+    private static final Integer TARGET_TEMPERATURE_0 = 5;
+    private static final Integer TARGET_TEMPERATURE_1 = 9;
+    private static final Integer TARGET_TEMPERATURE_2 = 11;
+
+    @Nullable
+    private DeviceState deviceState;
+
+    private DeviceState getDeviceState() {
+        assertNotNull(deviceState);
+        return Objects.requireNonNull(deviceState);
+    }
+
+    private void setUpDeviceStateMock(int numberOfTemperatures) {
+        deviceState = mock(DeviceState.class);
+        if (numberOfTemperatures > 0) {
+            when(getDeviceState().getTemperature(0)).thenReturn(Optional.of(TEMPERATURE_0));
+            when(getDeviceState().getTargetTemperature(0)).thenReturn(Optional.of(TARGET_TEMPERATURE_0));
+        } else {
+            when(getDeviceState().getTemperature(0)).thenReturn(Optional.empty());
+            when(getDeviceState().getTargetTemperature(0)).thenReturn(Optional.empty());
+        }
+        if (numberOfTemperatures > 1) {
+            when(getDeviceState().getTemperature(1)).thenReturn(Optional.of(TEMPERATURE_1));
+            when(getDeviceState().getTargetTemperature(1)).thenReturn(Optional.of(TARGET_TEMPERATURE_1));
+        } else {
+            when(getDeviceState().getTemperature(1)).thenReturn(Optional.empty());
+            when(getDeviceState().getTargetTemperature(1)).thenReturn(Optional.empty());
+        }
+        if (numberOfTemperatures > 2) {
+            when(getDeviceState().getTemperature(2)).thenReturn(Optional.of(TEMPERATURE_2));
+            when(getDeviceState().getTargetTemperature(2)).thenReturn(Optional.of(TARGET_TEMPERATURE_2));
+        } else {
+            when(getDeviceState().getTemperature(2)).thenReturn(Optional.empty());
+            when(getDeviceState().getTargetTemperature(2)).thenReturn(Optional.empty());
+        }
+    }
+
+    @Test
+    public void testGetTemperaturesForWineCabinetWithThreeCompartments() {
+        // given:
+        setUpDeviceStateMock(3);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTemperature();
+        Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetTemperaturesForWineCabinetWithTwoCompartments() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTemperature();
+        Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetTemperaturesForWineCabinetWithOneCompartment() {
+        // given:
+        setUpDeviceStateMock(1);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getTemperature().get();
+        Integer targetTemperature = state.getTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_0, temperature);
+        assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+    }
+
+    @Test
+    public void testGetTemperaturesForWineCabinetFreezerCombination() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTemperature();
+        Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetTemperaturesForOtherDeviceWithOneTemperature() {
+        // given:
+        setUpDeviceStateMock(1);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTemperature();
+        Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetTemperaturesWhenNoTemperaturesAreAvailable() {
+        // given:
+        setUpDeviceStateMock(0);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTemperature();
+        Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetTopTemperaturesForWineCabinetWithThreeCompartments() {
+        // given:
+        setUpDeviceStateMock(3);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getTopTemperature().get();
+        Integer targetTemperature = state.getTopTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_0, temperature);
+        assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+    }
+
+    @Test
+    public void testGetTopTemperaturesForWineCabinetWithTwoCompartments() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getTopTemperature().get();
+        Integer targetTemperature = state.getTopTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_0, temperature);
+        assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+    }
+
+    @Test
+    public void testGetTopTemperaturesForWineCabinetWithOneCompartment() {
+        // given:
+        setUpDeviceStateMock(1);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTopTemperature();
+        Optional<Integer> targetTemperature = state.getTopTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetTopTemperaturesForWineCabinetFreezerCombination() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getTopTemperature().get();
+        Integer targetTemperature = state.getTopTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_0, temperature);
+        assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+    }
+
+    @Test
+    public void testGetTopTemperaturesForOtherDeviceWithTwoTemperatures() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTopTemperature();
+        Optional<Integer> targetTemperature = state.getTopTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetTopTemperaturesWhenNoTemperaturesAreAvailable() {
+        // given:
+        setUpDeviceStateMock(0);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getTopTemperature();
+        Optional<Integer> targetTemperature = state.getTopTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetMiddleTemperaturesForWineCabinetWithThreeCompartments() {
+        // given:
+        setUpDeviceStateMock(3);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getMiddleTemperature().get();
+        Integer targetTemperature = state.getMiddleTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_1, temperature);
+        assertEquals(TARGET_TEMPERATURE_1, targetTemperature);
+    }
+
+    @Test
+    public void testGetMiddleTemperaturesForWineCabinetWithTwoCompartments() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getMiddleTemperature();
+        Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetMiddleTemperaturesForWineCabinetWithOneCompartment() {
+        // given:
+        setUpDeviceStateMock(1);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getMiddleTemperature();
+        Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetMiddleTemperaturesForWineCabinetFreezerCombination() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getMiddleTemperature();
+        Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetMiddleTemperaturesForOtherDeviceWithTwoTemperatures() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getMiddleTemperature();
+        Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetMiddleTemperaturesWhenNoTemperaturesAreAvailable() {
+        // given:
+        setUpDeviceStateMock(0);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getMiddleTemperature();
+        Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetBottomTemperaturesForWineCabinetWithThreeCompartments() {
+        // given:
+        setUpDeviceStateMock(3);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getBottomTemperature().get();
+        Integer targetTemperature = state.getBottomTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_2, temperature);
+        assertEquals(TARGET_TEMPERATURE_2, targetTemperature);
+    }
+
+    @Test
+    public void testGetBottomTemperaturesForWineCabinetWithTwoCompartments() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getBottomTemperature().get();
+        Integer targetTemperature = state.getBottomTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_1, temperature);
+        assertEquals(TARGET_TEMPERATURE_1, targetTemperature);
+    }
+
+    @Test
+    public void testGetBottomTemperaturesForWineCabinetWithOneCompartment() {
+        // given:
+        setUpDeviceStateMock(1);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getBottomTemperature();
+        Optional<Integer> targetTemperature = state.getBottomTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetBottomTemperaturesForWineCabinetFreezerCombination() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Integer temperature = state.getBottomTemperature().get();
+        Integer targetTemperature = state.getBottomTargetTemperature().get();
+
+        // then:
+        assertEquals(TEMPERATURE_1, temperature);
+        assertEquals(TARGET_TEMPERATURE_1, targetTemperature);
+    }
+
+    @Test
+    public void testGetBottomTemperaturesForOtherDeviceWithTwoTemperatures() {
+        // given:
+        setUpDeviceStateMock(2);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getBottomTemperature();
+        Optional<Integer> targetTemperature = state.getBottomTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+
+    @Test
+    public void testGetBottomTemperaturesWhenNoTemperaturesAreAvailable() {
+        // given:
+        setUpDeviceStateMock(0);
+        when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+        WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+        // when:
+        Optional<Integer> temperature = state.getBottomTemperature();
+        Optional<Integer> targetTemperature = state.getBottomTargetTemperature();
+
+        // then:
+        assertFalse(temperature.isPresent());
+        assertFalse(targetTemperature.isPresent());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ActionsTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ActionsTest.java
new file mode 100644 (file)
index 0000000..80e64ea
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ActionsTest {
+    @Test
+    public void testNullProcessActionInJsonIsConvertedToEmptyList() throws IOException {
+        // given:
+        String json = "{ \"processAction\": null, \"light\": [1], \"startTime\": [ [0, 0],[23,59] ] }";
+
+        // when:
+        Actions actions = new Gson().fromJson(json, Actions.class);
+
+        // then:
+        assertNotNull(actions.getProcessAction());
+        assertTrue(actions.getProcessAction().isEmpty());
+    }
+
+    @Test
+    public void testNullLightInJsonIsConvertedToEmptyList() throws IOException {
+        // given:
+        String json = "{ \"processAction\": [1], \"light\": null, \"startTime\": [ [0, 0],[23,59] ] }";
+
+        // when:
+        Actions actions = new Gson().fromJson(json, Actions.class);
+
+        // then:
+        assertNotNull(actions.getLight());
+        assertTrue(actions.getLight().isEmpty());
+    }
+
+    @Test
+    public void testNullStartTimeInJsonIsReturnedAsNull() throws IOException {
+        // given:
+        String json = "{ \"processAction\": [1], \"light\": [1], \"startTime\": null }";
+
+        // when:
+        Actions actions = new Gson().fromJson(json, Actions.class);
+
+        // then:
+        assertFalse(actions.getStartTime().isPresent());
+    }
+
+    @Test
+    public void testIdListIsEmptyWhenProgramIdFieldIsMissing() {
+        // given:
+        String json = "{ \"processAction\": [1] }";
+
+        // when:
+        Actions actions = new Gson().fromJson(json, Actions.class);
+
+        // then:
+        assertTrue(actions.getProgramId().isEmpty());
+    }
+
+    @Test
+    public void testIdListIsEmptyWhenProgramIdFieldIsNull() {
+        // given:
+        String json = "{ \"programId\": null }";
+
+        // when:
+        Actions actions = new Gson().fromJson(json, Actions.class);
+
+        // then:
+        assertTrue(actions.getProgramId().isEmpty());
+    }
+
+    @Test
+    public void testIdListContainsEntriesWhenProgramIdFieldIsPresent() {
+        // given:
+        String json = "{ \"programId\": [1,2,3,4] }";
+
+        // when:
+        Actions actions = new Gson().fromJson(json, Actions.class);
+
+        // then:
+        assertEquals(Arrays.asList(1, 2, 3, 4), actions.getProgramId());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceCollectionTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceCollectionTest.java
new file mode 100644 (file)
index 0000000..d23ef08
--- /dev/null
@@ -0,0 +1,290 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mielecloud.internal.util.ResourceUtil.getResourceAsString;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add plate step
+ */
+@NonNullByDefault
+public class DeviceCollectionTest {
+    @Test
+    public void testCreateDeviceCollection() throws IOException {
+        // given:
+        String json = getResourceAsString(
+                "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollection.json");
+
+        // when:
+        DeviceCollection collection = DeviceCollection.fromJson(json);
+
+        // then:
+        assertEquals(1, collection.getDeviceIdentifiers().size());
+        Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+        Ident ident = device.getIdent().get();
+        Type type = ident.getType().get();
+        assertEquals("Devicetype", type.getKeyLocalized().get());
+        assertEquals(DeviceType.HOOD, type.getValueRaw());
+        assertEquals("Ventilation Hood", type.getValueLocalized().get());
+
+        assertEquals("My Hood", ident.getDeviceName().get());
+
+        DeviceIdentLabel deviceIdentLabel = ident.getDeviceIdentLabel().get();
+        assertEquals("000124430017", deviceIdentLabel.getFabNumber().get());
+        assertEquals("00", deviceIdentLabel.getFabIndex().get());
+        assertEquals("DA-6996", deviceIdentLabel.getTechType().get());
+        assertEquals("10101010", deviceIdentLabel.getMatNumber().get());
+        assertEquals(Arrays.asList("4164", "20380", "25226"), deviceIdentLabel.getSwids());
+
+        XkmIdentLabel xkmIdentLabel = ident.getXkmIdentLabel().get();
+        assertEquals("EK039W", xkmIdentLabel.getTechType().get());
+        assertEquals("02.31", xkmIdentLabel.getReleaseVersion().get());
+
+        State state = device.getState().get();
+        Status status = state.getStatus().get();
+        assertEquals(Integer.valueOf(StateType.RUNNING.getCode()), status.getValueRaw().get());
+        assertEquals("In use", status.getValueLocalized().get());
+        assertEquals("State", status.getKeyLocalized().get());
+
+        ProgramType programType = state.getProgramType().get();
+        assertEquals(Integer.valueOf(0), programType.getValueRaw().get());
+        assertEquals("", programType.getValueLocalized().get());
+        assertEquals("Programme", programType.getKeyLocalized().get());
+
+        ProgramPhase programPhase = state.getProgramPhase().get();
+        assertEquals(Integer.valueOf(4609), programPhase.getValueRaw().get());
+        assertEquals("", programPhase.getValueLocalized().get());
+        assertEquals("Phase", programPhase.getKeyLocalized().get());
+
+        assertEquals(Arrays.asList(0, 0), state.getRemainingTime().get());
+        assertEquals(Arrays.asList(0, 0), state.getStartTime().get());
+
+        assertEquals(1, state.getTargetTemperature().size());
+        Temperature targetTemperature = state.getTargetTemperature().get(0);
+        assertNotNull(targetTemperature);
+        assertEquals(Integer.valueOf(-32768), targetTemperature.getValueRaw().get());
+        assertFalse(targetTemperature.getValueLocalized().isPresent());
+        assertEquals("Celsius", targetTemperature.getUnit().get());
+
+        assertEquals(3, state.getTemperature().size());
+        Temperature temperature0 = state.getTemperature().get(0);
+        assertNotNull(temperature0);
+        assertEquals(Integer.valueOf(-32768), temperature0.getValueRaw().get());
+        assertFalse(temperature0.getValueLocalized().isPresent());
+        assertEquals("Celsius", temperature0.getUnit().get());
+        Temperature temperature1 = state.getTemperature().get(1);
+        assertNotNull(temperature1);
+        assertEquals(Integer.valueOf(-32768), temperature1.getValueRaw().get());
+        assertFalse(temperature1.getValueLocalized().isPresent());
+        assertEquals("Celsius", temperature1.getUnit().get());
+        Temperature temperature2 = state.getTemperature().get(2);
+        assertNotNull(temperature2);
+        assertEquals(Integer.valueOf(-32768), temperature2.getValueRaw().get());
+        assertFalse(temperature2.getValueLocalized().isPresent());
+        assertEquals("Celsius", temperature2.getUnit().get());
+
+        assertEquals(false, state.getSignalInfo().get());
+        assertEquals(false, state.getSignalFailure().get());
+        assertEquals(false, state.getSignalDoor().get());
+
+        RemoteEnable remoteEnable = state.getRemoteEnable().get();
+        assertEquals(false, remoteEnable.getFullRemoteControl().get());
+        assertEquals(false, remoteEnable.getSmartGrid().get());
+
+        assertEquals(Light.ENABLE, state.getLight());
+        assertEquals(new ArrayList<Object>(), state.getElapsedTime().get());
+
+        SpinningSpeed spinningSpeed = state.getSpinningSpeed().get();
+        assertEquals(Integer.valueOf(1200), spinningSpeed.getValueRaw().get());
+        assertEquals("1200", spinningSpeed.getValueLocalized().get());
+        assertEquals("rpm", spinningSpeed.getUnit().get());
+
+        DryingStep dryingStep = state.getDryingStep().get();
+        assertFalse(dryingStep.getValueRaw().isPresent());
+        assertEquals("", dryingStep.getValueLocalized().get());
+        assertEquals("Drying level", dryingStep.getKeyLocalized().get());
+
+        VentilationStep ventilationStep = state.getVentilationStep().get();
+        assertEquals(Integer.valueOf(2), ventilationStep.getValueRaw().get());
+        assertEquals("2", ventilationStep.getValueLocalized().get());
+        assertEquals("Power Level", ventilationStep.getKeyLocalized().get());
+
+        List<PlateStep> plateStep = state.getPlateStep();
+        assertEquals(4, plateStep.size());
+        assertEquals(Integer.valueOf(0), plateStep.get(0).getValueRaw().get());
+        assertEquals("0", plateStep.get(0).getValueLocalized().get());
+        assertEquals("Plate Step", plateStep.get(0).getKeyLocalized().get());
+        assertEquals(Integer.valueOf(1), plateStep.get(1).getValueRaw().get());
+        assertEquals("1", plateStep.get(1).getValueLocalized().get());
+        assertEquals("Plate Step", plateStep.get(1).getKeyLocalized().get());
+        assertEquals(Integer.valueOf(2), plateStep.get(2).getValueRaw().get());
+        assertEquals("1.", plateStep.get(2).getValueLocalized().get());
+        assertEquals("Plate Step", plateStep.get(2).getKeyLocalized().get());
+        assertEquals(Integer.valueOf(3), plateStep.get(3).getValueRaw().get());
+        assertEquals("2", plateStep.get(3).getValueLocalized().get());
+        assertEquals("Plate Step", plateStep.get(3).getKeyLocalized().get());
+
+        assertEquals(Integer.valueOf(20), state.getBatteryLevel().get());
+    }
+
+    @Test
+    public void testCreateDeviceCollectionFromInvalidJsonThrowsMieleSyntaxException() throws IOException {
+        // given:
+        String invalidJson = getResourceAsString(
+                "/org/openhab/binding/mielecloud/internal/webservice/api/json/invalidDeviceCollection.json");
+
+        // when:
+        assertThrows(MieleSyntaxException.class, () -> {
+            DeviceCollection.fromJson(invalidJson);
+        });
+    }
+
+    @Test
+    public void testCreateDeviceCollectionWithLargeProgramID() throws IOException {
+        // given:
+        String json = getResourceAsString(
+                "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithLargeProgramID.json");
+
+        // when:
+        DeviceCollection collection = DeviceCollection.fromJson(json);
+
+        // then:
+        assertEquals(1, collection.getDeviceIdentifiers().size());
+        Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+        Ident ident = device.getIdent().get();
+        Type type = ident.getType().get();
+        assertEquals("Devicetype", type.getKeyLocalized().get());
+        assertEquals(DeviceType.UNKNOWN, type.getValueRaw());
+        assertEquals("", type.getValueLocalized().get());
+
+        assertEquals("Some Devicename", ident.getDeviceName().get());
+
+        DeviceIdentLabel deviceIdentLabel = ident.getDeviceIdentLabel().get();
+        assertEquals("", deviceIdentLabel.getFabNumber().get());
+        assertEquals("", deviceIdentLabel.getFabIndex().get());
+        assertEquals("", deviceIdentLabel.getTechType().get());
+        assertEquals("", deviceIdentLabel.getMatNumber().get());
+        assertEquals(Arrays.asList(), deviceIdentLabel.getSwids());
+
+        XkmIdentLabel xkmIdentLabel = ident.getXkmIdentLabel().get();
+        assertEquals("", xkmIdentLabel.getTechType().get());
+        assertEquals("", xkmIdentLabel.getReleaseVersion().get());
+
+        State state = device.getState().get();
+        ProgramId programId = state.getProgramId().get();
+        assertEquals(Long.valueOf(2499805184L), programId.getValueRaw().get());
+        assertEquals("", programId.getValueLocalized().get());
+        assertEquals("Program Id", programId.getKeyLocalized().get());
+
+        Status status = state.getStatus().get();
+        assertEquals(Integer.valueOf(StateType.RUNNING.getCode()), status.getValueRaw().get());
+        assertEquals("In use", status.getValueLocalized().get());
+        assertEquals("State", status.getKeyLocalized().get());
+
+        ProgramType programType = state.getProgramType().get();
+        assertEquals(Integer.valueOf(0), programType.getValueRaw().get());
+        assertEquals("Operation mode", programType.getValueLocalized().get());
+        assertEquals("Program type", programType.getKeyLocalized().get());
+
+        ProgramPhase programPhase = state.getProgramPhase().get();
+        assertEquals(Integer.valueOf(0), programPhase.getValueRaw().get());
+        assertEquals("", programPhase.getValueLocalized().get());
+        assertEquals("Phase", programPhase.getKeyLocalized().get());
+
+        assertEquals(Arrays.asList(0, 0), state.getRemainingTime().get());
+        assertEquals(Arrays.asList(0, 0), state.getStartTime().get());
+
+        assertTrue(state.getTargetTemperature().isEmpty());
+        assertTrue(state.getTemperature().isEmpty());
+
+        assertEquals(false, state.getSignalInfo().get());
+        assertEquals(false, state.getSignalFailure().get());
+        assertEquals(false, state.getSignalDoor().get());
+
+        RemoteEnable remoteEnable = state.getRemoteEnable().get();
+        assertEquals(true, remoteEnable.getFullRemoteControl().get());
+        assertEquals(false, remoteEnable.getSmartGrid().get());
+
+        assertEquals(Light.NOT_SUPPORTED, state.getLight());
+        assertEquals(new ArrayList<Object>(), state.getElapsedTime().get());
+
+        DryingStep dryingStep = state.getDryingStep().get();
+        assertFalse(dryingStep.getValueRaw().isPresent());
+        assertEquals("", dryingStep.getValueLocalized().get());
+        assertEquals("Drying level", dryingStep.getKeyLocalized().get());
+
+        VentilationStep ventilationStep = state.getVentilationStep().get();
+        assertFalse(ventilationStep.getValueRaw().isPresent());
+        assertEquals("", ventilationStep.getValueLocalized().get());
+        assertEquals("Power Level", ventilationStep.getKeyLocalized().get());
+
+        List<PlateStep> plateStep = state.getPlateStep();
+        assertEquals(0, plateStep.size());
+    }
+
+    @Test
+    public void testCreateDeviceCollectionWithSpinningSpeedObject() throws IOException {
+        // given:
+        String json = getResourceAsString(
+                "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithSpinningSpeedObject.json");
+
+        // when:
+        DeviceCollection collection = DeviceCollection.fromJson(json);
+
+        // then:
+        assertEquals(1, collection.getDeviceIdentifiers().size());
+        Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+        State state = device.getState().get();
+        SpinningSpeed spinningSpeed = state.getSpinningSpeed().get();
+        assertNotNull(spinningSpeed);
+        assertEquals(Integer.valueOf(1600), spinningSpeed.getValueRaw().get());
+        assertEquals("1600", spinningSpeed.getValueLocalized().get());
+        assertEquals("U/min", spinningSpeed.getUnit().get());
+    }
+
+    @Test
+    public void testCreateDeviceCollectionWithFloatingPointTemperature() throws IOException {
+        // given:
+        String json = getResourceAsString(
+                "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithFloatingPointTargetTemperature.json");
+
+        // when:
+        DeviceCollection collection = DeviceCollection.fromJson(json);
+
+        // then:
+        assertEquals(1, collection.getDeviceIdentifiers().size());
+        Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+        State state = device.getState().get();
+        List<Temperature> targetTemperatures = state.getTargetTemperature();
+        assertEquals(1, targetTemperatures.size());
+
+        Temperature targetTemperature = targetTemperatures.get(0);
+        assertEquals(Integer.valueOf(80), targetTemperature.getValueRaw().get());
+        assertEquals(Integer.valueOf(0), targetTemperature.getValueLocalized().get());
+        assertEquals("Celsius", targetTemperature.getUnit().get());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceIdentLabelTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/DeviceIdentLabelTest.java
new file mode 100644 (file)
index 0000000..29e2638
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceIdentLabelTest {
+    @Test
+    public void testNullSwidsInJsonAreConvertedToEmptyList() throws IOException {
+        // given:
+        String json = "{ \"swids\": null }";
+
+        // when:
+        DeviceIdentLabel deviceIdentLabel = new Gson().fromJson(json, DeviceIdentLabel.class);
+
+        // then:
+        assertNotNull(deviceIdentLabel.getSwids());
+        assertTrue(deviceIdentLabel.getSwids().isEmpty());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ErrorMessageTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/ErrorMessageTest.java
new file mode 100644 (file)
index 0000000..d0e84b4
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ErrorMessageTest {
+
+    @Test
+    public void testErrorMessageCanBeCreated() {
+        // given:
+        String json = "{\"message\": \"Unauthorized\"}";
+
+        // when:
+        ErrorMessage errorMessage = ErrorMessage.fromJson(json);
+
+        // then:
+        assertEquals("Unauthorized", errorMessage.getMessage().get());
+    }
+
+    @Test
+    public void testErrorMessageCreationThrowsMieleSyntaxExceptionWhenJsonIsInvalid() {
+        // given:
+        String json = "\"message\": \"Unauthorized}";
+
+        // when:
+        assertThrows(MieleSyntaxException.class, () -> {
+            ErrorMessage.fromJson(json);
+        });
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/LightTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/LightTest.java
new file mode 100644 (file)
index 0000000..8a3f73d
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class LightTest {
+    @Test
+    public void testFromNullId() {
+        // when:
+        Light light = Light.fromId(null);
+
+        // then:
+        assertEquals(Light.UNKNOWN, light);
+    }
+
+    @Test
+    public void testFromNotSupportedId() {
+        // when:
+        Light light = Light.fromId(0);
+
+        // then:
+        assertEquals(Light.NOT_SUPPORTED, light);
+    }
+
+    @Test
+    public void testFromNotSupportedAlternativeId() {
+        // when:
+        Light light = Light.fromId(255);
+
+        // then:
+        assertEquals(Light.NOT_SUPPORTED, light);
+    }
+
+    @Test
+    public void testFromEnabledId() {
+        // when:
+        Light light = Light.fromId(1);
+
+        // then:
+        assertEquals(Light.ENABLE, light);
+    }
+
+    @Test
+    public void testFromDisabledId() {
+        // when:
+        Light light = Light.fromId(2);
+
+        // then:
+        assertEquals(Light.DISABLE, light);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StateTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StateTest.java
new file mode 100644 (file)
index 0000000..1bc25be
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class StateTest {
+    @Test
+    public void testNullRemainingTimeInJsonCausesRemainingTimeListToBeNull() throws IOException {
+        // given:
+        String json = "{ \"remainingTime\": null, \"startTime\": [0, 0], \"targetTemperature\": [{}], \"temperature\": [{}], \"elapsedTime\": [0, 0] }";
+
+        // when:
+        State state = new Gson().fromJson(json, State.class);
+
+        // then:
+        assertFalse(state.getRemainingTime().isPresent());
+    }
+
+    @Test
+    public void testNullStartTimeInJsonCausesStartTimeListToBeNull() throws IOException {
+        // given:
+        String json = "{ \"remainingTime\": [0, 0], \"startTime\": null, \"targetTemperature\": [{}], \"temperature\": [{}], \"elapsedTime\": [0, 0] }";
+
+        // when:
+        State state = new Gson().fromJson(json, State.class);
+
+        // then:
+        assertFalse(state.getStartTime().isPresent());
+    }
+
+    @Test
+    public void testNullElapsedTimeInJsonCausesElapsedTimeListToBeNull() throws IOException {
+        // given:
+        String json = "{ \"remainingTime\": [0, 0], \"startTime\": [0, 0], \"targetTemperature\": [{}], \"temperature\": [{}], \"elapsedTime\": null }";
+
+        // when:
+        State state = new Gson().fromJson(json, State.class);
+
+        // then:
+        assertFalse(state.getElapsedTime().isPresent());
+    }
+
+    @Test
+    public void testNullTargetTemperatureInJsonIsConvertedToEmptyList() throws IOException {
+        // given:
+        String json = "{ \"remainingTime\": [0, 0], \"startTime\": [0, 0], \"targetTemperature\": null, \"temperature\": [{}], \"elapsedTime\": [0, 0] }";
+
+        // when:
+        State state = new Gson().fromJson(json, State.class);
+
+        // then:
+        assertNotNull(state.getTargetTemperature());
+        assertTrue(state.getTargetTemperature().isEmpty());
+    }
+
+    @Test
+    public void testNullTemperatureInJsonIsConvertedToEmptyList() throws IOException {
+        // given:
+        String json = "{ \"remainingTime\": [0, 0], \"startTime\": [0, 0], \"targetTemperature\": [{}], \"temperature\": null, \"elapsedTime\": [0, 0] }";
+
+        // when:
+        State state = new Gson().fromJson(json, State.class);
+
+        // then:
+        assertNotNull(state.getTemperature());
+        assertTrue(state.getTemperature().isEmpty());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StatusTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/StatusTest.java
new file mode 100644 (file)
index 0000000..f0567f6
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class StatusTest {
+    @Test
+    public void testParseStatusWithUnknownRawValue() {
+        // given:
+        String json = "{ \"key_localized\": \"State\", \"value_raw\": 99, \"value_localized\": \"Booting\" }";
+
+        // when:
+        Status status = new Gson().fromJson(json, Status.class);
+
+        // then:
+        assertNotNull(status);
+        assertEquals("State", status.getKeyLocalized().get());
+        assertEquals(Integer.valueOf(99), status.getValueRaw().get());
+        assertEquals("Booting", status.getValueLocalized().get());
+    }
+
+    @Test
+    public void testParseStatusWithKnownRawValue() {
+        // given:
+        String json = "{ \"key_localized\": \"State\", \"value_raw\": 1, \"value_localized\": \"Off\" }";
+
+        // when:
+        Status status = new Gson().fromJson(json, Status.class);
+
+        // then:
+        assertNotNull(status);
+        assertEquals("State", status.getKeyLocalized().get());
+        assertEquals(Integer.valueOf(StateType.OFF.getCode()), status.getValueRaw().get());
+        assertEquals("Off", status.getValueLocalized().get());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/TypeTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/api/json/TypeTest.java
new file mode 100644 (file)
index 0000000..87a1ce0
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TypeTest {
+    @Test
+    public void testParseTypeWithUnknownRawValue() {
+        // given:
+        String json = "{ \"key_localized\": \"Devicetype\", \"value_raw\": 99, \"value_localized\": \"Car Vaccuum Robot\" }";
+
+        // when:
+        Type type = new Gson().fromJson(json, Type.class);
+
+        // then:
+        assertNotNull(type);
+        assertEquals("Devicetype", type.getKeyLocalized().get());
+        assertEquals(DeviceType.UNKNOWN, type.getValueRaw());
+        assertEquals("Car Vaccuum Robot", type.getValueLocalized().get());
+    }
+
+    @Test
+    public void testParseTypeWithKnownRawValue() {
+        // given:
+        String json = "{ \"key_localized\": \"Devicetype\", \"value_raw\": 1, \"value_localized\": \"Washing Machine\" }";
+
+        // when:
+        Type type = new Gson().fromJson(json, Type.class);
+
+        // then:
+        assertNotNull(type);
+        assertEquals("Devicetype", type.getKeyLocalized().get());
+        assertEquals(DeviceType.WASHING_MACHINE, type.getValueRaw());
+        assertEquals("Washing Machine", type.getValueLocalized().get());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/exception/TooManyRequestsExceptionTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/exception/TooManyRequestsExceptionTest.java
new file mode 100644 (file)
index 0000000..0506f26
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TooManyRequestsExceptionTest {
+    @Test
+    public void testHasRetryAfterHintReturnsFalseWhenNoRetryAfterWasPassedToConstructor() {
+        // given:
+        TooManyRequestsException exception = new TooManyRequestsException("", null);
+
+        // when:
+        boolean result = exception.hasRetryAfterHint();
+
+        // then:
+        assertFalse(result);
+    }
+
+    @Test
+    public void testHasRetryAfterHintReturnsTrueWhenRetryAfterWasPassedToConstructor() {
+        // given:
+        TooManyRequestsException exception = new TooManyRequestsException("", "25");
+
+        // when:
+        boolean result = exception.hasRetryAfterHint();
+
+        // then:
+        assertTrue(result);
+    }
+
+    @Test
+    public void testGetSecondsUntilRetryReturnsMinusOneWhenNoRetryAfterHintIsPresent() {
+        // given:
+        TooManyRequestsException exception = new TooManyRequestsException("", null);
+
+        // when:
+        long result = exception.getSecondsUntilRetry();
+
+        // then:
+        assertEquals(-1L, result);
+    }
+
+    @Test
+    public void testGetSecondsUntilRetryParsesNumber() {
+        // given:
+        TooManyRequestsException exception = new TooManyRequestsException("", "30");
+
+        // when:
+        long result = exception.getSecondsUntilRetry();
+
+        // then:
+        assertEquals(30L, result);
+    }
+
+    @Test
+    public void testGetSecondsUntilRetryParsesDate() {
+        // given:
+        TooManyRequestsException exception = new TooManyRequestsException("", "Thu, 12 Jan 5015 15:02:30 GMT");
+
+        // when:
+        long result = exception.getSecondsUntilRetry();
+
+        // then:
+        assertNotEquals(0L, result);
+    }
+
+    @Test
+    public void testGetSecondsUntilRetryParsesDateFromThePast() {
+        // given:
+        TooManyRequestsException exception = new TooManyRequestsException("", "Wed, 21 Oct 2015 07:28:00 GMT");
+
+        // when:
+        long result = exception.getSecondsUntilRetry();
+
+        // then:
+        assertEquals(0L, result);
+    }
+
+    @Test
+    public void testGetSecondsUntilRetryReturnsMinusOneWhenDateCannotBeParsed() {
+        // given:
+        TooManyRequestsException exception = new TooManyRequestsException("", "50 Minutes");
+
+        // when:
+        long result = exception.getSecondsUntilRetry();
+
+        // then:
+        assertEquals(-1L, result);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/language/CombiningLanguageProviderTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/language/CombiningLanguageProviderTest.java
new file mode 100644 (file)
index 0000000..3ccc966
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CombiningLanguageProviderTest {
+    private static final Optional<String> PRIORITIZED_LANGUAGE = Optional.of("de");
+    private static final Optional<String> FALLBACK_LANGUAGE = Optional.of("en");
+
+    private static final LanguageProvider PRIORITIZED_PROVIDER = new LanguageProvider() {
+        @Override
+        public Optional<String> getLanguage() {
+            return PRIORITIZED_LANGUAGE;
+        }
+    };
+
+    private static final LanguageProvider FALLBACK_PROVIDER = new LanguageProvider() {
+        @Override
+        public Optional<String> getLanguage() {
+            return FALLBACK_LANGUAGE;
+        }
+    };
+
+    private static final LanguageProvider NULL_PROVIDER = new LanguageProvider() {
+        @Override
+        public Optional<String> getLanguage() {
+            return Optional.empty();
+        }
+    };
+
+    @Test
+    public void testPrioritizedLanguageProviderIsUsed() {
+        // given:
+        LanguageProvider provider = new CombiningLanguageProvider(PRIORITIZED_PROVIDER, FALLBACK_PROVIDER);
+
+        // when:
+        Optional<String> language = provider.getLanguage();
+
+        // then:
+        assertEquals(PRIORITIZED_LANGUAGE, language);
+    }
+
+    @Test
+    public void testFallbackProviderIsUsedWhenPrioritizedProviderIsNull() {
+        // given:
+        LanguageProvider provider = new CombiningLanguageProvider(null, FALLBACK_PROVIDER);
+
+        // when:
+        Optional<String> language = provider.getLanguage();
+
+        // then:
+        assertEquals(FALLBACK_LANGUAGE, language);
+    }
+
+    @Test
+    public void testFallbackProviderIsUsedWhenPrioritizedProviderProvidesNull() {
+        // given:
+        LanguageProvider provider = new CombiningLanguageProvider(NULL_PROVIDER, FALLBACK_PROVIDER);
+
+        // when:
+        Optional<String> language = provider.getLanguage();
+
+        // then:
+        assertEquals(FALLBACK_LANGUAGE, language);
+    }
+
+    @Test
+    public void testProvidesNullWhenBothProvidersAreNull() {
+        // given:
+        LanguageProvider provider = new CombiningLanguageProvider(null, null);
+
+        // when:
+        Optional<String> language = provider.getLanguage();
+
+        // then:
+        assertFalse(language.isPresent());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/language/OpenHabLanguageProviderTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/language/OpenHabLanguageProviderTest.java
new file mode 100644 (file)
index 0000000..9af9691
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Locale;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.i18n.LocaleProvider;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class OpenHabLanguageProviderTest {
+    @Test
+    public void whenTheLocaleIsSetToEnglishThenTheLanguageCodeIsEn() {
+        // given:
+        LocaleProvider localeProvider = mock(LocaleProvider.class);
+        when(localeProvider.getLocale()).thenReturn(Locale.ENGLISH);
+
+        LanguageProvider languageProvider = new OpenHabLanguageProvider(localeProvider);
+
+        // when:
+        Optional<String> language = languageProvider.getLanguage();
+
+        // then:
+        assertEquals(Optional.of("en"), language);
+    }
+
+    @Test
+    public void whenTheLocaleIsSetToGermanThenTheLanguageCodeIsDe() {
+        // given:
+        LocaleProvider localeProvider = mock(LocaleProvider.class);
+        when(localeProvider.getLocale()).thenReturn(Locale.GERMAN);
+
+        LanguageProvider languageProvider = new OpenHabLanguageProvider(localeProvider);
+
+        // when:
+        Optional<String> language = languageProvider.getLanguage();
+
+        // then:
+        assertEquals(Optional.of("de"), language);
+    }
+
+    @Test
+    public void whenTheLocaleIsSetToGermanyThenTheLanguageCodeIsDe() {
+        // given:
+        LocaleProvider localeProvider = mock(LocaleProvider.class);
+        when(localeProvider.getLocale()).thenReturn(Locale.GERMANY);
+
+        LanguageProvider languageProvider = new OpenHabLanguageProvider(localeProvider);
+
+        // when:
+        Optional<String> language = languageProvider.getLanguage();
+
+        // then:
+        assertEquals(Optional.of("de"), language);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/AuthorizationFailedRetryStrategyTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/AuthorizationFailedRetryStrategyTest.java
new file mode 100644 (file)
index 0000000..10f1b76
--- /dev/null
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class AuthorizationFailedRetryStrategyTest {
+    private static final String TEST_STRING = "Some Test String";
+
+    @Mock
+    @Nullable
+    private Supplier<@Nullable String> operationWithReturnValue;
+    @Mock
+    @Nullable
+    private Consumer<Exception> onException;
+    @Mock
+    @Nullable
+    private Runnable operation;
+
+    private final OAuthTokenRefresher refresher = mock(OAuthTokenRefresher.class);
+
+    private Supplier<@Nullable String> getOperationWithReturnValue() {
+        assertNotNull(operationWithReturnValue);
+        return Objects.requireNonNull(operationWithReturnValue);
+    }
+
+    private Consumer<Exception> getOnException() {
+        assertNotNull(onException);
+        return Objects.requireNonNull(onException);
+    }
+
+    private Runnable getOperation() {
+        assertNotNull(operation);
+        return Objects.requireNonNull(operation);
+    }
+
+    @Test
+    public void testPerformRetryableOperationWithReturnValueInvokesOperation() {
+        // given:
+        when(getOperationWithReturnValue().get()).thenReturn(TEST_STRING);
+
+        AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        String result = retryStrategy.performRetryableOperation(getOperationWithReturnValue(), getOnException());
+
+        // then:
+        assertEquals(TEST_STRING, result);
+    }
+
+    @Test
+    public void testPerformRetryableOperationWithReturnValueInvokesRefreshTokenAndRetriesOperation() {
+        // given:
+        when(getOperationWithReturnValue().get()).thenThrow(AuthorizationFailedException.class).thenReturn(TEST_STRING);
+
+        AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        String result = retryStrategy.performRetryableOperation(getOperationWithReturnValue(), getOnException());
+
+        // then:
+        assertEquals(TEST_STRING, result);
+        verify(getOnException()).accept(any());
+        verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+        verifyNoMoreInteractions(getOnException(), refresher);
+    }
+
+    @Test
+    public void testPerformRetryableOperationWithReturnValueThrowsMieleWebserviceExceptionWhenRetryingTheOperationFails() {
+        // given:
+        when(getOperationWithReturnValue().get()).thenThrow(AuthorizationFailedException.class);
+
+        AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        assertThrows(MieleWebserviceException.class, () -> {
+            try {
+                // when:
+                retryStrategy.performRetryableOperation(getOperationWithReturnValue(), getOnException());
+            } catch (Exception e) {
+                // then:
+                verify(getOnException()).accept(any());
+                verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+                verifyNoMoreInteractions(getOnException(), refresher);
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void testPerformRetryableOperationInvokesOperation() {
+        // given:
+        AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        retryStrategy.performRetryableOperation(getOperation(), getOnException());
+
+        // then:
+        verify(getOperation()).run();
+        verifyNoMoreInteractions(getOperation());
+    }
+
+    @Test
+    public void testPerformRetryableOperationInvokesRefreshTokenAndRetriesOperation() {
+        // given:
+        doThrow(AuthorizationFailedException.class).doNothing().when(getOperation()).run();
+
+        AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        // when:
+        retryStrategy.performRetryableOperation(getOperation(), getOnException());
+
+        // then:
+        verify(getOnException()).accept(any());
+        verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+        verify(getOperation(), times(2)).run();
+        verifyNoMoreInteractions(getOnException(), refresher, getOperation());
+    }
+
+    @Test
+    public void testPerformRetryableOperationThrowsMieleWebserviceExceptionWhenRetryingTheOperationFails() {
+        // given:
+        doThrow(AuthorizationFailedException.class).when(getOperation()).run();
+
+        AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        assertThrows(MieleWebserviceException.class, () -> {
+            try {
+                // when:
+                retryStrategy.performRetryableOperation(getOperation(), getOnException());
+            } catch (Exception e) {
+                // then:
+                verify(getOnException()).accept(any());
+                verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+                verify(getOperation(), times(2)).run();
+                verifyNoMoreInteractions(getOnException(), refresher, getOperation());
+                throw e;
+            }
+        });
+    }
+
+    @Test
+    public void testPerformRetryableOperationThrowsMieleWebserviceExceptionWhenTokenRefreshingFails() {
+        // given:
+        doThrow(AuthorizationFailedException.class).when(getOperation()).run();
+        doThrow(OAuthException.class).when(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+                MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+        assertThrows(MieleWebserviceException.class, () -> {
+            try {
+                // when:
+                retryStrategy.performRetryableOperation(getOperation(), getOnException());
+            } catch (Exception e) {
+                // then:
+                verify(getOnException()).accept(any());
+                verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+                verify(getOperation()).run();
+                verifyNoMoreInteractions(getOnException(), refresher, getOperation());
+                throw e;
+            }
+        });
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/NTimesRetryStrategyTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/NTimesRetryStrategyTest.java
new file mode 100644 (file)
index 0000000..febf3cb
--- /dev/null
@@ -0,0 +1,220 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class NTimesRetryStrategyTest {
+    private static final int SUCCESSFUL_RETURN_VALUE = 42;
+
+    @Mock
+    @Nullable
+    private Supplier<@Nullable Integer> operation;
+
+    @Mock
+    @Nullable
+    private Consumer<Exception> onTransientException;
+
+    private Supplier<@Nullable Integer> getOperation() {
+        assertNotNull(operation);
+        return Objects.requireNonNull(operation);
+    }
+
+    private Consumer<Exception> getOnTransientException() {
+        assertNotNull(onTransientException);
+        return Objects.requireNonNull(onTransientException);
+    }
+
+    @Test
+    public void testConstructorThrowsIllegalArgumentExceptionIfNumberOfRetriesIsSmallerThanZero() {
+        // when:
+        assertThrows(IllegalArgumentException.class, () -> {
+            new NTimesRetryStrategy(-1);
+        });
+    }
+
+    @Test
+    public void testSuccessfulOperationReturnsCorrectValue() {
+        // given:
+        when(getOperation().get()).thenReturn(SUCCESSFUL_RETURN_VALUE);
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+        // when:
+        Integer result = retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+        // then:
+        assertEquals(Integer.valueOf(SUCCESSFUL_RETURN_VALUE), result);
+        verifyNoMoreInteractions(onTransientException);
+    }
+
+    @Test
+    public void testFailingOperationReturnsCorrectValueOnRetry() {
+        // given:
+        when(getOperation().get()).thenThrow(MieleWebserviceTransientException.class)
+                .thenReturn(SUCCESSFUL_RETURN_VALUE);
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+        // when:
+        Integer result = retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+        // then:
+        assertEquals(Integer.valueOf(SUCCESSFUL_RETURN_VALUE), result);
+        verify(getOnTransientException()).accept(any());
+        verifyNoMoreInteractions(onTransientException);
+    }
+
+    @Test
+    public void testFailingOperationReturnsCorrectValueOnSecondRetry() {
+        // given:
+        when(getOperation().get()).thenThrow(MieleWebserviceTransientException.class)
+                .thenThrow(MieleWebserviceTransientException.class).thenReturn(SUCCESSFUL_RETURN_VALUE);
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(2);
+
+        // when:
+        Integer result = retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+        // then:
+        assertEquals(Integer.valueOf(SUCCESSFUL_RETURN_VALUE), result);
+        verify(getOnTransientException(), times(2)).accept(any());
+        verifyNoMoreInteractions(onTransientException);
+    }
+
+    @Test
+    public void testAlwaysFailingOperationThrowsMieleWebserviceException() {
+        // given:
+        when(getOperation().get()).thenThrow(MieleWebserviceTransientException.class);
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+        // when:
+        try {
+            retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+            fail();
+            return;
+        } catch (MieleWebserviceException e) {
+        }
+
+        // then:
+        verify(getOnTransientException()).accept(any());
+        verifyNoMoreInteractions(onTransientException);
+    }
+
+    @Test
+    public void testNullReturnValueDoesNotCauseMultipleRetries() {
+        // given:
+        when(getOperation().get()).thenReturn(null);
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+        // when:
+        retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+        // then:
+        verifyNoInteractions(getOnTransientException());
+    }
+
+    @Test
+    public void testSuccessfulOperation() {
+        // given:
+        Runnable operation = mock(Runnable.class);
+        doNothing().when(operation).run();
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+        // when:
+        retryStrategy.performRetryableOperation(operation, getOnTransientException());
+
+        // then:
+        verify(operation).run();
+        verifyNoInteractions(getOnTransientException());
+    }
+
+    @Test
+    public void testFailingOperationCausesRetry() {
+        // given:
+        Runnable operation = mock(Runnable.class);
+        doThrow(MieleWebserviceTransientException.class).doNothing().when(operation).run();
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+        // when:
+        retryStrategy.performRetryableOperation(operation, getOnTransientException());
+
+        // then:
+        verify(getOnTransientException()).accept(any());
+        verify(operation, times(2)).run();
+        verifyNoMoreInteractions(getOnTransientException());
+    }
+
+    @Test
+    public void testTwoTimesFailingOperationCausesTwoRetries() {
+        // given:
+        Runnable operation = mock(Runnable.class);
+        doThrow(MieleWebserviceTransientException.class).doThrow(MieleWebserviceTransientException.class).doNothing()
+                .when(operation).run();
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(2);
+
+        // when:
+        retryStrategy.performRetryableOperation(operation, getOnTransientException());
+
+        // then:
+        verify(getOnTransientException(), times(2)).accept(any());
+        verify(operation, times(3)).run();
+        verifyNoMoreInteractions(getOnTransientException());
+    }
+
+    @Test
+    public void testAlwaysFailingRunnableOperationThrowsMieleWebserviceException() {
+        // given:
+        Runnable operation = mock(Runnable.class);
+        doThrow(MieleWebserviceTransientException.class).when(operation).run();
+
+        NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+        // when:
+        try {
+            retryStrategy.performRetryableOperation(operation, getOnTransientException());
+            fail();
+            return;
+        } catch (MieleWebserviceException e) {
+        }
+
+        // then:
+        verify(getOnTransientException()).accept(any());
+        verifyNoMoreInteractions(getOnTransientException());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategyCombinerTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/retry/RetryStrategyCombinerTest.java
new file mode 100644 (file)
index 0000000..b05d3bf
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class RetryStrategyCombinerTest {
+    private static final String STRING_CONSTANT = "Some String";
+
+    private final RetryStrategy first = mock(RetryStrategy.class);
+    private final RetryStrategy second = mock(RetryStrategy.class);
+
+    @Mock
+    @Nullable
+    private Supplier<@Nullable String> supplier;
+    @Mock
+    @Nullable
+    private Consumer<Exception> consumer;
+
+    private Supplier<@Nullable String> getSupplier() {
+        assertNotNull(supplier);
+        return Objects.requireNonNull(supplier);
+    }
+
+    private Consumer<Exception> getConsumer() {
+        assertNotNull(consumer);
+        return Objects.requireNonNull(consumer);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testPerformRetryableOperationInvokesRetryStrategiesInCorrectOrder() {
+        // given:
+        when(first.<@Nullable String> performRetryableOperation(any(Supplier.class), any()))
+                .thenAnswer(new Answer<@Nullable String>() {
+                    @Override
+                    @Nullable
+                    public String answer(@Nullable InvocationOnMock invocation) throws Throwable {
+                        Supplier<String> inner = MockUtil.requireNonNull(invocation).getArgument(0);
+                        return inner.get();
+                    }
+                });
+        when(second.<@Nullable String> performRetryableOperation(any(Supplier.class), any()))
+                .thenAnswer(new Answer<@Nullable String>() {
+                    @Override
+                    @Nullable
+                    public String answer(@Nullable InvocationOnMock invocation) throws Throwable {
+                        Supplier<String> inner = MockUtil.requireNonNull(invocation).getArgument(0);
+                        return inner.get();
+                    }
+                });
+        when(getSupplier().get()).thenReturn(STRING_CONSTANT);
+
+        RetryStrategyCombiner combiner = new RetryStrategyCombiner(first, second);
+
+        // when:
+        String result = combiner.performRetryableOperation(getSupplier(), getConsumer());
+
+        // then:
+        assertEquals(STRING_CONSTANT, result);
+        verify(first).performRetryableOperation(any(Supplier.class), eq(getConsumer()));
+        verify(second).performRetryableOperation(any(Supplier.class), eq(getConsumer()));
+        verify(getSupplier()).get();
+        verifyNoMoreInteractions(first, second, getSupplier());
+        verifyNoInteractions(getConsumer());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/ExponentialBackoffWithJitterTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/ExponentialBackoffWithJitterTest.java
new file mode 100644 (file)
index 0000000..1c705f9
--- /dev/null
@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Random;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ExponentialBackoffWithJitterTest {
+    private static final long RETRY_INTERVAL = 2;
+    private static final long ALTERNATIVE_RETRY_INTERVAL = 50;
+    private static final long MINIMUM_WAIT_TIME = 1;
+    private static final long ALTERNATIVE_MINIMUM_WAIT_TIME = 2;
+    private static final long MAXIMUM_WAIT_TIME = 100;
+    private static final long ALTERNATIVE_MAXIMUM_WAIT_TIME = 150;
+
+    @Test
+    public void whenMinimumWaitTimeIsSmallerThanZeroThenAnIllegalArgumentExceptionIsThrown() {
+        // when:
+        assertThrows(IllegalArgumentException.class, () -> {
+            new ExponentialBackoffWithJitter(-MINIMUM_WAIT_TIME, MAXIMUM_WAIT_TIME, RETRY_INTERVAL);
+        });
+    }
+
+    @Test
+    public void whenMaximumWaitTimeIsSmallerThanZeroThenAnIllegalArgumentExceptionIsThrown() {
+        // when:
+        assertThrows(IllegalArgumentException.class, () -> {
+            new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME, -MAXIMUM_WAIT_TIME, RETRY_INTERVAL);
+        });
+    }
+
+    @Test
+    public void whenRetryIntervalIsSmallerThanZeroThenAnIllegalArgumentExceptionIsThrown() {
+        // when:
+        assertThrows(IllegalArgumentException.class, () -> {
+            new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME, MAXIMUM_WAIT_TIME, -RETRY_INTERVAL);
+        });
+    }
+
+    @Test
+    public void whenMinimumWaitTimeIsLargerThanMaximumWaitTimeThenAnIllegalArgumentExceptionIsThrown() {
+        // when:
+        assertThrows(IllegalArgumentException.class, () -> {
+            new ExponentialBackoffWithJitter(MAXIMUM_WAIT_TIME, MINIMUM_WAIT_TIME, RETRY_INTERVAL);
+        });
+    }
+
+    @Test
+    public void whenRetryIntervalIsLargerThanMaximumWaitTimeThenAnIllegalArgumentExceptionIsThrown() {
+        // when:
+        assertThrows(IllegalArgumentException.class, () -> {
+            new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME, RETRY_INTERVAL, MAXIMUM_WAIT_TIME);
+        });
+    }
+
+    @Test
+    public void whenTheNumberOfFailedAttemptsIsNegativeThenZeroIsAssumedInstead() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(RETRY_INTERVAL);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+                MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(-10);
+
+        // then:
+        assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL, result);
+    }
+
+    @Test
+    public void whenThereIsNoFailedAttemptThenTheMaximalResultIsMinimumWaitTimePlusRetryInterval() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(RETRY_INTERVAL);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+                MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(0);
+
+        // then:
+        assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL, result);
+    }
+
+    @Test
+    public void whenThereIsOneFailedAttemptThenTheMaximalResultIsMinimumWaitTimePlusTwiceTheRetryInterval() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(RETRY_INTERVAL * 2);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+                MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(1);
+
+        // then:
+        assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL * 2, result);
+    }
+
+    @Test
+    public void whenThereAreTwoFailedAttemptsThenTheMaximalResultIsMinimumWaitTimePlusFourTimesTheRetryInterval() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(RETRY_INTERVAL * 4);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+                MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(2);
+
+        // then:
+        assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL * 4, result);
+    }
+
+    @Test
+    public void whenThereAreTwoFailedAttemptsThenTheMinimalResultIsTheMinimumWaitTime() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(0L);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+                MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(2);
+
+        // then:
+        assertEquals(MINIMUM_WAIT_TIME, result);
+    }
+
+    @Test
+    public void whenTheDrawnRandomValueIsNegativeThenItIsProjectedToAPositiveValue() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(-RETRY_INTERVAL * 4 - 1);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+                MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(2);
+
+        // then:
+        assertEquals(MINIMUM_WAIT_TIME, result);
+    }
+
+    @Test
+    public void whenTheResultWouldBeLargerThanTheMaximumThenItIsCappedToTheMaximum() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(MAXIMUM_WAIT_TIME - ALTERNATIVE_MINIMUM_WAIT_TIME);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(ALTERNATIVE_MINIMUM_WAIT_TIME,
+                MAXIMUM_WAIT_TIME, ALTERNATIVE_RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(2);
+
+        // then:
+        assertEquals(MAXIMUM_WAIT_TIME, result);
+    }
+
+    @Test
+    public void whenTheResultWouldBeLargerThanTheAlternativeMaximumThenItIsCappedToTheAlternativeMaximum() {
+        // given:
+        Random random = mock(Random.class);
+        when(random.nextLong()).thenReturn(ALTERNATIVE_MAXIMUM_WAIT_TIME - ALTERNATIVE_MINIMUM_WAIT_TIME);
+
+        ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(ALTERNATIVE_MINIMUM_WAIT_TIME,
+                ALTERNATIVE_MAXIMUM_WAIT_TIME, ALTERNATIVE_RETRY_INTERVAL, random);
+
+        // when:
+        long result = backoffStrategy.getSecondsUntilRetry(2);
+
+        // then:
+        assertEquals(ALTERNATIVE_MAXIMUM_WAIT_TIME, result);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseConnectionTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseConnectionTest.java
new file mode 100644 (file)
index 0000000..8ad3efa
--- /dev/null
@@ -0,0 +1,600 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.*;
+
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Response.CompleteListener;
+import org.eclipse.jetty.client.api.Response.HeadersListener;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.http.HttpFields;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class SseConnectionTest {
+    private final String URL = "https://openhab.org/";
+
+    @Nullable
+    private Request request;
+
+    @Nullable
+    private SseRequestFactory sseRequestFactory;
+
+    @Nullable
+    private ScheduledExecutorService scheduler;
+
+    @Nullable
+    private BackoffStrategy backoffStrategy;
+
+    @Nullable
+    private SseListener sseListener;
+
+    @Nullable
+    private SseConnection sseConnection;
+
+    @Nullable
+    private HeadersListener registeredHeadersListener;
+
+    @Nullable
+    private CompleteListener registeredCompleteListener;
+
+    private SseRequestFactory mockSseRequestFactory(@Nullable Request request) {
+        SseRequestFactory factory = mock(SseRequestFactory.class);
+        when(factory.createSseRequest(URL)).thenReturn(request);
+        return factory;
+    }
+
+    private ScheduledExecutorService mockScheduler() {
+        return mock(ScheduledExecutorService.class);
+    }
+
+    private Request mockRequest() {
+        Request request = mock(Request.class);
+        when(request.onResponseHeaders(any())).thenAnswer(invocation -> {
+            registeredHeadersListener = invocation.getArgument(0);
+            return request;
+        });
+        when(request.onComplete(any())).thenAnswer(invocation -> {
+            registeredCompleteListener = invocation.getArgument(0);
+            return request;
+        });
+        when(request.idleTimeout(anyLong(), any())).thenReturn(request);
+        when(request.timeout(anyLong(), any())).thenReturn(request);
+        return request;
+    }
+
+    private BackoffStrategy mockBackoffStrategy() {
+        BackoffStrategy backoffStrategy = mock(BackoffStrategy.class);
+        when(backoffStrategy.getSecondsUntilRetry(anyInt())).thenReturn(10L);
+        when(backoffStrategy.getMinimumSecondsUntilRetry()).thenReturn(5L);
+        when(backoffStrategy.getMaximumSecondsUntilRetry()).thenReturn(3600L);
+        return backoffStrategy;
+    }
+
+    private void setUpRunningConnection() {
+        request = mockRequest();
+        sseRequestFactory = mockSseRequestFactory(request);
+        scheduler = mockScheduler();
+        backoffStrategy = mockBackoffStrategy();
+        sseConnection = new SseConnection(URL, getMockedSseRequestFactory(), getMockedScheduler(),
+                getMockedBackoffStrategy());
+
+        sseListener = mock(SseListener.class);
+        getSseConnection().addSseListener(getMockedSseListener());
+        getSseConnection().connect();
+
+        getRegisteredHeadersListener().onHeaders(null);
+    }
+
+    private Request getMockedRequest() {
+        Request request = this.request;
+        assertNotNull(request);
+        return Objects.requireNonNull(request);
+    }
+
+    private SseRequestFactory getMockedSseRequestFactory() {
+        SseRequestFactory sseRequestFactory = this.sseRequestFactory;
+        assertNotNull(sseRequestFactory);
+        return Objects.requireNonNull(sseRequestFactory);
+    }
+
+    private ScheduledExecutorService getMockedScheduler() {
+        ScheduledExecutorService scheduler = this.scheduler;
+        assertNotNull(scheduler);
+        return Objects.requireNonNull(scheduler);
+    }
+
+    private BackoffStrategy getMockedBackoffStrategy() {
+        BackoffStrategy backoffStrategy = this.backoffStrategy;
+        assertNotNull(backoffStrategy);
+        return Objects.requireNonNull(backoffStrategy);
+    }
+
+    private SseListener getMockedSseListener() {
+        SseListener sseListener = this.sseListener;
+        assertNotNull(sseListener);
+        return Objects.requireNonNull(sseListener);
+    }
+
+    private SseConnection getSseConnection() {
+        SseConnection sseConnection = this.sseConnection;
+        assertNotNull(sseConnection);
+        return Objects.requireNonNull(sseConnection);
+    }
+
+    private HeadersListener getRegisteredHeadersListener() {
+        HeadersListener headersListener = registeredHeadersListener;
+        assertNotNull(headersListener);
+        return Objects.requireNonNull(headersListener);
+    }
+
+    private CompleteListener getRegisteredCompleteListener() {
+        CompleteListener completeListener = registeredCompleteListener;
+        assertNotNull(completeListener);
+        return Objects.requireNonNull(completeListener);
+    }
+
+    @Test
+    public void whenSseConnectionIsConnectedThenTheConnectionRequestIsMade() throws Exception {
+        // given:
+        Request request = mockRequest();
+        SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+        ScheduledExecutorService scheduler = mockScheduler();
+        SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+
+        // when:
+        sseConnection.connect();
+
+        // then:
+        verify(request).send(any());
+    }
+
+    @Test
+    public void whenSseConnectionIsConnectedButNoRequestIsCreatedThenOnlyTheDesiredConnectionStateChanges()
+            throws Exception {
+        // given:
+        SseRequestFactory sseRequestFactory = mockSseRequestFactory(null);
+        ScheduledExecutorService scheduler = mockScheduler();
+        SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+
+        // when:
+        sseConnection.connect();
+
+        // then:
+        assertTrue(((Boolean) getPrivate(sseConnection, "active")).booleanValue());
+    }
+
+    @Test
+    public void whenHeadersAreReceivedAfterTheSseConnectionWasConnectedThenTheEventStreamParserIsScheduled()
+            throws Exception {
+        // given:
+        Request request = mockRequest();
+        SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+        ScheduledExecutorService scheduler = mockScheduler();
+        SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+        sseConnection.connect();
+        HeadersListener headersListener = registeredHeadersListener;
+        assertNotNull(headersListener);
+
+        // when:
+        headersListener.onHeaders(null);
+
+        // then:
+        verify(scheduler).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+    }
+
+    @Test
+    public void whenTheSseStreamIsClosedWithATimeoutThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        // when:
+        invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class }, new TimeoutException());
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.TIMEOUT, 0);
+        verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+    }
+
+    @Test
+    public void whenTheSseStreamIsClosedDueToAJetty401ErrorThenNoReconnectIsScheduledAndATokenRefreshIsRequested()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        // when:
+        invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class }, new RuntimeException(
+                AuthorizationFailedRetryStrategy.JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE));
+
+        // then:
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verifyNoMoreInteractions(getMockedScheduler());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+    }
+
+    @Test
+    public void whenTheSseStreamIsClosedWithADifferentExceptionThanATimeoutThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        // when:
+        invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class },
+                new IllegalStateException());
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+        verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithoutResultThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        // when:
+        getRegisteredCompleteListener().onComplete(null);
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+        verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithoutResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Result result = mock(Result.class);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+        verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithASuccessfulResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(200);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+        verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithAnAuthorizationFailedResponseThenTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(401);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithoutRetryAfterHeaderThenAReconnectIsScheduledAccordingToTheBackoffStrategyAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(429);
+        when(response.getHeaders()).thenReturn(new HttpFields());
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(10L), eq(TimeUnit.SECONDS));
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+        verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithRetryAfterHeaderThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(429);
+        HttpFields httpFields = new HttpFields();
+        httpFields.add("Retry-After", "3600");
+        when(response.getHeaders()).thenReturn(httpFields);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(3600L), eq(TimeUnit.SECONDS));
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithRetryAfterHeaderWithTooLowValueThenAReconnectIsScheduledWithTheMinimumWaitTime()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(429);
+        HttpFields httpFields = new HttpFields();
+        httpFields.add("Retry-After", "1");
+        when(response.getHeaders()).thenReturn(httpFields);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(5L), eq(TimeUnit.SECONDS));
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithRetryAfterHeaderWithTooHighValueThenAReconnectIsScheduledWithTheMaximumWaitTime()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(429);
+        HttpFields httpFields = new HttpFields();
+        httpFields.add("Retry-After", "3601");
+        when(response.getHeaders()).thenReturn(httpFields);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(3600L), eq(TimeUnit.SECONDS));
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithAnInternalServerErrorResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(500);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.SERVER_ERROR, 0);
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithAnInternalServerErrorResponseMultipleTimesThenTheConnectionFailedCounterIsIncrementedEachTime()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(500);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.SERVER_ERROR, 0);
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.SERVER_ERROR, 1);
+    }
+
+    @Test
+    public void whenTheSseRequestCompletesWithAnUnknownErrorResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        Response response = mock(Response.class);
+        when(response.getStatus()).thenReturn(600);
+
+        Result result = mock(Result.class);
+        when(result.getResponse()).thenReturn(response);
+
+        // when:
+        getRegisteredCompleteListener().onComplete(result);
+
+        // then:
+        verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verify(getMockedSseListener()).onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 0);
+        verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+    }
+
+    @Test
+    public void whenAServerSentEventIsReceivedThenItIsForwardedToTheListenersAndTheFailedConnectionCounterIsReset()
+            throws Exception {
+        // given:
+        Request request = mockRequest();
+        SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+        ScheduledExecutorService scheduler = mockScheduler();
+
+        BackoffStrategy backoffStrategy = mock(BackoffStrategy.class);
+        when(backoffStrategy.getSecondsUntilRetry(anyInt())).thenReturn(10L);
+
+        SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler, backoffStrategy);
+        SseListener sseListener = mock(SseListener.class);
+        sseConnection.addSseListener(sseListener);
+        setPrivate(sseConnection, "failedConnectionAttempts", 10);
+        sseConnection.connect();
+
+        HeadersListener headersListener = registeredHeadersListener;
+        assertNotNull(headersListener);
+        headersListener.onHeaders(null);
+
+        ServerSentEvent serverSentEvent = new ServerSentEvent("ping", "ping");
+
+        // when:
+        invokePrivate(sseConnection, "onServerSentEvent", serverSentEvent);
+
+        // then:
+        verify(sseListener).onServerSentEvent(serverSentEvent);
+        assertEquals(0, (int) getPrivate(sseConnection, "failedConnectionAttempts"));
+    }
+
+    @Test
+    public void whenTheSseStreamIsDisconnectedThenTheRunningRequestIsAborted() throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        // when:
+        getSseConnection().disconnect();
+
+        // then:
+        verify(getMockedRequest()).abort(any());
+        assertNull(getPrivate(getSseConnection(), "sseRequest"));
+    }
+
+    @Test
+    public void whenTheSseStreamIsDisconnectedThenTheConnectionIsClosedAndNoReconnectIsScheduledAndTheListenersAreNotNotified()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+
+        // when:
+        getSseConnection().disconnect();
+        invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class },
+                new MieleWebserviceDisconnectSseException());
+
+        // then:
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verifyNoMoreInteractions(getMockedScheduler());
+        verifyNoInteractions(getMockedSseListener());
+    }
+
+    @Test
+    public void whenAPendingReconnectAttemptIsPerformedAfterTheSseConnectionWasDisconnectedThenTheConnectionIsNotRestored()
+            throws Exception {
+        // given:
+        setUpRunningConnection();
+        getSseConnection().disconnect();
+
+        // when:
+        invokePrivate(getSseConnection(), "connectInternal");
+
+        // then:
+        verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+        verifyNoMoreInteractions(getMockedScheduler());
+        verifyNoInteractions(getMockedSseListener());
+    }
+
+    @Test
+    public void whenTheSseConnectionIsConnectedMultipleTimesWithoutDisconnectingThenOnlyTheFirstConnectResultsInAnConnectionAttempt()
+            throws Exception {
+        // given:
+        Request request = mockRequest();
+        SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+        ScheduledExecutorService scheduler = mockScheduler();
+        SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+        sseConnection.connect();
+
+        // when:
+        sseConnection.connect();
+
+        // then:
+        verify(request, times(1)).onResponseHeaders(any());
+    }
+
+    @Test
+    public void whenTheSseConnectionIsDisconnectedMultipleTimesWithoutConnectingAgainThenOnlyTheFirstDisconnectIsPerformed()
+            throws Exception {
+        // given:
+        Request request = mockRequest();
+        SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+        ScheduledExecutorService scheduler = mockScheduler();
+        SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+        sseConnection.connect();
+        sseConnection.disconnect();
+
+        // when:
+        sseConnection.disconnect();
+
+        // then:
+        verify(request, times(1)).abort(any());
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseStreamParserTest.java b/bundles/org.openhab.binding.mielecloud/src/test/java/org/openhab/binding/mielecloud/internal/webservice/sse/SseStreamParserTest.java
new file mode 100644 (file)
index 0000000..631a324
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class SseStreamParserTest {
+    @Mock
+    @NonNullByDefault({})
+    private Consumer<ServerSentEvent> serverSentEventCallback;
+
+    @Mock
+    @NonNullByDefault({})
+    private Consumer<@Nullable Throwable> streamClosedCallback;
+
+    private InputStream getInputStreamReadingUtf8Data(String data) {
+        return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void whenNoEventIsProvidedThenTheStreamClosedCallbackIsInvoked() {
+        // given:
+        InputStream inputStream = getInputStreamReadingUtf8Data("");
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verify(streamClosedCallback).accept(null);
+        verifyNoMoreInteractions(streamClosedCallback);
+        verifyNoInteractions(serverSentEventCallback);
+    }
+
+    @Test
+    public void whenNoEventAndOnlyWhitespaceIsProvidedThenTheStreamClosedCallbackIsInvoked() {
+        // given:
+        InputStream inputStream = getInputStreamReadingUtf8Data("\r\n");
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verify(streamClosedCallback).accept(null);
+        verifyNoMoreInteractions(streamClosedCallback);
+        verifyNoInteractions(serverSentEventCallback);
+    }
+
+    @Test
+    public void whenAnEventIsProvidedThenItIsPassedToTheCallback() {
+        // given:
+        InputStream inputStream = getInputStreamReadingUtf8Data("event: ping\r\ndata: pong\r\n");
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verify(streamClosedCallback).accept(null);
+        verify(serverSentEventCallback).accept(new ServerSentEvent("ping", "pong"));
+        verifyNoMoreInteractions(streamClosedCallback, serverSentEventCallback);
+    }
+
+    @Test
+    public void whenALineWithInvalidKeyIsProvidedThenItIsIgnored() {
+        // given:
+        InputStream inputStream = getInputStreamReadingUtf8Data("name: ping\r\n");
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verify(streamClosedCallback).accept(null);
+        verifyNoMoreInteractions(streamClosedCallback);
+        verifyNoInteractions(serverSentEventCallback);
+    }
+
+    @Test
+    public void whenDataWithoutEventIsProvidedThenItIsIgnored() {
+        // given:
+        InputStream inputStream = getInputStreamReadingUtf8Data("data: ping\r\n");
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verify(streamClosedCallback).accept(null);
+        verifyNoMoreInteractions(streamClosedCallback);
+        verifyNoInteractions(serverSentEventCallback);
+    }
+
+    @Test
+    public void whenTheEventStreamBreaksThenTheStreamClosedCallbackIsNotifiedWithTheCause() throws IOException {
+        // given:
+        InputStream inputStream = mock(InputStream.class);
+        TimeoutException timeoutException = new TimeoutException();
+        when(inputStream.read(any(), anyInt(), anyInt())).thenThrow(new IOException(timeoutException));
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verify(streamClosedCallback).accept(timeoutException);
+        verifyNoMoreInteractions(streamClosedCallback);
+        verifyNoInteractions(serverSentEventCallback);
+    }
+
+    @Test
+    public void whenTheEventStreamBreaksBecauseOfAnSseDisconnectThenTheStreamCloseCallbackIsNotNotifiedToPreventSseReconnect()
+            throws IOException {
+        // given:
+        InputStream inputStream = mock(InputStream.class);
+        when(inputStream.read(any(), anyInt(), anyInt()))
+                .thenThrow(new IOException(new MieleWebserviceDisconnectSseException()));
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verifyNoInteractions(streamClosedCallback, serverSentEventCallback);
+    }
+
+    @Test
+    public void whenTheEventStreamBreaksAndTheResourceCleanupFailsThenItIsIgnored() throws IOException {
+        // given:
+        InputStream inputStream = mock(InputStream.class);
+        when(inputStream.read(any(), anyInt(), anyInt()))
+                .thenThrow(new IOException(new MieleWebserviceDisconnectSseException()));
+        doThrow(new IOException()).when(inputStream).close();
+
+        SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+        // when:
+        parser.parseAndDispatchEvents();
+
+        // then:
+        verifyNoInteractions(streamClosedCallback, serverSentEventCallback);
+    }
+}
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollection.json b/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollection.json
new file mode 100644 (file)
index 0000000..6de734f
--- /dev/null
@@ -0,0 +1,123 @@
+{
+    "000124430017": {
+        "ident": {
+            "type": {
+                "key_localized": "Devicetype",
+                "value_raw": 18,
+                "value_localized": "Ventilation Hood"
+            },
+            "deviceName": "My Hood",
+                "deviceIdentLabel": {
+                "fabNumber": "000124430017",
+                "fabIndex": "00",
+                "techType": "DA-6996",
+                "matNumber": "10101010",
+                "swids": [
+                    "4164",
+                    "20380",
+                    "25226"
+                ]
+            },
+            "xkmIdentLabel": {
+                "techType": "EK039W",
+                "releaseVersion": "02.31"
+            }
+        },
+        "state": {
+            "status": {
+                "value_raw": 5,
+                "value_localized": "In use",
+                "key_localized": "State"
+            },
+            "programType": {
+                "value_raw": 0,
+                "value_localized": "",
+                "key_localized": "Programme"
+            },
+            "programPhase": {
+                "value_raw": 4609,
+                "value_localized": "",
+                "key_localized": "Phase"
+            },
+            "remainingTime": [
+                0,
+                0
+            ],
+            "startTime": [
+                0,
+                0
+            ],
+            "targetTemperature": [
+                {
+                    "value_raw": -32768,
+                    "value_localized": null,
+                    "unit": "Celsius"
+                }
+            ],
+            "temperature": [
+                {
+                    "value_raw": -32768,
+                    "value_localized": null,
+                    "unit": "Celsius"
+                },
+                {
+                    "value_raw": -32768,
+                    "value_localized": null,
+                    "unit": "Celsius"
+                },
+                {
+                    "value_raw": -32768,
+                    "value_localized": null,
+                    "unit": "Celsius"
+                }
+            ],
+            "signalInfo": false,
+            "signalFailure": false,
+            "signalDoor": false,
+            "remoteEnable": {
+                "fullRemoteControl": false,
+                "smartGrid": false
+            },
+            "light": 1,
+            "elapsedTime": [],
+            "spinningSpeed": {
+                "value_raw" : 1200,
+                "value_localized" : 1200,
+                "unit" : "rpm"
+            },
+            "dryingStep": {
+                "value_raw": null,
+                "value_localized": "",
+                "key_localized": "Drying level"
+            },
+            "ventilationStep": {
+                "value_raw": 2,
+                "value_localized": "2",
+                "key_localized": "Power Level"
+            },
+            "plateStep": [
+                {
+                    "value_raw": 0,
+                    "value_localized": "0",
+                    "key_localized": "Plate Step"
+                },
+                {
+                    "value_raw": 1,
+                    "value_localized": "1",
+                    "key_localized": "Plate Step"
+                },
+                {
+                    "value_raw": 2,
+                    "value_localized": "1.",
+                    "key_localized": "Plate Step"
+                },
+                {
+                    "value_raw": 3,
+                    "value_localized": "2",
+                    "key_localized": "Plate Step"
+                }
+            ],
+            "batteryLevel": 20
+        }
+    }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithFloatingPointTargetTemperature.json b/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithFloatingPointTargetTemperature.json
new file mode 100644 (file)
index 0000000..a68bca7
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "000091465021": {
+        "state": {
+            "targetTemperature": [
+                {
+                    "value_raw": 80,
+                    "value_localized": 0.8,
+                    "unit": "Celsius"
+                }
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithLargeProgramID.json b/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithLargeProgramID.json
new file mode 100644 (file)
index 0000000..3e10b01
--- /dev/null
@@ -0,0 +1,73 @@
+{
+    "mac-00124B000AE539D6": {
+        "ident": {
+            "type": {
+                "key_localized": "Devicetype",
+                "value_raw": 100,
+                "value_localized": ""
+            },
+            "deviceName": "Some Devicename",
+            "deviceIdentLabel": {
+                "fabNumber": "",
+                "fabIndex": "",
+                "techType": "",
+                "matNumber": "",
+                "swids": []
+            },
+            "xkmIdentLabel": {
+                "techType": "",
+                "releaseVersion": ""
+            }
+        },
+        "state": {
+            "ProgramID": {
+                "value_raw": 2499805184,
+                "value_localized": "",
+                "key_localized": "Program Id"
+            },
+            "status": {
+                "value_raw": 5,
+                "value_localized": "In use",
+                "key_localized": "State"
+            },
+            "programType": {
+                "value_raw": 0,
+                "value_localized": "Operation mode",
+                "key_localized": "Program type"
+            },
+            "programPhase": {
+                "value_raw": 0,
+                "value_localized": "",
+                "key_localized": "Phase"
+            },
+            "remainingTime": [
+                0,
+                0
+            ],
+            "startTime": [
+                0,
+                0
+            ],
+            "targetTemperature": [],
+            "signalInfo": false,
+            "signalFailure": false,
+            "signalDoor": false,
+            "remoteEnable": {
+                "fullRemoteControl": true,
+                "smartGrid": false
+            },
+            "light": 0,
+            "elapsedTime": [],
+            "dryingStep": {
+                "value_raw": null,
+                "value_localized": "",
+                "key_localized": "Drying level"
+            },
+            "ventilationStep": {
+                "value_raw": null,
+                "value_localized": "",
+                "key_localized": "Power Level"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithSpinningSpeedObject.json b/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithSpinningSpeedObject.json
new file mode 100644 (file)
index 0000000..583e673
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "000124430017": {
+        "state": {
+            "spinningSpeed": {
+                "value_raw" : 1600,
+                "value_localized" : 1600,
+                "unit" : "U/min"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/invalidDeviceCollection.json b/bundles/org.openhab.binding.mielecloud/src/test/resources/org/openhab/binding/mielecloud/internal/webservice/api/json/invalidDeviceCollection.json
new file mode 100644 (file)
index 0000000..62e7f68
--- /dev/null
@@ -0,0 +1,73 @@
+{
+    "000124430017": {
+        "ident": {
+            "type": {
+                "key_localized": "Devicetype",
+                10",
+                "swids": [
+                    "4164",
+                    "20380",
+                    "25226"
+                ]
+            },
+            "xkmIdentLabel": {
+                "techType": "EK039W",
+                "releaseVersion": "02.31"
+            }
+        },
+        "state": {
+            "status": {
+                "value_raw": 5,
+                "value_localized": "In use",
+                "key_localized": "State"
+            },
+            "programType": {
+                "value_raw": 0,
+                "value_localized": "",
+                "key_localized": "Programme"
+            },
+            "programPhase": {
+                "value_raw": 4609,
+                "value_localized": "",
+                "key_localized": "Phase"
+            },
+            "remainingTime": [
+                0,
+                0
+            ],
+            "startTime": [
+                0,
+                0
+            ],
+            "targetT
+                    "unit": "Celsius"
+                },
+                {
+                    "value_raw": -32768,
+                    "value_localized": null,
+                    "unit": "Celsius"
+                }
+            ],
+            "signalInfo": false,
+            "signalFailure": false,
+            "signalDoor": false,
+            "remoteEnable": {
+                "fullRemoteControl": false,
+                "smartGrid": false
+            },
+            "light": 1,
+            "elapsedTime": [],
+            "dryingStep": {
+                "value_raw": null,
+                "value_localized": "",
+                "key_localized": "Drying level"
+            },
+            "ventilationStep": {
+                "value_raw": 2,
+                "value_localized": "2",
+                "key_localized": "Power Level"
+            }
+        }
+    },
+    "$$ref": "#/components/examples/devicesExample"
+}
\ No newline at end of file
index 2205dbaa7c12a4c45f18078b0b1263f593952d63..fef76662048d3d820e7713fc19c18a470db1738a 100644 (file)
     <module>org.openhab.binding.meteoblue</module>
     <module>org.openhab.binding.meteostick</module>
     <module>org.openhab.binding.miele</module>
+    <module>org.openhab.binding.mielecloud</module>
     <module>org.openhab.binding.mihome</module>
     <module>org.openhab.binding.miio</module>
     <module>org.openhab.binding.millheat</module>
diff --git a/itests/org.openhab.binding.mielecloud.tests/NOTICE b/itests/org.openhab.binding.mielecloud.tests/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/itests/org.openhab.binding.mielecloud.tests/itest.bndrun b/itests/org.openhab.binding.mielecloud.tests/itest.bndrun
new file mode 100644 (file)
index 0000000..1c4f635
--- /dev/null
@@ -0,0 +1,88 @@
+-include: ../itest-common.bndrun
+
+Bundle-SymbolicName: ${project.artifactId}
+Fragment-Host: org.openhab.binding.mielecloud
+
+-runrequires: \
+    bnd.identity;id='org.openhab.binding.mielecloud.tests',\
+    bnd.identity;id='org.openhab.core.binding.xml',\
+    bnd.identity;id='org.openhab.core.thing.xml'
+
+-runblacklist: \
+    bnd.identity;id='org.openhab.core.storage.json',\
+    bnd.identity;id='org.openhab.core.storage.mapdb'
+
+#
+# done
+#
+-runbundles: \
+       org.eclipse.equinox.event;version='[1.4.300,1.4.301)',\
+       org.osgi.service.event;version='[1.4.0,1.4.1)',\
+       org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)',\
+       org.apache.felix.http.servlet-api;version='[1.1.2,1.1.3)',\
+       com.sun.xml.bind.jaxb-osgi;version='[2.3.3,2.3.4)',\
+       jakarta.xml.bind-api;version='[2.3.3,2.3.4)',\
+       org.opentest4j;version='[1.2.0,1.2.1)',\
+       org.hamcrest;version='[2.2.0,2.2.1)',\
+       junit-jupiter-api;version='[5.7.0,5.7.1)',\
+       junit-jupiter-engine;version='[5.7.0,5.7.1)',\
+       junit-platform-commons;version='[1.7.0,1.7.1)',\
+       junit-platform-engine;version='[1.7.0,1.7.1)',\
+       junit-platform-launcher;version='[1.7.0,1.7.1)',\
+       net.bytebuddy.byte-buddy;version='[1.10.19,1.10.20)',\
+       net.bytebuddy.byte-buddy-agent;version='[1.10.19,1.10.20)',\
+       org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
+       org.mockito.mockito-core;version='[3.7.0,3.7.1)',\
+       org.objenesis;version='[3.1.0,3.1.1)',\
+       org.openhab.binding.mielecloud;version='[3.1.0,3.1.1)',\
+       org.openhab.binding.mielecloud.tests;version='[3.1.0,3.1.1)',\
+       org.openhab.core;version='[3.1.0,3.1.1)',\
+       org.openhab.core.auth.oauth2client;version='[3.1.0,3.1.1)',\
+       org.openhab.core.binding.xml;version='[3.1.0,3.1.1)',\
+       org.openhab.core.config.core;version='[3.1.0,3.1.1)',\
+       org.openhab.core.config.discovery;version='[3.1.0,3.1.1)',\
+       org.openhab.core.config.xml;version='[3.1.0,3.1.1)',\
+       org.openhab.core.io.console;version='[3.1.0,3.1.1)',\
+       org.openhab.core.io.net;version='[3.1.0,3.1.1)',\
+       org.openhab.core.test;version='[3.1.0,3.1.1)',\
+       org.openhab.core.thing;version='[3.1.0,3.1.1)',\
+       org.openhab.core.thing.xml;version='[3.1.0,3.1.1)',\
+       biz.aQute.tester.junit-platform;version='[5.3.0,5.3.1)',\
+       com.google.gson;version='[2.8.6,2.8.7)',\
+       org.apache.felix.scr;version='[2.1.26,2.1.27)',\
+       org.objectweb.asm;version='[9.1.0,9.1.1)',\
+       org.objectweb.asm.commons;version='[9.0.0,9.0.1)',\
+       org.objectweb.asm.tree;version='[9.0.0,9.0.1)',\
+       org.osgi.util.function;version='[1.1.0,1.1.1)',\
+       org.osgi.util.promise;version='[1.1.1,1.1.2)',\
+       jakarta.annotation-api;version='[2.0.0,2.0.1)',\
+       jakarta.inject.jakarta.inject-api;version='[2.0.0,2.0.1)',\
+       javax.measure.unit-api;version='[2.1.2,2.1.3)',\
+       org.apache.felix.configadmin;version='[1.9.22,1.9.23)',\
+       org.apache.xbean.bundleutils;version='[4.19.0,4.19.1)',\
+       org.apache.xbean.finder;version='[4.19.0,4.19.1)',\
+       org.eclipse.jetty.client;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.http;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.io;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.security;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.server;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.servlet;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.util;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.util.ajax;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.websocket.api;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.websocket.client;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.websocket.common;version='[9.4.40,9.4.41)',\
+       org.eclipse.jetty.xml;version='[9.4.40,9.4.41)',\
+       org.glassfish.hk2.external.javax.inject;version='[2.4.0,2.4.1)',\
+       org.jsr-305;version='[3.0.2,3.0.3)',\
+       org.ops4j.pax.web.pax-web-api;version='[7.3.16,7.3.17)',\
+       org.ops4j.pax.web.pax-web-jetty;version='[7.3.16,7.3.17)',\
+       org.ops4j.pax.web.pax-web-runtime;version='[7.3.16,7.3.17)',\
+       org.ops4j.pax.web.pax-web-spi;version='[7.3.16,7.3.17)',\
+       org.osgi.service.cm;version='[1.6.0,1.6.1)',\
+       si-units;version='[2.0.1,2.0.2)',\
+       si.uom.si-quantity;version='[2.0.1,2.0.2)',\
+       tech.units.indriya;version='[2.1.2,2.1.3)',\
+       uom-lib-common;version='[2.1.0,2.1.1)',\
+       xstream;version='[1.4.17,1.4.18)',\
+       org.ops4j.pax.logging.pax-logging-api;version='[2.0.9,2.0.10)'
diff --git a/itests/org.openhab.binding.mielecloud.tests/pom.xml b/itests/org.openhab.binding.mielecloud.tests/pom.xml
new file mode 100644 (file)
index 0000000..1bb4520
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.itests</groupId>
+    <artifactId>org.openhab.addons.reactor.itests</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.mielecloud.tests</artifactId>
+
+  <name>openHAB Add-ons :: Integration Tests :: mielecloud Binding Tests</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.mielecloud</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/ConfigFlowTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/ConfigFlowTest.java
new file mode 100644 (file)
index 0000000..70f545c
--- /dev/null
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.binding.mielecloud.internal.handler.MieleHandlerFactory;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ConfigFlowTest extends AbstractConfigFlowTest {
+    private void setUpAuthorizationHandler() throws NoSuchFieldException, IllegalAccessException {
+        OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+        when(authorizationHandler.getAccessToken(MieleCloudBindingIntegrationTestConstants.EMAIL))
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.ACCESS_TOKEN);
+        when(authorizationHandler.getBridgeUid())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+        when(authorizationHandler.getEmail()).thenReturn(MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        setPrivate(getResultServlet(), "authorizationHandler", authorizationHandler);
+    }
+
+    private void setUpWebservice() throws Exception {
+        MieleWebservice webservice = mock(MieleWebservice.class);
+        doAnswer(invocation -> {
+            Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+            assertNotNull(bridge);
+            ThingHandler handler = bridge.getHandler();
+            if (handler instanceof MieleBridgeHandler) {
+                ((MieleBridgeHandler) handler).onConnectionAlive();
+            }
+            return null;
+        }).when(webservice).addConnectionStatusListener(any());
+
+        MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
+        when(webserviceFactory.create(any())).thenReturn(webservice);
+
+        MieleHandlerFactory handlerFactory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+        assertNotNull(handlerFactory);
+        setPrivate(Objects.requireNonNull(handlerFactory), "webserviceFactory", webserviceFactory);
+    }
+
+    private Website configureBridgeWithConfigFlow() throws Exception {
+        Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+        String pairAccountUrl = accountOverviewSite.getTargetOfLink("Pair Account");
+
+        Website pairAccountSite = getCrawler().doGetRelative(pairAccountUrl);
+        String forwardToLoginUrl = pairAccountSite.getFormAction();
+
+        Website mieleLoginSite = getCrawler().doGetRelative(forwardToLoginUrl + "?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+        String redirectionUrl = mieleLoginSite.getValueOfInput("redirect_uri").replace("http://127.0.0.1:8080", "");
+        String state = mieleLoginSite.getValueOfInput("state");
+
+        Website resultSite = getCrawler().doGetRelative(redirectionUrl + "?code="
+                + MieleCloudBindingIntegrationTestConstants.AUTHORIZATION_CODE + "&state=" + state);
+        String createBridgeUrl = resultSite.getFormAction();
+
+        Website finalOverview = getCrawler().doGetRelative(createBridgeUrl + "?"
+                + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.toString() + "&"
+                + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+        return finalOverview;
+    }
+
+    @Test
+    public void configFlowHappyPathCreatesABridge() throws Exception {
+        // given:
+        setUpAuthorizationHandler();
+        setUpWebservice();
+
+        // when:
+        Website finalOverview = configureBridgeWithConfigFlow();
+
+        // then:
+        assertTrue(finalOverview.contains("<span class=\"status online\">ONLINE</span>"));
+
+        Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+        assertNotNull(bridge);
+        assertEquals(ThingStatus.ONLINE, bridge.getStatus());
+    }
+
+    @Test
+    public void configFlowWaitTimeoutExpiresWhenBridgeDoesNotComeOnline() throws Exception {
+        // given:
+        setUpAuthorizationHandler();
+        ReflectionUtil.setPrivateStaticFinal(CreateBridgeServlet.class, "ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS", 0);
+
+        // when:
+        configureBridgeWithConfigFlow();
+
+        // then:
+        Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+        assertNotNull(bridge);
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServletTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServletTest.java
new file mode 100644 (file)
index 0000000..da318dc
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.*;
+
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class AccountOverviewServletTest extends AbstractConfigFlowTest {
+    @Test
+    public void whenAccountOverviewServletIsCalledOverNonSslConnectionThenAWarningIsShown() throws Exception {
+        // when:
+        Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+
+        // then:
+        assertTrue(accountOverviewSite
+                .contains("Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange."));
+        assertTrue(accountOverviewSite.contains(
+                "See <a href=\"https://www.openhab.org/docs/installation/security.html\">Securing access to openHAB</a> for details."));
+    }
+
+    @Test
+    public void whenAccountOverviewServletIsCalledAndNoBridgeIsPresentThenThePageSaysThatThereIsNoBridgePaired()
+            throws Exception {
+        // when:
+        Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+
+        // then:
+        assertTrue(accountOverviewSite.contains("There is no account paired at the moment."));
+    }
+
+    @Test
+    public void whenAccountOverviewServletIsCalledAndBridgesArePresentThenThePageDisplaysInformationAboutThem()
+            throws Exception {
+        // given:
+        Configuration configuration1 = mock(Configuration.class);
+        when(configuration1.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn("de");
+        when(configuration1.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn("openhab@openhab.org");
+
+        Bridge bridge1 = mock(Bridge.class);
+        when(bridge1.getThingTypeUID()).thenReturn(MieleCloudBindingConstants.THING_TYPE_BRIDGE);
+        when(bridge1.getUID()).thenReturn(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+        when(bridge1.getStatus()).thenReturn(ThingStatus.ONLINE);
+        when(bridge1.getStatusInfo()).thenReturn(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
+        when(bridge1.getConfiguration()).thenReturn(configuration1);
+
+        Configuration configuration2 = mock(Configuration.class);
+        when(configuration2.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn("en");
+        when(configuration2.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn("everyone@openhab.org");
+
+        Bridge bridge2 = mock(Bridge.class);
+        when(bridge2.getThingTypeUID()).thenReturn(MieleCloudBindingConstants.THING_TYPE_BRIDGE);
+        when(bridge2.getUID()).thenReturn(new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, "test"));
+        when(bridge2.getStatus()).thenReturn(ThingStatus.OFFLINE);
+        when(bridge2.getStatusInfo()).thenReturn(
+                new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error message"));
+        when(bridge2.getConfiguration()).thenReturn(configuration2);
+
+        ThingRegistry thingRegistry = mock(ThingRegistry.class);
+        when(thingRegistry.stream()).thenAnswer(invocation -> Stream.of(bridge1, bridge2));
+        ReflectionUtil.setPrivate(getAccountOverviewServlet(), "thingRegistry", thingRegistry);
+
+        // when:
+        Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+
+        // then:
+        assertTrue(accountOverviewSite.contains("The following bridges are paired"));
+        assertTrue(accountOverviewSite.contains("openhab@openhab.org"));
+        assertTrue(accountOverviewSite.contains("mielecloud:account:genesis"));
+        assertTrue(accountOverviewSite.contains("<span class=\"status online\">"));
+        assertTrue(accountOverviewSite.contains("everyone@openhab.org"));
+        assertTrue(accountOverviewSite.contains("mielecloud:account:test"));
+        assertTrue(accountOverviewSite.contains("<span class=\"status offline\">"));
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServletTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServletTest.java
new file mode 100644 (file)
index 0000000..5010aed
--- /dev/null
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.config.MieleCloudConfigService;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class CreateBridgeServletTest extends AbstractConfigFlowTest {
+    @Test
+    public void whenBridgeCreationFailsThenAWarningIsShownOnTheSuccessPage() throws Exception {
+        // given:
+        MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+        assertNotNull(configService);
+
+        CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+        assertNotNull(createBridgeServlet);
+
+        Inbox inbox = mock(Inbox.class);
+        when(inbox.add(any())).thenReturn(true);
+        when(inbox.approve(any(), anyString(), anyString())).thenReturn(null);
+        setPrivate(Objects.requireNonNull(createBridgeServlet), "inbox", inbox);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+                + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing successful!"));
+        assertTrue(website.contains(
+                "Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again."));
+    }
+
+    @Test
+    public void whenBridgeReconfigurationFailsDueToMissingBridgeThenAWarningIsShownOnTheSuccessPage() throws Exception {
+        // given:
+        MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+        assertNotNull(configService);
+
+        CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+        assertNotNull(createBridgeServlet);
+
+        Inbox inbox = mock(Inbox.class);
+        when(inbox.add(any())).thenReturn(false);
+        setPrivate(Objects.requireNonNull(createBridgeServlet), "inbox", inbox);
+
+        ThingRegistry thingRegistry = mock(ThingRegistry.class);
+        when(thingRegistry.get(any())).thenReturn(null);
+        setPrivate(Objects.requireNonNull(createBridgeServlet), "thingRegistry", thingRegistry);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+                + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing successful!"));
+        assertTrue(website.contains(
+                "Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again."));
+    }
+
+    @Test
+    public void whenBridgeReconfigurationFailsDueToMissingBridgeHandlerThenAWarningIsShownOnTheSuccessPage()
+            throws Exception {
+        // given:
+        MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+        assertNotNull(configService);
+
+        CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+        assertNotNull(createBridgeServlet);
+
+        Inbox inbox = mock(Inbox.class);
+        when(inbox.add(any())).thenReturn(false);
+        setPrivate(Objects.requireNonNull(createBridgeServlet), "inbox", inbox);
+
+        Thing bridge = mock(Thing.class);
+        when(bridge.getHandler()).thenReturn(null);
+
+        ThingRegistry thingRegistry = mock(ThingRegistry.class);
+        when(thingRegistry.get(any())).thenReturn(bridge);
+        setPrivate(Objects.requireNonNull(createBridgeServlet), "thingRegistry", thingRegistry);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+                + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing successful!"));
+        assertTrue(website.contains(
+                "Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again."));
+    }
+
+    @Test
+    public void whenBridgeIsReconfiguredThenTheConfigurationParametersAreUpdatedAndTheOverviewPageIsDisplayed()
+            throws Exception {
+        // given:
+        setUpBridge();
+
+        MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+        assertNotNull(configService);
+
+        CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+        assertNotNull(createBridgeServlet);
+
+        OAuthTokenRefresher tokenRefresher = mock(OAuthTokenRefresher.class);
+        when(tokenRefresher.getAccessTokenFromStorage(anyString()))
+                .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.ALTERNATIVE_ACCESS_TOKEN));
+
+        Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+        assertNotNull(bridge);
+        ThingHandler bridgeHandler = bridge.getHandler();
+        assertNotNull(bridgeHandler);
+        setPrivate(Objects.requireNonNull(bridgeHandler), "tokenRefresher", tokenRefresher);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+                + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("<li class=\"active\">Overview</li>"));
+
+        assertEquals(MieleCloudBindingIntegrationTestConstants.ALTERNATIVE_ACCESS_TOKEN,
+                bridge.getProperties().get(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN));
+    }
+
+    @Test
+    public void whenNoBridgeUidIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+                + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing bridge UID."));
+    }
+
+    @Test
+    public void whenAnEmptyBridgeUidIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+                + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "=&" + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing bridge UID."));
+    }
+
+    @Test
+    public void whenAMalformedBridgeUidIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+                + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "=gen!e!sis&"
+                + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Malformed bridge UID."));
+    }
+
+    @Test
+    public void whenNoEmailIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+            throws Exception {
+        // when:
+        Website website = getCrawler()
+                .doGetRelative("/mielecloud/createBridgeThing?" + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString());
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing e-mail address."));
+    }
+
+    @Test
+    public void whenAnEmptyEmailIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+            throws Exception {
+        // when:
+        Website website = getCrawler()
+                .doGetRelative("/mielecloud/createBridgeThing?" + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                        + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing e-mail address."));
+    }
+
+    @Test
+    public void whenAMalformedEmailIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+            throws Exception {
+        // when:
+        Website website = getCrawler()
+                .doGetRelative("/mielecloud/createBridgeThing?" + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                        + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=openhab.openhab.org");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Malformed e-mail address."));
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServletTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServletTest.java
new file mode 100644 (file)
index 0000000..d0c2818
--- /dev/null
@@ -0,0 +1,289 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ForwardToLoginServletTest extends AbstractConfigFlowTest {
+    @Test
+    public void whenAuthorizationCannotBeBegunThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // given:
+        OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+        doThrow(new OngoingAuthorizationException("", LocalDateTime.now().plusMinutes(3))).when(authorizationHandler)
+                .beginAuthorization(anyString(), anyString(), any(), anyString());
+        setPrivate(getForwardToLoginServlet(), "authorizationHandler", authorizationHandler);
+
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains(
+                "There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in 3 minutes."));
+    }
+
+    @Test
+    public void whenNoAuthorizationIsOngoingWhenTheAuthorizationUrlIsRequestedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // given:
+        OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+        doThrow(new NoOngoingAuthorizationException("")).when(authorizationHandler).getAuthorizationUrl(anyString());
+        setPrivate(getForwardToLoginServlet(), "authorizationHandler", authorizationHandler);
+
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains(
+                "Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?"));
+    }
+
+    @Test
+    public void whenNoClientIdIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed() throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing client ID."));
+    }
+
+    @Test
+    public void whenAnEmptyClientIdIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "=&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing client ID."));
+    }
+
+    @Test
+    public void whenNoClientSecretIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing client secret."));
+    }
+
+    @Test
+    public void whenAnEmptyClientSecretIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "=" + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing client secret."));
+    }
+
+    @Test
+    public void whenOAuthClientDoesNotProvideAnAuthorizationUrlThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // given:
+        OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+        doThrow(new OAuthException("")).when(authorizationHandler).getAuthorizationUrl(anyString());
+        setPrivate(getForwardToLoginServlet(), "authorizationHandler", authorizationHandler);
+
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Failed to derive redirect URL."));
+    }
+
+    @Test
+    public void whenNoBridgeUidIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing bridge ID."));
+    }
+
+    @Test
+    public void whenAnEmptyBridgeUidIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "=" + "&"
+                + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing bridge ID."));
+    }
+
+    @Test
+    public void whenAMalformedBridgeUidIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+                + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "=genesis!" + "&"
+                + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite
+                .contains("Malformed bridge ID. A bridge ID may only contain letters, numbers, '-' and '_'!"));
+    }
+
+    @Test
+    public void whenNoEmailIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed() throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler()
+                .doGetRelative("/mielecloud/forwardToLogin?" + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                        + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                        + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID);
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing e-mail address."));
+    }
+
+    @Test
+    public void whenAnEmptyEmailIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler()
+                .doGetRelative("/mielecloud/forwardToLogin?" + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                        + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                        + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&"
+                        + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=");
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Missing e-mail address."));
+    }
+
+    @Test
+    public void whenAMalformedEmailIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+            throws Exception {
+        // when:
+        Website maybePairAccountSite = getCrawler()
+                .doGetRelative("/mielecloud/forwardToLogin?" + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+                        + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+                        + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&"
+                        + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=not_an_Email");
+
+        // then:
+        assertTrue(maybePairAccountSite.contains(
+                "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+        assertTrue(maybePairAccountSite.contains("Malformed e-mail address"));
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServletTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServletTest.java
new file mode 100644 (file)
index 0000000..9cd3d02
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class PairAccountServletTest extends AbstractConfigFlowTest {
+    private static final String CLIENT_ID_INPUT_NAME = "clientId";
+    private static final String CLIENT_SECRET_INPUT_NAME = "clientSecret";
+
+    @Test
+    public void whenPairAccountIsInvokedWithClientIdParameterThenTheParameterIsPlacedInTheInputBox() throws Exception {
+        // when:
+        Website pairAccountSite = getCrawler()
+                .doGetRelative("/mielecloud/pair?" + PairAccountServlet.CLIENT_ID_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_ID);
+
+        // then:
+        assertEquals(MieleCloudBindingIntegrationTestConstants.CLIENT_ID,
+                pairAccountSite.getValueOfInput(CLIENT_ID_INPUT_NAME));
+        assertEquals("", pairAccountSite.getValueOfInput(CLIENT_SECRET_INPUT_NAME));
+    }
+
+    @Test
+    public void whenPairAccountIsInvokedWithClientSecretParameterThenTheParameterIsPlacedInTheInputBox()
+            throws Exception {
+        // when:
+        Website pairAccountSite = getCrawler()
+                .doGetRelative("/mielecloud/pair?" + PairAccountServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+                        + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET);
+
+        // then:
+        assertEquals("", pairAccountSite.getValueOfInput(CLIENT_ID_INPUT_NAME));
+        assertEquals(MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET,
+                pairAccountSite.getValueOfInput(CLIENT_SECRET_INPUT_NAME));
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServletTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServletTest.java
new file mode 100644 (file)
index 0000000..226743c
--- /dev/null
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.Website;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ResultServletTest extends AbstractConfigFlowTest {
+    @Test
+    public void whenOAuthErrorAccessDeniedIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+                + FailureServlet.OAUTH2_ERROR_ACCESS_DENIED);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Access denied."));
+    }
+
+    @Test
+    public void whenOAuthErrorInvalidRequestIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+                + FailureServlet.OAUTH2_ERROR_INVALID_REQUEST);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Malformed request."));
+    }
+
+    @Test
+    public void whenOAuthErrorUnauthorizedClientIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+                + FailureServlet.OAUTH2_ERROR_UNAUTHORIZED_CLIENT);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains(
+                "OAuth2 authentication with Miele cloud service failed: Account not authorized to request authorization code."));
+    }
+
+    @Test
+    public void whenOAuthErrorUnsupportedResponseTypeIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+                + FailureServlet.OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains(
+                "OAuth2 authentication with Miele cloud service failed: Obtaining an authorization code is not supported."));
+    }
+
+    @Test
+    public void whenOAuthErrorInvalidScopeIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+                + FailureServlet.OAUTH2_ERROR_INVALID_SCOPE);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Invalid scope."));
+    }
+
+    @Test
+    public void whenOAuthErrorServerErrorIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+                + FailureServlet.OAUTH2_ERROR_SERVER_ERROR);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Unexpected server error."));
+    }
+
+    @Test
+    public void whenOAuthErrorTemporarilyUnavailableIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+                + FailureServlet.OAUTH2_ERROR_TEMPORARY_UNAVAILABLE);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains(
+                "OAuth2 authentication with Miele cloud service failed: Authorization server temporarily unavailable."));
+    }
+
+    @Test
+    public void whenUnknwonOAuthErrorIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler()
+                .doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "=unknown_oauth_2_error");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains(
+                "OAuth2 authentication with Miele cloud service failed: Unknown error code \"unknown_oauth_2_error\"."));
+    }
+
+    @Test
+    public void whenCodeParameterIsNotPassedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler()
+                .doGetRelative("/mielecloud/result?" + ResultServlet.STATE_PARAMETER_NAME + "=state");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Miele cloud service returned an illegal response."));
+    }
+
+    @Test
+    public void whenStateParameterIsNotPassedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // when:
+        Website website = getCrawler()
+                .doGetRelative("/mielecloud/result?" + ResultServlet.CODE_PARAMETER_NAME + "=code");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Miele cloud service returned an illegal response."));
+    }
+
+    @Test
+    public void whenNoAuthorizationIsOngoingThenTheFailurePageWithAccordingErrorMessageIsDisplayed() throws Exception {
+        // given:
+        OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+        doThrow(new NoOngoingAuthorizationException("")).when(authorizationHandler).completeAuthorization(anyString());
+        setPrivate(getResultServlet(), "authorizationHandler", authorizationHandler);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.CODE_PARAMETER_NAME
+                + "=code&" + ResultServlet.STATE_PARAMETER_NAME + "=state");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("There is no ongoing authorization. Please start an authorization first."));
+    }
+
+    @Test
+    public void whenLastStepOfAuthorizationFailsThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+            throws Exception {
+        // given:
+        OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+        doThrow(new OAuthException("")).when(authorizationHandler).completeAuthorization(anyString());
+        setPrivate(getResultServlet(), "authorizationHandler", authorizationHandler);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.CODE_PARAMETER_NAME
+                + "=code&" + ResultServlet.STATE_PARAMETER_NAME + "=state");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website
+                .contains("Completing the final authorization request failed. Please try the config flow again."));
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServletTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServletTest.java
new file mode 100644 (file)
index 0000000..288aff7
--- /dev/null
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class SuccessServletTest extends AbstractConfigFlowTest {
+    @Test
+    public void whenTheSuccessPageIsShownThenAThingsFileTemplateIsPresent() throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\""));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsShownThenTheLocaleIsSelectedAutomatically() throws Exception {
+        // given:
+        LanguageProvider languageProvider = mock(LanguageProvider.class);
+        when(languageProvider.getLanguage()).thenReturn(Optional.of("de"));
+        setPrivate(getSuccessServlet(), "languageProvider", languageProvider);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("<option value=\"de\" selected=\"selected\">Deutsch - de</option>"));
+        assertTrue(website.contains("locale=\"de\""));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsShownAndNoLocaleIsProvidedThenEnglishIsSelectedAutomatically() throws Exception {
+        // given:
+        LanguageProvider languageProvider = mock(LanguageProvider.class);
+        when(languageProvider.getLanguage()).thenReturn(Optional.of("en"));
+        setPrivate(getSuccessServlet(), "languageProvider", languageProvider);
+
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("<option value=\"en\" selected=\"selected\">English - en</option>"));
+        assertTrue(website.contains("locale=\"en\""));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsRequestedAndNoBridgeUidIsPassedThenTheFailurePageIsShown() throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing bridge UID."));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsRequestedAndAnEmptyBridgeUidIsPassedThenTheFailurePageIsShown() throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+                + "=&" + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing bridge UID."));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsRequestedAndAMalformedBridgeUidIsPassedThenTheFailurePageIsShown()
+            throws Exception {
+        // when:
+        Website website = getCrawler()
+                .doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=!genesis&"
+                        + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Malformed bridge UID."));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsRequestedAndNoEmailIsPassedThenTheFailurePageIsShown() throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString());
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing e-mail address."));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsRequestedAndAnEmptyEmailIsPassedThenTheFailurePageIsShown() throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + SuccessServlet.EMAIL_PARAMETER_NAME + "=");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Missing e-mail address."));
+    }
+
+    @Test
+    public void whenTheSuccessPageIsRequestedAndAMalformedEmailIsPassedThenTheFailurePageIsShown() throws Exception {
+        // when:
+        Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+                + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+                + SuccessServlet.EMAIL_PARAMETER_NAME + "=not:an!email");
+
+        // then:
+        assertTrue(website.contains("Pairing failed!"));
+        assertTrue(website.contains("Malformed e-mail address."));
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingDiscoveryTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/discovery/ThingDiscoveryTest.java
new file mode 100644 (file)
index 0000000..d51345a
--- /dev/null
@@ -0,0 +1,406 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.OpenHabOsgiTest;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.config.discovery.inbox.InboxPredicates;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ThingDiscoveryTest extends OpenHabOsgiTest {
+    private static final String DEVICE_TYPE_NAME_COFFEE_SYSTEM = "Coffee System";
+    private static final String DEVICE_TYPE_NAME_DISHWASHER = "Dishwasher";
+    private static final String DEVICE_TYPE_NAME_DISH_WARMER = "Dish Warmer";
+    private static final String DEVICE_TYPE_NAME_DRYER = "Dryer";
+    private static final String DEVICE_TYPE_NAME_FRIDGE_FREEZER = "Fridge Freezer";
+    private static final String DEVICE_TYPE_NAME_HOB = "Hob";
+    private static final String DEVICE_TYPE_NAME_HOOD = "Hood";
+    private static final String DEVICE_TYPE_NAME_OVEN = "Oven";
+    private static final String DEVICE_TYPE_NAME_ROBOTIC_VACUUM_CLEANER = "Robotic Vacuum Cleaner";
+    private static final String DEVICE_TYPE_NAME_WASHING_MACHINE = "Washing Machine";
+    private static final String DEVICE_TYPE_NAME_WINE_STORAGE = "Wine Storage";
+
+    private static final String TECH_TYPE = "WM1234";
+    private static final String TECH_TYPE_2 = "CM1234";
+    private static final String DEVICE_NAME = "My Device";
+    private static final String DEVICE_NAME_2 = "My Other Device";
+    private static final String SERIAL_NUMBER_2 = "900124430017";
+
+    private static final ThingUID DISHWASHER_DEVICE_THING_UID_WITH_SERIAL_NUMBER_2 = new ThingUID(
+            new ThingTypeUID(MieleCloudBindingConstants.BINDING_ID, "dishwasher"), BRIDGE_THING_UID, SERIAL_NUMBER_2);
+
+    @Nullable
+    private ThingDiscoveryService discoveryService;
+
+    private ThingDiscoveryService getDiscoveryService() {
+        assertNotNull(discoveryService);
+        return Objects.requireNonNull(discoveryService);
+    }
+
+    @BeforeEach
+    public void setUp()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        setUpBridge();
+        setUpDiscoveryService();
+    }
+
+    private void setUpDiscoveryService()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        waitForAssert(() -> {
+            discoveryService = getService(DiscoveryService.class, ThingDiscoveryService.class);
+            assertNotNull(discoveryService);
+        });
+
+        getDiscoveryService().activate();
+    }
+
+    private DeviceState createDeviceState(String fabNumber, String techType, String deviceName, DeviceType deviceType,
+            String deviceTypeText) {
+        // given:
+        DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+        when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(fabNumber));
+        when(deviceIdentLabel.getTechType()).thenReturn(Optional.of(techType));
+
+        Type type = mock(Type.class);
+        when(type.getValueRaw()).thenReturn(deviceType);
+        when(type.getValueLocalized()).thenReturn(Optional.of(deviceTypeText));
+
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+        when(ident.getType()).thenReturn(Optional.of(type));
+        when(ident.getDeviceName()).thenReturn(Optional.of(deviceName));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+
+        return new DeviceState(fabNumber, device);
+    }
+
+    private void assertValidDiscoveryResult(ThingUID expectedThingUID, String expectedSerialNumber,
+            String expectedDeviceIdentifier, String expectedLabel, String expectedModelId) {
+        List<DiscoveryResult> results = getInbox().stream().filter(InboxPredicates.forThingUID(expectedThingUID))
+                .collect(Collectors.toList());
+        assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+        DiscoveryResult result = results.get(0);
+        assertEquals(MieleCloudBindingConstants.BINDING_ID, result.getBindingId(), "Invalid binding ID");
+        assertEquals(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID, result.getBridgeUID(),
+                "Invalid bridge UID");
+        assertEquals(Thing.PROPERTY_SERIAL_NUMBER, result.getRepresentationProperty(),
+                "Invalid representation property");
+        assertEquals(expectedModelId, result.getProperties().get(Thing.PROPERTY_MODEL_ID), "Invalid model ID");
+        assertEquals(expectedLabel, result.getLabel(), "Invalid label");
+        assertEquals(expectedSerialNumber, result.getProperties().get(Thing.PROPERTY_SERIAL_NUMBER),
+                "Invalid serial number");
+        assertEquals(expectedDeviceIdentifier,
+                result.getProperties().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER),
+                "Invalid serial number");
+    }
+
+    private void testMieleDeviceInboxDiscoveryResult(DeviceType deviceType, ThingUID expectedThingUid,
+            String deviceTypeName) {
+        // given:
+        DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, deviceType, deviceTypeName);
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+        // then:
+        assertValidDiscoveryResult(expectedThingUid, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
+                deviceTypeName + " " + TECH_TYPE);
+    }
+
+    @Test
+    public void testWashingDeviceInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.WASHING_MACHINE, WASHING_MACHINE_THING_UID,
+                DEVICE_TYPE_NAME_WASHING_MACHINE);
+    }
+
+    @Test
+    public void testOvenInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.OVEN, OVEN_DEVICE_THING_UID, DEVICE_TYPE_NAME_OVEN);
+    }
+
+    @Test
+    public void testHobInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.HOB_HIGHLIGHT, HOB_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOB);
+    }
+
+    @Test
+    public void testCoolingDeviceInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.FRIDGE_FREEZER_COMBINATION, FRIDGE_FREEZER_DEVICE_THING_UID,
+                DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+    }
+
+    @Test
+    public void testHoodInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.HOOD, HOOD_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOOD);
+    }
+
+    @Test
+    public void testCoffeeDeviceInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.COFFEE_SYSTEM, COFFEE_SYSTEM_THING_UID,
+                DEVICE_TYPE_NAME_COFFEE_SYSTEM);
+    }
+
+    @Test
+    public void testWineStorageDeviceInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.WINE_CABINET, WINE_STORAGE_DEVICE_THING_UID,
+                DEVICE_TYPE_NAME_WINE_STORAGE);
+    }
+
+    @Test
+    public void testDryerInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.TUMBLE_DRYER, DRYER_DEVICE_THING_UID, DEVICE_TYPE_NAME_DRYER);
+    }
+
+    @Test
+    public void testDishwasherInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.DISHWASHER, DISHWASHER_DEVICE_THING_UID,
+                DEVICE_TYPE_NAME_DISHWASHER);
+    }
+
+    @Test
+    public void testDishWarmerInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.DISH_WARMER, DISH_WARMER_DEVICE_THING_UID,
+                DEVICE_TYPE_NAME_DISH_WARMER);
+    }
+
+    @Test
+    public void testRoboticVacuumCleanerInboxDiscoveryResult() {
+        testMieleDeviceInboxDiscoveryResult(DeviceType.VACUUM_CLEANER, ROBOTIC_VACUUM_CLEANER_THING_UID,
+                DEVICE_TYPE_NAME_ROBOTIC_VACUUM_CLEANER);
+    }
+
+    @Test
+    public void testUnknownDeviceCreatesNoInboxDiscoveryResult() {
+        // given:
+        DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.VACUUM_DRAWER,
+                "Vacuum Drawer");
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(0, results.size(), "Amount of things in inbox does not match expected number");
+        });
+    }
+
+    @Test
+    public void testDeviceDiscoveryResultOfDeviceRemovedInTheCloudIsRemovedFromTheInbox() throws InterruptedException {
+        // given:
+        testMieleDeviceInboxDiscoveryResult(DeviceType.HOOD, HOOD_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOOD);
+
+        Thread.sleep(10);
+
+        // when:
+        getDiscoveryService().onDeviceRemoved(SERIAL_NUMBER);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(0, results.size(), "Amount of things in inbox does not match expected number");
+        });
+    }
+
+    @Test
+    public void testDiscoveryResultsForTwoDevices() {
+        // given:
+        DeviceState hoodDevice = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.HOOD,
+                DEVICE_TYPE_NAME_HOOD);
+        DeviceState dishwasherDevice = createDeviceState(SERIAL_NUMBER_2, TECH_TYPE_2, DEVICE_NAME_2,
+                DeviceType.DISHWASHER, DEVICE_TYPE_NAME_DISHWASHER);
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(hoodDevice);
+        getDiscoveryService().onDeviceStateUpdated(dishwasherDevice);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(2, results.size(), "Amount of things in inbox does not match expected number");
+
+            assertValidDiscoveryResult(HOOD_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
+                    "Hood " + TECH_TYPE);
+            assertValidDiscoveryResult(DISHWASHER_DEVICE_THING_UID_WITH_SERIAL_NUMBER_2, SERIAL_NUMBER_2,
+                    SERIAL_NUMBER_2, DEVICE_NAME_2, DEVICE_TYPE_NAME_DISHWASHER + " " + TECH_TYPE_2);
+        });
+    }
+
+    @Test
+    public void testOnlyDeviceDiscoveryResultsOfDevicesRemovedInTheCloudAreRemovedFromTheInbox()
+            throws InterruptedException {
+        // given:
+        DeviceState hoodDevice = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.HOOD,
+                DEVICE_TYPE_NAME_HOOD);
+        DeviceState dishwasherDevice = createDeviceState(SERIAL_NUMBER_2, TECH_TYPE_2, DEVICE_NAME_2,
+                DeviceType.DISHWASHER, DEVICE_TYPE_NAME_DISHWASHER);
+        getDiscoveryService().onDeviceStateUpdated(hoodDevice);
+        getDiscoveryService().onDeviceStateUpdated(dishwasherDevice);
+
+        Thread.sleep(10);
+
+        // when:
+        // This order of invocation is enforced by the webservice implementation.
+        getDiscoveryService().onDeviceRemoved(SERIAL_NUMBER_2);
+        getDiscoveryService().onDeviceStateUpdated(hoodDevice);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+            assertValidDiscoveryResult(HOOD_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
+                    DEVICE_TYPE_NAME_HOOD + " " + TECH_TYPE);
+        });
+    }
+
+    @Test
+    public void testIfNoDeviceNameIsSetThenTheDiscoveryLabelIsTheDeviceTypePlusTheTechType() {
+        // given:
+        DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, "", DeviceType.FRIDGE_FREEZER_COMBINATION,
+                DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+            assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
+                    "Fridge Freezer " + TECH_TYPE, DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE);
+        });
+    }
+
+    @Test
+    public void testIfNeitherDeviceTypeNorDeviceNameAreSetThenTheDiscoveryModelIdAndTheLabelAreTheTechType() {
+        // given:
+        DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, "", DeviceType.FRIDGE_FREEZER_COMBINATION,
+                "");
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+            assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, TECH_TYPE,
+                    TECH_TYPE);
+        });
+    }
+
+    @Test
+    public void testIfNeitherTechTypeNorDeviceNameAreSetThenTheDiscoveryModelIdAndTheLabelAreTheDeviceType() {
+        // given:
+        DeviceState deviceState = createDeviceState(SERIAL_NUMBER, "", "", DeviceType.FRIDGE_FREEZER_COMBINATION,
+                DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+            assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
+                    DEVICE_TYPE_NAME_FRIDGE_FREEZER, DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+        });
+    }
+
+    @Test
+    public void testIfNeitherTechTypeNorDeviceTypeNorDeviceNameAreSetThenTheDiscoveryModelIdIsUnknownAndTheLabelIsMieleDevice() {
+        // given:
+        DeviceState deviceState = createDeviceState(SERIAL_NUMBER, "", "", DeviceType.FRIDGE_FREEZER_COMBINATION, "");
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+            assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, "Miele Device",
+                    "Unknown");
+        });
+    }
+
+    @Test
+    public void testIfNoSerialNumberIsSetThenTheDeviceIdentifierIsUsedAsReplacement() {
+        // given:
+        DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+        when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(""));
+        when(deviceIdentLabel.getTechType()).thenReturn(Optional.of(TECH_TYPE));
+
+        Type type = mock(Type.class);
+        when(type.getValueRaw()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        when(type.getValueLocalized()).thenReturn(Optional.of(DEVICE_TYPE_NAME_FRIDGE_FREEZER));
+
+        Ident ident = mock(Ident.class);
+        when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+        when(ident.getType()).thenReturn(Optional.of(type));
+        when(ident.getDeviceName()).thenReturn(Optional.of(""));
+
+        Device device = mock(Device.class);
+        when(device.getIdent()).thenReturn(Optional.of(ident));
+        DeviceState deviceState = new DeviceState(SERIAL_NUMBER, device);
+
+        // when:
+        getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+            assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+            assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
+                    DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE,
+                    DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE);
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/AbstractMieleThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/AbstractMieleThingHandlerTest.java
new file mode 100644 (file)
index 0000000..0b44c5a
--- /dev/null
@@ -0,0 +1,578 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.items.Item;
+import org.openhab.core.items.ItemBuilder;
+import org.openhab.core.items.ItemBuilderFactory;
+import org.openhab.core.items.ItemRegistry;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.test.java.JavaOSGiTest;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.link.ItemChannelLink;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.openhab.core.thing.type.ChannelDefinition;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.thing.type.ThingType;
+import org.openhab.core.thing.type.ThingTypeRegistry;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractMieleThingHandlerTest extends JavaOSGiTest {
+    protected static final State NULL_VALUE_STATE = UnDefType.UNDEF;
+
+    @Nullable
+    private Bridge bridge;
+    @Nullable
+    private MieleBridgeHandler bridgeHandler;
+    @Nullable
+    private ThingRegistry thingRegistry;
+    @Nullable
+    private MieleWebservice webserviceMock;
+    @Nullable
+    private AbstractMieleThingHandler thingHandler;
+
+    @Nullable
+    private ItemRegistry itemRegistry;
+
+    protected Bridge getBridge() {
+        assertNotNull(bridge);
+        return Objects.requireNonNull(bridge);
+    }
+
+    protected MieleBridgeHandler getBridgeHandler() {
+        assertNotNull(bridgeHandler);
+        return Objects.requireNonNull(bridgeHandler);
+    }
+
+    protected ThingRegistry getThingRegistry() {
+        assertNotNull(thingRegistry);
+        return Objects.requireNonNull(thingRegistry);
+    }
+
+    protected MieleWebservice getWebserviceMock() {
+        assertNotNull(webserviceMock);
+        return Objects.requireNonNull(webserviceMock);
+    }
+
+    protected AbstractMieleThingHandler getThingHandler() {
+        assertNotNull(thingHandler);
+        return Objects.requireNonNull(thingHandler);
+    }
+
+    protected ItemRegistry getItemRegistry() {
+        assertNotNull(itemRegistry);
+        return Objects.requireNonNull(itemRegistry);
+    }
+
+    private void setUpThingRegistry() {
+        thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
+        assertNotNull(thingRegistry, "Thing registry is missing");
+    }
+
+    private void setUpItemRegistry() {
+        itemRegistry = getService(ItemRegistry.class, ItemRegistry.class);
+        assertNotNull(itemRegistry);
+    }
+
+    private void setUpWebservice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        webserviceMock = mock(MieleWebservice.class);
+        when(getWebserviceMock().hasAccessToken()).thenReturn(true);
+
+        MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
+        when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
+
+        MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+        assertNotNull(factory);
+        setPrivate(Objects.requireNonNull(factory), "webserviceFactory", webserviceFactory);
+    }
+
+    private void setUpBridge() throws Exception {
+        AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+        accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+        OAuthClientService oAuthClientService = mock(OAuthClientService.class);
+        when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
+
+        OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+        when(oAuthFactory
+                .getOAuthClientService(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString()))
+                        .thenReturn(oAuthClientService);
+
+        OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+                OpenHabOAuthTokenRefresher.class);
+        assertNotNull(tokenRefresher);
+        setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+
+        bridge = BridgeBuilder
+                .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+                        MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+                .withLabel("Miele@home Account")
+                .withConfiguration(
+                        new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+                                MieleCloudBindingIntegrationTestConstants.EMAIL)))
+                .build();
+        assertNotNull(bridge);
+
+        getThingRegistry().add(getBridge());
+
+        // then:
+        waitForAssert(() -> {
+            assertNotNull(getBridge().getHandler());
+            assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+        });
+
+        MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) getBridge().getHandler();
+        assertNotNull(bridgeHandler);
+
+        waitForAssert(() -> {
+            assertNotNull(bridgeHandler.getThing());
+        });
+
+        bridgeHandler.initialize();
+        bridgeHandler.onConnectionAlive();
+        setPrivate(bridgeHandler, "discoveryService", null);
+        this.bridgeHandler = bridgeHandler;
+    }
+
+    protected AbstractMieleThingHandler createThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid,
+            Class<? extends AbstractMieleThingHandler> expectedHandlerClass, String deviceIdentifier) {
+        ThingRegistry registry = getThingRegistry();
+
+        List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
+
+        Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
+                .withConfiguration(new Configuration(Collections
+                        .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
+                .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996").build();
+        assertNotNull(thing);
+
+        registry.add(thing);
+
+        waitForAssert(() -> {
+            ThingHandler handler = thing.getHandler();
+            assertNotNull(handler);
+            assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
+        });
+
+        createItemsForChannels(thing);
+        linkChannelsToItems(thing);
+
+        ThingHandler handler = thing.getHandler();
+        assertNotNull(handler);
+        return (AbstractMieleThingHandler) Objects.requireNonNull(handler);
+    }
+
+    private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
+        ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
+        assertNotNull(channelTypeRegistry);
+
+        ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
+        assertNotNull(thingTypeRegistry);
+
+        ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
+        assertNotNull(thingType);
+
+        List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
+        assertNotNull(channelDefinitions);
+
+        List<Channel> channels = new ArrayList<Channel>();
+        for (ChannelDefinition channelDefinition : channelDefinitions) {
+            ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
+            assertNotNull(channelTypeUid);
+
+            ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
+            assertNotNull(channelType);
+
+            String acceptedItemType = channelType.getItemType();
+            assertNotNull(acceptedItemType);
+
+            String channelId = channelDefinition.getId();
+            assertNotNull(channelId);
+
+            ChannelUID channelUid = new ChannelUID(thingUid, channelId);
+            assertNotNull(channelUid);
+
+            Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
+            assertNotNull(channel);
+
+            channels.add(channel);
+        }
+
+        return channels;
+    }
+
+    private void createItemsForChannels(Thing thing) {
+        ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
+        assertNotNull(itemBuilderFactory);
+
+        for (Channel channel : thing.getChannels()) {
+            String acceptedItemType = channel.getAcceptedItemType();
+            assertNotNull(acceptedItemType);
+
+            ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
+                    channel.getUID().getId());
+            assertNotNull(itemBuilder);
+
+            Item item = itemBuilder.build();
+            assertNotNull(item);
+
+            getItemRegistry().add(item);
+        }
+    }
+
+    private void linkChannelsToItems(Thing thing) {
+        ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
+                ItemChannelLinkRegistry.class);
+        assertNotNull(itemChannelLinkRegistry);
+
+        for (Channel channel : thing.getChannels()) {
+            String itemName = channel.getUID().getId();
+            assertNotNull(itemName);
+
+            ChannelUID channelUid = channel.getUID();
+            assertNotNull(channelUid);
+
+            ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
+            assertNotNull(link);
+        }
+    }
+
+    protected ChannelUID channel(String id) {
+        return new ChannelUID(getThingHandler().getThing().getUID(), id);
+    }
+
+    @BeforeEach
+    public void setUpAbstractMieleThingHandlerTest() throws Exception {
+        registerVolatileStorageService();
+        setUpThingRegistry();
+        setUpItemRegistry();
+        setUpWebservice();
+        setUpBridge();
+        thingHandler = setUpThingHandler();
+    }
+
+    private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
+        assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
+    }
+
+    private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
+            @Nullable String expectedDescription) {
+        assertEquals(expectedStatus, thing.getStatus());
+        assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
+        if (expectedDescription == null) {
+            assertNull(thing.getStatusInfo().getDescription());
+        } else {
+            assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
+        }
+    }
+
+    protected State getChannelState(String channelUid) {
+        Item item = getItemRegistry().get(channelUid);
+        assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
+        return item.getState();
+    }
+
+    /**
+     * Sets up the {@link ThingHandler} under test.
+     *
+     * @return The created {@link ThingHandler}.
+     */
+    protected abstract AbstractMieleThingHandler setUpThingHandler();
+
+    @Test
+    public void testCachedStateIsQueriedOnInitialize() throws Exception {
+        // then:
+        verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() {
+        // when:
+        getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
+
+        // then:
+        Thing thing = getThingHandler().getThing();
+        assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
+                "@text/mielecloud.thing.status.removed");
+    }
+
+    private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
+        when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
+        when(deviceState.isInState(any())).thenCallRealMethod();
+        when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
+        return deviceState;
+    }
+
+    @Test
+    public void testStatusIsSetToOnlineWhenDeviceStateIsValid() {
+        // given:
+        DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+    }
+
+    @Test
+    public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() {
+        // given:
+        DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                "@text/mielecloud.thing.status.disconnected");
+    }
+
+    @Test
+    public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() {
+        // given:
+        DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+        assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+
+        doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
+                eq(ProcessAction.STOP));
+
+        // when:
+        getThingHandler().triggerProcessAction(ProcessAction.STOP);
+
+        // then:
+        assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+    }
+
+    @Test
+    public void testHandleCommandProgramStartToStartStopChannel() {
+        // when:
+        getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
+                new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
+        });
+    }
+
+    @Test
+    public void testHandleCommandProgramStopToStartStopChannel() {
+        // when:
+        getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
+                new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
+        });
+    }
+
+    @Test
+    public void testHandleCommandProgramStartToStartStopPauseChannel() {
+        // when:
+        getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
+                new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
+        });
+    }
+
+    @Test
+    public void testHandleCommandProgramStopToStartStopPauseChannel() {
+        // when:
+        getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
+                new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
+        });
+    }
+
+    @Test
+    public void testHandleCommandProgramPauseToStartStopPauseChannel() {
+        // when:
+        getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
+                new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
+        });
+    }
+
+    @Test
+    public void testFailingPutLightDoesNotSetTheDeviceToOffline() {
+        // given:
+        DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+        assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+
+        doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
+
+        // when:
+        getThingHandler().triggerLight(true);
+
+        // then:
+        assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+    }
+
+    @Test
+    public void testHandleCommandLightOff() {
+        // when:
+        getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
+        });
+    }
+
+    @Test
+    public void testHandleCommandLightOn() {
+        // when:
+        getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
+        });
+    }
+
+    @Test
+    public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() {
+        // when:
+        getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
+
+        // then:
+        verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
+    }
+
+    @Test
+    public void testHandleCommandPowerOn() {
+        // when:
+        getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
+        });
+    }
+
+    @Test
+    public void testHandleCommandPowerOff() {
+        // when:
+        getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
+        });
+    }
+
+    @Test
+    public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() {
+        // when:
+        getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
+
+        // then:
+        verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
+    }
+
+    @Test
+    public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() {
+        // given:
+        assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
+        assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
+
+        var deviceState = mock(DeviceState.class);
+        when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
+        when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+        when(deviceState.getFabNumber())
+                .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
+        when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
+        when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
+                getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
+        assertEquals("Unknown device type UK-4567",
+                getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoffeeDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoffeeDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..49b9f5b
--- /dev/null
@@ -0,0 +1,211 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.COFFEE_SYSTEM_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class CoffeeDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, COFFEE_SYSTEM_THING_UID,
+                CoffeeSystemThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+        when(deviceState.getLightState()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Latte Macchiato"));
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(5L));
+        when(deviceState.getProgramPhase()).thenReturn(Optional.of("Spühlen"));
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(1));
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getLightState()).thenReturn(Optional.of(false));
+        when(deviceState.getElapsedTime()).thenReturn(Optional.of(3));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Latte Macchiato"), getChannelState(PROGRAM_ACTIVE));
+            assertEquals(new DecimalType(5), getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(new StringType("Spühlen"), getChannelState(PROGRAM_PHASE));
+            assertEquals(new DecimalType(1), getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new DecimalType(3), getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+        });
+    }
+
+    @Test
+    public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+        getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+        when(actionsState.canControlLight()).thenReturn(true);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+            assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoolingDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/CoolingDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..78777bb
--- /dev/null
@@ -0,0 +1,252 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.FRIDGE_FREEZER_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add door state and door alarm
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class CoolingDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, FRIDGE_FREEZER_DEVICE_THING_UID,
+                CoolingDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(1)).thenReturn(Optional.empty());
+        when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getTemperature(1)).thenReturn(Optional.empty());
+        when(deviceState.getDoorState()).thenReturn(Optional.empty());
+        when(deviceState.getDoorAlarm()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(FRIDGE_SUPER_COOL));
+            assertEquals(NULL_VALUE_STATE, getChannelState(FREEZER_SUPER_FREEZE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(FRIDGE_TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(FREEZER_TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(FRIDGE_TEMPERATURE_CURRENT));
+            assertEquals(NULL_VALUE_STATE, getChannelState(FREEZER_TEMPERATURE_CURRENT));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_ALARM));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERCOOLING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Super Cooling"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.SUPERCOOLING.getCode()));
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(6));
+        when(deviceState.getTargetTemperature(1)).thenReturn(Optional.of(-18));
+        when(deviceState.getTemperature(0)).thenReturn(Optional.of(8));
+        when(deviceState.getTemperature(1)).thenReturn(Optional.of(-10));
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.getDoorState()).thenReturn(Optional.of(true));
+        when(deviceState.getDoorAlarm()).thenReturn(Optional.of(false));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Super Cooling"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.SUPERCOOLING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(OnOffType.ON, getChannelState(FRIDGE_SUPER_COOL));
+            assertEquals(OnOffType.OFF, getChannelState(FREEZER_SUPER_FREEZE));
+            assertEquals(new QuantityType<>(6, SIUnits.CELSIUS), getChannelState(FRIDGE_TEMPERATURE_TARGET));
+            assertEquals(new QuantityType<>(-18, SIUnits.CELSIUS), getChannelState(FREEZER_TEMPERATURE_TARGET));
+            assertEquals(new QuantityType<>(8, SIUnits.CELSIUS), getChannelState(FRIDGE_TEMPERATURE_CURRENT));
+            assertEquals(new QuantityType<>(-10, SIUnits.CELSIUS), getChannelState(FREEZER_TEMPERATURE_CURRENT));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(DOOR_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(DOOR_ALARM));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForSuperCooling() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERCOOLING));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FRIDGE_SUPER_COOL));
+            assertEquals(OnOffType.OFF, getChannelState(FREEZER_SUPER_FREEZE));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForSuperFreezing() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERFREEZING));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.OFF, getChannelState(FRIDGE_SUPER_COOL));
+            assertEquals(OnOffType.ON, getChannelState(FREEZER_SUPER_FREEZE));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForSuperCollingSuperFreezing() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERCOOLING_SUPERFREEZING));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FRIDGE_SUPER_COOL));
+            assertEquals(OnOffType.ON, getChannelState(FREEZER_SUPER_FREEZE));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+        when(actionsState.canContolSupercooling()).thenReturn(true);
+        when(actionsState.canControlSuperfreezing()).thenReturn(false);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(SUPER_COOL_CAN_BE_CONTROLLED));
+            assertEquals(OnOffType.OFF, getChannelState(SUPER_FREEZE_CAN_BE_CONTROLLED));
+        });
+    }
+
+    @Override
+    @Test
+    public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() {
+        // when:
+        getThingHandler().handleCommand(channel(FRIDGE_SUPER_COOL), new DecimalType(50));
+
+        // then:
+        verify(getWebserviceMock(), never()).putProcessAction(anyString(), any());
+    }
+
+    @Test
+    public void testHandleCommandStartsSupercoolingWhenRequested() {
+        // when:
+        getThingHandler().handleCommand(channel(FRIDGE_SUPER_COOL), OnOffType.ON);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+                    ProcessAction.START_SUPERCOOLING);
+        });
+    }
+
+    @Test
+    public void testHandleCommandStopsSupercoolingWhenRequested() {
+        // when:
+        getThingHandler().handleCommand(channel(FRIDGE_SUPER_COOL), OnOffType.OFF);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+                    ProcessAction.STOP_SUPERCOOLING);
+        });
+    }
+
+    @Test
+    public void testHandleCommandStartsSuperfreezingWhenRequested() {
+        // when:
+        getThingHandler().handleCommand(channel(FREEZER_SUPER_FREEZE), OnOffType.ON);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+                    ProcessAction.START_SUPERFREEZING);
+        });
+    }
+
+    @Test
+    public void testHandleCommandStopsSuperfreezingWhenRequested() {
+        // when:
+        getThingHandler().handleCommand(channel(FREEZER_SUPER_FREEZE), OnOffType.OFF);
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+                    ProcessAction.STOP_SUPERFREEZING);
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishWarmerDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishWarmerDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..ec887fe
--- /dev/null
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DishWarmerDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_DISH_WARMER,
+                MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID,
+                DishWarmerDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.hasInfo()).thenReturn(false);
+        when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(DISH_WARMER_PROGRAM_ACTIVE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(INFO_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(2L));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.getElapsedTime()).thenReturn(Optional.of(98));
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(30));
+        when(deviceState.getTemperature(0)).thenReturn(Optional.of(29));
+        when(deviceState.hasError()).thenReturn(true);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getDoorState()).thenReturn(Optional.of(false));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("2"), getChannelState(DISH_WARMER_PROGRAM_ACTIVE));
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(new DecimalType(98), getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(new QuantityType<>(30, SIUnits.CELSIUS), getChannelState(TEMPERATURE_TARGET));
+            assertEquals(new QuantityType<>(29, SIUnits.CELSIUS), getChannelState(TEMPERATURE_CURRENT));
+            assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+        getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+        });
+    }
+
+    @Test
+    public void testHandleCommandDishWarmerProgramActive() {
+        // when:
+        getThingHandler().handleCommand(channel(DISH_WARMER_PROGRAM_ACTIVE), new StringType("3"));
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProgram(getThingHandler().getDeviceId(), 3);
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishwasherDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DishwasherDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..e5c311e
--- /dev/null
@@ -0,0 +1,227 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.DISHWASHER_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DishwasherDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_DISHWASHER, DISHWASHER_DEVICE_THING_UID,
+                DishwasherDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStartTime()).thenReturn(Optional.empty());
+        when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+        when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.isInState(any())).thenCallRealMethod();
+        when(deviceState.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Eco"));
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(4L));
+        when(deviceState.getProgramPhase()).thenReturn(Optional.of("Spülen"));
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(2));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+        when(deviceState.getElapsedTime()).thenReturn(Optional.of(4));
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getDoorState()).thenReturn(Optional.of(true));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Eco"), getChannelState(PROGRAM_ACTIVE));
+            assertEquals(new DecimalType(4), getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(new StringType("Spülen"), getChannelState(PROGRAM_PHASE));
+            assertEquals(new DecimalType(2), getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+            assertEquals(new DecimalType(4), getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(OnOffType.ON, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+        when(actionsState.canBeStarted()).thenReturn(true);
+        when(actionsState.canBeStopped()).thenReturn(false);
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DryerDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/DryerDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..15d4190
--- /dev/null
@@ -0,0 +1,241 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.DRYER_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DryerDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_DRYER, DRYER_DEVICE_THING_UID,
+                DryerDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStartTime()).thenReturn(Optional.empty());
+        when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+        when(deviceState.getDryingTarget()).thenReturn(Optional.empty());
+        when(deviceState.getDryingTargetRaw()).thenReturn(Optional.empty());
+        when(deviceState.getLightState()).thenReturn(Optional.empty());
+        when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DRYING_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DRYING_TARGET_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.isInState(any())).thenCallRealMethod();
+        when(deviceState.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Baumwolle"));
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(34L));
+        when(deviceState.getProgramPhase()).thenReturn(Optional.of("Schleudern"));
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(3));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+        when(deviceState.getElapsedTime()).thenReturn(Optional.of(61));
+        when(deviceState.getDryingTarget()).thenReturn(Optional.of("Schranktrocken"));
+        when(deviceState.getDryingTargetRaw()).thenReturn(Optional.of(3));
+        when(deviceState.hasError()).thenReturn(true);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getLightState()).thenReturn(Optional.of(false));
+        when(deviceState.getDoorState()).thenReturn(Optional.of(false));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Baumwolle"), getChannelState(PROGRAM_ACTIVE));
+            assertEquals(new DecimalType(34), getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(new StringType("Schleudern"), getChannelState(PROGRAM_PHASE));
+            assertEquals(new DecimalType(3), getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+            assertEquals(new DecimalType(61), getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(new StringType("Schranktrocken"), getChannelState(DRYING_TARGET));
+            assertEquals(new DecimalType(3), getChannelState(DRYING_TARGET_RAW));
+            assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+            assertEquals(OnOffType.OFF, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+        when(actionsState.canBeStarted()).thenReturn(true);
+        when(actionsState.canBeStopped()).thenReturn(false);
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+        when(actionsState.canControlLight()).thenReturn(true);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+            assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/HobDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/HobDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..1249b97
--- /dev/null
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.HOB_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add plate step
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class HobDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_HOB, HOB_DEVICE_THING_UID,
+                HobDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(HOB_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getPlateStep(anyInt())).thenReturn(Optional.empty());
+        when(deviceState.getPlateStepRaw(anyInt())).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_1_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_1_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP_RAW));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(HOB_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(false));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getPlateStep(0)).thenReturn(Optional.of("1."));
+        when(deviceState.getPlateStepRaw(0)).thenReturn(Optional.of(2));
+        when(deviceState.getPlateStep(1)).thenReturn(Optional.empty());
+        when(deviceState.getPlateStep(2)).thenReturn(Optional.empty());
+        when(deviceState.getPlateStep(3)).thenReturn(Optional.empty());
+        when(deviceState.getPlateStep(4)).thenReturn(Optional.empty());
+        when(deviceState.getPlateStep(5)).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(new StringType("1."), getChannelState(PLATE_1_POWER_STEP));
+            assertEquals(new DecimalType(2), getChannelState(PLATE_1_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP_RAW));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/HoodDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/HoodDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..9610181
--- /dev/null
@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.HOOD_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class HoodDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_HOOD, HOOD_DEVICE_THING_UID,
+                HoodDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(HOOD_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getVentilationStep()).thenReturn(Optional.empty());
+        when(deviceState.getVentilationStepRaw()).thenReturn(Optional.empty());
+        when(deviceState.getLightState()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(VENTILATION_POWER));
+            assertEquals(NULL_VALUE_STATE, getChannelState(VENTILATION_POWER_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(HOOD_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(false));
+        when(deviceState.getProgramPhase()).thenReturn(Optional.of("Kochen"));
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(5));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getVentilationStep()).thenReturn(Optional.of("2"));
+        when(deviceState.getVentilationStepRaw()).thenReturn(Optional.of(2));
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getLightState()).thenReturn(Optional.of(false));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Kochen"), getChannelState(PROGRAM_PHASE));
+            assertEquals(new DecimalType(5), getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(new StringType("2"), getChannelState(VENTILATION_POWER));
+            assertEquals(new DecimalType(2), getChannelState(VENTILATION_POWER_RAW));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(HOOD_DEVICE_THING_UID.getId());
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+        when(actionsState.canControlLight()).thenReturn(true);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+            assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleBridgeHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleBridgeHandlerTest.java
new file mode 100644 (file)
index 0000000..2a87607
--- /dev/null
@@ -0,0 +1,562 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.*;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.OpenHabOsgiTest;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleBridgeHandlerTest extends OpenHabOsgiTest {
+    private static final String SERVICE_HANDLE = MieleCloudBindingIntegrationTestConstants.EMAIL;
+    private static final String CONFIG_PARAM_LOCALE = "locale";
+
+    @Nullable
+    private MieleWebservice webserviceMock;
+    @Nullable
+    private String webserviceAccessToken;
+    @Nullable
+    private OAuthFactory oauthFactoryMock;
+    @Nullable
+    private OAuthClientService oauthClientServiceMock;
+
+    @Nullable
+    private Bridge bridge;
+    @Nullable
+    private MieleBridgeHandler handler;
+
+    private MieleWebservice getWebserviceMock() {
+        assertNotNull(webserviceMock);
+        return Objects.requireNonNull(webserviceMock);
+    }
+
+    private OAuthFactory getOAuthFactoryMock() {
+        assertNotNull(oauthFactoryMock);
+        return Objects.requireNonNull(oauthFactoryMock);
+    }
+
+    private OAuthClientService getOAuthClientServiceMock() {
+        OAuthClientService oauthClientServiceMock = this.oauthClientServiceMock;
+        assertNotNull(oauthClientServiceMock);
+        return Objects.requireNonNull(oauthClientServiceMock);
+    }
+
+    private Bridge getBridge() {
+        assertNotNull(bridge);
+        return Objects.requireNonNull(bridge);
+    }
+
+    private MieleBridgeHandler getHandler() {
+        assertNotNull(handler);
+        return Objects.requireNonNull(handler);
+    }
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        setUpWebservice();
+        setUpBridgeThingAndHandler();
+        setUpOAuthFactory();
+    }
+
+    private void setUpWebservice() throws NoSuchFieldException, IllegalAccessException {
+        webserviceMock = mock(MieleWebservice.class);
+        doAnswer(invocation -> {
+            if (invocation != null) {
+                webserviceAccessToken = invocation.getArgument(0);
+            }
+            return null;
+        }).when(getWebserviceMock()).setAccessToken(anyString());
+        when(getWebserviceMock().hasAccessToken()).then(invocation -> webserviceAccessToken != null);
+
+        MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
+        when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
+
+        MieleHandlerFactory handlerFactory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+        assertNotNull(handlerFactory);
+        setPrivate(Objects.requireNonNull(handlerFactory), "webserviceFactory", webserviceFactory);
+    }
+
+    private void setUpBridgeThingAndHandler() {
+        when(getWebserviceMock().hasAccessToken()).thenReturn(false);
+
+        bridge = BridgeBuilder
+                .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+                        MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+                .withConfiguration(
+                        new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+                                MieleCloudBindingIntegrationTestConstants.EMAIL)))
+                .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+        assertNotNull(bridge);
+
+        getThingRegistry().add(getBridge());
+
+        waitForAssert(() -> {
+            assertNotNull(getBridge().getHandler());
+            assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+        });
+        handler = (MieleBridgeHandler) getBridge().getHandler();
+    }
+
+    private void setUpOAuthFactory() throws Exception {
+        AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+        accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+        oauthClientServiceMock = mock(OAuthClientService.class);
+        when(oauthClientServiceMock.getAccessTokenResponse()).thenReturn(accessTokenResponse);
+
+        OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+        Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(getOAuthClientServiceMock());
+        oauthFactoryMock = oAuthFactory;
+
+        OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+                OpenHabOAuthTokenRefresher.class);
+        assertNotNull(tokenRefresher);
+        setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+    }
+
+    private void initializeBridgeWithTokens() {
+        getHandler().initialize();
+        assertThingStatusIs(ThingStatus.UNKNOWN);
+    }
+
+    private void assertThingStatusIs(ThingStatus expectedStatus) {
+        assertThingStatusIs(expectedStatus, ThingStatusDetail.NONE);
+    }
+
+    private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
+        assertThingStatusIs(expectedStatus, expectedStatusDetail, null);
+    }
+
+    private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
+            @Nullable String expectedDescription) {
+        assertEquals(expectedStatus, getBridge().getStatus());
+        assertEquals(expectedStatusDetail, getBridge().getStatusInfo().getStatusDetail());
+        if (expectedDescription == null) {
+            assertNull(getBridge().getStatusInfo().getDescription());
+        } else {
+            assertEquals(expectedDescription, getBridge().getStatusInfo().getDescription());
+        }
+    }
+
+    @Test
+    public void testThingStatusIsSetToOfflineWithDetailConfigurationPendingAndDescriptionWhenTokensAreNotPassedViaInitialConfiguration()
+            throws Exception {
+        when(getOAuthClientServiceMock().getAccessTokenResponse()).thenReturn(null);
+
+        // when:
+        getHandler().initialize();
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
+    }
+
+    @Test
+    public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheEmailAddressIsInvalid()
+            throws Exception {
+        // given:
+        getBridge().getConfiguration().setProperties(
+                Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, "not!a!mail$address"));
+
+        // when:
+        getHandler().initialize();
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
+    }
+
+    @Test
+    public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
+            throws Exception {
+        // given:
+        OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+        Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
+
+        OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+                OpenHabOAuthTokenRefresher.class);
+        assertNotNull(tokenRefresher);
+        // Clear the setup configuration and use the failing one for this test.
+        setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+
+        // when:
+        getHandler().initialize();
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
+    }
+
+    @Test
+    public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
+            throws Exception {
+        // given:
+        OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+        Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
+
+        OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+                OpenHabOAuthTokenRefresher.class);
+        assertNotNull(tokenRefresher);
+        // Clear the setup configuration and use the failing one for this test.
+        setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+
+        getHandler().initialize();
+
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
+
+        setUpOAuthFactory();
+
+        // when:
+        getHandler().dispose();
+        getHandler().initialize();
+
+        // then:
+        assertThingStatusIs(ThingStatus.UNKNOWN);
+    }
+
+    @Test
+    public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionAlive();
+
+        // then:
+        assertThingStatusIs(ThingStatus.ONLINE);
+    }
+
+    @Test
+    public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
+            throws Exception {
+        // given:
+        AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+        accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+        when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
+
+        initializeBridgeWithTokens();
+        getHandler().onConnectionAlive();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+
+        // then:
+        verify(getOAuthClientServiceMock()).refreshToken();
+        verify(getWebserviceMock()).connectSse();
+        assertThingStatusIs(ThingStatus.ONLINE);
+    }
+
+    @Test
+    public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
+            throws Exception {
+        // given:
+        when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
+        initializeBridgeWithTokens();
+        getHandler().onConnectionAlive();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+
+        // then:
+        verify(getOAuthClientServiceMock()).refreshToken();
+        verify(getWebserviceMock()).disconnectSse();
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
+    }
+
+    @Test
+    public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+        getHandler().onConnectionAlive();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
+
+        // then:
+        assertThingStatusIs(ThingStatus.ONLINE);
+    }
+
+    @Test
+    public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+        getHandler().onConnectionAlive();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+    }
+
+    @Test
+    public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+    }
+
+    @Test
+    public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+        getHandler().onConnectionAlive();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+    }
+
+    @Test
+    public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+    }
+
+    @Test
+    public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+    }
+
+    @Test
+    public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+    }
+
+    @Test
+    public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+                I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+    }
+
+    @Test
+    public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+                I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+    }
+
+    @Test
+    public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+            throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
+
+        // then:
+        assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+                I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+    }
+
+    @Test
+    public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
+
+        // then:
+        assertThingStatusIs(ThingStatus.UNKNOWN);
+    }
+
+    @Test
+    public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        // given:
+        getHandler().initialize();
+
+        // when:
+        getHandler().onNewAccessToken(ACCESS_TOKEN);
+
+        // then:
+        verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
+        verify(getWebserviceMock(), atLeast(1)).connectSse();
+    }
+
+    @Test
+    public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        // given:
+        getHandler().initialize();
+
+        // when:
+        getHandler().dispose();
+
+        // then:
+        verify(getWebserviceMock()).disconnectSse();
+
+        CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
+        assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
+    }
+
+    @Test
+    public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
+        // when:
+        Optional<String> language = getHandler().getLanguage();
+
+        // then:
+        assertFalse(language.isPresent());
+    }
+
+    @Test
+    public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
+        // given:
+        getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, ""));
+
+        // when:
+        Optional<String> language = getHandler().getLanguage();
+
+        // then:
+        assertFalse(language.isPresent());
+    }
+
+    @Test
+    public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotAValidTwoLetterLanguageCode() {
+        // given:
+        getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "Deutsch"));
+
+        // when:
+        Optional<String> language = getHandler().getLanguage();
+
+        // then:
+        assertFalse(language.isPresent());
+    }
+
+    @Test
+    public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
+        // given:
+        getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "DE"));
+
+        // when:
+        String language = getHandler().getLanguage().get();
+
+        // then:
+        assertEquals("DE", language);
+    }
+
+    @Test
+    public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getThingRegistry().remove(getHandler().getThing().getUID());
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).logout();
+        });
+    }
+
+    @Test
+    public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
+        // given:
+        initializeBridgeWithTokens();
+
+        // when:
+        getThingRegistry().remove(getHandler().getThing().getUID());
+
+        // then:
+        waitForAssert(() -> {
+            verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleHandlerFactoryTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/MieleHandlerFactoryTest.java
new file mode 100644 (file)
index 0000000..2291f7c
--- /dev/null
@@ -0,0 +1,310 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.*;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.test.java.JavaOSGiTest;
+import org.openhab.core.test.storage.VolatileStorageService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleHandlerFactoryTest extends JavaOSGiTest {
+    private static final String DEVICE_IDENTIFIER = "000124430016";
+
+    private static final ThingUID WASHING_MACHINE_TYPE = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, DEVICE_IDENTIFIER);
+    private static final ThingUID OVEN_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_OVEN,
+            DEVICE_IDENTIFIER);
+    private static final ThingUID HOB_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOB,
+            DEVICE_IDENTIFIER);
+    private static final ThingUID FRIDGE_FREEZER_DEVICE_TYPE = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, DEVICE_IDENTIFIER);
+    private static final ThingUID HOOD_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOOD,
+            DEVICE_IDENTIFIER);
+    private static final ThingUID COFFEE_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM,
+            DEVICE_IDENTIFIER);
+    private static final ThingUID WINE_STORAGE_DEVICE_TYPE = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE, DEVICE_IDENTIFIER);
+    private static final ThingUID DRYER_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_DRYER,
+            DEVICE_IDENTIFIER);
+    private static final ThingUID DISHWASHER_DEVICE_TYPE = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_DISHWASHER, DEVICE_IDENTIFIER);
+    private static final ThingUID DISH_WARMER_DEVICE_TYPE = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_DISH_WARMER, DEVICE_IDENTIFIER);
+    private static final ThingUID ROBOTIC_VACUUM_CLEANER_DEVICE_TYPE = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER, DEVICE_IDENTIFIER);
+
+    @Nullable
+    private ThingRegistry thingRegistry;
+
+    private ThingRegistry getThingRegistry() {
+        assertNotNull(thingRegistry);
+        return Objects.requireNonNull(thingRegistry);
+    }
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        registerVolatileStorageService();
+        thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
+        assertNotNull(thingRegistry, "Thing registry is missing");
+
+        // Ensure the MieleWebservice is not initialized.
+        MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+        assertNotNull(factory);
+
+        // Assume an access token has already been stored
+        AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+        accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+        OAuthClientService oAuthClientService = mock(OAuthClientService.class);
+        when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
+
+        OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+        when(oAuthFactory.getOAuthClientService(MieleCloudBindingIntegrationTestConstants.EMAIL))
+                .thenReturn(oAuthClientService);
+
+        OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+                OpenHabOAuthTokenRefresher.class);
+        assertNotNull(tokenRefresher);
+        setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForGenesisBridge() throws Exception {
+        // when:
+        Bridge bridge = BridgeBuilder
+                .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+                        MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+                .withConfiguration(
+                        new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+                                MieleCloudBindingIntegrationTestConstants.EMAIL)))
+                .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+        assertNotNull(bridge);
+
+        getThingRegistry().add(bridge);
+
+        // then:
+        waitForAssert(() -> {
+            assertNotNull(bridge.getHandler());
+            assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+        });
+
+        MieleBridgeHandler handler = (MieleBridgeHandler) bridge.getHandler();
+        assertNotNull(handler);
+    }
+
+    @Test
+    public void testWebserviceIsInitializedOnHandlerInitialization() throws Exception {
+        // given:
+        Bridge bridge = BridgeBuilder
+                .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+                        MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+                .withConfiguration(
+                        new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+                                MieleCloudBindingIntegrationTestConstants.EMAIL)))
+                .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+        assertNotNull(bridge);
+
+        getThingRegistry().add(bridge);
+
+        waitForAssert(() -> {
+            assertNotNull(bridge.getHandler());
+            assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+        });
+
+        MieleBridgeHandler handler = (MieleBridgeHandler) bridge.getHandler();
+        assertNotNull(handler);
+
+        // when:
+        handler.initialize();
+
+        // then:
+        assertEquals(ACCESS_TOKEN,
+                handler.getThing().getProperties().get(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN));
+
+        MieleWebservice webservice = getPrivate(handler, "webService");
+        assertNotNull(webservice);
+        Optional<String> accessToken = getPrivate(webservice, "accessToken");
+        assertEquals(Optional.of(ACCESS_TOKEN), accessToken);
+    }
+
+    private void verifyHandlerCreation(MieleWebservice webservice, Thing thing,
+            Class<? extends ThingHandler> expectedHandlerClass)
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        getThingRegistry().add(thing);
+
+        // then:
+        waitForAssert(() -> {
+            ThingHandler handler = thing.getHandler();
+            assertNotNull(handler);
+            assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
+        });
+    }
+
+    private void testHandlerCanBeCreatedForMieleDevice(ThingTypeUID thingTypeUid, ThingUID thingUid, String label,
+            Class<? extends ThingHandler> expectedHandlerClass)
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        // given:
+        MieleWebservice webservice = mock(MieleWebservice.class);
+
+        MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+        assertNotNull(factory);
+
+        // when:
+        Thing device = ThingBuilder.create(thingTypeUid, thingUid)
+                .withConfiguration(new Configuration(Collections
+                        .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, DEVICE_IDENTIFIER)))
+                .withLabel(label).build();
+
+        assertNotNull(device);
+        verifyHandlerCreation(webservice, device, expectedHandlerClass);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForGenesisBridgeWithEmptyConfiguration() throws Exception {
+        // when:
+        Bridge bridge = BridgeBuilder
+                .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+                        MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+                .withConfiguration(
+                        new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+                                MieleCloudBindingIntegrationTestConstants.EMAIL)))
+                .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+        assertNotNull(bridge);
+
+        getThingRegistry().add(bridge);
+
+        // then:
+        waitForAssert(() -> {
+            assertNotNull(bridge.getHandler());
+            assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+        });
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForWashingDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE,
+                WASHING_MACHINE_TYPE, "DA-6996", WashingDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForOvenDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_OVEN, OVEN_DEVICE_TYPE, "OV-6887",
+                OvenDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForHobDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_HOB, HOB_DEVICE_TYPE, "HB-3887",
+                HobDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForFridgeFreezerDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER,
+                FRIDGE_FREEZER_DEVICE_TYPE, "CD-6097", CoolingDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForHoodDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_HOOD, HOOD_DEVICE_TYPE, "HD-2097",
+                HoodDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForCoffeeDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, COFFEE_DEVICE_TYPE,
+                "DA-6997", CoffeeSystemThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForWineStorageDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE,
+                WINE_STORAGE_DEVICE_TYPE, "WS-6907", WineStorageDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForDryerDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_DRYER, DRYER_DEVICE_TYPE, "DR-0907",
+                DryerDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForDishwasherDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_DISHWASHER, DISHWASHER_DEVICE_TYPE,
+                "DR-0907", DishwasherDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForDishWarmerDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_DISH_WARMER,
+                DISH_WARMER_DEVICE_TYPE, "DW-0907", DishWarmerDeviceThingHandler.class);
+    }
+
+    @Test
+    public void testHandlerCanBeCreatedForRoboticVacuumCleanerDevice()
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER,
+                ROBOTIC_VACUUM_CLEANER_DEVICE_TYPE, "RVC-0907", RoboticVacuumCleanerDeviceThingHandler.class);
+    }
+
+    /**
+     * Registers a volatile storage service.
+     */
+    @Override
+    protected void registerVolatileStorageService() {
+        registerService(new VolatileStorageService());
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/OvenDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/OvenDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..ccd32f7
--- /dev/null
@@ -0,0 +1,248 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.OVEN_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add pre-heat finished channel
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class OvenDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_OVEN, OVEN_DEVICE_THING_UID,
+                OvenDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStartTime()).thenReturn(Optional.empty());
+        when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+        when(deviceState.hasPreHeatFinished()).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getLightState()).thenReturn(Optional.empty());
+        when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PRE_HEAT_FINISHED));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+            assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.isInState(any())).thenCallRealMethod();
+        when(deviceState.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(false));
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Grill"));
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(6L));
+        when(deviceState.getProgramPhase()).thenReturn(Optional.of("Heat"));
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(6));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+        when(deviceState.getElapsedTime()).thenReturn(Optional.of(62));
+        when(deviceState.hasPreHeatFinished()).thenReturn(Optional.of(true));
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(180));
+        when(deviceState.getTemperature(0)).thenReturn(Optional.of(181));
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getLightState()).thenReturn(Optional.of(false));
+        when(deviceState.getDoorState()).thenReturn(Optional.of(false));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Grill"), getChannelState(PROGRAM_ACTIVE));
+            assertEquals(new DecimalType(6), getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(new StringType("Heat"), getChannelState(PROGRAM_PHASE));
+            assertEquals(new DecimalType(6), getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+            assertEquals(new DecimalType(62), getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(OnOffType.ON, getChannelState(PRE_HEAT_FINISHED));
+            assertEquals(new QuantityType<>(180, SIUnits.CELSIUS), getChannelState(TEMPERATURE_TARGET));
+            assertEquals(new QuantityType<>(181, SIUnits.CELSIUS), getChannelState(TEMPERATURE_CURRENT));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+            assertEquals(OnOffType.OFF, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+        getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+        when(actionsState.canBeStarted()).thenReturn(true);
+        when(actionsState.canBeStopped()).thenReturn(false);
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+        when(actionsState.canControlLight()).thenReturn(true);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+            assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/RoboticVacuumCleanerDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/RoboticVacuumCleanerDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..a20bf8a
--- /dev/null
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RoboticVacuumCleanerDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER,
+                MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID,
+                RoboticVacuumCleanerDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.hasError()).thenReturn(false);
+        when(deviceState.hasInfo()).thenReturn(false);
+        when(deviceState.getBatteryLevel()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(VACUUM_CLEANER_PROGRAM_ACTIVE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()),
+                    getChannelState(PROGRAM_START_STOP_PAUSE));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(INFO_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(BATTERY_LEVEL));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.isInState(any())).thenCallRealMethod();
+        when(deviceState.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(1L));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.hasError()).thenReturn(true);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getBatteryLevel()).thenReturn(Optional.of(25));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("1"), getChannelState(VACUUM_CLEANER_PROGRAM_ACTIVE));
+            assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()),
+                    getChannelState(PROGRAM_START_STOP_PAUSE));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(new DecimalType(25), getChannelState(BATTERY_LEVEL));
+        });
+    }
+
+    @Test
+    public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier())
+                .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+        when(actionsState.canBeStarted()).thenReturn(true);
+        when(actionsState.canBeStopped()).thenReturn(false);
+        when(actionsState.canBePaused()).thenReturn(true);
+        when(actionsState.canSetActiveProgramId()).thenReturn(false);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_PAUSED));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE));
+        });
+    }
+
+    @Test
+    public void testHandleCommandVacuumCleanerProgramActive() {
+        // when:
+        getThingHandler().handleCommand(channel(VACUUM_CLEANER_PROGRAM_ACTIVE), new StringType("1"));
+
+        // then:
+        waitForAssert(() -> {
+            verify(getWebserviceMock()).putProgram(getThingHandler().getDeviceId(), 1);
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/WashingDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/WashingDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..8217165
--- /dev/null
@@ -0,0 +1,248 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.WASHING_MACHINE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class WashingDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, WASHING_MACHINE_THING_UID,
+                WashingDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getSpinningSpeed()).thenReturn(Optional.empty());
+        when(deviceState.getSpinningSpeedRaw()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getStartTime()).thenReturn(Optional.empty());
+        when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getLightState()).thenReturn(Optional.empty());
+        when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(SPINNING_SPEED));
+            assertEquals(NULL_VALUE_STATE, getChannelState(SPINNING_SPEED_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+            assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.isInState(any())).thenCallRealMethod();
+        when(deviceState.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+        when(deviceState.getSpinningSpeed()).thenReturn(Optional.of("1200"));
+        when(deviceState.getSpinningSpeedRaw()).thenReturn(Optional.of(1200));
+        when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Buntwäsche"));
+        when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(1L));
+        when(deviceState.getProgramPhase()).thenReturn(Optional.of("Waschen"));
+        when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(7));
+        when(deviceState.getStatus()).thenReturn(Optional.of("Läuft"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+        when(deviceState.getElapsedTime()).thenReturn(Optional.of(63));
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(30));
+        when(deviceState.hasError()).thenReturn(true);
+        when(deviceState.hasInfo()).thenReturn(true);
+        when(deviceState.getLightState()).thenReturn(Optional.of(false));
+        when(deviceState.getDoorState()).thenReturn(Optional.of(true));
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("1200"), getChannelState(SPINNING_SPEED));
+            assertEquals(new DecimalType(1200), getChannelState(SPINNING_SPEED_RAW));
+            assertEquals(new StringType("Buntwäsche"), getChannelState(PROGRAM_ACTIVE));
+            assertEquals(new DecimalType(1), getChannelState(PROGRAM_ACTIVE_RAW));
+            assertEquals(new StringType("Waschen"), getChannelState(PROGRAM_PHASE));
+            assertEquals(new DecimalType(7), getChannelState(PROGRAM_PHASE_RAW));
+            assertEquals(new StringType("Läuft"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+            assertEquals(new DecimalType(63), getChannelState(PROGRAM_ELAPSED_TIME));
+            assertEquals(new QuantityType<>(30, SIUnits.CELSIUS), getChannelState(TEMPERATURE_TARGET));
+            assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+            assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+            assertEquals(OnOffType.ON, getChannelState(DOOR_STATE));
+        });
+    }
+
+    @Test
+    public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+        getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testTransitionChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceStateBefore = mock(DeviceState.class);
+        when(deviceStateBefore.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+        when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+        getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+        DeviceState deviceStateAfter = mock(DeviceState.class);
+        when(deviceStateAfter.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+        when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+        when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+        // when:
+        getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+        waitForAssert(() -> {
+            assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+            assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+        when(actionsState.canBeStarted()).thenReturn(true);
+        when(actionsState.canBeStopped()).thenReturn(false);
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+        when(actionsState.canControlLight()).thenReturn(false);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+            assertEquals(OnOffType.OFF, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/WineStorageDeviceThingHandlerTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/handler/WineStorageDeviceThingHandlerTest.java
new file mode 100644 (file)
index 0000000..ef219b8
--- /dev/null
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.WINE_STORAGE_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class WineStorageDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+    @Override
+    protected AbstractMieleThingHandler setUpThingHandler() {
+        return createThingHandler(MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE, WINE_STORAGE_DEVICE_THING_UID,
+                WineStorageDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+    }
+
+    @Test
+    public void testChannelUpdatesForNullValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(WINE_STORAGE_DEVICE_THING_UID.getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.WINE_CONDITIONING_UNIT);
+        when(deviceState.getStateType()).thenReturn(Optional.empty());
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.empty());
+        when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(1)).thenReturn(Optional.empty());
+        when(deviceState.getTemperature(1)).thenReturn(Optional.empty());
+        when(deviceState.getTargetTemperature(2)).thenReturn(Optional.empty());
+        when(deviceState.getTemperature(2)).thenReturn(Optional.empty());
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+            assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TOP_TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TOP_TEMPERATURE_CURRENT));
+            assertEquals(NULL_VALUE_STATE, getChannelState(MIDDLE_TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(MIDDLE_TEMPERATURE_CURRENT));
+            assertEquals(NULL_VALUE_STATE, getChannelState(BOTTOM_TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(BOTTOM_TEMPERATURE_CURRENT));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+        });
+    }
+
+    @Test
+    public void testChannelUpdatesForValidValues() {
+        // given:
+        DeviceState deviceState = mock(DeviceState.class);
+        when(deviceState.getDeviceIdentifier()).thenReturn(WINE_STORAGE_DEVICE_THING_UID.getId());
+        when(deviceState.getRawType()).thenReturn(DeviceType.WINE_CONDITIONING_UNIT);
+        when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+        when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+        when(deviceState.getStatus()).thenReturn(Optional.of("Im Betrieb"));
+        when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+        when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(8));
+        when(deviceState.getTemperature(0)).thenReturn(Optional.of(9));
+        when(deviceState.getTargetTemperature(1)).thenReturn(Optional.of(10));
+        when(deviceState.getTemperature(1)).thenReturn(Optional.of(11));
+        when(deviceState.getTargetTemperature(2)).thenReturn(Optional.of(12));
+        when(deviceState.getTemperature(2)).thenReturn(Optional.of(14));
+        when(deviceState.hasError()).thenReturn(true);
+        when(deviceState.hasInfo()).thenReturn(true);
+
+        // when:
+        getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(new StringType("Im Betrieb"), getChannelState(OPERATION_STATE));
+            assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+            assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+            assertEquals(new QuantityType<>(8, SIUnits.CELSIUS), getChannelState(TOP_TEMPERATURE_TARGET));
+            assertEquals(new QuantityType<>(9, SIUnits.CELSIUS), getChannelState(TOP_TEMPERATURE_CURRENT));
+            assertEquals(new QuantityType<>(10, SIUnits.CELSIUS), getChannelState(MIDDLE_TEMPERATURE_TARGET));
+            assertEquals(new QuantityType<>(11, SIUnits.CELSIUS), getChannelState(MIDDLE_TEMPERATURE_CURRENT));
+            assertEquals(new QuantityType<>(12, SIUnits.CELSIUS), getChannelState(BOTTOM_TEMPERATURE_TARGET));
+            assertEquals(new QuantityType<>(14, SIUnits.CELSIUS), getChannelState(BOTTOM_TEMPERATURE_CURRENT));
+            assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+            assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+            assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+        });
+    }
+
+    @Test
+    public void testActionsChannelUpdatesForValidValues() {
+        // given:
+        ActionsState actionsState = mock(ActionsState.class);
+        when(actionsState.getDeviceIdentifier()).thenReturn(WINE_STORAGE_DEVICE_THING_UID.getId());
+        when(actionsState.canBeSwitchedOn()).thenReturn(true);
+        when(actionsState.canBeSwitchedOff()).thenReturn(false);
+
+        // when:
+        getBridgeHandler().onProcessActionUpdated(actionsState);
+
+        // then:
+        waitForAssert(() -> {
+            assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+            assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+        });
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/AbstractConfigFlowTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/AbstractConfigFlowTest.java
new file mode 100644 (file)
index 0000000..3f74288
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.openhab.binding.mielecloud.internal.config.MieleCloudConfigService;
+import org.openhab.binding.mielecloud.internal.config.servlet.AccountOverviewServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ResultServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.SuccessServlet;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Common base class for all config flow tests.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractConfigFlowTest extends OpenHabOsgiTest {
+    @Nullable
+    private WebsiteCrawler crawler;
+
+    @Nullable
+    private AccountOverviewServlet accountOverviewServlet;
+
+    @Nullable
+    private ForwardToLoginServlet forwardToLoginServlet;
+
+    @Nullable
+    private ResultServlet resultServlet;
+
+    @Nullable
+    private SuccessServlet successServlet;
+
+    @Nullable
+    private CreateBridgeServlet createBridgeServlet;
+
+    protected final WebsiteCrawler getCrawler() {
+        final WebsiteCrawler crawler = this.crawler;
+        assertNotNull(crawler);
+        return Objects.requireNonNull(crawler);
+    }
+
+    protected final AccountOverviewServlet getAccountOverviewServlet() {
+        final AccountOverviewServlet accountOverviewServlet = this.accountOverviewServlet;
+        assertNotNull(accountOverviewServlet);
+        return Objects.requireNonNull(accountOverviewServlet);
+    }
+
+    protected final ForwardToLoginServlet getForwardToLoginServlet() {
+        final ForwardToLoginServlet forwardToLoginServlet = this.forwardToLoginServlet;
+        assertNotNull(forwardToLoginServlet);
+        return Objects.requireNonNull(forwardToLoginServlet);
+    }
+
+    protected final ResultServlet getResultServlet() {
+        final ResultServlet resultServlet = this.resultServlet;
+        assertNotNull(resultServlet);
+        return Objects.requireNonNull(resultServlet);
+    }
+
+    protected final SuccessServlet getSuccessServlet() {
+        final SuccessServlet successServlet = this.successServlet;
+        assertNotNull(successServlet);
+        return Objects.requireNonNull(successServlet);
+    }
+
+    protected final CreateBridgeServlet getCreateBridgeServlet() {
+        final CreateBridgeServlet createBridgeServlet = this.createBridgeServlet;
+        assertNotNull(createBridgeServlet);
+        return Objects.requireNonNull(createBridgeServlet);
+    }
+
+    @BeforeEach
+    public final void setUpConfigFlowTest() {
+        setUpCrawler();
+        setUpServlets();
+    }
+
+    private void setUpCrawler() {
+        HttpClientFactory clientFactory = getService(HttpClientFactory.class);
+        assertNotNull(clientFactory);
+        crawler = new WebsiteCrawler(Objects.requireNonNull(clientFactory));
+    }
+
+    private void setUpServlets() {
+        MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+        assertNotNull(configService);
+
+        accountOverviewServlet = configService.getAccountOverviewServlet();
+        forwardToLoginServlet = configService.getForwardToLoginServlet();
+        resultServlet = configService.getResultServlet();
+        successServlet = configService.getSuccessServlet();
+        createBridgeServlet = configService.getCreateBridgeServlet();
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/MieleCloudBindingIntegrationTestConstants.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/MieleCloudBindingIntegrationTestConstants.java
new file mode 100644 (file)
index 0000000..d7bab98
--- /dev/null
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * The {@link MieleCloudBindingIntegrationTestConstants} class holds common constants used in integration tests.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MieleCloudBindingIntegrationTestConstants {
+    private MieleCloudBindingIntegrationTestConstants() {
+    }
+
+    public static final String SERIAL_NUMBER = "000124430017";
+
+    public static final String BRIDGE_ID = "genesis";
+
+    public static final ThingUID BRIDGE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+            BRIDGE_ID);
+
+    public static final ThingUID WASHING_MACHINE_THING_UID = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID OVEN_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_OVEN,
+            BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID HOB_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOB,
+            BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID FRIDGE_FREEZER_DEVICE_THING_UID = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID HOOD_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOOD,
+            BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID COFFEE_SYSTEM_THING_UID = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID WINE_STORAGE_DEVICE_THING_UID = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE, BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID DRYER_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_DRYER,
+            BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID DISHWASHER_DEVICE_THING_UID = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_DISHWASHER, BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID DISH_WARMER_DEVICE_THING_UID = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_DISH_WARMER, BRIDGE_THING_UID, SERIAL_NUMBER);
+    public static final ThingUID ROBOTIC_VACUUM_CLEANER_THING_UID = new ThingUID(
+            MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER, BRIDGE_THING_UID, SERIAL_NUMBER);
+
+    public static final String MIELE_CLOUD_ACCOUNT_LABEL = "Miele Cloud Account";
+    public static final String CONFIG_PARAM_REFRESH_TOKEN = "refreshToken";
+
+    public static final String ACCESS_TOKEN = "DE_ABCDE";
+    public static final String ALTERNATIVE_ACCESS_TOKEN = "DE_01234";
+    public static final String REFRESH_TOKEN = "AT_12345";
+
+    public static final String CLIENT_ID = "01234567-890a-bcde-f012-34567890abcd";
+    public static final String CLIENT_SECRET = "0123456789abcdefghijklmnopqrstiu";
+
+    public static final String AUTHORIZATION_CODE = "0123456789";
+
+    public static final String EMAIL = "openhab@openhab.org";
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/OpenHabOsgiTest.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/OpenHabOsgiTest.java
new file mode 100644 (file)
index 0000000..b0522d3
--- /dev/null
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.MIELE_CLOUD_ACCOUNT_LABEL;
+
+import java.util.Collections;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.test.java.JavaOSGiTest;
+import org.openhab.core.test.storage.VolatileStorageService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ManagedThingProvider;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+
+/**
+ * Parent class for openHAB OSGi tests offering helper methods for common interactions with the openHAB runtime and its
+ * services.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public abstract class OpenHabOsgiTest extends JavaOSGiTest {
+    @Nullable
+    private Inbox inbox;
+    @Nullable
+    private ThingRegistry thingRegistry;
+
+    protected Inbox getInbox() {
+        assertNotNull(inbox);
+        return Objects.requireNonNull(inbox);
+    }
+
+    protected ThingRegistry getThingRegistry() {
+        assertNotNull(thingRegistry);
+        return Objects.requireNonNull(thingRegistry);
+    }
+
+    @BeforeEach
+    public void setUpEshOsgiTest() {
+        registerVolatileStorageService();
+        inbox = getService(Inbox.class);
+        setUpThingRegistry();
+    }
+
+    private void setUpThingRegistry() {
+        thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
+        assertNotNull(thingRegistry, "Thing registry is missing");
+    }
+
+    /**
+     * Sets up a {@link Bridge} with an attached {@link MieleBridgeHandler} and registers it with the
+     * {@link ManagedThingProvider} and {@link ThingRegistry}.
+     */
+    public void setUpBridge() {
+        ManagedThingProvider managedThingProvider = getService(ManagedThingProvider.class);
+        assertNotNull(managedThingProvider);
+
+        Bridge bridge = BridgeBuilder
+                .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+                        MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+                .withConfiguration(
+                        new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+                                MieleCloudBindingIntegrationTestConstants.EMAIL)))
+                .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+        assertNotNull(bridge);
+
+        managedThingProvider.add(bridge);
+
+        waitForAssert(() -> {
+            assertNotNull(bridge.getHandler());
+            assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+        });
+    }
+
+    /**
+     * Registers a volatile storage service.
+     */
+    @Override
+    protected void registerVolatileStorageService() {
+        registerService(new VolatileStorageService());
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/ReflectionUtil.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/ReflectionUtil.java
new file mode 100644 (file)
index 0000000..c53b168
--- /dev/null
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Utility class for reflection operations such as accessing private fields or methods.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ReflectionUtil {
+    private ReflectionUtil() {
+    }
+
+    /**
+     * Gets a private attribute.
+     *
+     * @param object The object to get the attribute from.
+     * @param fieldName The name of the field to get.
+     * @return The obtained value.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws NoSuchFieldException if no field with the given name exists.
+     * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T getPrivate(Object object, String fieldName)
+            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+        Field field = getFieldFromClassHierarchy(object.getClass(), fieldName);
+        field.setAccessible(true);
+        return (T) field.get(object);
+    }
+
+    private static Field getFieldFromClassHierarchy(Class<?> clazz, String fieldName)
+            throws NoSuchFieldException, SecurityException {
+        Class<?> iteratedClass = clazz;
+        do {
+            try {
+                return iteratedClass.getDeclaredField(fieldName);
+            } catch (NoSuchFieldException e) {
+            }
+            iteratedClass = iteratedClass.getSuperclass();
+        } while (iteratedClass != null);
+        throw new NoSuchFieldException();
+    }
+
+    /**
+     * Sets a private attribute.
+     *
+     * @param object The object to set the attribute on.
+     * @param fieldName The name of the field to set.
+     * @param value The value to set.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws NoSuchFieldException if no field with the given name exists.
+     * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     */
+    public static void setPrivate(Object object, String fieldName, @Nullable Object value)
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        Field field = object.getClass().getDeclaredField(fieldName);
+        field.setAccessible(true);
+        field.set(object, value);
+    }
+
+    /**
+     * Sets an attribute declared as {@code private static final}.
+     *
+     * @param clazz The class owning the static attribute.
+     * @param fieldName The name of the attribute.
+     * @param value The new value.
+     * @throws NoSuchFieldException if no field with the given name exists.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+     */
+    public static void setPrivateStaticFinal(Class<?> clazz, String fieldName, @Nullable Object value)
+            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+        Field field = clazz.getDeclaredField(fieldName);
+        field.setAccessible(true);
+
+        Field modifiersField = Field.class.getDeclaredField("modifiers");
+        modifiersField.setAccessible(true);
+        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+
+        field.set(null, value);
+    }
+
+    /**
+     * Invokes a private method on an object.
+     *
+     * @param object The object to invoke the method on.
+     * @param methodName The name of the method to invoke.
+     * @param parameters The parameters of the method invocation.
+     * @return The method call's return value.
+     * @throws NoSuchMethodException if no method with the given parameters or name exists.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     * @throws InvocationTargetException if the invoked method throws an exception.
+     */
+    public static <T> T invokePrivate(Object object, String methodName, Object... parameters)
+            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+        Class<?>[] parameterTypes = new Class[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            parameterTypes[i] = parameters[i].getClass();
+        }
+
+        return invokePrivate(object, methodName, parameterTypes, parameters);
+    }
+
+    /**
+     * Invokes a private method on an object.
+     *
+     * @param object The object to invoke the method on.
+     * @param methodName The name of the method to invoke.
+     * @param parameterTypes The types of the parameters.
+     * @param parameters The parameters of the method invocation.
+     * @return The method call's return value.
+     * @throws NoSuchMethodException if no method with the given parameters or name exists.
+     * @throws SecurityException if the operation is not allowed.
+     * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+     * @throws IllegalArgumentException if one of the passed parameters is invalid.
+     * @throws InvocationTargetException if the invoked method throws an exception.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T invokePrivate(Object object, String methodName, Class<?>[] parameterTypes, Object... parameters)
+            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+        Method method = getMethodFromClassHierarchy(object.getClass(), methodName, parameterTypes);
+        method.setAccessible(true);
+        try {
+            return (T) method.invoke(object, parameters);
+        } catch (InvocationTargetException e) {
+            throw new IllegalStateException(e.getCause());
+        }
+    }
+
+    private static Method getMethodFromClassHierarchy(Class<?> clazz, String methodName, Class<?>[] parameterTypes)
+            throws NoSuchMethodException {
+        Class<?> iteratedClass = clazz;
+        do {
+            try {
+                return iteratedClass.getDeclaredMethod(methodName, parameterTypes);
+            } catch (NoSuchMethodException e) {
+            }
+            iteratedClass = iteratedClass.getSuperclass();
+        } while (iteratedClass != null);
+        throw new NoSuchMethodException();
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/Website.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/Website.java
new file mode 100644 (file)
index 0000000..010a650
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Helper class for testing websites. Allows for easy access to the document contents.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class Website {
+    private String content;
+
+    protected Website(String content) {
+        this.content = content;
+    }
+
+    /**
+     * Gets the part of the content representing the element that surrounds the given text.
+     */
+    private String getElementSurrounding(String text) {
+        int index = content.indexOf(text);
+        if (index == -1) {
+            throw new IllegalStateException("Could not find \"" + text + "\" in \"" + content + "\"");
+        }
+
+        int elementBegin = content.lastIndexOf('<', index);
+        if (elementBegin == -1) {
+            throw new IllegalStateException("\"" + text + "\" is not contained in \"" + content + "\"");
+        }
+
+        int elementEnd = content.indexOf('>', index);
+        if (elementEnd == -1) {
+            throw new IllegalStateException("Malformatted HTML content: " + content);
+        }
+
+        return content.substring(elementBegin, elementEnd + 1);
+    }
+
+    /**
+     * Gets the value of an attribute from an element.
+     */
+    private String getAttributeFromElement(String element, String attribute) {
+        int valueStart = element.indexOf(attribute + "=\"");
+        if (valueStart == -1) {
+            throw new IllegalStateException("Element \"" + element + "\" has no " + attribute);
+        }
+
+        int valueEnd = element.indexOf('\"', valueStart + attribute.length() + 2);
+        if (valueEnd == -1) {
+            throw new IllegalStateException("Malformatted HTML content in element: " + element);
+        }
+
+        return element.substring(valueStart + attribute.length() + 2, valueEnd);
+    }
+
+    /**
+     * Gets the value of the input field with the given name.
+     *
+     * @param inputName Name of the input field.
+     * @return The value of the input field.
+     */
+    public String getValueOfInput(String inputName) {
+        return getAttributeFromElement(getElementSurrounding("name=\"" + inputName + "\""), "value");
+    }
+
+    /**
+     * Gets the value of the href attribute of the link with the given title text.
+     */
+    public String getTargetOfLink(String linkTitle) {
+        return getAttributeFromElement(getElementSurrounding(linkTitle), "href");
+    }
+
+    /**
+     * Checks whether the given raw text is contained in the raw website code.
+     */
+    public boolean contains(String expectedContent) {
+        return this.content.contains(expectedContent);
+    }
+
+    /**
+     * Gets the value of the action attribute of the first form found in the website body.
+     */
+    public String getFormAction() {
+        int formActionStart = content.indexOf("<form action=\"");
+        if (formActionStart == -1) {
+            throw new IllegalStateException("Could not find a form in \"" + content + "\"");
+        }
+
+        int formActionEnd = content.indexOf('\"', formActionStart + 15);
+        if (formActionEnd == -1) {
+            throw new IllegalStateException("Malformatted HTML content in form: " + content);
+        }
+
+        return content.substring(formActionStart + 14, formActionEnd);
+    }
+
+    public String getContent() {
+        return content;
+    }
+}
diff --git a/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/WebsiteCrawler.java b/itests/org.openhab.binding.mielecloud.tests/src/main/java/org/openhab/binding/mielecloud/internal/util/WebsiteCrawler.java
new file mode 100644 (file)
index 0000000..50ab0cc
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Allows for requesting website content from URLs.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class WebsiteCrawler {
+    private HttpClient httpClient;
+
+    public WebsiteCrawler(HttpClientFactory httpClientFactory) {
+        this.httpClient = httpClientFactory.getCommonHttpClient();
+    }
+
+    /**
+     * Gets a website relative to the address of the openHAB installation running in test mode during integration tests.
+     *
+     * @param relativeUrl The relative URL.
+     * @return The website.
+     * @throws Exception if anything goes wrong.
+     */
+    public Website doGetRelative(String relativeUrl) throws Exception {
+        ContentResponse response = httpClient.GET("http://127.0.0.1:8080" + relativeUrl);
+        assertEquals(200, response.getStatus());
+        return new Website(response.getContentAsString());
+    }
+}
index 22f83d39db11960c43a020b1f4aba4bb2d8666da..423c637c17ef7c043b9262cce114e0ff886f31cd 100644 (file)
@@ -22,6 +22,7 @@
     <module>org.openhab.binding.feed.tests</module>
     <module>org.openhab.binding.hue.tests</module>
     <module>org.openhab.binding.max.tests</module>
+    <module>org.openhab.binding.mielecloud.tests</module>
     <module>org.openhab.binding.modbus.tests</module>
     <!-- MQTT tests need to be refactored to not use the embedded broker bundle anymore
       <module>org.openhab.binding.mqtt.homeassistant.tests</module>