/** Authors Notes: * For more information on Activity Monitor & Attribute Monitor check out these resources. * Original posting on Hubitat Community forum: https://community.hubitat.com/t/release-tile-builder-build-beautiful-dashboards/118822 * Tile Builder Documentation: https://github.com/GaryMilne/Hubitat-TileBuilder/blob/main/Tile%20Builder%20Help.pdf * * Copyright 2022 Gary J. Milne * 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. * License: * You are free to use this software in an un-modified form. Software cannot be modified or redistributed. * You may use the code for educational purposes or for use within other applications as long as they are unrelated to the * production of tabular data in HTML form, unless you have the prior consent of the author. * You are granted a license to use Tile Builder in its standard configuration without limits. * Use of Tile Builder in it's Advanced requires a license key that must be issued to you by the original developer. TileBuilderApp@gmail.com * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * CHANGELOG * Version 1.0.0 - Internal * Version 1.0.1 - Cleaned up some UI pieces. Removed tile1 as the default when publishing so that it is a conscious choice to avoid overrides. * Version 1.0.2 - Fixed bug with 'No Selection' in UI. Changed logic to handle 0 rows in table. * Version 1.0.3 - Allows 'Tables' with only a title for use as a placeholder on the Dashboard. * Version 1.0.4 - Added logic to hide\show publishing buttons based on required fields. * Version 1.0.5 - Consolidate Attribute Monitor and Activity Monitor into unified code. * Version 1.0.6 - Added removal of all items with opacity=0 from the final HTML. * Version 1.0.7 - Added custom size option for preview window. * Version 1.0.8 - Added append option on overrides. Added extra options to sample overrides. * Version 1.0.9 - Fixed issue with importing of overrides string. * Version 1.1.0 - Added %count% as a macro for number of displayed records. Useful for scrolling windows or null results. * Version 1.1.1 - Added tags for #high1# and #high2# for modifying highlight classes. Provides an alternate method of formatting a result vs using a class. * Version 1.1.2 - Added filtering ability for integer and float values. Made filtering an advanced feature. * Version 1.1.3 - Added ability to merge Column header fields. * Version 1.1.4 - Fixed bug with display of floating point numbers when filtering is enabled. * Version 1.2.0 - Cleaned up a variety of message text. Version revved to match other components and Help file for first public release. * Version 1.2.1 - Minor bug fix relating to the handling of Units * Version 1.2.2 - Roughed in File Support * Version 1.2.3 - Convert Overrides from string to textarea. * Version 1.2.4 - Update screen handling for > 1024. Eliminate #pre# and #post#, add animation examples to overrides helper. * Version 1.2.5 - Splits Overrides Helper examples into categories for easier navigation. * Version 1.2.6 - Expanded Keywords and Thresholds to 5 values. Added 'isCompactDisplay' to free up some screen space. * Version 1.2.7 - Fixed bug in applyStyle not handling "textArea" data type introduced in 1.2.3 * Version 1.2.8 - Cleaned up handling of some style settings. * Version 1.3.0 - Multiple updates and fixes. Implements %value% macro, use search and replace strings vs just strip strings. Added button type to Activity Monitor list, added valve, healthStatus and variable types, added padding to floats, \ * reduced floating point options to 0 or 1. Added opacity option to table background. Converted Thresholds to use numbered comparators. Changed storage of #top variables. Implemented supportFunction for child recovery. * Version 1.3.1 - Added null checking to multiple lines to correct app errors, especially when picking "No Selection" which returns null. Fixed bug with substituting values for fields #22 and #27. Fixed bug when subscribing to camelCase attributes. * Version 1.4.0 - Added improvements first introduced in Multi-Attribute Monitor such as Attribute and Color compression. Added %time1% and %time2% for proper 24hr and 12hr times. Added selector for Device Naming. Added attribute "level". Updated Threshold operators and variables from using numbers 1-5 to 6-10. * Version 1.4.1 - Bugfix: Make sure that the eventTimeout variable has a value if detected as null. * Version 1.4.2 - Bugfix: Units were not displaying when selected. * Version 1.4.3 - Cosmetic Changes to the Menu Bar and Title. Adds a counter to a comment field for results > 1024 which ensures that every update is unique and causes the file to be reloaded in the Dashboard on any change. Added Character Replacement capability. * Version 1.4.4 - Bugfix: Added ternary operators in highlightValue for float values that come back as null because the attribute is not populated. * Version 1.4.5 - Bugfix: Incorrect Module Name * Version 1.4.6 - Bugfix: Correct issue with logic in highlightValue related to use of Ternary operators (introduced v1.4.4). Corrected issue with duplicate units under some conditions. * Version 1.4.7 - Added Minimum Republish interval as introduced in Grid 1.0. Only applies to Attribute Monitor. * Version 1.4.8 - Bugfix: Corrected issue with newly initialised variables. * Version 1.4.9 - Bugfix: Improved handling of the lastPublished info and checking. Better null handling for input controls. * Version 1.5.0 - Bugfix: Added error handling when the device has a Null value for a monitored attribute. * Version 1.5.1 - Feature: Expanded the Device Name Modification from 3 to 5 values. - No External Release * Version 1.5.2 - Feature: Added Cloud Endpoints as a publishing option for output > 1,024 bytes. * Version 1.5.3 - Bugfix: Handle errors that are caused by OAuth not being enabled on the app. Cloud Endpoints only active as needed. * * Gary Milne - July 15th, 2024 * * This code is Activity Monitor and Attribute Monitor combined. * The personality is dictated by @Field static moduleName a few lines ahead of this. * You must comment out the moduleName line that does not apply. * You must also comment out the 3 lines in the definition that do not apply. * That is all that needs to be done. * **/ import groovy.transform.Field //These are supported capabilities. Layout is "device.selector":"attribute". Keeping them in 3 separate maps makes it more readable and easier to identify the sort criteria. @Field static final capabilitiesInteger = ["airQuality":"airQualityIndex", "battery":"battery", "colorTemperature":"colorTemperature","illuminanceMeasurement":"illuminance", "signalStrength":"rssi", "switchLevel":"level"] @Field static final capabilitiesString = ["*":"variable","carbonDioxideDetector":"carbonMonoxide", "contactSensor":"contact", "healthCheck":"healthStatus", "lock":"lock", "motionSensor":"motion", "presenceSensor":"presence", "smokeDetector":"smoke", "switch":"switch", "valve":"valve", "waterSensor":"water", "windowBlind":"windowBlind"] //The first field has to be unique so we append the capability with a number so that all of the entries appear in the list even when the capability is really the same. Without this we can only use "*" once. //These three are used by Zigbee Monitor Driver. @Field static final capabilitiesCustom = ["signalStrength1":"deviceNeighbors", "signalStrength2":"deviceRepeaters", "signalStrength3":"deviceRoutes", "signalStrength4":"deviceChildren", "signalStrength5":"deviceChildCount", "signalStrength6":"deviceRouteCount", "signalStrength7":"deviceRepeaterCount"] @Field static final capabilitiesFloat = ["currentMeter": "amperage", "energyMeter":"energy", "powerMeter":"power", "relativeHumidityMeasurement":"humidity", "temperatureMeasurement":"temperature","voltageMeasurement":"voltage"] //These are unknown as to whether they report integer or float values. //capabilitiesUnknown = [" "carbonDioxideMeasurement":"carbonDioxide","pressureMeasurement":"pressure","relativeHumidityMeasurement":"humidity", "ultravioletIndex":"ultravioletIndex"] //Cloud Endpoint Mapping mappings { path("/tb") { action: [GET: "getTile"] } } @Field static final codeDescription = "Tile Builder Activity Monitor v1.5.3 (7/15/24)" @Field static final codeVersion = 153 @Field static final moduleName = "Activity Monitor" //@Field static final moduleName = "Attribute Monitor" definition( name: "Tile Builder - Activity Monitor", description: "Monitors a list of devices to look for those that are inactive\\overactive and may need attention. Publishes an HTML table of results for a quick and attractive display in the Hubitat Dashboard environment.", importUrl: "https://raw.githubusercontent.com/GaryMilne/Hubitat-TileBuilder/main/Activity_Monitor.groovy", //name: "Tile Builder - Attribute Monitor", //description: "Monitors a single attribute for a list of devices. Publishes an HTML table of results for a quick and attractive display in the Hubitat Dashboard environment.", //importUrl: "https://raw.githubusercontent.com/GaryMilne/Hubitat-TileBuilder/main/Attribute_Monitor.groovy", namespace: "garyjmilne", author: "Gary J. Milne", category: "Utilities", iconUrl: "", iconX2Url: "", iconX3Url: "", singleThreaded: true, parent: "garyjmilne:Tile Builder", installOnOpen: true ) preferences { page(name: "mainPage") if (moduleName == "Activity Monitor") page (name: "devicePage") } def mainPage() { //Basic initialization for the initial release if (state.initialized == null ) initialize() //Handles the initialization of new variables added after the original release. if (state.variablesVersion == null || state.variablesVersion < codeVersion) updateVariables() //Checks for critical Null values that can be introduced by the user by clicking "No Selection" in an enum dialog. checkNulls() //Checks to see if there are any messages for this child app. This is used to recover broken child apps from certain error conditions //Although this function is complete I'm leaving it dormant for the present release - 1.4.0 myMessage = parent.messageForTile( app.label ) if ( myMessage != "" ) supportFunction ( myMessage ) if (moduleName == "Attribute Monitor") { //See if the user has selected a different capability. If so a flag is set and the device list is cleared on the refresh. isMyCapabilityChanged() } refreshTable() refreshUIbefore() def pageTitle = (parent.checkLicense() == true) ? moduleName + " - Advanced" : moduleName + " - Standard"; dynamicPage(name: "mainPage", title: titleise("
" + buttonLink ('General', 'General', 1) + " | " + buttonLink ('Title', 'Title', 2) + " | " + buttonLink ('Headers', 'Headers', 3) + " | " part2 = "" + buttonLink ('Borders', 'Borders', 4) + " | " + buttonLink ('Rows', 'Rows', 5) + " | " + buttonLink ('Footer', 'Footer', 6) + " | " //These Tabs may be Enabled or Disabled depending on the Activation Status. if (moduleName == "Attribute Monitor") part3 = advancedStyle + buttonLink ('Highlights', 'Highlights', 7) + "" + advancedStyle + buttonLink ('Styles', 'Styles', 8) + "" + advancedStyle + buttonLink ('Advanced', 'Advanced', 9) + "" if (moduleName == "Activity Monitor") part3 = advancedStyle + buttonLink ('Styles', 'Styles', 8) + "" + advancedStyle + buttonLink ('Advanced', 'Advanced', 9) + "" table = part1 + part2 + part3 + "
" input (name: "clearOverrides", type: "button", title: "Clear the Overrides", backgroundColor: "#27ae61", textColor: "white", submitOnChange: true, width: 2, newLine: true, newLineAfter: false ) input (name: "copyOverrides", type: "button", title: "Copy To Overrides", backgroundColor: "#27ae61", textColor: "white", submitOnChange: true, width: 2, newLine: true, newLineAfter: false ) input (name: "appendOverrides", type: "button", title: "Append To Overrides", backgroundColor: "#27ae61", textColor: "white", submitOnChange: true, width: 2, newLine: true, newLineAfter: false ) input (name: "Refresh", type: "button", title: "Refresh Table", backgroundColor: "#27ae61", textColor: "white", submitOnChange: true, width: 2) input (name: "overrides", type: "textarea", title: titleise("Settings Overrides"), required: false, defaultValue: "?", width:12, rows:5, submitOnChange: true) if (isCompactDisplay == false) paragraph summary("About Overrides", parent.overrideNotes() ) } if (isShowSettings == true) { paragraph line(1) paragraph "Effective Settings" paragraph "
" } if (isShowHTML == true) { paragraph line(1) paragraph "Pseudo HTML" myHTML = state.iFrameHTML paragraph "
" } if (isCompactDisplay == false) { paragraph line(1) paragraph summary("Advanced Notes", parent.advancedNotes() ) } } if (isCompactDisplay == false) paragraph line(2) } //End of isCustomize //Display Table if (isCompactDisplay == false) paragraph summary("Display Tips", parent.displayTips() ) myHTML = toHTML(state.iframeHTML) myHTML = myHTML.replace("#iFrame1#","body{background:${iFrameColor};font-size:${bfs}px;}") state.iFrameFinalHTML = myHTML if (isCustomSize == false){ if (tilePreview == "1" ) paragraph '' if (tilePreview == "2" ) paragraph '' if (tilePreview == "3" ) paragraph '' if (tilePreview == "4" ) paragraph '' if (tilePreview == "5" ) paragraph '' if (tilePreview == "6" ) paragraph '' if (tilePreview == "7" ) paragraph '' if (tilePreview == "8" ) paragraph '' if (tilePreview == "9" ) paragraph '' } else { //Use a custom size for the preview window. myString = '' myString = myString.replace("XXX", "${settings.customWidth}") myString = myString.replace("YYY", "${settings.customHeight}") paragraph myString } if (state.HTMLsizes.Final < 4096 ){ if (isCompactDisplay == false) paragraph "
" } else { if (isCompactDisplay == false) paragraph "
"
}
if (isCustomize == true){
overridesSize = 0
if (settings.overrides?.size() != null && isOverrides == true) overridesSize = settings.overrides?.size()
line = "Enabled Features: Comment:${isComment}, Frame:${isFrame}, Title:${isTitle}, Title Shadow:${isTitleShadow}, Headers:${isHeaders}, Border:${isBorder}, Alternate Rows:${isAlternateRows}, Footer:${isFooter}, Overrides:${isOverrides} ($overridesSize bytes)
"
line += "Space Usage: Comment: ${state.HTMLsizes.Comment} Head: ${state.HTMLsizes.Head} Body: ${state.HTMLsizes.Body} Interim Size: ${state.HTMLsizes.Interim} Final Size: ${state.HTMLsizes.Final} (Scrubbing level is: ${parent.htmlScrubLevel()[scrubHTMLlevel.toInteger()] })
"
//line += "Devices: Selected: ${myDeviceList?.size() || 0} Limit: ${myDeviceLimit?.toInteger() || 0}"
line = line.replace("true"," On")
line = line.replace("false"," Off")
if (isCompactDisplay == false) {
paragraph note("", line)
if (state.HTMLsizes.Final < 1024 ) paragraph note("Note: ","Current tile is less than 1,024 bytes and will be stored within an attribute.")
else paragraph note("Note: ","Current tile is greater than 1,024 bytes and will be stored as a file in File Manager and linked with an attribute.")
}
}
} //End of showDesign
else input(name: 'btnShowDesign', type: 'button', title: 'Design Table ▶', backgroundColor: 'dodgerBlue', textColor: 'white', submitOnChange: true, width: 3, newLine: true) //▼ ◀ ▶ ▲
paragraph line(2)
//End of Display Table
//Configure Data Refresh
if (state.show.Publish == true) {
input(name: 'btnShowPublish', type: 'button', title: 'Publish Table ▼', backgroundColor: 'navy', textColor: 'white', submitOnChange: true, width: 3, newLineAfter: true) //▼ ◀ ▶ ▲
if (moduleName == "Attribute Monitor") myText = "Here you will configure where the table will be stored. It will be refreshed whenever a monitored attribute changes."
if (moduleName == "Activity Monitor") myText = "Here you will configure where the table will be stored. It will be refreshed at the frequency you specify."
//myText += "HTML data is less than 1,024 bytes it will be published via a tile attribute on the storage device.
"
//myText += "If HTML data is greater than 1,024 it will be published via file with the tile attribute being link to that file.
"
paragraph myText
input (name: "myTile", title: "Tile Attribute to store the table?", type: "enum", options: parent.allTileList(), required:true, submitOnChange:true, width:2, defaultValue: 0, newLine:false)
input (name:"myTileName", type:"text", title: "Name this Tile", submitOnChange: true, width:2, newLine:false, required: true)
input (name: "tilesAlreadyInUse", type: "enum", title: bold("For Reference Only: Tiles in Use"), options: parent.getTileList(), required: false, defaultValue: "Tile List", submitOnChange: false, width: 2)
input (name: "eventTimeout", type: "enum", title: "Event Timeout (millis)", required: false, multiple: false, defaultValue: "2000", options: ["0","250","500","1000","2000","5000","10000"], submitOnChange: true, width: 2)
if (moduleName == "Attribute Monitor") input (name: "republishDelay", type: "enum", title: "Republish Delay (minutes)", required: false, multiple: false, defaultValue: 0, options: [0,1,2,3,4,5,10,15,20,25,30,35,40,45,50,55,60], submitOnChange: true, width: 2)
input (name: "oversizeTileHandling", type: "enum", title: "Oversize Tile Handling", required: false, multiple: false, defaultValue: "File Manager", options: ["File Manager","Cloud Endpoint"], submitOnChange: true, width: 2, newLineAfter:true)
if (myTileName) app.updateLabel(myTileName)
myText = "The Tile Name given here will also be used as the name for this instance of Tile Builder. Appending the name with your chosen tile number can make parent display more readable.
"
myText += "The Event Timeout period is how long Tile Builder will wait for subsequent events before publishing the table. Devices that do bulk updates create a lot of events in a short period of time. This setting batches requests within this period into a single publishing event. "
myText += "The default timeout period is 2000 milliseconds (2 seconds). If you want a more responsive table you can lower this number, but it will slightly increase the CPU utilization.
"
myText += "The Republish Delay sets a minimum amount of time before a Tile is re-published. This can be used to prevent chatty sensors from causing a Tile to republish too frequently. The default value for this setting is 0 (no delay)." +\
"Republish Delay is not available in Activity Monitor because it already works on a timed basis.
"
myText += "When immediate updates are important such as switches, locks, motion or contact sensors then set both the the Event Timeout and Republish Delay to 0.
"
myText += "Oversize Tiles: Tiles over 1,024 bytes can either be stored in Hubitat File Manager or published to a Hubitat Cloud Endpoint
"
myText += "Tiles stored to the Hubitat File Manager are only accessible from the local LAN or when using a VPN.
"
myText += "Tiles stored to a Hubitat Cloud Endpoint are accessible from the local LAN or the cloud via the Hubitat App but does require the internet to be operational in order to display.
"
myText += "Storing oversize tiles in File Manager is the faster of the two options and is the default operation. Only use cloud endpoints when you have an explicit need for the information to be availble via the internet.
"
myText += "Important: If you wish to use cloud endpoints you must go to the Apps / Code for this module and enable OAuth using the default Auto-Generated values.
"
if (state.cloudEndpoint != null ) myText += "The Hubitat Cloud Endpoint for this Tile Builder table is: ${state.cloudEndpoint}"
paragraph summary("Publishing Notes", myText)
paragraph line(1)
if ( state.HTMLsizes.Final < 4096 && settings.myTile != null && myTileName != null ) {
if (moduleName == "Activity Monitor") {
input (name:"publishInterval", title: "Table Refresh Interval", type: "enum", options: parent.refreshInterval(), required:false, submitOnChange:true, width:2, defaultValue: 1)
input (name: "publish", type: "button", title: "Publish Table", backgroundColor: "#27ae61", textColor: "white", submitOnChange: true, width: 12)
}
if (moduleName == "Attribute Monitor") {
input (name: "publishSubscribe", type: "button", title: "Publish and Subscribe", backgroundColor: "#27ae61", textColor: "white", submitOnChange: true, width: 12)
input (name: "unsubscribe", type: "button", title: "Delete Subscription", backgroundColor: "#27ae61", textColor: "white", submitOnChange: true, width: 12)
}
}
else input (name: "cannotPublish", type: "button", title: "Publish", backgroundColor: "#D3D3D3", textColor: "black", submitOnChange: false, width: 12)
}
else input(name: 'btnShowPublish', type: 'button', title: 'Publish Table ▶', backgroundColor: 'dodgerBlue', textColor: 'white', submitOnChange: true, width: 3, newLineAfter: true) //▼ ◀ ▶ ▲
if (isCompactDisplay == false) paragraph line(2)
input (name:"isMore", type: "bool", title: "More Options", required: false, multiple: false, defaultValue: false, submitOnChange: true, width: 2)
if (isMore == true){
paragraph "
" //Horizontal Line input (name: "isLogInfo", type: "bool", title: "Enable info logging?", defaultValue: false, submitOnChange: true, width: 2) input (name: "isLogTrace", type: "bool", title: "Enable trace logging?", defaultValue: false, submitOnChange: true, width: 2) input (name: "isLogDebug", type: "bool", title: "Enable debug logging?", defaultValue: false, submitOnChange: true, width: 2) input (name: "isLogWarn", type: "bool", title: "Enable warn logging?", defaultValue: true, submitOnChange: true, width: 2) input (name: "isLogError", type: "bool", title: "Enable error logging?", defaultValue: true, submitOnChange: true, width: 2) input (name: "isLogEvents", type: "bool", title: "Enable Device Event logging?", defaultValue: false, submitOnChange: true, width: 2, newLine:true) } //Now add a footer. myDocURL = " Tile Builder Help" myText = '
' paragraph myText } //End Configure Data Refresh refreshUIafter() } } //Checks for critical Null values that can be introduced by the user by clicking "No Selection" in an enum dialog. //This occurs when the specific control value is accessed by various functions during the screen refresh before the "defaultValue" can be applied. def checkNulls(){ if (myThresholdCount == null ) app.updateSetting("myThresholdCount", [value:"0", type:"enum"]) if (myKeywordCount == null ) app.updateSetting("myKeywordCount", [value:"0", type:"enum"]) if (myDeviceLimit == null ) app.updateSetting("myDeviceLimit", [value:"0", type:"enum"]) if (eventTimeout == null) app.updateSetting("eventTimeout", "2000") if (tbo == null) app.updateSetting("tbo", [value:"1", type:"enum"]) if (to == null) app.updateSetting("to", [value:"1", type:"enum"]) if (bo == null) app.updateSetting("bo", [value:"1", type:"enum"]) if (hbo == null) app.updateSetting("hbo", [value:"1", type:"enum"]) if (hto == null) app.updateSetting("hto", [value:"1", type:"enum"]) if (rbo == null) app.updateSetting("rbo", [value:"1", type:"enum"]) if (rto == null) app.updateSetting("rto", [value:"1", type:"enum"]) if (bp == null) app.updateSetting("bp", [value:"0", type:"enum"]) if (scrubLevelHTML == null ) app.updateSetting("scrubHTMLlevel", [value:"1", type:"enum"]) } //Get a list of supported attributes for a given device and return a sorted list. def getAttributeList (thisDevice){ if (thisDevice != null) { myAttributesList = [] supportedAttributes = thisDevice.supportedAttributes supportedAttributes.each { attributeName -> myAttributesList << attributeName.name } return myAttributesList.unique().sort() } } //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************** //************** Functions Related to the Management of the UI //************** //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //This is the refresh routine called at the start of the page. This is used to replace\clear screen values that do not respond when performed in the mainline code. //This function is unique between modules void refreshUIbefore(){ //Get the oveerrides helper selection and look it up in the global map and use the key pair value as an on-screen guide. state.currentHelperCommand = "" overridesHelperMap = parent.getOverridesListAll() state.currentHelperCommand = overridesHelperMap.get(overridesHelperSelection) if (state.flags.isClearOverridesHelperCommand == true){ if (isLogTrace == true) log.trace ("Clearing overrides.") app.updateSetting("overrides", [value:"", type:"textarea"]) //Works state.flags.isClearOverridesHelperCommand = false } if (moduleName == "Activity Monitor") { if (mySelectedTile != null){ details = mySelectedTile.tokenize(":") if (details[0] != null ) { tileName = details[0].trim() if (isLogDebug) log.debug ("tileName is $tileName") //We use the tile number when publishing so we strip off the leading word tile. tileNumber = tileName.replace("tile","") app.updateSetting("myTile", tileNumber) } if (details[1] != null ) { tileName = details[1].trim() if (isLogInfo) log.info ("tileName is $tileName") app.updateSetting("myTileName", tileName) } } } } //This is the refresh routine called at the end of the page. This is used to replace\clear screen values that do not respond when performed in the mainline code. void refreshUIafter(){ //This checks a flag for the saveStlye operation and clears the text field if the flag has been set. Neccessary to do this so the UI updates correctly. if (state.flags.styleSaved == true ){ app.updateSetting("saveStyleName","?") state.flags.styleSaved = false } //If the myCapability flag has been changed then the myDeviceList is cleared as the potential device list would be different based on the capability selected. //Only applies to Activity Monitor but retained for ease of maintenance. if (state.flags.myCapabilityChanged == true ) { //log.info ("Reset list") app.updateSetting("myDeviceList",[type:"capability",value:[]]) state.flags.myCapabilityChanged == false } //Copy the selected command to the Overrides field and replace any existing text. if (state.flags.isCopyOverridesHelperCommand == true){ myCommand = state.currentHelperCommand app.updateSetting("overrides", [value:myCommand, type:"textarea"]) //Works state.flags.isCopyOverridesHelperCommand = false } //Appends the selected command to current contents of the Overrides field. if (state.flags.isAppendOverridesHelperCommand == true){ myCurrentCommand = overrides.toString() myCurrentCommand = myCurrentCommand.replace("[", "") myCurrentCommand = myCurrentCommand.replace("]", "") combinedCommand = myCurrentCommand.toString() + " | \n" + state.currentHelperCommand.toString() app.updateSetting("overrides", [value:combinedCommand.toString(), type:"textarea"]) //Works state.flags.isAppendOverridesHelperCommand = false } } //Runs recovery functions when messaged from the parent app. This can be used to recoved a child app when an error condidtion arises. def supportFunction ( supportCode ){ if ( supportCode.toString() == "0" ) return log.info "Running supportFunction with code: $supportCode" switch(supportCode) { case "disableOverrides": app.updateSetting("isOverrides", false) break case "disableKeywords": app.updateSetting("myKeywordCount", 0) break case "disableThresholds": app.updateSetting("myThresholdCount", 0) break case "clearDeviceList": app.updateSetting("myDeviceList",[type:"capability",value:[]]) break } } //Generic placeholder for test function. void test(){ //top6 = top1 app.updateSetting("scrubHTMLlevel", [value:"1", type:"enum"]) app.updateSetting("myDeviceNaming", "Use Device Label") if (myKeywordCount > 0) state.show.Keywords = true if (myThresholdCount > 0) state.show.Thresholds = true } //This is the standard button handler that receives the click of any button control. def appButtonHandler(btn) { switch(btn) { case 'btnShowReport': state.show.Report = state.show.Report ? false : true; break case 'btnShowFilter': state.show.Filter = state.show.Filter ? false : true; break case 'btnShowDevices': state.show.Devices = state.show.Devices ? false : true; break case 'btnShowKeywords': state.show.Keywords = state.show.Keywords ? false : true; break case 'btnShowThresholds': state.show.Thresholds = state.show.Thresholds ? false : true; break case 'btnShowFormatRules': state.show.FormatRules = state.show.FormatRules ? false : true; break case 'btnShowReplaceCharacters': state.show.ReplaceCharacters = state.show.ReplaceCharacters ? false : true; break case 'btnShowDesign': state.show.Design = state.show.Design ? false : true; break case 'btnShowPublish': state.show.Publish = state.show.Publish ? false : true; break case "Refresh": //We don't need to do anything. The refreshTable will be called by the submitOnChange. if (isLogTrace==true) log.trace("appButtonHandler: Clicked on Refresh") break case "publish": //We will publish it right away and then schedule the refresh as requested. if (isLogTrace) log.trace("appButtonHandler: Clicked on publish") publishTable() createSchedule() break case "cannotPublish": if (isLogTrace) log.trace("appButtonHandler: Clicked on publish (cannotPublish)") cannotPublishTable() break case "General": if (isLogTrace) log.trace("appButtonHandler: Clicked on General") app.updateSetting("activeButton", 1) break case "Title": if (isLogTrace) log.trace("appButtonHandler: Clicked on Title") app.updateSetting("activeButton", 2) break case "Headers": if (isLogTrace) log.trace("appButtonHandler: Clicked on Headers") app.updateSetting("activeButton", 3) break case "Borders": if (isLogTrace) log.trace("appButtonHandler: Clicked on Borders") app.updateSetting("activeButton", 4) break case "Rows": if (isLogTrace) log.trace("appButtonHandler: Clicked on Rows") app.updateSetting("activeButton", 5) break case "Footer": if (isLogTrace) log.trace("appButtonHandler: Clicked on Footer") app.updateSetting("activeButton", 6) break case "Highlights": if (isLogTrace) log.trace("appButtonHandler: Clicked on Highlights") app.updateSetting("activeButton", 7) break case "Styles": if (isLogTrace) log.trace("appButtonHandler: Clicked on Styles") app.updateSetting("activeButton", 8) break case "Advanced": if (isLogTrace) log.trace("appButtonHandler: Clicked on Advanced") app.updateSetting("activeButton", 9) break case "test": test() break case "copyOverrides": if (isLogTrace) log.trace("appButtonHandler: Clicked on copyOverrides") state.flags.isCopyOverridesHelperCommand = true break case "appendOverrides": if (isLogTrace) log.trace("appButtonHandler: Clicked on appendOverrides") state.flags.isAppendOverridesHelperCommand = true break case "clearOverrides": if (isLogTrace) log.trace("appButtonHandler: Clicked on clearOverrides") state.flags.isClearOverridesHelperCommand = true break case "applyStyle": if (isLogTrace) log.trace("appButtonHandler: Clicked on applyStyle") myStyle = loadStyle(applyStyleName.toString()) applyStyle(myStyle) refreshTable() break case "saveStyle": if (isLogTrace) log.trace("appButtonHandler: Clicked on saveStyle") saveCurrentStyle(saveStyleName) state.flags.styleSaved = true break case "deleteStyle": if (isLogTrace) log.trace("appButtonHandler: Clicked on deleteStyle") deleteSelectedStyle(deleteStyleName) break case "importStyle": if (isLogTrace) log.trace("appButtonHandler: Clicked on Importing Style") app.updateSetting("overrides", importStyleOverridesText) def myImportMap = [:] def myOverridesMap = [:] //Add an overrides item to the the empty map. myOverridesMap.overrides = importStyleOverrides //Convert the base settings string to a map. myImportMap = importStyleString(settings.importStyleText) myImportStyle = myImportMap.clone() applyStyle(myImportStyle) break case "clearImport": if (isLogTrace) log.trace("appButtonHandler: Clicked on clearImport") app.updateSetting("importStyleText", "") app.updateSetting("importStyleOverridesText", "") break case "publishSubscribe": publishSubscribe() break case "unsubscribe": deleteSubscription() break } if (isLogDebug) log.debug("appButtonHandler: activeButton is: ${activeButton}") } //Return the appropriate list of sample override commands that is usable by the drop down control. def getOverrideCommands(myCategory){ def commandList = [] overridesHelperMap = [:] switch(myCategory) { case "Animation": overridesHelperMap = parent.getOverrideAnimationList() break case "Background": overridesHelperMap = parent.getOverrideBackgroundList() break case "Border": overridesHelperMap = parent.getOverrideBorderList() break case "Classes": overridesHelperMap = parent.getOverrideClassList() break case "Cell Operations": overridesHelperMap = parent.getOverrideCellOperationsList() break case "Field Replacement": overridesHelperMap = parent.getOverrideFieldReplacementList() break case "Font": overridesHelperMap = parent.getOverrideFontList() break case "Margin & Padding": overridesHelperMap = parent.getOverrideMarginPaddingList() break case "Misc": overridesHelperMap = parent.getOverrideMiscList() break case "Text": overridesHelperMap = parent.getOverrideTextList() break case "Transform": overridesHelperMap = parent.getOverrideTransformList() break } overridesHelperMap.each { key = it.key.toString() value = it.value.toString() //Split the value into two strings, before the | and after. details = value.tokenize('|') commandList.add (key) } //log("getSampleCommands", "commandList is: ${return commandList.unique().sort()}", 2) return commandList.unique().sort() } //Returns a map of device activity using the parameters provided by the selection boxes. //This function is used exclusively by Activity Monitor def getDeviceMapActMon(){ if (isLogTrace) log.trace("getDeviceMapActMon: Entering getDeviceMapActMon") def inactivityMap = [:] def sortedMap = [:] def sortedMap1 = [:] def sortedMap2 = [:] def myDevices = getDeviceList() def now = new Date() myDevices.each { deviceName = "" lastActivity = it.getLastActivity() myDeviceLabel = it.toString() myDeviceName = "${it.getName().toString()}" if (isLogDebug) log.debug("getDeviceMapActMon: deviceName is: $myDeviceName, it is: $it, deviceLabel is: $myDeviceLabel and lastActivity is: $lastActivity") if (myDeviceNaming == "Use Device Label") deviceName = myDeviceLabel if (myDeviceNaming == "Use Device Name") deviceName = myDeviceName //Handle any null values. if (myReplaceText1 == null || myReplaceText1 == "?" ) myReplaceText1 = "" if (myReplaceText2 == null || myReplaceText2 == "?" ) myReplaceText2 = "" if (myReplaceText3 == null || myReplaceText3 == "?" ) myReplaceText3 = "" //Replaces any undesireable characters in the devicename - Case Sensitive if (mySearchText1 != null && mySearchText1 != "?") deviceName = deviceName.replace(mySearchText1, myReplaceText1) if (mySearchText2 != null && mySearchText2 != "?") deviceName = deviceName.replace(mySearchText2, myReplaceText2) if (mySearchText3 != null && mySearchText3 != "?") deviceName = deviceName.replace(mySearchText3, myReplaceText3) if (mySearchText4 != null && mySearchText4 != "?") deviceName = deviceName.replace(mySearchText4, myReplaceText4) if (mySearchText5 != null && mySearchText5 != "?") deviceName = deviceName.replace(mySearchText5, myReplaceText5) if (isAbbreviations == true) deviceName = abbreviate(deviceName) def diff def hours if (lastActivity == null) { if (isLogDebug) log.debug ("LastActivityAt field is blank for device: $deviceName") lastActivity = new Date(2000-1900, 1, 1, 1, 0, 0) } use(groovy.time.TimeCategory) { diff = now - lastActivity hours = diff.days * 24 + diff.hours //if (isLogDebug) log.debug ("getDeviceMapActMon: diff is:${hours}") } //if (isLogDebug) log.debug ("getDeviceMapActMon: Hours is:${hours}") //Limit the entries in the new map to those that fit the inactivity filter if ( hours >= inactivityThreshold.toInteger() ){ //Put all of the qualified canditates into a map inactivityMap["${deviceName}"] = lastActivity if (isLogDebug) log.debug("getDeviceMapActMon: Added row: ${deviceName} Diff is: ${diff}") } else if (isLogDebug) log.debug("getDeviceMapActMon: Did not add row: ${it} - ${lastActivity}") } //End of myDevices.each //Now sort the map based on the last activity date. sortedMap = inactivityMap.sort {it.value} //If it is reverse order we need to reverse the map. if (mySortOrder == "2" ) { sortedMap.reverseEach { it, value -> sortedMap2["${it}"] = value } sortedMap = sortedMap2 } if (isLogDebug) log.debug("getDeviceMapActMon: SortedMap: ${sortedMap}") def newMap = [:] sortedMap.each{ it1, value1 -> if (isLogDebug) log.debug("getDeviceMapActMon: it is: ${it1} value:${value1}") use(groovy.time.TimeCategory) { def elapsed = now - value1 //if (isLogDebug) log.debug("getDeviceMapActMon: Elapsed days is: ${elapsed.days}") if ( elapsed.minutes < 10 ) strMinutes = "0${elapsed.minutes}" else strMinutes = elapsed.minutes.toString() if (elapsed.days > 0 ) newMap["${it1}"] = "${elapsed.days}d ${elapsed.hours}h" else newMap["${it1}"] = elapsed.hours + ":" + strMinutes } } if (isLogDebug) log.debug("getDeviceMapActMon: newMap is: ${newMap}") return newMap } //Returns a map of device activity using the parameters provided by the selection boxes. //This function is used exclusively by Attribute Monitor def getDeviceMapAttrMon(){ if (isLogTrace) log.trace("getDeviceMapAttrMon: Entering.") def myMap = [:] //["My Fake":"not present"] deviceType = state.myAttribute if (isLogDebug) log.debug ("DeviceType = $deviceType") //Go through each of the results in the result set. myDeviceList.each { it -> deviceName = "" myDeviceLabel = it.toString() myDeviceName = "${it.getName().toString()}" if (isLogDebug) log.debug("getDeviceMapAttrMon: deviceName is: $myDeviceName, deviceLabel is: $myDeviceLabel") if (myDeviceNaming == "Use Device Label") deviceName = myDeviceLabel if (myDeviceNaming == "Use Device Name") deviceName = myDeviceName //Handle any null values. if (myReplaceText1 == null || myReplaceText1 == "?" ) myReplaceText1 = "" if (myReplaceText2 == null || myReplaceText2 == "?" ) myReplaceText2 = "" if (myReplaceText3 == null || myReplaceText3 == "?" ) myReplaceText3 = "" if (myReplaceText4 == null || myReplaceText4 == "?" ) myReplaceText4 = "" if (myReplaceText5 == null || myReplaceText5 == "?" ) myReplaceText5 = "" //Replaces any undesireable characters in the devicename - Case Sensitive if (mySearchText1 != null && mySearchText1 != "?") deviceName = deviceName.replace(mySearchText1, myReplaceText1) if (mySearchText2 != null && mySearchText2 != "?") deviceName = deviceName.replace(mySearchText2, myReplaceText2) if (mySearchText3 != null && mySearchText3 != "?") deviceName = deviceName.replace(mySearchText3, myReplaceText3) if (mySearchText4 != null && mySearchText4 != "?") deviceName = deviceName.replace(mySearchText4, myReplaceText4) if (mySearchText5 != null && mySearchText5 != "?") deviceName = deviceName.replace(mySearchText5, myReplaceText5) if (isAbbreviations == true) deviceName = abbreviate(deviceName) myVal = it."current${deviceType}" dataType = getDataType(myVal.toString()) //Force any drivers reporting temperature as an integer to be treated as a float. if (myCapability == "temperatureMeasurement") dataType = "Float" if (isLogInfo) log.info ("2) myFilterType is: $myFilterType. myFilterText is: $myFilterText. dataType is: $dataType. myFilterTextDataType is: $myFilterTextDataType") myFilterTextDataType = getDataType(myFilterText) includeResult = false //Determine whether the result should be filtered out. if (myFilterType != null && myFilterText != null && myFilterType.toInteger() >= 1 && myFilterText != "?" ){ if (dataType == "String" && myFilterTextDataType == "String") { //log.info ("myString is: ") if (myFilterType == "1" && myVal == settings.myFilterText) myMap["${deviceName}"] = myVal if (myFilterType == "2" && myVal != settings.myFilterText) myMap["${deviceName}"] = myVal if (myFilterType != "1" && myFilterType != "2") myMap["${deviceName}"] = myVal } if (dataType == "Integer" && myFilterTextDataType != "String") { //log.info ("myInt is: ") if (myFilterType == "3" && myVal.toInteger() == settings.myFilterText.toInteger() ) myMap["${deviceName}"] = myVal.toInteger() if (myFilterType == "4" && myVal.toInteger() <= settings.myFilterText.toInteger() ) myMap["${deviceName}"] = myVal.toInteger() if (myFilterType == "5" && myVal.toInteger() >= settings.myFilterText.toInteger() ) myMap["${deviceName}"] = myVal.toInteger() if (myFilterType != "3" && myFilterType != "4" && myFilterType != "5") myMap["${deviceName}"] = myVal.toInteger() } if (dataType == "Float"){ // && myFilterTextDataType == "Float") { float myFloat = myVal.toFloat() if (isLogInfo) log.info ("2 - Filter) myFloat is: $myFloat, $myDecimalPlaces, $myFilterType ") if (myFilterType == "3" && Float.compare(myFloat, settings.myFilterText.toFloat()) == 0 ){ //log.info ("== Match $myFloat") includeResult = true } if (myFilterType == "4" && Float.compare(myFloat, settings.myFilterText.toFloat()) <= 0 ){ //log.info ("<= than Match $myFloat") includeResult = true } if (myFilterType == "5" && Float.compare(myFloat, settings.myFilterText.toFloat()) >= 0 ){ //log.info (">= than Match $myFloat") includeResult = true } //This condition occurs if they have selected a text filter but entered a float value. if (myFilterType != "3" && myFilterType != "4" && myFilterType != "5") { includeResult == true } //If the selected number of decimal places is 0 then return an integer, otherwise the float preserves the trailing 0 after the decimal point. if (includeResult == true && myDecimalPlaces.toInteger() == 0) myMap["${deviceName}"] = myFloat.toInteger() if (includeResult == true && myDecimalPlaces.toInteger() != 0) myMap["${deviceName}"] = myFloat.round(myDecimalPlaces.toInteger()) } if (dataType == "Null") log.warn("getDeviceMapAttrMon: Device $deviceName has a null field for attribute '$deviceType' and will be skipped.") } //If it's not going to be filtered else { if (isLogInfo) log.info("Not filtering") if (dataType == "Float") { try { float myFloat = myVal.toFloat() if (myDecimalPlaces.toInteger() == 0) myMap["${deviceName}"] = myFloat.toInteger() else myMap["${deviceName}"] = myFloat.round(myDecimalPlaces.toInteger()) } catch (ex) { log.error('getDeviceMapAttrMon(): Cannot cast myVal:$myVal to type Float.') } } else myMap["${deviceName}"] = myVal } } //Sort Orders 1 is a forward alpha sort on device name, 2 is a forward alpha sort on value, 3 is a reverse alpha sort on value, 4 is a high to low numeric sort, 5 is a low to high numeric sort if (mySortOrder == "1") myMap = myMap.sort(it) if (mySortOrder == "2") myMap = myMap.sort{it.value} if (mySortOrder == "3") { myMap = myMap.sort{it.value} myMap = reverseSortMap(myMap) } try { if (mySortOrder == "4" ) myMap = myMap.sort { -it.value } if (mySortOrder == "5" ) myMap = myMap.sort { it.value } } catch (ex) { log.error('getDeviceMapAttrMon(): Error sorting devices. Check that all selected device has a non null value for this attribute.') } return myMap } //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************** //************** Support Functions //************** //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //Abbreviates common words to reduce space consumption within the table. def abbreviate(deviceName){ deviceName = deviceName.replaceAll(" (?i)room", " Rm") deviceName = deviceName.replaceAll(" (?i)Door", " Dr") deviceName = deviceName.replaceAll(" (?i)Bedroom", " BedRm") deviceName = deviceName.replaceAll(" (?i)Bathroom", " Bath") deviceName = deviceName.replaceAll(" (?i)Living Room", " Living") deviceName = deviceName.replaceAll(" (?i)Dining Room", " Living") deviceName = deviceName.replaceAll(" (?i)Windows", " Win") deviceName = deviceName.replaceAll(" (?i)Window", " Win") deviceName = deviceName.replaceAll(" (?i)Sensor", " Sns") deviceName = deviceName.replaceAll(" (?i)Motion", " Mtn") deviceName = deviceName.replaceAll(" (?i)Temperature", " Temp") deviceName = deviceName.replaceAll(" (?i)Thermostat", " Thermo") deviceName = deviceName.replaceAll(" (?i)North", " N.") deviceName = deviceName.replaceAll(" (?i)South", " S.") deviceName = deviceName.replaceAll(" (?i)East", " E.") deviceName = deviceName.replaceAll(" (?i)West", " W.") return deviceName } //Returns the appropriate sort order for alphabetic or numeric values. def sortOrder(){ if (moduleName == "Attribute Monitor" ) { if (state.attributeType != "String" ) return [1:"Sort alphabetically by device name", 4:"Sort by Highest Value first", 5:"Sort by Lowest Value first"] else return [1:"Sort alphabetically by device name", 2:"Forward Sort Alphabetically by Value", 3:"Reverse Sort Alphabetically by Value"] //Works } if (moduleName == "Activity Monitor" ) { return [1:"Show devices with longest Inactivity period first.", 2:"Show devices with most recent Inactivity period first."] } } //Returns a map in reverse order Map reverseSortMap(Map sortedMap) { def revSortedMap = [:] sortedMap.reverseEach { it, value -> revSortedMap["${it}"] = value } return revSortedMap } //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************** //************** Functions for HTML generation //************** //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //************************************************************************************************************************************************************************************************************************ //Collates the most recent data and calls the makeHTML function void refreshTable(){ if (isLogTrace) log.trace("refreshTable: Entering refreshTable") //Create the template for the data def data = ["#A01#":"A01","#B01#":"B01","#A02#":"A02","#B02#":"B02","#A03#":"A03","#B03#":"B03","#A04#":"A04","#B04#":"B04","#A05#":"A05","#B05#":"B05", "#A06#":"A06","#B06#":"B06","#A07#":"A07","#B07#":"B07","#A08#":"A08","#B08#":"B08","#A09#":"A09","#B09#":"B09","#A10#":"A10",\ "#B10#":"B10","#A11#":"A11","#B11#":"B11","#A12#":"A12","#B12#":"B12","#A13#":"A13","#B13#":"B13","#A14#":"A14","#B14#":"B14","#A15#":"A15", "#B15#":"B15","#A16#":"A16","#B16#":"B16","#A17#":"A17","#B17#":"B17","#A18#":"A18","#B18#":"B18","#A19#":"A19","#B19#":"B19","#A20#":"A20","#B20#":"B20",\ "#B20#":"B20","#A21#":"A21","#B21#":"B21","#A22#":"A22","#B22#":"B22","#A23#":"A23","#B23#":"B23","#A24#":"A24","#B24#":"B24","#A25#":"A25", "#B25#":"B25","#A26#":"A26","#B26#":"B26","#A27#":"A27","#B27#":"B27","#A28#":"A28","#B28#":"B28","#A29#":"A29","#B29#":"B29","#A30#":"A30","#B30#":"B30"] if (moduleName == "Activity Monitor") sortedMap = getDeviceMapActMon() if (moduleName == "Attribute Monitor") sortedMap = getDeviceMapAttrMon() if (isLogDebug) log.debug("refreshTable: sortedMap is: ${sortedMap}") //Iterate through the sortedMap and take the number of entries corresponding to the number set by the deviceLimit recordCount = sortedMap.size() //Make myDeviceLimit = 0 if the they choose 'No Selection' from the drop down or have not selected anything into the devicelist. if ( myDeviceLimit == null || myDeviceList == null) myDeviceLimit = 0 sortedMap.eachWithIndex{ key, value, i -> if (i + 1 <= myDeviceLimit.toInteger() ){ //Make sure all of the device names meet the minimum length by padding the end with spaces. shortName = key + " " //Truncate the name if required //if (isLogDebug) log.debug ("refreshTable: myTruncateLength.toInteger() is ${myTruncateLength.toInteger() }") if ( myTruncateLength != null && myTruncateLength.toInteger() == 96) shortName = findSpace(shortName, 3) if ( myTruncateLength != null && myTruncateLength.toInteger() == 97 ) shortName = findSpace(shortName, 2) if ( myTruncateLength != null && myTruncateLength.toInteger() == 98 ) shortName = findSpace(shortName, 1) if ( myTruncateLength != null && myTruncateLength.toInteger() <= 30 ) { if ( key.size() > myTruncateLength.toInteger() ) { shortName = shortName.substring(0, myTruncateLength.toInteger() ) } } //Data starts at row 1. Row 0 is the headers. i = i + 1 if (i < 10){ mapKeyA = "#A0" + i + "#" mapKeyB = "#B0" + i + "#" } else { mapKeyA = "#A" + i + "#" mapKeyB = "#B" + i + "#" } data."${mapKeyA}" = shortName.trim() data."${mapKeyB}" = value //if (isLogDebug) log.debug("refreshTable: key is: ${key} value is: ${value}, index is: ${i} shortName is: ${shortName}") } } //End of sortedMap.eachWithIndex int myRows = Math.min(recordCount, myDeviceLimit.toInteger()) if (isLogDebug) log.debug ("refreshTable: calling makeHTML: ${data} and myRows:${myRows} with deviceLimit: ${myDeviceLimit}") state.recordCount = myRows makeHTML(data, myRows) } //Creates the HTML data void makeHTML(data, int myRows){ if (isLogTrace) log.trace("makeHTML: Entering makeHTML") //Configure all of the HTML template lines. String HTMLCOMMENT = "" String HTMLSTYLE1 = "
#head#" //End of the Table Style block - Always included. //String HTMLSTYLE2 = ".#id# tr{color:#rtc#;text-align:#rta#;#row#}.#id# td{font-size:#rts#%;padding:#rp#px;#data#}" //End of the Table Style block - Always included. String HTMLBORDERSTYLE = "" //End of the Table Style block. Sets border style for TD and TH elements. - Always included. String HTMLTITLESTYLE = "" //This is the row for the Title Style - May be omitted. String HTMLHEADERSTYLE = "" //This is the row for Header Style - Will be ommitted String HTMLARSTYLE = "" //This is the row for Alternating Row Style - May be omitted. String HTMLFOOTERSTYLE = "" //Footer Style - May be omitted String HTMLHIGHLIGHT1STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT2STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT3STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT4STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT5STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT6STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT7STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT8STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT9STYLE = "" //Highlighting Styles - May be ommitted. String HTMLHIGHLIGHT10STYLE = "" //Highlighting Styles - May be ommitted. String HTMLDIVSTYLE = "" //Div container - May be ommitted. String HTMLDIVSTART = "
#A00# | |
---|---|
#A00# | #B00# |
#A01# | #B01# |
#A02# | #B02# |
#A03# | #B03# |
#A04# | #B04# |
#A05# | #B05# |
#A06# | #B06# |
#A07# | #B07# |
#A08# | #B08# |
#A09# | #B09# |
#A10# | #B10# |
#A11# | #B11# |
#A12# | #B12# |
#A13# | #B13# |
#A14# | #B14# |
#A15# | #B15# |
#A16# | #B16# |
#A17# | #B17# |
#A18# | #B18# |
#A19# | #B19# |
#A20# | #B20# |
#A21# | #B21# |
#A22# | #B22# |
#A23# | #B23# |
#A24# | #B24# |
#A25# | #B25# |
#A26# | #B26# |
#A27# | #B27# |
#A28# | #B28# |
#A29# | #B29# |
#A30# | #B30# |