Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
iOS Scriptable Widget for Pi-hole
// Parameters:
// {"url":"https://pihole","apikey":"123abc"}
// Optional key in parameters: "theme": system|light|dark
let piholeURL = "" //set the URL here for debug http://
let piholeAPIkey = "" // set the API-key here for debug
let wTheme = "system" // set the theme for debug
if (config.runsInWidget) {
const widgetParams = (args.widgetParameter != null ? JSON.parse(args.widgetParameter) : null)
if (widgetParams==null) {
throw new Error("Please long press the widget and add the parameters.")
} else if (!widgetParams.hasOwnProperty("url") && !widgetParams.hasOwnProperty("apikey")) {
throw new Error("Wrong parameters.")
}
piholeURL = widgetParams.url
piholeAPIkey = widgetParams.apikey
if (widgetParams.hasOwnProperty("theme")) {
wTheme = widgetParams.theme
}
}
let wBackground = new LinearGradient()
let wColor = new Color("#ffffff")
setTheme()
let piholeStats = await getStats()
let wSize = config.widgetFamily || "small" //set size of widget for debug
let widget = await createWidget() || null
if (!config.runsInWidget) {
if (wSize=="large") { await widget.presentLarge() }
else if (wSize=="medium") { await widget.presentMedium() }
else { await widget.presentSmall() }
}
Script.setWidget(widget)
Script.complete()
async function createWidget() {
let w = new ListWidget()
w.backgroundGradient = wBackground
w.addSpacer()
w.setPadding(5, 15, 0, (wSize=="small" ? 0 : 10))
let state = (piholeStats!=null ? (piholeStats.status=="enabled" ? true : false) : null)
let icn = null
if (state==true) {
icn = SFSymbol.named((state ? "checkmark.shield.fill" : "xmark.shield.fill"))
} else {
icn = SFSymbol.named("xmark.circle.fill")
state = false
}
let topStack = w.addStack()
let content = topStack.addImage(icn.image)
content.tintColor = (state ? Color.green() : Color.red())
content.imageSize = new Size(16,16)
topStack.addSpacer(5)
content = topStack.addText("Pi-hole")
content.font = Font.blackSystemFont(16)
content.textColor = wColor
w.addSpacer(8)
if (piholeStats==null) {
content = w.addText("No connection")
content.font = Font.thinSystemFont(14)
content.textColor = wColor
w.addSpacer()
return w
}
w.url = piholeURL
addItem(w, "Total Queries", piholeStats.dns_queries_all_types)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
addItem(w, "Queries Blocked", piholeStats.ads_blocked_today)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
addItem(w, "Percent Blocked", piholeStats.ads_percentage_today + "%")
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
layoutStack.centerAlignContent()
addItem(w, "Domains on Blocklist", piholeStats.domains_being_blocked)
if (wSize=="large") {
addItem(w, "Unique Domains", piholeStats.unique_domains)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
addItem(w, "Cached Queries", piholeStats.queries_cached)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
addItem(w, "Queries Forwarded", piholeStats.queries_forwarded)
layoutStack = w.addStack()
layoutStack.setPadding(5, 0, 0, 10)
addItem(w, "Clients Seen / Unique", piholeStats.clients_ever_seen + " / " + piholeStats.unique_clients)
}
w.addSpacer()
return w
}
function addItem(w, strHeadline, strData) {
let fontSizeHeadline = 12
let fontSizeString = 9
switch (wSize) {
case "large":
fontSizeHeadline = 18
fontSizeString = 14
break;
case "medium":
fontSizeHeadline = 14
fontSizeString = 11
break;
}
let layoutStack = w.addStack()
layoutStack.setPadding(3, 0, 0, 10)
layoutStack.centerAlignContent()
content = layoutStack.addText(strHeadline)
content.font = Font.mediumSystemFont(fontSizeHeadline)
content.textColor = wColor
layoutStack.addSpacer()
content = layoutStack.addText(strData)
content.font = Font.mediumSystemFont(fontSizeString)
content.textColor = wColor
}
function setTheme() {
if (wTheme=="system") {
if (Device.isUsingDarkAppearance()) {
wTheme = "dark"
} else {
wTheme = "light"
}
}
wBackground.locations = [0, 1]
if (wTheme=="dark") {
wBackground.colors = [
new Color("#384d54"),
new Color("#384d54")
]
wColor = new Color("#ffffff")
} else {
wBackground.colors = [
new Color("#ffffffe6"),
new Color("#ffffffe6")
]
wColor = new Color("#000000")
}
}
async function getStats() {
try {
let req = new Request(piholeURL + "/admin/api.php?summary&auth=" + piholeAPIkey)
let json = await req.loadJSON()
return json
} catch {
return null
}
}
@dennerforen

This comment has been minimized.

Copy link

@dennerforen dennerforen commented Nov 3, 2020

Thx for your script. But what is apikey?

I have url, username and pass phrase for login. Is that a combo?

Greets

@pbalthasar

This comment has been minimized.

Copy link

@pbalthasar pbalthasar commented Nov 3, 2020

Thx for your script. But what is apikey?

In your PiHole web interface go to settings and in the API tab select "Show API".

@dennerforen

This comment has been minimized.

Copy link

@dennerforen dennerforen commented Nov 3, 2020

Thx balthasar,

I found the api key

My pihole is only found with direct ip adress like 192.168.10.5 and has no https, only http.

If I put the parameters in the widget

192.168.10.5,Apikey

I’ve got no connection. If I chance Code direkt and put them between the „“ it works within scriptable but not shows the widget but if i try it with the witget and put 192.168.10.6, Apikeyt. Or leave the parameters empty, I’ve got

Fehler in line 10:8 eof

@malesfth

This comment has been minimized.

Copy link
Owner Author

@malesfth malesfth commented Nov 3, 2020

Thx balthasar,

I found the api key

My pihole is only found with direct ip adress like 192.168.10.5 and has no https, only http.

If I put the parameters in the widget

192.168.10.5,Apikey

I’ve got no connection. If I chance Code direkt and put them between the „“ it works within scriptable but not shows the widget but if i try it with the witget and put 192.168.10.6, Apikeyt. Or leave the parameters empty, I’ve got

Fehler in line 10:8 eof

Hey dennerforen,

press and hold on the widget. There you see input field „Parameter“.
Put in there:
{"url":"http://192.168.10.5","apikey":"dein api-key 😉"}

@Gwai

This comment has been minimized.

Copy link

@Gwai Gwai commented Nov 4, 2020

i get the same error and with the published solution i still get the error... What can i do?!

@dennerforen

This comment has been minimized.

Copy link

@dennerforen dennerforen commented Nov 4, 2020

@ malesfth thx that does the trick

@Gwai

It is important to use { the brackets at begin and end of the phrase.

@Gwai

This comment has been minimized.

Copy link

@Gwai Gwai commented Nov 4, 2020

@ malesfth thx that does the trick

@Gwai

It is important to use { the brackets at begin and end of the phrase.

I write this:

{“url“:“https://192.168.180.23“,“apikey“:“longapikey“}

Get this:
18D08E91-B0B3-44F6-B6D3-8300A7145AD1

@pbalthasar

This comment has been minimized.

Copy link

@pbalthasar pbalthasar commented Nov 4, 2020

{“url“:“https://192.168.180.23“,“apikey“:“longapikey“}

Write "http" not "https"

@Gwai

This comment has been minimized.

Copy link

@Gwai Gwai commented Nov 4, 2020

{“url“:“https://192.168.180.23“,“apikey“:“longapikey“}

Write "http" not "https"

Thx i tried without s, doesn‘t work. Tried again and it works now

@JoeGit42

This comment has been minimized.

Copy link

@JoeGit42 JoeGit42 commented Jan 17, 2021

Hello malesfth,
I've done some changes, you might like and want to use in your widget:

  1. deleted apikey-usage - it is not necessary for the used functions
  2. added support to see status of available updates (similar to homebridge widget)

IMG_1126

// Parameters:
// {"url":"https://pihole","apikey":"123abc"}
// Optional key in parameters: "theme": system|light|dark

let piholeURL = "" //set the URL here for debug http://
let piholeAPIkey = "" // set the API-key here for debug
let wTheme = "" // set the theme for debug

if (config.runsInWidget) {
	const widgetParams = (args.widgetParameter != null ? JSON.parse(args.widgetParameter) : null)
	if (widgetParams==null) {
		throw new Error("Please long press the widget and add the parameters.")
	} else if (!widgetParams.hasOwnProperty("url") && !widgetParams.hasOwnProperty("apikey")) {
		throw new Error("Wrong parameters.")
	}
	
	piholeURL = widgetParams.url
	piholeAPIkey = widgetParams.apikey
	if (widgetParams.hasOwnProperty("theme")) {
		wTheme = widgetParams.theme
	}
}

let wBackground = new LinearGradient()
let wColor = new Color("#ffffff")
setTheme()

let piholeStats = await getStats()
let adminPage   = await getAdminPage()
let isLatestVer = getUpdateStats(adminPage)

let wSize = config.widgetFamily || "large" //set size of widget for debug
let widget = await createWidget() || null

if (!config.runsInWidget) {
	if (wSize=="large") { await widget.presentLarge() }
	else if (wSize=="medium") { await widget.presentMedium() }
	else { await widget.presentSmall() }
}
Script.setWidget(widget)
Script.complete()

async function createWidget() {
	let w = new ListWidget()
	w.backgroundGradient = wBackground
	w.addSpacer()
	w.setPadding(5, 15, 0, (wSize=="small" ? 0 : 10))
	
	let state = (piholeStats!=null ? (piholeStats.status=="enabled" ? true : false) : null)
	let icn = null
	
	if (state==true) {
		icn = SFSymbol.named((state ? "checkmark.shield.fill" : "xmark.shield.fill"))
	} else {
		icn = SFSymbol.named("xmark.circle.fill")
		state = false
	}
	
	let topStack = w.addStack()
	topStack.layoutHorizontally()
	topStack.setPadding(5, 0, 0, 10)

	let content = topStack.addImage(icn.image)
	content.tintColor = (state ? Color.green() : Color.red())
	content.imageSize = new Size(16,16)
	topStack.addSpacer(5)
	
	content = topStack.addText("Pi-hole")
	content.font = Font.blackSystemFont(16)
	content.textColor = wColor

	// added version status
	if (wSize=="small") {
		topStack = w.addStack() // new line
  	topStack.layoutHorizontally()
	} else { // medium or large
		topStack.addSpacer()
  	topStack.addText("    ") // same line with distance to title
	}

	addUpdateItem(topStack, isLatestVer[0], "Pi" + ((wSize=="small") ? "" : "-hole"))
	topStack.addText("  ")
	addUpdateItem(topStack, isLatestVer[1], "WebUI")
	topStack.addText("  ")
	addUpdateItem(topStack, isLatestVer[2], "FTL")
	
	w.addSpacer(8)
	
	if (piholeStats==null) {
		content = w.addText("No connection")
	    content.font = Font.thinSystemFont(14)
	    content.textColor = wColor
		w.addSpacer()
		return w
	}
	
	w.url = piholeURL + "/admin/"
	
	addItem(w, "Total Queries", piholeStats.dns_queries_all_types)
		
	layoutStack = w.addStack()
	layoutStack.setPadding(5, 0, 0, 10)
	layoutStack.centerAlignContent()
		
	addItem(w, "Queries Blocked", piholeStats.ads_blocked_today)
		
	layoutStack = w.addStack()
	layoutStack.setPadding(5, 0, 0, 10)
	layoutStack.centerAlignContent()
		
	addItem(w, "Percent Blocked", piholeStats.ads_percentage_today + "%")
		
	layoutStack = w.addStack()
	layoutStack.setPadding(5, 0, 0, 10)
	layoutStack.centerAlignContent()
		
	addItem(w, "Domains on Blocklist", piholeStats.domains_being_blocked)

	if (wSize=="large") {
		addItem(w, "Unique Domains", piholeStats.unique_domains)
		
		layoutStack = w.addStack()
		layoutStack.setPadding(5, 0, 0, 10)
		
		addItem(w, "Cached Queries", piholeStats.queries_cached)
		
		layoutStack = w.addStack()
		layoutStack.setPadding(5, 0, 0, 10)
		
		addItem(w, "Queries Forwarded", piholeStats.queries_forwarded)
		
		layoutStack = w.addStack()
		layoutStack.setPadding(5, 0, 0, 10)
		
		addItem(w, "Clients Seen / Unique", piholeStats.clients_ever_seen + " / " + piholeStats.unique_clients)
	}
  
	w.addSpacer()
	return w
}

function addItem(w, strHeadline, strData) {
	let fontSizeHeadline = 12
	let fontSizeString = 9
	switch (wSize) {
		case "large":
			fontSizeHeadline = 18
			fontSizeString = 14
		break;
		case "medium":
			fontSizeHeadline = 14
			fontSizeString = 11
		break;
	}
	
	let layoutStack = w.addStack()
	layoutStack.setPadding(3, 0, 0, 10)
	layoutStack.centerAlignContent()
		
	content = layoutStack.addText(strHeadline)
	content.font = Font.mediumSystemFont(fontSizeHeadline)
	content.textColor = wColor
	layoutStack.addSpacer()
		
	content = layoutStack.addText(strData)
	content.font = Font.mediumSystemFont(fontSizeString)
	content.textColor = wColor
}

function addUpdateItem(stack, status, text) {
	let icn = SFSymbol.named((status ? "checkmark.circle.fill" : "exclamationmark.triangle.fill"))
	let content = stack.addImage(icn.image)
	content.tintColor = (status ? Color.green() : Color.red())
	content.imageSize = new Size(14,14)
	content = stack.addText(((wSize!="small") ? " " : "" ) + text)
	content.font = Font.semiboldMonospacedSystemFont(12)
	content.textColor = wColor
}

function getUpdateStats (webPage) {
	// check how adminPage has to be parsed to get necessary updates
	var recognition = ["pi-hole/releases/v", "AdminLTE/releases/v", "FTL/releases/v"];
	var updStr = "pdate"
	let i
	let pos, posUpdate
	let page = webPage
	let numCharRec2Upd = 300  // the maximum number of characters between the identifier and the update info
	var isLatestVersion = new Array();
	
	for (i=0; i<recognition.length; i++) {
		isLatestVersion[i] = true
		pos = page.indexOf(recognition[i])
		if (pos >= 0) {
			page = page.substring(pos + recognition[i].length)
			posUpdate = page.indexOf(updStr)
			if (posUpdate >= 0) {
				if ((i+1) < recognition.length) {
					pos = page.indexOf(recognition[i+1])
					if (posUpdate < pos) { 	// Update between two version info
						isLatestVersion[i] = false   
					}									
				}	else if (	posUpdate <= numCharRec2Upd ) { // Update not far away from last version info
					isLatestVersion[i] = false					
				}		
			}	
		}
	}
	return isLatestVersion
}


function setTheme() {
	if (wTheme=="system") {
		if (Device.isUsingDarkAppearance()) {
			wTheme = "dark"
		} else {
		    wTheme = "light"
		}
	}
	wBackground.locations = [0, 1]
	if (wTheme=="dark") {
		wBackground.colors = [
			new Color("#384d54"), 
			new Color("#384d54") 
		]
		wColor = new Color("#ffffff")
	} else {
		wBackground.colors = [
			new Color("#ffffffe6"), //ffffffe6
			new Color("#ffffffe6")
		]
		wColor = new Color("#000000")
	}
}

async function getStats() {
	try {
		let req = new Request(piholeURL + "/admin/api.php?summary")
		let json = await req.loadJSON()
		return json
	} catch {
		return null
	}
}

async function getAdminPage() {
	try {
		let req = new Request(piholeURL + "/admin/")
		let adminHTMLPage = await req.loadString()
		return adminHTMLPage
	} catch {
		return null
	}
}

@F3000

This comment has been minimized.

Copy link

@F3000 F3000 commented Jan 25, 2021

Thanks for the improved version . Wouldn’t it make sense to create an one GitHub project with this ?

@kraemjo

This comment has been minimized.

Copy link

@kraemjo kraemjo commented Jan 28, 2021

It runs under scriptable, but not as a widget.
IMG_5913

@Fennek88

This comment has been minimized.

Copy link

@Fennek88 Fennek88 commented Jan 28, 2021

@kraemjo
It worked for me after I deleted this first part in the beginning:

if (config.runsInWidget) {

const widgetParams = (args.widgetParameter != null ? JSON.parse(args.widgetParameter) : null)
if (widgetParams==null) {
	throw new Error("Please long press the widget and add the parameters.")
} else if (!widgetParams.hasOwnProperty("url") && !widgetParams.hasOwnProperty("apikey")) {
	throw new Error("Wrong parameters.")
}

piholeURL = widgetParams.url
piholeAPIkey = widgetParams.apikey
if (widgetParams.hasOwnProperty("theme")) {
	wTheme = widgetParams.theme
}

}

@kraemjo

This comment has been minimized.

Copy link

@kraemjo kraemjo commented Jan 29, 2021

@Fennek88 thanks, that was the solution

@r8ders2k

This comment has been minimized.

Copy link

@r8ders2k r8ders2k commented Apr 15, 2021

Just wanted to say, "Thanks!" I'm new to Homebridge and Pi-Hole, but I've got both the Homebridge and Pi-hole scripts running on my iPhone, after I figured out the correct url's and apikey go in the scripts. Very nice!!!

IMG_B1F57068922E-1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment