[RELEASE] Fully Kiosk Browser Controller

Just trying this now and it's great, thanks mate.

Howdy mate, I just wanted to follow-up to see if you had a chance to make this change?

Oh yes. I think I updated on the day we talked about it. 1.22 has the fix in it.

2 Likes

Thanks mate I will give it a crack this evening!

1 Like

Thanks @gavincampbell I have confirmed it's working, I can now press the doorbell and it will snap an image from the ip camera at my entry and show on the tablet in the loungeroom which is brilliant!

1 Like

Testing FKBC devices using the device's speak "box" command, with embedded SSML text:
Hello <break time="3s"/> goodbye

  • Works on my Kindle Fire 8's
  • Fails, by speaking the control characters and command on a Samsung J2 phone with Android 6.01

Would appreciate any suggestions or ideas for updating the TTS engine on the J2 phone to support SSML. Neither installed TTS engine: Samsung or Google, works. The Google TTS app version is dated Nov 13, 2019

Update: link to Amazon SSML support

In the latest version of the code (I can't remember what version exactly) but I changed the TTS to utilize the Hubitat TTS instead of utilizing the FKB TTS.

That being said, embedded SSML probably won't work because it is sending a URL over to the device to play that it creates on the hubitat device.

If it works on the Kindle's, your TTS is probably the previous method that sent the text over, and the fact that it works is a good thing. The J2 phone though probably doesn't know what to do with the SSML text.

I actually didn't know abut SSML until now and looked it up. Just curious, can you try adding the tags and try on the J2? I see that mentioned on the google sites about SSML and just curious if its needed. Otherwise , not sure.

Don't understand why the Android 6 J2 does not understand SSML, it's using the latest Google text to speech app

My current FKBC is a version I ported to ST back when, that also works on HE. To test your update I changed the speak command from

sendCommandPost("cmd=textToSpeech&text=${java.net.URLEncoder.encode(text, "UTF-8")}")

to

def sound = textToSpeech(text)
playSound(sound.uri)

The updated code works on the Fires and the J2 in HE, have not tried as yet in ST. I'm 100% on HE, but still have ST setup and working for testing my SHM Delay smartapp.

However, it now uses a deep male voice vs the phone selected female voice. Assuming that is a HE selected voice. Any way to change the voice?

Thats why I was curious if you wrapped it in the tag if it worked because when looking at the google links they show that, I didn't see that tag with the amazon stuff.

Even with the SSML tags?

In hubitat look under settings and then hub details. It has like 50 voices to choose from.

Went back to the textToSpeech method and wrapped the command in <speak>...</speak> tags. It works perfectly on the Fires and J2!

Tested text commands
<speak>Hello <break time="3s"/> goodbye</speak>
<speak>Hello goodbye</speak>

Using your updated code anything wrapped in tags played my Chime sound file, but did not create an error--WTF??? Works ok without tags

Thanks, got it. Had to tap/click on down arrow to see the choices

Did some quick tests.

Looks like the built in textToSpeech method does support SSML but if you wrap it with it doesn't work. That throws it off. So leave that off and everything should translate fine.

This should technically work on everything then as all it does is pass the mp3 link over to the device to play. If you turn on logging you can see the mp3 link in there. And you can select the voice.

Using the other method of sending the text could probably be less reliable as you now depend on the device to translate it, and as we see, every device can be different.

Tested older method on ST, and wrapping text with embedded SSML in <speak> tags worked. Did not try any Amazon extensions with the Fires.

Thank you for your help and assistance.

1 Like

I can't get this to work on my Fire Tablet 8". Every command returns " response.status: 408" I have tried every setting available and can't seem to get it to work. I did see mention of this above and @gavincampbell suggested it was a timeout. However, there was no real suggestion on how to address it. Every command seems to return this. Can someone help me figure out what setting on the device, or in the controller I do not have set correctly? I am quite confident it must be setting issue, though it may be a Fire tablet-specific issue. Does anyone have this working on a Fire tablet 8"?

LJ

Yes, it works on my Fire devices and also Android devices. Issues are usually caused by a mismatch in one or more of the following device settings or FKB settings

In the FKBC device

  • Server IP Address: The IP address of the device running FKB as shown in the Remote Administration: Enable Admin from Local Network setting. This IP address should be permanently reserved for the device in your router.
  • Server Port: The port used for the FKB REST API. Default: 2323.
  • Server Password: The password set in FKB to connect to the REST API

In the Fully Kiosk Browser settings: Remote administration

  • Enable Remote Administration: On
  • Remote Admin Password: must match Server Password above
  • Enable Admin from Local Network: On

Thanks for the reply. I have checked each those things dozens of times to make sure I am not blind or crazy. Those all match up. Still no luck. Might there be any Android settings which affect this? There are dozens of setting in FKB and Android None of them look as though they are set in a way which would conflict or prevent functionality but I may be missing something. any other thoughts?

LJ

EDIT: also, I can Ping the ip address and port of the device so it appears open. If indeed this is a timeout, is there something in the code of this Device which could allow one to adjust the timeout? I am going to try looking at the code and see if i can identify where, if anywhere, I might adjust this.

Implemented this on a couple Fire 10's and 3 Fire 8's over the weekend. Works brilliantly, except for audio (TTS and mp3). I tried it out first on a Fire 10 yesterday and it was flawless, audio and all.
Then today when I rolled it out to my other Fire 10 and the 3 Fire 8's, I kept getting the below error message on the tablet whenever I issue an audio or TTS command.
"Failed loading sound: Wrong URL or unsupported format."

Now even the first Fire 10 that was working yesterday is returning the same error message (I didn't change anything). No matter what I select for Volume Stream, I get the same results. But again, everything else in the DH works fine.

Anyone have any suggestions?

I think since I last checked this device handler the Fully developer has published a lot more documentation on the REST API here ozerov .de/fully-kiosk-browser/#rest

I was able to use the set the motion detection on and off using a POST command from my computer (cmd=setBooleanSetting&key=motionDetection&value=true) but when I try to add it to the device handler it is not working. Not sure why. I even enabled debug logging but nothing about the commands shows up. If somebody could figure out why I'd be very grateful. Here is the device handler (for Smartthings)
metadata {
definition (name: "Fully Kiosk Browser Controller", namespace: "GvnCampbell", author: "Gavin Campbell", ") {
capability "Tone"
if (isSmartThings())
{
capability "Speech Synthesis"
// capability "Audio Notification"
}
else
{
capability "SpeechSynthesis"
// capability "AudioNotification"
}
capability "AudioVolume"
capability "Refresh"
capability "Actuator"
capability "Alarm"
if (isSmartThings())
attribute "volume","number"
command "launchAppPackage"
command "bringFullyToFront"
command "screenOn"
command "screenOff"
command "triggerMotion"
command "startScreensaver"
command "stopScreensaver"
command "loadURL",["String"]
command "loadStartURL"
command "setScreenBrightness",["Number"]
command "chime"
command "alarm"
command "playSound",["String"]
command "stopSound"
command "turnOnMotion"
command "turnOffMotion"

    }
	preferences {
		input(name:"serverIP",type:"string",title:"Server IP Address",defaultValue:"",required:true)
		input(name:"serverPort",type:"string",title:"Server Port",defaultValue:"2323",required:true)
		input(name:"serverPassword",type:"string",title:"Server Password",defaultValue:"",required:true)
		input(name:"toneFile",type:"string",title:"Tone Audio File URL",defaultValue:"",required:false)
		input(name:"alarmFile",type:"string",title:"Alarm Audio File URL",defaultValue:"",required:false)
		input(name:"appPackage",type:"string",title:"Application to Launch",defaultValue:"",required:false)
		if (isSmartThings())
			input(name:"urlToLoad",type:"string",title:"URL to load",defaultValue:"google.com",required:false)
		input(name:"loggingLevel",type:"enum",title:"Logging Level",description:"Set the level of logging.",options:["none","debug","trace","info","warn","error"],defaultValue:"debug",required:true)
    }
	if (isSmartThings())
		{
		tiles
			{
			standardTile("screenOn", "device.switch", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Screen On', action:"screenOn"
				}
			controlTile("levelSliderControl", "device.levelx", "slider", height: 1,
	             width: 1, inactiveLabel: false, range:"(0..255)")
	            {
			    state "levelx", action:"setScreenBrightness", label:'${currentValue}'
				}
			standardTile("screenOff", "device.switch", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Screen Off', action:"screenOff", backgroundColor: "#ffffff"
				}
			standardTile("speak", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Speak', action:"Speech Synthesis.speak", icon:"st.Electronics.electronics13"
				}
			standardTile("beep", "device.tone", inactiveLabel: false, decoration: "flat")
				{
				state "default", label:'Play Beep', action:'beep', inactiveLabel:false, decoration: "flat"
				}
			standardTile("launchapp", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Launch App', action:"launchAppPackage"
				}
			standardTile("fullyfront", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Fully to Front', action:"bringFullyToFront"
				}
			standardTile("motionOn", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Motion On', action:"turnMotionOn"
				}
			standardTile("motionOff", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Motion Off', action:"turnMotionOff"
				}
			standardTile("setmotion", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Trigger Motion', action:"triggerMotion"
				}
			standardTile("saverOn", "device.switch", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Screen Saver On', action:"startScreensaver"
				}
			standardTile("saverOff", "device.switch", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Screen Saver Off', action:"stopScreensaver"
				}
			standardTile("loadUrl", "device.switch", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Load Url', action:"loadURL"
				}
			standardTile("loadStartUrl", "device.switch", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Load Start URL', action:"loadStartURL"
				}
//			standardTile("mute", "device.switch", inactiveLabel: false, decoration: "flat") 
//				{
//				state "default", label:'Mute Sound', action:"mute"
//				}
//			standardTile("Unmute", "device.speech", inactiveLabel: false, decoration: "flat") 
//				{
//				state "default", label:'UnMute', action:"unmute"
//				}
			standardTile("volumeup", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Volume up 10%', action:"volumeUp"
				}
			controlTile("volumeSliderControl", "device.volume", "slider", height: 1,
	             width: 1, inactiveLabel: false, range:"(0..100)")
	            {
			    state "volume", action:"setVolume", label:'${currentValue}'
				}
			standardTile("volumedown", "device.speech", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Volume down 10%', action:"volumeDown"
				}
			standardTile("soundalarm", "device.alarm", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Sound Alarm', action:"siren"
				}
			standardTile("playsound", "device.tone", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Play Sound', action:'playSound'
				}
			standardTile("stopsound", "device.tone", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Stop Sound', action:'stopSound'
				}
			standardTile("refresh", "device.tone", inactiveLabel: false, decoration: "flat") 
				{
				state "default", label:'Screen Refresh', action:"refresh"
				}
			main ('speak')	
			}
		}	
	}

def parse(String description) {
	if (isSmartThings())
		{	
		def logprefix = "[parse]"
		logger(logprefix+' '+description,"trace")
		def map
		map = stringToMap(description)
		def headerString = new String(map.headers.decodeBase64())
		def bodyString = new String(map.body.decodeBase64())
		logger(logprefix+' headers:'+headerString,'trace')
		if (headerString.contains("200 OK"))
			{
			if (bodyString.contains("Error"))
				logger (logprefix+' '+bodyString,'error')  
			}

		else
			{
			logger (logprefix+' '+headerString+' '+bodyString,'error')  
			}
		}	
	}


// *** [ Initialization Methods ] *********************************************
def installed() {
	def logprefix = "[installed] "
    logger logprefix
    initialize()
}
def updated() {
	def logprefix = "[updated] "
    logger logprefix
	initialize()
}
def initialize() {
	def logprefix = "[initialize] "
    logger logprefix
}

// *** [ Device Methods ] *****************************************************
def beep() {
	def logprefix = "[beep] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=playSound&url=${toneFile}")
}
def chime() {beep()}
def launchAppPackage(appName) {
	def logprefix = "[launchAppPackage] "
	if (isSmartThings())
		{
		if (appName)
			{
			logger(logprefix+"ST appName:${appName}","trace")
			sendCommandPost("cmd=startApplication&package=${appName}")
			}
		else
		if (appPackage)
			{	
			logger(logprefix+"ST appPackage:"+appPackage,"trace")
			sendCommandPost("cmd=startApplication&package=${appPackage}")
			}
		else
			logger(logprefix+"no package name or setting provided","error")
		}
	else
		{
		logger(logprefix+"HE appPackage:${url}","trace")
		sendCommandPost("cmd=startApplication&package=${appPackage}")
		}
	}
def bringFullyToFront() {
	def logprefix = "[bringFullyToFront] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=toForeground")
}
def screenOn() {
	def logprefix = "[screenOn] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=screenOn")
}
def screenOff() {
	def logprefix = "[screenOff] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=screenOff")
}
def setScreenBrightness(value) {
	def logprefix = "[setScreenBrightness] "
	logger(logprefix+"value:${value}","trace")
	sendEvent([name:"levelx",value:value])
	sendCommandPost("cmd=setStringSetting&key=screenBrightness&value=${value}")
}
def turnMotionOn() {
	def logprefix = "[turnMotionOn] "
	logger(logprefix,"trace")
	sendCommandPost("cmd=setBooleanSetting&key=motionDetection&value=true")
}
def turnMotionOff() {
	def logprefix = "[turnMotionOff] "
	logger(logprefix,"trace")
	sendCommandPost("cmd=setBooleanSetting&key=motionDetection&value=false")
}
def triggerMotion() {
	def logprefix = "[triggerMotion] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=triggerMotion")
}
def startScreensaver() {
	def logprefix = "[startScreensaver] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=startScreensaver")
}
def stopScreensaver() {
	def logprefix = "[stopScreensaver] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=stopScreensaver")
}
def loadURL(url='') {
	def logprefix = "[loadURL] "
	logger(logprefix+"entered","trace")
	if (isSmartThings())
		{
		if (url>'')
			{
			logger(logprefix+"ST url:${url}","trace")
			sendCommandPost("cmd=loadURL&url=${url}")
			}
		else
		if (urlToLoad)
			{	
			logger(logprefix+"ST urlToLoad:"+urlToLoad,"trace")
			sendCommandPost("cmd=loadURL&url="+urlToLoad)
			}
		else
			logger(logprefix+"no url provided","error")
		}
	else
		{
		logger(logprefix+"HE url:${url}","trace")
		sendCommandPost("cmd=loadURL&url=${url}")
		}
}
def loadStartURL() {
	def logprefix = "[loadStartURL] "
	logger(logprefix,"trace")
	sendCommandPost("cmd=loadStartURL")
}
def speak(text="Fully Kiosk TTS Device Handler") {
	def logprefix = "[speak] "
	logger(logprefix+"text:${text}","trace")
	sendCommandPost("cmd=textToSpeech&text=${java.net.URLEncoder.encode(text, "UTF-8")}")
}
def setVolume(volumeLevel) {
	def logprefix = "[setVolume] "
	logger(logprefix+"volumeLevel:${volumeLevel}")
	sendEvent([name:"volume",value:volumeLevel])
//	for (def i=1;i<=10;i++) {
//		sendCommandPost("cmd=setAudioVolume&level=${volumeLevel}&stream=${i}")
//	}
	sendCommandPost("cmd=setAudioVolume&level=${volumeLevel}&stream=2")	//MP3 Sound stream
	if (isSmartThings())
		{
		def now = new Date()
		def runTime = new Date(now.getTime() + 3000)
		runOnce(runTime, setVolumeDelayed9,[data: [volume: volumeLevel]])
		}
	else
		{
		sendCommandPost("cmd=setAudioVolume&level=${volumeLevel}&stream=9")  //TTS stream
		}
	}
	
def setVolumeDelayed9(evt)
	{
	def logprefix = "[setVolumeDelayed2] + ${evt}" 
	logger(logprefix)
	sendCommandPost("cmd=setAudioVolume&level=${evt.volume}&stream=9") //MP3 Sound stream 
	}
def volumeUp() {
	def logprefix = "[volumeUp] "
	logger(logprefix)
	def newVolume = device.currentValue("volume")
	if (newVolume) {
		newVolume = newVolume.toInteger() + 10
		newVolume = Math.min(newVolume,100)
		setVolume(newVolume)
	}
}
def volumeDown() {
	def logprefix = "[volumeDown] "
	logger(logprefix)
	def newVolume = device.currentValue("volume")
	if (newVolume) {
		newVolume = newVolume.toInteger() - 10
		newVolume = Math.max(newVolume,0)
		setVolume(newVolume)
	}
}
def refresh() {
  	def logprefix = "[refresh] "
  	logger logprefix
	sendCommandPost("cmd=deviceInfo")
}
// *** [Alarm commands]********************************************************
def off()		//alias for stopSound
	{stopSound()}
def both()		//alias for alarm
	{alarm()}
def siren()		//alias for alarm
	{alarm()}
def alarm()
	{
	def logprefix = "[alarm] "
    logger(logprefix,"trace")
    setVolume(100)
	sendCommandPost("cmd=playSound&url=${alarmFile}&loop=true")
	}
// *** [Play Stop any sound file command]*******************************************
def playTrack(track=""){playSound(track)} 
def playSound(SoundFile="")
	{
	def logprefix = "[playSound] "
    logger(logprefix,"trace")
    if (SoundFile>"")
		sendCommandPost("cmd=playSound&url=${SoundFile}")
	else 
	if (toneFile>"")
		sendCommandPost("cmd=playSound&url=${toneFile}")
	else	
		sendCommandPost("cmd=playSound&url=https://www.arnb.org/sounds/doorbell.mp3")

	}
def stopSound()
	{
	def logprefix = "[stopSound] "
    logger(logprefix,"trace")
	sendCommandPost("cmd=stopSound")
	}

// *** [ Platform Determination Methods ] *************************************
private isSmartThings()
	{ 
	return (physicalgraph?.device?.HubAction)
//	if (!hubitat?.device?.HubAction)
//		return true
//	else
//		return false
	}
private isHubitat() 
	{ 
	return (hubitat?.device?.HubAction)
	}

// *** [ Communication Methods ] **********************************************
def sendCommandPost(cmdDetails="")
	{
  	def logprefix = "[sendCommandPost] "
  	logger logprefix
	if (isSmartThings())
		STsendCommandPost(cmdDetails)
	else
		HEsendCommandPost(cmdDetails)
	}

// [Hubitat Communications]****************************************************
def HEsendCommandPost(cmdDetails="") {
	def logprefix = "[HEsendCommandPost] "
	logger(logprefix+"cmdDetails:${cmdDetails}","trace")
	def postParams = [
		uri: "http://${serverIP}:${serverPort}/?type=json&password=${serverPassword}&${cmdDetails}",
		requestContentType: 'application/json',
		contentType: 'application/json'
	]
	logger(logprefix+postParams)
	asynchttpPost("sendCommandCallback", postParams, null)
}
def sendCommandCallback(response, data) {
	def logprefix = "[sendCommandCallback] "
    logger(logprefix+"response.status: ${response.status}","trace")
	if (response?.status == 200) {
		logger(logprefix+"response.data: ${response.data}","trace")
		def jsonData = parseJson(response.data)
		if (jsonData?.ip4 || jsonData?.status == "OK") {
			logger(logprefix+"Updating last activity.","trace")
			sendEvent([name:"refresh"])
		}
	}
}

//	[SmartThing Communications] *********************************************** 
def STsendCommandPost(cmdDetails="") 
	{
	def logprefix = "[STsendCommandPost] "
	logger(logprefix+"cmdDetails:${cmdDetails} to ${serverIP}:${serverPort}","trace")
    if (serverIP?.trim()) 
    	{
        def hosthex = convertIPtoHex(serverIP)
        def porthex = convertPortToHex(serverPort)
        device.deviceNetworkId = "$hosthex:$porthex"
        def headers = [:] 
        headers.put("HOST", "$serverIP:$serverPort")
        def method = "POST"
	    def hubAction = physicalgraph.device.HubAction.newInstance(
            method: method,
            path: "/?type=json&password=${serverPassword}&${cmdDetails}",
            headers: headers
            );
		logger(logprefix+"hubAction: ${hubAction}","trace")
        return hubAction
		}
	}

private String convertIPtoHex(ipAddress) { 
    String hex = ipAddress.tokenize( '.' ).collect {  String.format( '%02X', it.toInteger() ) }.join()
//    log.debug "IP address entered is $ipAddress and the converted hex code is $hex"
    return hex
}

private String convertPortToHex(port) {
    String hexport = port.toString().format( '%04X', port.toInteger() )
//    log.debug hexport
    return hexport
}	
// *** [ Logger ] *************************************************************
private logger(loggingText,loggingType="debug") {
	def internalLogging = false
	def internalLoggingSize = 500
	if (internalLogging) { if (!state.logger) {	state.logger = [] }	} else { state.logger = [] }

	loggingType = loggingType.toLowerCase()
	def forceLog = false
	if (loggingType.endsWith("!")) {
		forceLog = true
		loggingType = loggingType.substring(0, loggingType.length() - 1)
	}
	def loggingTypeList = ["trace","debug","warn","info","error"]
	if (!loggingTypeList.contains(loggingType)) { loggingType="debug" }
	if ((!loggingLevel||loggingLevel=="none") && loggingType == "error") {
	} else if (forceLog) {
	} else if (loggingLevel == "debug" || (loggingType == "error")) {
	} else if (loggingLevel == "trace" && (loggingType == "trace" || loggingType == "info")) {
	} else if (loggingLevel == "info"  && (loggingType == "info")) {
	} else if (loggingLevel == "warn"  && (loggingType == "warn")) {
	} else { loggingText = null }
	if (loggingText) {
		log."${loggingType}" loggingText
		if (internalLogging) {
			if (state.logger.size() >= internalLoggingSize) { state.logger.pop() }
			state.logger.push("<b>log.${loggingType}:</b>\t${loggingText}")
		}
	}

}

The error suggest it's something to do with the file not being found (wrong url) or not being able to be played due to it's file format (unsupported format). Is the file definitely there and can be played manually on the device in some way outside of Fully Kiosk?

Why are you using the smartthings device handler on HE? The proper driver for HE is at the top of this thread.