/*
* File Manager Device
*
* Licensed Virtual 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
* ---- --- ----
* 21Mar2022 thebearmay take the text file methods and place them into a simple to use device driver
* 22Mar2022 thebearmay add listFiles command
* 23Mar2022 thebearmay add a temporary fileContent attribute, copyFile and fileTrimTop commands
* 25Mar2022 thebearmay add callback option for non-child application usage (input( "x", "capability.*"...))
* 27Mar2022 thebearmay allow hub w/security to be external file source
* 08Apr2022 thebearmay ensure correct encoding returned for extended characters
* 03Oct2022 thebearmay add image file capabilities
* 04Oct2022 thebearmay combine write methods
* 01Nov2022 thebearmay add file delete
* 05Nov2022 thebearmay exist attribute instantiate
* 08Dec2022 thebearmay Fix typo in fileDelete method
* 17Feb2023 thebearmay add lastFileWritten and lastFileWrittenTimeStamp
* 29Dec2023 thebearmay Append File to create if doesn't exist
* 03Jan2024 thebearmay convert \n in strings to the newline character for write file and append file commands
* 08Jan2024 thebearmay missed a debug line
* 08Feb2024 thebearmay try block around image read
*/
import java.net.URLEncoder
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import groovy.transform.Field
import java.text.SimpleDateFormat
@Field sdfList = ["yyyy-MM-dd","yyyy-MM-dd HH:mm","yyyy-MM-dd h:mma","yyyy-MM-dd HH:mm:ss","ddMMMyyyy HH:mm","ddMMMyyyy HH:mm:ss","ddMMMyyyy hh:mma", "dd/MM/yyyy HH:mm:ss", "MM/dd/yyyy HH:mm:ss", "dd/MM/yyyy hh:mma", "MM/dd/yyyy hh:mma", "MM/dd HH:mm", "HH:mm", "H:mm","h:mma", "HH:mm:ss", "Milliseconds"]
@SuppressWarnings('unused')
static String version() {return "0.2.11"}
metadata {
definition (
name: "File Manager Device",
namespace: "thebearmay",
author: "Jean P. May, Jr.",
description: "Device (or child device) that allows string data to be save and retrieved from HE Local File Storage",
importUrl:"https://raw.githubusercontent.com/thebearmay/hubitat/main/fileMgr.groovy"
) {
capability "Actuator"
attribute "exist", "string"
attribute "fileList", "string"
attribute "fileContent", "string"
attribute "lastFileWritten", "string"
attribute "lastFileWrittenTimeStamp", "string"
command "writeFile",[[name:"fileName", type:"STRING", description:"File Manager Destination Name"],
[name:"writeString", type:"STRING", description:"String to Store"]
]
command "writeFileAsHex",[[name:"fileName", type:"STRING", description:"File Manager Hex Encoded Destination Name"],
[name:"writeString", type:"STRING", description:"String to Store"]
]
command "xferFile",[[name:"inputURL", type:"STRING", description:"Input URL"],
[name:"fileName", type:"STRING", description:"File Manager Destination Name"]
]
command "copyFile",[[name:"inputURL", type:"STRING", description:"File Manager Source Name"],
[name:"fileName", type:"STRING", description:"File Manager Destination Name"]
]
command "appendFile",[[name:"fileName", type:"STRING", description:"File Manager Destination Name"],
[name:"appendString", type:"STRING", description:"String to Append"]
]
command "readFile",[[name:"fileName", type:"STRING",description:"Local File to Read"]]
command "readFileAsHex",[[name:"fileName", type:"STRING",description:"Local Hex Encoded File to Read"]]
command "fileExists",[[name:"fileName", type:"STRING",description:"File to Look for"]]
command "fileTrimTop",[[name:"fileName", type:"STRING",description:"File to Trim"],
[name:"offset", type:"NUMBER",description:"Number of Characters to Remove from the Beginning of the File"]
]
command "listFiles"
command "uploadImage",[[name:"iPath", type:"STRING",title:"Path Image"],
[name:"oName", type:"STRING",title:"File Manager Name"]
]
command "deleteFile", [[name:"f2Delete", type:"STRING", title: "Name of File to Delete"]]
command "readExternalFile",[[name:"fUrl", type:"STRING", title: "Path of file to read"]]
}
}
preferences {
input("security", "bool", title: "Hub Security Enabled", defaultValue: false, submitOnChange: true)
if (security) {
input("username", "string", title: "Hub Security Username", required: false)
input("password", "password", title: "Hub Security Password", required: false)
}
input("debugEnabled", "bool", title: "Enable debug logging?", width:4)
input("logResponses", "bool", title: "Log Responses to Commands", width:4)
input("sdfFormat", "enum", title: "Date/Time format", options: sdfList, submitOnChange:true, width:4)
input("allowAttrib", "bool", title: "Allow a TEMPORARY attribute for File Contents\nIf you use this set Event and State History to 1", width:4)
}
@SuppressWarnings('unused')
def installed() {
}
void updateAttr(String aKey, aValue, String aUnit = ""){
sendEvent(name:aKey, value:aValue, unit:aUnit)
}
def updated(){
if(debugEnable) {
log.debug "updated()"
runIn(1800,logsOff)
}
}
@SuppressWarnings('unused')
HashMap securityLogin(){
def result = false
try{
httpPost(
[
uri: "http://127.0.0.1:8080",
path: "/login",
query:
[
loginRedirect: "/"
],
body:
[
username: username,
password: password,
submit: "Login"
],
textParser: true,
ignoreSSLIssues: true
]
)
{ resp ->
// log.debug resp.data?.text
if (resp.data?.text?.contains("The login information you supplied was incorrect."))
result = false
else {
cookie = resp?.headers?.'Set-Cookie'?.split(';')?.getAt(0)
result = true
}
}
}catch (e){
log.error "Error logging in: ${e}"
result = false
cookie = null
}
return [result: result, cookie: cookie]
}
@SuppressWarnings('unused')
def fileTrimTop(fname, trimOffset, Closure closure) {
closure(fileTrimTop(fname, trimOffset))
}
@SuppressWarnings('unused')
Boolean fileTrimTop(fname, trimOffset){
fileData = readFile(fname)
return writeFile(fname, fileData.substring(trimOffset.toInteger(),fileData.length()-1))
}
@SuppressWarnings('unused')
def copyFile(fnameIn, fnameOut, Closure closure) {
closure(copyFile(fnameIn, fnameOut))
}
@SuppressWarnings('unused')
Boolean copyFile(fnameIn, fnameOut){
fileData = readFile(fnameIn)
return writeFile(fnameOut, fileData.substring(0,fileData.length()-1))
}
@SuppressWarnings('unused')
def fileExists(fName, Closure closure) {
closure(fileExists(fName))
}
@SuppressWarnings('unused')
Boolean fileExists(fName){
uri = "http://${location.hub.localIP}:8080/local/${fName}";
def params = [
uri: uri
]
try {
httpGet(params) { resp ->
if (resp != null){
if(logResponses) log.info "File Exist: true"
updateAttr("exist","true")
return true;
} else {
if(logResponses) log.info "File Exist: true"
updateAttr("exist","false")
return false
}
}
} catch (exception){
if (exception.message == "Not Found"){
if(logResponses) log.info "File Exist: false"
} else {
log.error("Find file $fName) :: Connection Exception: ${exception.message}")
}
updateAttr("exist","false")
return false;
}
}
@SuppressWarnings('unused')
def readFile(fName, Closure closure) {
closure(readFile(fName))
}
@SuppressWarnings('unused')
String readFile(fName){
if(security) cookie = securityLogin().cookie
uri = "http://${location.hub.localIP}:8080/local/${fName}"
def params = [
uri: uri,
contentType: "text/html",
textParser: true,
headers: [
"Cookie": cookie,
"Accept": "application/octet-stream"
]
]
try {
httpGet(params) { resp ->
if(resp!= null) {
int i = 0
String delim = ""
i = resp.data.read()
while (i != -1){
char c = (char) i
delim+=c
i = resp.data.read()
}
if(logResponses) log.info "File Read Data: $delim"
if(allowAttrib) {
updateAttr("fileContent", "$delim")
runIn(30,"removeAttr")
}
return delim
}
else {
log.error "Null Response"
}
}
} catch (exception) {
log.error "Read Error: ${exception.message}"
return null
}
}
@SuppressWarnings('unused')
def appendFile(fName,newData,Closure closure) {
closure(appendFile(fName,newData))
}
@SuppressWarnings('unused')
Boolean appendFile(fName,newData){
try {
fileData = (String) readFile(fName)
if(fileData?.length()>0)
fileData = fileData.substring(0,fileData.length()-1)
else fileData = ""
return writeFile(fName,fileData+newData)
} catch (exception){
if (exception.message == "Not Found"){
writeStatus = writeFile(fName, newData)
if(logResponses) log.info "Append Status: $writeStatus"
return writeStatus
} else {
log.error("Append $fName Exception: ${exception}")
return false
}
}
}
@SuppressWarnings('unused')
def writeFileAsHex(String fName, String fData, Closure closure) {
closure(writeFileAsHex(fName, fData))
}
@SuppressWarnings('unused')
Boolean writeFileAsHex(String fName, String fData) {
byte[] bStr = fData.getBytes()
if(debubEnabled) log.debug "$bStr"
String hexStr = hubitat.helper.HexUtils.byteArrayToHexString(bStr)
return writeFile(fName, hexStr)
}
@SuppressWarnings('unused')
def readFileAsHex(fName, Closure closure) {
closure(readFileAsHex(fName))
}
@SuppressWarnings('unused')
String readFileAsHex(String fName) {
String dataRet = readFile(fName)
byte[] bStr = hubitat.helper.HexUtils.hexStringToByteArray(dataRet)
String retStr = new String(bStr)
if(debugEnabled) log.debug "$retStr"
return retStr
}
@SuppressWarnings('unused')
def writeFile(String fName, String fData, Closure closure) {
closure(writeFile(fName, fData))
}
@SuppressWarnings('unused')
Boolean writeFile(String fName, String fData) {
fData = fData.replace("\\n","\n")
if(debugEnabled) log.debug fData
byte[] fDataB = fData.getBytes("UTF-8")
return writeImageFile(fName, fDataB, "text/html")
}
@SuppressWarnings('unused')
def xferFile(fileIn, fileOut, Closure closure) {
closure(xferFile(fileIn, fileOut))
}
@SuppressWarnings('unused')
Boolean xferFile(fileIn, fileOut) {
fileBuffer = (String) readExtFile(fileIn)
retStat = writeFile(fileOut, fileBuffer)
if(logResponses) log.info "File xFer Status: $retStat"
return retStat
}
def readExternalFile(fName){
updateAttr("fileContent",readExtFile(fName))
}
@SuppressWarnings('unused')
def readExtFile(fName, Closure closure) {
closure(readExtFile(fName))
}
@SuppressWarnings('unused')
String readExtFile(fName){
if(security) cookie = securityLogin().cookie
def params = [
uri: fName,
contentType: "text/html",
textParser: true,
headers: [
"Cookie": cookie
]
]
try {
httpGet(params) { resp ->
if(resp!= null) {
int i = 0
String delim = ""
i = resp.data.read()
while (i != -1){
char c = (char) i
delim+=c
i = resp.data.read()
}
if(logResponses) log.info "Read External File result: delim"
return delim
}
else {
log.error "Null Response"
}
}
} catch (exception) {
log.error "Read Ext Error: ${exception.message}"
return null;
}
}
@SuppressWarnings('unused')
def listFiles(Closure closure) {
closure(listFiles())
}
@SuppressWarnings('unused')
List listFiles(){
if(security) cookie = securityLogin().cookie
if(debugEnabled) log.debug "Getting list of files"
uri = "http://${location.hub.localIP}:8080/hub/fileManager/json";
def params = [
uri: uri,
headers: [
"Cookie": cookie
]
]
try {
fileList = []
httpGet(params) { resp ->
if (resp != null){
if(logEnable) log.debug "Found the files"
def json = resp.data
for (rec in json.files) {
fileList << rec.name
}
} else {
//
}
}
if(debugEnabled) log.debug fileList.sort()
updateAttr("fileList", fileList.sort())
return fileList.sort()
} catch (e) {
log.error e
}
}
@SuppressWarnings('unused')
def uploadImage(imagePath, oName){
imageData = readImage(imagePath)
if(imageData.iType != 'Error')
writeImageFile(oName, imageData.iContent, imageData.iType)
}
@SuppressWarnings('unused')
String deleteFile(fName){
bodyText = JsonOutput.toJson(name:"$fName",type:"file")
params = [
uri: "http://127.0.0.1:8080",
path: "/hub/fileManager/delete",
contentType:"text/plain",
requestContentType:"application/json",
body: bodyText
]
httpPost(params) { resp ->
return resp.data.toString()
}
}
@SuppressWarnings('unused')
def readImage(Closure closure) {
closure(readImage(imagePath))
}
HashMap readImage(imagePath){
def imageData
if(security) cookie = securityLogin().cookie
if(debugEnabled) log.debug "Getting Image $imagePath"
try{
httpGet([
uri: "$imagePath",
contentType: "*/*",
headers: [
"Cookie": cookie
],
textParser: false]){ response ->
if(debugEnabled) log.debug "${response.properties}"
imageData = response.data
if(debugEnabled) log.debug "Image Size (${imageData.available()} ${response.headers['Content-Length']})"
def bSize = imageData.available()
def imageType = response.contentType
byte[] imageArr = new byte[bSize]
imageData.read(imageArr, 0, bSize)
if(debugEnabled) log.debug "Image size: ${imageArr.length} Type:$imageType"
return [iContent: imageArr, iType: imageType]
}
}catch (ex){
log.error ex.message
return [iContent:null, iType:'Error']
}
}
@SuppressWarnings('unused')
def writeImageFile(String fName, byte[] fData, String imageType, Closure closure) {
closure(writeImageFile(fName, fData, imageType))
}
Boolean writeImageFile(String fName, byte[] fData, String imageType) {
now = new Date()
String encodedString = "thebearmay$now".bytes.encodeBase64().toString();
bDataTop = """--${encodedString}\r\nContent-Disposition: form-data; name="uploadFile"; filename="${fName}"\r\nContent-Type:${imageType}\r\n\r\n"""
bDataBot = """\r\n\r\n--${encodedString}\r\nContent-Disposition: form-data; name="folder"\r\n\r\n--${encodedString}--"""
byte[] bDataTopArr = bDataTop.getBytes("UTF-8")
byte[] bDataBotArr = bDataBot.getBytes("UTF-8")
ByteArrayOutputStream bDataOutputStream = new ByteArrayOutputStream();
bDataOutputStream.write(bDataTopArr);
bDataOutputStream.write(fData);
bDataOutputStream.write(bDataBotArr);
byte[] postBody = bDataOutputStream.toByteArray();
try {
def params = [
uri: 'http://127.0.0.1:8080',
path: '/hub/fileManager/upload',
query: [
'folder': '/'
],
requestContentType: "application/octet-stream",
headers: [
'Content-Type': "multipart/form-data; boundary=$encodedString"
],
body: postBody,
timeout: 300,
ignoreSSLIssues: true
]
httpPost(params) { resp ->
if(debugEnabled) log.debug "writeImageFile ${resp.properties}"
if(logResponses) log.info "${resp.data.success} ${resp.data.status}"
updateAttr("lastFileWritten", "$fName")
if(sdfFormat == null) device.updateSetting("sdfFormat",[value:"Milliseconds",type:"string"])
if(sdfFormat == "Milliseconds" || sdfFormat == null)
updateAttr("lastFileWrittenTimeStamp", new Date().getTime())
else {
SimpleDateFormat sdf = new SimpleDateFormat(sdfFormat)
updateAttr("lastFileWrittenTimeStamp", sdf.format(new Date().getTime()))
}
return resp.data.success == 'true' ? true:false
}
}
catch (e) {
log.error "Error writing file $fName: ${e}"
}
return false
}
@SuppressWarnings('unused')
void removeAttr(){
if(location.hub.firmwareVersionString >= "2.2.8.141")
device.deleteCurrentState("fileContent")
else
updateAttr("fileContent", "expired")
}
@SuppressWarnings('unused')
void logsOff(){
device.updateSetting("debugEnabled",[value:"false",type:"bool"])
}