Can I Get a List of Installed Apps?

I'm interested in organising my apps and devices to suit my way of understanding things. I am interested to, within an App, get a list of installed apps, so a listing of all my RM rules, dashboards, maker api instances, Better Laundry Monitor, etc. Reason being is I would like to derive the URL for each app, so need the App Id and should be able to use the URL methods already provided to construct the full URL for the App. I then want to tag them in different ways for what they are used for, so lighting, presence / mode changes, dashboard content, etc. So I can then have a list of Apps and devices relating to a common purpose, including links to the devices, perhaps even including Room tags for the apps as well. But it is all dependent on being able to easily get a list of the Apps, which I haven't been able to find.

Want to say the last time I went down this road I ended up using an asynchttpGet against /installedapp/list and pulled the data from the response. I’ll check to see if I still have the code laying around.

2 Likes

This should get you going...

def getAppsList() {        // Modified from code by gavincampbell
    login() 
    //if(logEnable) log.debug "In getAppsList (${state.version}) - Getting installed Apps list"
	def params = [
		uri: "http://127.0.0.1:8080/app/list",
		textParser: true,
		headers: [
			Cookie: state.cookie
		]
	  ]
	
	def allAppsList = []
    def allAppNames = []
	try {
		httpGet(params) { resp ->     
			def matcherText = resp.data.text.replace("\n","").replace("\r","")
			def matcher = matcherText.findAll(/(<tr class="app-row" data-app-id="[^<>]+">.*?<\/tr>)/).each {
				def allFields = it.findAll(/(<td .*?<\/td>)/) // { match,f -> return f } 
				def id = it.find(/data-app-id="([^"]+)"/) { match,i -> return i.trim() }
				def title = allFields[0].find(/title="([^"]+)/) { match,t -> return t.trim() }
				allAppsList += [id:id,title:title]
                allAppNames << title
			}
		}
	} catch (e) {
		log.error "Error retrieving installed apps: ${e}"
        log.error(getExceptionMessageWithLine(e))
	}
    state.allAppsList = allAppsList
    state.allAppNames = allAppNames.sort { a, b -> a.toLowerCase() <=> b.toLowerCase() }
}
3 Likes

I'm probably after the installed apps, but I can see that both the idea and your code certainly gives me a good start, thanks @bptworld and @thebearmay.

2 Likes

I just need to learn some more regex ... :slight_smile:

Looks like I just need to find the lines like this from the installed apps list URL:

<a href="[/installedapp/configure/3645](http:// << Hub-IP >> /installedapp/configure/3645)" class="row_wrap">CoCoHue - Hue Bridge Integration</a>

Pulling out the 3645 in this case and then hopefully use the built in methods to more easily get the name and other relationships, though I could grab the name from the anchor text.

This was getting close until I realised they include similar links for installing the apps, so I think I will move from Regex to parsing the text as HTML and trying to traverse the structure.

def refresh() {
    
    
    def params = [
		uri: "http://127.0.0.1:8080/installedapp/list",
		textParser: true,
		headers: [:]
	  ]
	
	def allAppsList = []
    def allAppNames = []
	try {
		httpGet(params) { resp ->     
			def matcherText = resp.data.text.replace("\n","").replace("\r","")
			def matcher = matcherText.findAll(/<a href=\"\/installedapp\/configure\/([^"]+)\"(.*?)<\/a>/).each {
				def id = it.find(/configure\/([^"]+)\"/) { match,i -> return i.trim() };
				def title = it.find(/>([^"]+)<\/a>/) { match,t -> return t.trim() };
                //log.debug("id = " + id);
                //log.debug("title = " + title);
				allAppsList += [id:id,title:title]
                allAppNames << title
			}
		}
	} catch (e) {
		log.error "Error retrieving installed apps: ${e}"
        log.error(getExceptionMessageWithLine(e))
	}
    state.allAppsList = allAppsList
    //state.allAppNames = allAppNames.sort { a, b -> a.toLowerCase() <=> b.toLowerCase() }
}

The parsing of HTML seemed to be a dead-end, at least with my skillset.... So I improved on the regex that I posted before, removing duplicates, and tweaked the structure slightly to allow for searching on app id and the option to expand the list of attributes recorded for an app.

The next problem to solve is identifying parent / child app relationships, so I can present the list more logically. I feel like I need to do something similar to the "allFields" match that @bptworld has in the original code.

I'm also running this on an old C-4 and often get a timeout. Apart from probably needing to restart, I'll look at introducing the timeout option that I believe can be included in the httpGet.

def refresh() {
    
    
    def params = [
		uri: "http://127.0.0.1:8080/installedapp/list",
        textParser: true,
		headers: [:]
	  ]
	
	def allAppsList = [:]
	try {
		httpGet(params) { resp ->     
			
            def matcherText = resp.data.text.replace("\n","").replace("\r","")
			def matcher = matcherText.findAll(/<a href=\"\/installedapp\/configure\/([^"]+)\"(.*?)<\/a>/).each {
				def id = it.find(/configure\/([^"]+)\"/) { match,i -> return i.trim() };
				def title = it.find(/>([^"]+)<\/a>/) { match,t -> return t.trim() };
                if(allAppsList.get(id) == null) {
				    allAppsList.putAll([(id):[title:(title)]]);
                }
            }
		}
	} catch (e) {
		log.error "Error retrieving installed apps: ${e}"
        log.error(getExceptionMessageWithLine(e))
	}
    state.allAppsList = allAppsList
}

Well I think this code at least proves the theory that we can scrape the Installed App page to get a list of Apps, both what I am referring to as child and parent apps, plus their relationship, and strip the status decorations like those on Scenes. Don;t mark me too harshly on my use of regex, this is my first use in any significant way....

Now I need to make use of the data. Like I mentioned in my original post, I am keen to see my various apps organised into logical groups, such as those that are used for lighting in a particular room, mode management, etc. Essentially tagging each app, potentially with more than one tag, so I can group links to each one, maybe even listing associated devices....

Thanks to @thebearmay and @bptworld for your help. Bryan - I did realise later I used somewhat ambiguous references to "installed apps", so the code you provided makes sense now that I look back, installed "code" vs an instance of that app, with the latter being what I was after.

/**
 *  App List Test
 *
 *  Copyright 2022 Simon Burke
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who            What
 *    ----        ---            ----
 * 
 */
metadata {
	definition (name: "App List Test", namespace: "simnet", author: "Simon Burke") {
        capability "Refresh"
        
	}


}

def refresh() {
    
    log.debug("refresh: Refresh Process called");
    def params = [
		uri: "http://127.0.0.1:8080/installedapp/list",
        textParser: true,
        timeout: 60,
		headers: [:]
	  ]
	
	def allAppsList = [:]
	try {
		httpGet(params) { resp ->     
			
            def matcherText = resp.data.text.replace("\n","").replace("\r","")
            
            def grid = matcherText.find(/(<div class=\"mdl-grid\"> .*?<div id=\"list-view\">)/) { match,f -> return f }
            
            def parentMatcher = grid.findAll(/(<div class=\"app-grid grid-parentArea .*?<\/div)/).each {
                
                def id = it.find(/configure\/([^"]+)\"/) { match,i -> return i.trim() };
                def title = it.find(/>([^"]+)<\/a>/) { match,t -> 
                                                        t = t.replaceAll(/<span(.*?)<\/span>/,"");
                                                        return t.trim() };
                allAppsList.putAll([(id):[parentId:null,title:(title)]]);
            }
            
            def childMatcher = grid.findAll(/<div class=\"grid-childArea(.*?)<\/div/).each {
                
                def parentId = it.find(/childOf([^"]+)\"/) { match,i -> return i.trim() };
                def id = it.find(/configure\/([^"]+)\"/) { match,i -> return i.trim() };
                def childTitle;
                it.findAll(/app-row(.*?)<\/div/).each { children ->
                    childTitle = children.find(/>([^"]+)<\/a>/) { match,t -> 
                                                                     t = t.replaceAll(/<span(.*?)<\/span>/,"");
                                                                     return t.trim() };
                };
                
                allAppsList.putAll([(id):[parentId:(parentId),title:(childTitle)]]);
            }

		}
	} catch (e) {
		log.error "Error retrieving installed apps: ${e}"
        log.error(getExceptionMessageWithLine(e))
	}
    state.allAppsList = allAppsList
    log.debug("refresh: Refresh process finished");
}

Example output:

{
    2870 = {
        title = Rule Machine,
        parentId = null
    },
    1064 = {
        title = Harmony Hub - Volume,
        parentId = 12
    },
    3482 = {
        title = Mode Change To Night or Away,
        parentId = 2870
    },
    591 = {
        title = Grafana Maker API,
        parentId = null
    },
    3481 = {
        title = PC Controller(By Ramdev),
        parentId = null
    },
    196 = {
        title = Notifications,
        parentId = null
    },
    1732 = {
        title = Infrastructure,
        parentId = 6
    },
    2348 = {
        title = Living Chromecast Media Reset,
        parentId = 12
    },
    3513 = {
        title = Mode Change to Evening,
        parentId = 2870
    },
    2742 = {
        title = Ducted Air Conditioner,
        parentId = 6
    },
    2903 = {
        title = Living Room,
        parentId = 6
    },
    12 = {
        title = Rule Machine Legacy,
        parentId = null
    },
    14 = {
        title = Mode Manager,
        parentId = null
    },
    2 = {
        title = Chromecast Integration(beta),
        parentId = null
    },
    2357 = {
        title = Mode Button,
        parentId = 2356
    },
    2356 = {
        title = Button Controllers,
        parentId = null
    },
    5 = {
        title = Google Home,
        parentId = null
    },
    3323 = {
        title = Device Data Item Display,
        parentId = null
    },
    6 = {
        title = Hubitat® Dashboard,
        parentId = null
    },
    1189 = {
        title = Web Pinger,
        parentId = 1188
    },
    3322 = {
        title = Custom Device Note,
        parentId = null
    },
    1188 = {
        title = Web Pinger,
        parentId = null
    },
    2277 = {
        title = Kitchen Chromecast Media Reset,
        parentId = 12
    },
    3643 = {
        title = Simple Automation Rules,
        parentId = null
    },
    3609 = {
        title = Room Lighting,
        parentId = null
    },
    2839 = {
        title = Lights Status Report,
        parentId = 6
    },
    3645 = {
        title = CoCoHue - Hue Bridge Integration,
        parentId = null
    },
    804 = {
        title = Hubitat Package Manager,
        parentId = null
    },
    2838 = {
        title = Infrastructure Status Report,
        parentId = 6
    },
    3646 = {
        title = CoCoHue - Hue Bridge Integration(7A4BE9 - Philips hue),
        parentId = null
    },
    3385 = {
        title = Enhanced Dashboard Experience,
        parentId = null
    },
    173 = {
        title = Better Laundry Monitor,
        parentId = null
    },
    450 = {
        title = Washing Machine Notifications,
        parentId = 260
    },
    3417 = {
        title = Hubitat® Safety Monitor,
        parentId = null
    },
    2966 = {
        title = TestPopUpDash,
        parentId = 6
    },
    3613 = {
        title = Study Lights,
        parentId = 3609
    },
    2402 = {
        title = Test Dashboard,
        parentId = 6
    },
    2401 = {
        title = Preference Manager,
        parentId = null
    },
    3258 = {
        title = Sonos Integration,
        parentId = null
    },
    419 = {
        title = General Maker API,
        parentId = null
    },
    1956 = {
        title = Device Watchdog,
        parentId = null
    },
    3354 = {
        title = Outside Lights,
        parentId = 6
    },
    2342 = {
        title = Kitchen Chromecast Refresh,
        parentId = 12
    },
    260 = {
        title = Notifications,
        parentId = null
    },
    3193 = {
        title = Notification Proxy,
        parentId = null
    },
    2612 = {
        title = Study Speaker Media Reset,
        parentId = 12
    },
    421 = {
        title = Zone Control,
        parentId = 6
    },
    1445 = {
        title = Package Manager Notifications App,
        parentId = 260
    },
    587 = {
        title = Climate,
        parentId = 6
    },
    3226 = {
        title = Simple CSS Editor Dashboard,
        parentId = 6
    },
    2577 = {
        title = Dashboards - Update AV Content and Device,
        parentId = 12
    },
    3628 = {
        title = Hallway Lights,
        parentId = 3609
    }
}
2 Likes