/** * * Hubitat Package Manager v1.9.3 * * Copyright 2020 Dominick Meglio * * If you find this useful, donations are always appreciated * https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7LBRPJRLJSDDN&source=url * * * * csteele v1.9.3 improved displayHeader to include the Main Menu Option selected * refactored delete app to use new endpoint * csteele v1.9.2 added 'Connection': 'keep-alive' to install/uninstall Apps, Drivers and Bundles * extended timeout to 7 min (420 seconds) * Take advantage of v2.3.8 Bundle install/uninstall * csteele v1.9.1 updatePackage() ignores blank app or driver definitions * Take advantage of endpoints in v2.3.7 for app and driver uninstall * take advantage of endpoint for driver code list in v2.3.6 * clarify "failed download" error message with App or Driver hint * change updateTime to be NOT the 00 second * extended installBundle() timeout to 5 min (300 seconds) * csteele v1.9.0 Install 'location:' can't be empty. * add color to download icon * take advantage of endpoint for app code list in v2.3.6 * csteele v1.8.11 merge of: Add support for hub SSL (HTTPS) from kevdliu PR * csteele v1.8.10 correct File where contents are HTML [installFile() & uninstallFile()] * fix for default installBundle() (tomw) * csteele v1.8.9 allow required: true as string also * fix for a ProBundle install (a) * csteele v1.8.8 check for Null for Invalid Category & Tags * log.error changed to log.warn where the event was handled * detect/delete missing Repositories * csteele v1.8.7 fix to Update bundle. (birdslikewires) * csteele v1.8.6 Un-Match added * moved Bundles before Apps/Drivers for Install and Repair * included Bundles into modify * csteele v1.8.5 Bundles support added * csteele v1.8.4 Migrated to HubitatCommunity * added txtEnable to silence log.info messages * use httpS for Fast Search * csteele v1.8.3 No change here. Changes were to Dominic's repo to cause an upgrade to here * csteele v1.8.2.A Converted to using HubitatCommunity.com as the search resource. [Lines 66-67 & 379-380] * added footer to display version and copyright fields * added feature to identify Azure search vs sql search */ public static String version() { return "v1.9.3" } def getThisCopyright(){"© 2020 Dominick Meglio"} definition( name: "Hubitat Package Manager", namespace: "dcm.hpm", author: "Dominick Meglio", description: "Provides a utility to maintain the apps and drivers on your Hubitat making both installation and updates easier", category: "My Apps", importUrl: "https://raw.githubusercontent.com/HubitatCommunity/hubitatpackagemanager/main/apps/Package_Manager.groovy", documentationLink: "https://hubitatpackagemanager.hubitatcommunity.com/", iconUrl: "", iconX2Url: "", iconX3Url: "", singleInstance: true) preferences { page(name: "prefSettings") page(name: "prefOptions") page(name: "prefPkgInstall") page(name: "prefPkgInstallUrl") page(name: "prefInstallRepositorySearch") page(name: "prefInstallRepositorySearchResults") page(name: "prefPkgInstallRepository") page(name: "prefPkgInstallRepositoryChoose") page(name: "prefPkgModify") page(name: "prefPkgRepair") page(name: "prefPkgRepairExecute") page(name: "prefPkgUpdate") page(name: "prefPkgUninstall") page(name: "prefInstallChoices") page(name: "prefInstallVerify") page(name: "prefInstall") page(name: "prefPkgModifyChoices") page(name: "prefVerifyPackageChanges") page(name: "prefMakePackageChanges") page(name: "prefPkgUninstallConfirm") page(name: "prefUninstall") page(name: "prefPkgVerifyUpdates") page(name: "prefPkgUpdatesComplete") page(name: "prefPkgMatchUp") page(name: "prefPkgMatchUpVerify") page(name: "prefPkgMatchUpComplete") page(name: "prefPkgView") page(name: "prefPkgUnMatch") page(name: "prefPkgUnMatchVerify") page(name: "prefPkgUnMatchComplete") } import groovy.transform.Field import java.util.regex.Matcher @Field static String repositoryListing = "https://raw.githubusercontent.com/HubitatCommunity/hubitat-packagerepositories/master/repositories.json" @Field static String settingsFile = "https://raw.githubusercontent.com/HubitatCommunity/hubitat-packagerepositories/master/settings.json" @Field static String searchFuzzyApiUrl = "https://hubitatpackagemanager.azurewebsites.net/graphql" @Field static String searchFastApiUrl = "https://hubitatpackagemanager.hubitatcommunity.com/searchHPMpkgs2.php" @Field static List categories = [] @Field static List allPackages = [] @Field static def completedActions = [:] @Field static def manifestForRollback = null @Field static def downloadQueue = [:] @Field static Integer maxDownloadQueueSize = 10 @Field static String installAction = "" @Field static String installMode = "" @Field static String statusMessage = "" @Field static String errorTitle = "" @Field static String errorMessage = "" @Field static Boolean errorOccurred = false @Field static def packagesWithUpdates = [:] @Field static def optionalItemsToShow = [:] @Field static def updateDetails = [:] @Field static List appsToInstallForModify = [] @Field static List appsToUninstallForModify = [] @Field static List driversToInstallForModify = [] @Field static List driversToUninstallForModify = [] @Field static List packagesMatchingInstalledEntries = [] @Field static List iconTags = ["ZWave", "Zigbee", "Cloud", "LAN"] @Field static String srchSrcTxt = "" @Field static String searchApiUrl = "" def installed() { initialize() } def updated() { unschedule() initialize() } def initialize() { def timeOfDayForUpdateChecks if (updateCheckTime == null) timeOfDayForUpdateChecks = timeToday("00:00") else timeOfDayForUpdateChecks = timeToday(updateCheckTime, location.timeZone) schedule("13 ${timeOfDayForUpdateChecks.minutes} ${timeOfDayForUpdateChecks.hours} ? * *", checkForUpdates) performMigrations() } def uninstalled() { log.warn "uninstalling app" unschedule() } def appButtonHandler(btn) { switch (btn) { case "btnMainMenu": state.mainMenu = true break case "btnBack": state.back = true case "btnAddRepo": state.customRepo = true break case "btnUnMatch": state.UnMatch = true break case ~/^btnDeleteRepo(\d+)/: deleteCustomRepository(Matcher.lastMatcher[0][1].toInteger()) break } } def prefOptions() { state.remove("mainMenu") if (state.customRepo && customRepo != "" && customRepo != null) { def repoListing = getJSONFile(customRepo) if (repoListing == null) { clearStateSettings(true) return buildErrorPage("Error loading repository", "The repository file you specified could not be loaded.") } else { installedRepositories << customRepo if (state.customRepositories == null) state.customRepositories = [:] if (state.customRepositories[customRepo] == null) state.customRepositories << ["${customRepo}":repoListing.author] } } if (state.firstRun == true) return prefPkgMatchUp() else { clearStateSettings(true) initialize() installHPMManifest() } if (installedRepositories == null) { logDebug "No installed repositories, grabbing all" def repos = [] as List state.repositoryListingJSON.repositories.each { it -> repos << it.location } app.updateSetting("installedRepositories", repos) } state.categoriesAndTags = loadSettingsFile() return dynamicPage(name: "prefOptions", title: "", install: true, uninstall: false) { displayHeader() if (isHubSecurityEnabled() && !hpmSecurity) { section { paragraph "Hub Security appears to be enabled but is not configured in HPM. Please configure hub security." } } if (state.newRepoMessage != "") { section { paragraph state.newRepoMessage state.newRepoMessage = "" } } section { paragraph "What would you like to do?" href(name: "prefPkgInstall", title: "Install", required: false, page: "prefPkgInstall", description: "Install a new package.") href(name: "prefPkgUpdate", title: "Update", required: false, page: "prefPkgUpdate", description: "Check for updates for your installed packages.") href(name: "prefPkgModify", title: "Modify", required: false, page: "prefPkgModify", description: "Modify an already installed package. This allows you to add or remove optional components.") href(name: "prefPkgRepair", title: "Repair", required: false, page: "prefPkgRepair", description: "Repair a package by ensuring all of the newest versions are installed in case something went wrong.") href(name: "prefPkgUninstall", title: "Uninstall", required: false, page: "prefPkgUninstall", description: "Uninstall packages.") href(name: "prefPkgMatchUp", title: "Match Up", required: false, page: "prefPkgMatchUp", description: "Match up the apps and drivers you already have installed with packages available so that you can use the package manager to get future updates.") href(name: "prefPkgView", title: "View Apps and Drivers", required: false, page: "prefPkgView", description: "View the apps and drivers that are managed by packages.") href(name: "prefSettings", title: "Package Manager Settings", required: false, page: "prefSettings", params: [force:true], description: "Modify Hubitat Package Manager Settings.") } displayFooter() } } def prefSettings(params) { if (state.UnMatch) return prefPkgUnMatch() def showSettingsForSecurityEnablement = false state.newRepoMessage = "" if (state.manifests == null) state.manifests = [:] performMigrations() if (updateRepositoryListing()?.size() > 0) { state.newRepoMessage = "One or more new repositories have been added. You may want to do a Match Up to ensure all of your packages are detected." } if (isHubSecurityEnabled() && !hpmSecurity) { showSettingsForSecurityEnablement = true } installHPMManifest() if (app.getInstallationState() == "COMPLETE" && params?.force != true && !showSettingsForSecurityEnablement) return prefOptions() else { def showInstall = app.getInstallationState() == "INCOMPLETE" if (showInstall) state.firstRun = true return dynamicPage(name: "prefSettings", title: "", nextPage: "prefOptions", install: showInstall, uninstall: false) { displayHeader(' Settings') section ("Hub Security") { if (showSettingsForSecurityEnablement) { paragraph "Hub Security appears to be enabled on your hub but is not enabled within HPM. Please configure hub security below" } paragraph "In order to automatically install apps and drivers you must specify your Hubitat admin username and password if Hub Security is enabled." input "hpmSecurity", "bool", title: "Hub Security Enabled", submitOnChange: true if (hpmSecurity) { input "hpmUsername", "string", title: "Hub Security username", required: true input "hpmPassword", "password", title: "Hub Security password", required: true } paragraph "If you have SSL (HTTPS) enabled on your hub, you must enable the option below. If you're not sure, leave the option disabled." input "sslEnabled", "bool", title: "SSL (HTTPS) Enabled", submitOnChange: true if (showInstall) paragraph "Please click Done and restart the app to continue." } if (!state.firstRun) { section ("General") { input "debugOutput", "bool", title: "Enable debug logging", defaultValue: true input "txtEnable", "bool", title: "Enable text logging", defaultValue: true input "includeBetas", "bool", title: "When updating, install pre-release versions. Note: Pre-releases often include more bugs and should be considered beta software" } section { paragraph "


Package Updates" input "updateCheckTime", "time", title: "Specify what time update checking should be performed", defaultValue: "00:00", required: true input "notifyUpdatesAvailable", "bool", title: "Notify me when updates are available", submitOnChange: true if (notifyUpdatesAvailable) input "notifyDevices", "capability.notification", title: "Devices to notify", required: true, multiple: true input "autoUpdateMode", "enum", title: "Install updates automatically when", options: ["Always":"Always", "Never":"Never", "Include":"Only those I list", "Exclude":"All packages except the ones I list"], submitOnChange: true if (autoUpdateMode != "Never") { if (autoUpdateMode == "Include") { def listOfPackages = getInstalledPackages(false) input "appsToAutoUpdate", "enum", title: "Which packages should be automatically updated?", required: true, multiple: true, options:listOfPackages } else if (autoUpdateMode == "Exclude") { def listOfPackages = getInstalledPackages(false) input "appsNotToAutoUpdate", "enum", title: "Which packages should NOT be automatically updated?", required: true, multiple: true, options:listOfPackages } input "notifyOnSuccess", "bool", title: "Notify me if automatic updates are successful", submitOnChange: true if (notifyOnSuccess) input "notifyUpdateSuccessDevices", "capability.notification", title: "Devices to notify", required: true, multiple: true input "notifyOnFailure", "bool", title: "Notify me if automatic updates are unsuccessful", submitOnChange: true if (notifyOnFailure) input "notifyUpdateFailureDevices", "capability.notification", title: "Devices to notify", required: true, multiple: true } if (notifyUpdatesAvailable || notifyOnSuccess || notifyOnFailure) input "notifyIncludeHubName", "bool", title: "Include hub name in notifications", defaultValue: false if (notifyOnSuccess || notifyOnFailure) input "notifySpecificPackages", "bool", title: "Send notifications for each specific package", defaultValue: false paragraph "
" } def reposToShow = [:] state.repositoryListingJSON.repositories.each { r -> reposToShow << ["${r.location}":r.name] } if (state.customRepositories != null) state.customRepositories.each { r -> reposToShow << ["${r.key}":r.value] } reposToShow = reposToShow.sort { r -> r.value } section ("Repositories") { input "installedRepositories", "enum", title: "Available repositories", options: reposToShow, multiple: true, required: true if (state.customRepositories && state.customRepositories.size()) { def i = 0 for (customRepo in state.customRepositories.keySet()) { paragraph "${state.customRepositories[customRepo]} - ${customRepo}", width: 10 input "btnDeleteRepo${i}", "button", title: "Remove", width: 2 i++ } } if (!state.customRepo) input "btnAddRepo", "button", title: "Add a Custom Repository", submitOnChange: false if (state.customRepo) input "customRepo", "text", title: "Enter the URL of the repository's directory listing file", required: true paragraph "
" } section ("Un-Match a Package") { paragraph "Un-Match selected Apps or Drivers by removing the cached manifest. Follow this step with a Match Up to install the latest Manifest. This feature is best used by experienced users. Or when instructed by the owner of a Package due to a change they made in their Manifest structure." input "btnUnMatch", "button", title: "Remove a Matched Package" } } } } } // Install a package pathway def prefPkgInstall() { if (state.mainMenu) return prefOptions() logDebug "prefPkgInstall" return dynamicPage(name: "prefPkgInstall", title: "", install: true, uninstall: false) { displayHeader(' Install') section { paragraph "Install a Package" paragraph "How would you like to install this package?" href(name: "prefInstallRepositorySearch", title: "Search by Keywords", required: false, page: "prefInstallRepositorySearch", description: "Search for packages by searching for keywords. This will only include the standard repositories, not custom repositories.") href(name: "prefPkgInstallRepository", title: "Browse by Tags", required: false, page: "prefPkgInstallRepository", description: "Choose a package from a repository browsing by tags. This will include both the standard repositories and any custom repositories you have setup.") href(name: "prefPkgInstallUrl", title: "From a URL", required: false, page: "prefPkgInstallUrl", description: "Install a package using a URL to a specific package. This is an advanced feature, only use it if you know how to find a package's manifest manually.") } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } def prefInstallRepositorySearch() { if (state.mainMenu) return prefOptions() state.remove("back") logDebug "prefInstallRepositorySearch" installMode = "search" searchApiUrl = searchFuzzyApiUrl srchSrcTxt = "Fuzzy" if (settings?.srchMethod != false) { srchSrcTxt = "Fast" searchApiUrl = searchFastApiUrl } return dynamicPage(name: "prefInstallRepositorySearch", title: "", nextPage: "prefInstallRepositorySearchResults", install: false, uninstall: false) { displayHeader(' Install') section { paragraph "Search by $srchSrcTxt" input "pkgSearch", "text", title: "Enter your search criteria", required: true paragraph "Search Method - Fast or Fuzzy Search" input "srchMethod", "bool", title: "Fast Search", defaultValue: true, submitOnChange: true paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } def prefInstallRepositorySearchResults() { if (state.mainMenu) return prefOptions() if (state.back) return prefInstallRepositorySearch() logDebug "prefInstallRepositorySearchResults" installMode = "search" def params = [ uri: searchApiUrl, contentType: "application/json", requestContentType: "application/json", body: [ "operationName": null, "variables": [ "searchQuery": pkgSearch ], "query": 'query Search($searchQuery: String) { repositories { author, gitHubUrl, payPalUrl, packages (search: $searchQuery) {name, description, location, tags}}}' ] ] def result = null httpPost(params) { resp -> result = resp.data } if (result?.data?.repositories) { def searchResults = [] for (repo in result.data.repositories) { for (packageItem in repo.packages) { if (settings?.srchMethod != false) { def pkg_tags = packageItem.tags[1..-2].tokenize(',') packageItem.tags = pkg_tags } packageItem << [author: repo.author, gitHubUrl: repo.gitHubUrl, payPalUrl: repo.payPalUrl, installed: state.manifests[packageItem.location] != null] searchResults << packageItem } } searchResults = searchResults.sort { it -> it.name } return dynamicPage(name: "prefInstallRepositorySearch", title: "", nextPage: "prefInstallRepositorySearchResults", install: false, uninstall: false) { displayHeader(' Install') section { paragraph "Search Results for ${pkgSearch} by $srchSrcTxt Search" addCss() } section { if (searchResults.size() > 0) { def i = 0 for (searchResult in searchResults) { renderPackageButton(searchResult,i) i++ } } else paragraph "No matching packages were found. Click Back to return to the search screen." } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 input "btnBack", "button", title: "Back", width: 3 } } } } def prefPkgInstallUrl() { if (state.mainMenu) return prefOptions() logDebug "prefPkgInstallUrl" installMode = "url" return dynamicPage(name: "prefPkgInstallUrl", title: "", nextPage: "prefInstallChoices", install: false, uninstall: false) { displayHeader(' Install') section { paragraph "Install a Package from URL" input "pkgInstall", "text", title: "Enter the URL of a package you wish to install (this should be a path to a packageManifest.json file).", required: true } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } def prefPkgInstallRepository() { if (errorOccurred == true) { return buildErrorPage(errorTitle, errorMessage) } if (atomicState.backgroundActionInProgress == null) { logDebug "prefPkgInstallRepository" atomicState.backgroundActionInProgress = true getMultipleJSONFiles(installedRepositories, performRepositoryRefreshComplete, performRepositoryRefreshStatus) } if (atomicState.backgroundActionInProgress != false) { return dynamicPage(name: "prefPkgInstallRepository", title: "", nextPage: "prefPkgInstallRepository", install: false, uninstall: false, refreshInterval: 2) { section { showHideNextButton(false) paragraph "Install a Package" paragraph "Refreshing repositories... Please wait..." paragraph getBackgroundStatusMessage() } } } else { installMode = "repository" return prefInstallChoices(null) } } def renderTags(pkgList) { def tags = [] for (pkg in pkgList) { if (state.categoriesAndTags.categories.contains(pkg.category)) { if (!tags.contains(pkg.category)) tags << pkg.category } else if (pkg.category?.trim()) { log.warn "Invalid category found: ${pkg.category} for ${pkg.location}" } for (tag in pkg.tags) { if (state.categoriesAndTags.tags.contains(tag)) { if (!tags.contains(tag)) tags << tag } else if (tag?.trim()) { log.warn "Invalid tag found: ${tag} for ${pkg.location}" } } } tags = tags.sort() input "pkgTags", "enum", title: "Choose tag(s)", options: tags, submitOnChange: true, multiple: true } def renderSortBy() { input "pkgSortBy", "enum", title: "Sort By", options: ["Author", "Name"], submitOnChange: true, defaultValue: "Name" } def prefInstallChoices(params) { if (state.mainMenu) return prefOptions() logDebug "prefInstallChoices" return dynamicPage(name: "prefInstallChoices", title: "", nextPage: "prefInstallVerify", install: false, uninstall: false) { displayHeader(' Install') section { addCss() paragraph "Install a Package from a Repository" if (installMode == "repository" && params == null) { //input "pkgCategory", "enum", title: "Choose a category", options: categories, required: true, submitOnChange: true renderTags(allPackages) if(pkgTags) { renderSortBy() def matchingPackages = [] for (pkg in allPackages) { if (state.manifests.containsKey(pkg.location)) { pkg.installed = true } if (pkgTags.contains(pkg.category) || pkg.tags.find { pkgTags.contains(it)}) { matchingPackages << pkg } } def sortedMatchingPackages if (pkgSortBy == "Name") sortedMatchingPackages = matchingPackages.sort { x -> x.name } else sortedMatchingPackages = matchingPackages.sort { y -> y.author } if (sortedMatchingPackages.size() > 0) { def i = 0 for (pkg in sortedMatchingPackages) { renderPackageButton(pkg,i) i++ } } else paragraph "No matching packages were found. Please choose a different category." } } } if (installMode == "search" || (installMode == "repository" && params != null)) { pkgInstall = params.location app.updateSetting("pkgInstall", params.location) state.payPalUrl = params.payPalUrl } if(pkgInstall) { if (state.manifests == null) state.manifests = [:] def manifest = getJSONFile(pkgInstall) if (manifest == null) { return buildErrorPage("Invalid Package File", "${pkgInstall} does not appear to be a valid Hubitat Package or does not exist.") } if (state.manifests[pkgInstall] != null) { return buildErrorPage("Package Already Installed", "${pkgInstall} has already been installed. If you would like to look for upgrades, use the Update function.") } if (manifest.minimumHEVersion != null && !verifyHEVersion(manifest.minimumHEVersion)) { return buildErrorPage("Unsupported Hubitat Firmware", "Your Hubitat Elevation firmware is not supported. You are running ${location.hub.firmwareVersionString} and this package requires at least ${manifest.minimumHEVersion}. Please upgrade your firmware to continue installing.") } else { def apps = getOptionalAppsFromManifest(manifest) def drivers = getOptionalDriversFromManifest(manifest) def bundles = getOptionalBundlesFromManifest(manifest) def title = "Choose the components to install" if (apps.size() == 0 && drivers.size() == 0) title = "Ready to install" section("${title}") { if (apps.size() > 0 || drivers.size() > 0 || bundles.size() > 0) paragraph "You are about to install ${manifest.packageName}. This package includes some optional components. Please choose which ones you would like to include below. Click Next when you are ready." else paragraph "You are about to install ${manifest.packageName}. Click next when you are ready." if (apps.size() > 0) input "appsToInstall", "enum", title: "Select the apps to install", options: apps, hideWhenEmpty: true, multiple: true if (drivers.size() > 0) input "driversToInstall", "enum", title: "Select the drivers to install", options: drivers, hideWhenEmpty: true, multiple: true if (bundles.size() > 0) input "bundlesToInstall", "enum", title: "Select the bundles to install", options: bundles, hideWhenEmpty: true, multiple: true } } } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } def getRepoName(location) { return state.repositoryListingJSON.repositories.find { it -> it.location == location }?.name } def performRepositoryRefreshStatus(uri, data) { def repoName = getRepoName(uri) setBackgroundStatusMessage("Refreshed ${repoName}") } def performRepositoryRefreshComplete(results, data) { allPackages = [] categories = [] for (uri in results.keySet()) { def repoName = getRepoName(uri) def result = results[uri] def fileContents = result.result if (fileContents == null) { setBackgroundStatusMessage("Unable to Refresh ${repoName}", "warn") continue } for (pkg in fileContents.packages) { def pkgDetails = [ repository: repoName, author: fileContents.author, githubUrl: fileContents.gitHubUrl, payPalUrl: fileContents.payPalUrl, name: pkg.name, description: pkg.description, location: pkg.location, category: pkg.category, tags: pkg.tags ] allPackages << pkgDetails if (!categories.contains(pkgDetails.category)) categories << pkgDetails.category } } allPackages = allPackages.sort() categories = categories.sort() atomicState.backgroundActionInProgress = false logDebug "Repositories refreshed" } def prefInstallVerify() { if (state.mainMenu) return prefOptions() logDebug "prefInstallVerify" if(!pkgInstall) return buildErrorPage("no package selected", "whats up?") // pr #114 atomicState.backgroundActionInProgress = null statusMessage = "" errorOccurred = null errorTitle = null errorMessage = null return dynamicPage(name: "prefInstallVerify", title: "", nextPage: "prefInstall", install: false, uninstall: false) { displayHeader(' Install') section { paragraph "Ready to install" def manifest = getJSONFile(pkgInstall) if (manifest.licenseFile) { def license = downloadFile(manifest.licenseFile) paragraph "By clicking next you accept the below license agreement:" paragraph "" paragraph "Click next to continue. This make take some time..." } else paragraph "Click the next button to install your selections. This may take some time..." def primaryApp = manifest?.apps?.find { item -> item.primary == true } if (primaryApp) input "launchInstaller", "bool", defaultValue: true, title: "Configure the installed package after installation completes." } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } def prefInstall() { if (state.mainMenu) return prefOptions() if (errorOccurred == true) { return buildErrorPage(errorTitle, errorMessage) } if (atomicState.backgroundActionInProgress == null) { logDebug "prefInstall" logDebug "Install beginning" atomicState.backgroundActionInProgress = true runInMillis(1,performInstallation) } if (atomicState.backgroundActionInProgress != false) { return dynamicPage(name: "prefInstall", title: "", nextPage: "prefInstall", install: false, uninstall: false, refreshInterval: 2) { displayHeader(' Install') section { showHideNextButton(false) paragraph "Installing" paragraph "Your installation is currently in progress... Please wait..." paragraph getBackgroundStatusMessage() } } } else { def primaryApp = state.manifests[pkgInstall]?.apps?.find {it -> it.primary == true } if (primaryApp == null || !launchInstaller) return complete("Installation complete", "The package was sucessfully installed, click Next to return to the Main Menu.") else return complete("Installation complete", "The package was sucessfully installed, click Next to configure your new package.", false, primaryApp.heID) } } def performInstallation() { if (!login()) return triggerError("Error logging in to hub", "An error occurred logging into the hub. Please verify your Hub Security username and password.", false) def manifest = getJSONFile(pkgInstall) if (shouldInstallBeta(manifest)) { manifest = getJSONFile(getItemDownloadLocation(manifest)) manifest.beta = true } else manifest.beta = false state.manifests[pkgInstall] = manifest state.manifests[pkgInstall].payPalUrl = state.payPalUrl state.payPalUrl = null minimizeStoredManifests() // Download all files first to reduce the chances of a network error def appFiles = [:] def driverFiles = [:] def fileManagerFiles = [:] def bundleFiles = [:] def fileMgrResults = downloadFileManagerFiles(manifest) if (fileMgrResults.success) fileManagerFiles = fileMgrResults.files else return triggerError("Failed download of file", "An error occurred downloading ${fileMgrResults.name}", false) def requiredApps = getRequiredAppsFromManifest(manifest) def requiredDrivers = getRequiredDriversFromManifest(manifest) def requiredBundles = getRequiredBundlesFromManifest(manifest) for (requiredApp in requiredApps) { // required = true def location = getItemDownloadLocation(requiredApp.value) setBackgroundStatusMessage("Downloading ${requiredApp.value.name}") def fileContents = downloadFile(location) if (fileContents == null) { state.manifests.remove(pkgInstall) return triggerError("Failed download of required app file", "An error occurred downloading ${location}", false) } appFiles[location] = fileContents } for (appToInstall in appsToInstall) { // required = false (aka optional) def matchedApp = manifest.apps.find { it.id == appToInstall} if (matchedApp != null) { def location = getItemDownloadLocation(matchedApp) setBackgroundStatusMessage("Downloading ${matchedApp.name}") def fileContents = downloadFile(location) if (fileContents == null) { state.manifests.remove(pkgInstall) return triggerError("Failed download of app file", "An error occurred downloading ${location}", false) } appFiles[location] = fileContents } } for (requiredDriver in requiredDrivers) { // required = true def location = getItemDownloadLocation(requiredDriver.value) setBackgroundStatusMessage("Downloading ${requiredDriver.value.name}") def fileContents = downloadFile(location) if (fileContents == null) { state.manifests.remove(pkgInstall) return triggerError("Failed download of required driver file", "An error occurred downloading ${location}", false) } driverFiles[location] = fileContents } for (driverToInstall in driversToInstall) { // required = false (aka optional) def matchedDriver = manifest.drivers.find { it.id == driverToInstall} if (matchedDriver != null) { def location = getItemDownloadLocation(matchedDriver) setBackgroundStatusMessage("Downloading ${matchedDriver.name}") def fileContents = downloadFile(location) if (fileContents == null) { state.manifests.remove(pkgInstall) return triggerError("Failed download of driver file", "An error occurred downloading ${location}", false) } driverFiles[location] = fileContents } } initializeRollbackState("install") // All files downloaded, execute installs for (bundleToInstall in requiredBundles) { // required = true def location = getItemDownloadLocation(bundleToInstall.value) setBackgroundStatusMessage("Installing ${bundleToInstall.value.name}") // from $location") if (!installBundle(location, false)) { state.manifests.remove(pkgInstall) return rollback("Failed to install bundle ${bundleToInstall.value.name} using ${location}. Please notify the package developer.", false) } } for (bundleToInstall in bundlesToInstall) { // required = false (aka optional) def matchedBundle = manifest.bundles.find { it.id == bundleToInstall} if (matchedBundle != null) { def location = getItemDownloadLocation(matchedBundle) def primary = matchedBundle.primary ?: false setBackgroundStatusMessage("Installing ${matchedBundle.name} from $location") if (!installBundle(location, primary)) { state.manifests.remove(pkgInstall) return rollback("Failed to install bundle ${matchedBundle.name} using ${location}. Please notify the package developer.", false) } } } for (requiredApp in requiredApps) { // required = true def location = getItemDownloadLocation(requiredApp.value) setBackgroundStatusMessage("Installing ${requiredApp.value.name}") def id = installApp(appFiles[location]) if (id == null) { state.manifests.remove(pkgInstall) return rollback("Failed to install app ${location}. Please notify the package developer.", false) } requiredApp.value.heID = id requiredApp.value.beta = shouldInstallBeta(requiredApp.value) if (requiredApp.value.oauth) enableOAuth(requiredApp.value.heID) } for (appToInstall in appsToInstall) { // required = false (aka optional) def matchedApp = manifest.apps.find { it.id == appToInstall} if (matchedApp != null) { def location = getItemDownloadLocation(matchedApp) setBackgroundStatusMessage("Installing ${matchedApp.name}") def id = installApp(appFiles[location]) if (id == null) { state.manifests.remove(pkgInstall) return rollback("Failed to install app ${location}. Please notify the package developer.", false) } matchedApp.heID = id matchedApp.beta = shouldInstallBeta(matchedApp) if (matchedApp.oauth) enableOAuth(matchedApp.heID) } } for (requiredDriver in requiredDrivers) { // required = true def location = getItemDownloadLocation(requiredDriver.value) setBackgroundStatusMessage("Installing ${requiredDriver.value.name}") def id = installDriver(driverFiles[location]) if (id == null) { state.manifests.remove(pkgInstall) return rollback("Failed to install driver ${location}. Please notify the package developer.", false) } requiredDriver.value.heID = id requiredDriver.value.beta = shouldInstallBeta(requiredDriver.value) } for (driverToInstall in driversToInstall) { // required = false (aka optional) def matchedDriver = manifest.drivers.find { it.id == driverToInstall} if (matchedDriver != null) { def location = getItemDownloadLocation(matchedDriver) setBackgroundStatusMessage("Installing ${matchedDriver.name}") def id = installDriver(driverFiles[location]) if (id == null) { state.manifests.remove(pkgInstall) return rollback("Failed to install driver ${location}. Please notify the package developer.", false) } matchedDriver.heID = id matchedDriver.beta = shouldInstallBeta(matchedDriver) } } for (fileToInstall in manifest.files) { def location = getItemDownloadLocation(fileToInstall) def fileContents = fileManagerFiles[location] setBackgroundStatusMessage("Installing ${location}") if (!installFile(fileToInstall.id, fileToInstall.name, fileContents)) { state.manifests.remove(pkgInstall) return rollback("Failed to install file ${location}. Please notify the package developer.", false) } else completedActions["fileInstalls"] << fileToInstall } atomicState.backgroundActionInProgress = false } // Modify a package pathway def prefPkgModify() { if (state.mainMenu) return prefOptions() logDebug "prefPkgModify" def pkgsToList = getInstalledPackages(true) return dynamicPage(name: "prefPkgModify", title: "", nextPage: "prefPkgModifyChoices", install: false, uninstall: false) { displayHeader(' Modify') section { paragraph "Modify a Package" paragraph "Only packages that have optional components are shown below." input "pkgModify", "enum", title: "Choose the package to modify", options: pkgsToList, required: true } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } def prefPkgModifyChoices() { if (state.mainMenu) return prefOptions() logDebug "prefPkgModifyChoices" def manifest = getInstalledManifest(pkgModify) def optionalApps = getOptionalAppsFromManifest(manifest) def optionalDrivers = getOptionalDriversFromManifest(manifest) def optionalBundles = getOptionalBundlesFromManifest(manifest) if (optionalApps?.size() > 0 || optionalDrivers?.size() > 0 || optionalBundles > 0) { def installedOptionalApps = [] def installedOptionalDrivers = [] def installedOptionalBundles = [] for (optApp in optionalApps) { if (isAppInstalled(manifest, optApp.key)) { installedOptionalApps << optApp.key } } for (optDriver in optionalDrivers) { if (isDriverInstalled(manifest, optDriver.key)) { installedOptionalDrivers << optDriver.key } } for (optBundle in optionalBundles) { if (isBundleInstalled(manifest, optBundle.key)) { installedOptionalBundles << optBundle.key } } return dynamicPage(name: "prefPkgModifyChoices", title: "", nextPage: "prefVerifyPackageChanges", install: false, uninstall: false) { displayHeader(' Modify') section { paragraph "Modify a Package" paragraph "Items below that are checked are currently installed. Those that are not checked are currently not installed." if (optionalApps.size() > 0) input "appsToModify", "enum", title: "Select the apps to install/uninstall", options: optionalApps, hideWhenEmpty: true, multiple: true, defaultValue: installedOptionalApps if (optionalDrivers.size() > 0) input "driversToModify", "enum", title: "Select the drivers to install/uninstall", options: optionalDrivers, hideWhenEmpty: true, multiple: true, defaultValue: installedOptionalDrivers } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } else { return dynamicPage(name: "prefPkgModifyChoices", title: "", install: true, uninstall: false) { section { paragraph "Nothing to modify" paragraph "This package does not have any optional components that you can modify." } section { paragraph "
" input "btnMainMenu", "button", title: "Main Menu", width: 3 } } } } def prefVerifyPackageChanges() { if (state.mainMenu) return prefOptions() logDebug "prefVerifyPackageChanges" def appsToUninstallStr = "