def appVersion() { return "4.7.2.1" }
/**
* GCal Search Trigger Child Application
* https://raw.githubusercontent.com/HubitatCommunity/Google_Calendar_Search/main/Apps/GCal_Search_Trigger.groovy
*
* Credits:
* Originally posted on the SmartThings Community in 2017:https://community.smartthings.com/t/updated-3-27-18-gcal-search/80042
* Special thanks to Mike Nestor & Anthony Pastor for creating the original SmartApp and DTH
* UI/UX contributions made by Michael Struck and OAuth improvements by Gary Spender
* Code was ported for use on Hubitat Elevation by cometfish in 2019: https://github.com/cometfish/hubitat_app_gcalsearch
* Further improvements made by ritchierich and posted to the HubitatCommunity GitHub Repository so other community members can continue to improve this application
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in wr iting, 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.
*
*/
import hubitat.helper.RMUtils
import groovy.json.JsonSlurper
import org.apache.commons.lang3.time.DateUtils
definition(
name: "GCal Search Trigger",
namespace: "HubitatCommunity",
author: "Mike Nestor & Anthony Pastor, cometfish, ritchierich",
description: "Integrates Hubitat with Google Calendar, Tasks, and Gmail.",
category: "Convenience",
parent: "HubitatCommunity:GCal Search",
documentationLink: "https://community.hubitat.com/t/release-google-calendar-search/71397",
importUrl: "https://raw.githubusercontent.com/HubitatCommunity/Google_Calendar_Search/main/Apps/GCal_Search_Trigger.groovy",
iconUrl: "",
iconX2Url: "",
iconX3Url: "",
)
preferences {
page(name: "mainPage")
}
def mainPage() {
return dynamicPage(name: "mainPage", title: "${parent.getFormat("title", "GCal Search Trigger Version ${appVersion()}, ${(state.installed == true) ? "Update" : "Create new"} search trigger")}", install: true, uninstall: true, nextPage: "" ) {
section() {
if (!state.isPaused) {
input name: "pauseButton", type: "button", title: "Pause", backgroundColor: "Green", textColor: "white", width: 4, submitOnChange: true
} else {
input name: "resumeButton", type: "button", title: "Resume", backgroundColor: "Crimson", textColor: "white", width: 4, submitOnChange: true
}
if (state.refreshed) {
def refreshText = parseDateTime(state.refreshed)
if (state.installed == true && settings.createChildSwitch && childCreated() == true) {
def childSwitch = getChildDevice("GCal_${app.id}")
refreshText = "Last Refreshed:\n${refreshText}"
} else {
refreshText = "Last Refreshed:\n${refreshText}"
}
paragraph "${refreshText}", width: 4
}
if (!state.isPaused) {
input name: "refreshButton", type: "button", title: "Refresh", width: 4, submitOnChange: true
}
}
section() {
def nextItemDescription = getNextItemDescription()
if (nextItemDescription) {
paragraph "${nextItemDescription}"
}
}
section("${parent.getFormat("box", "Search Preferences")}") {
def isAuthorized = parent.authTokenValid("search trigger mainPage")
if (!isAuthorized) {
paragraph "${parent.getFormat("warning", "Authentication Problem! Please setup again in the parent GCal Search app.")}"
} else {
def scopesAuthorized = parent.getScopesAuthorized()
if (scopesAuthorized == null) {
log.error "The parent GCal Search is using an old OAuth credential type. Please open this app and follow the steps to complete the upgrade and click Done."
}
//Reminder Remove at a later time...
if (settings.searchType == "Reminder") {
scopesAuthorized.push("Reminder")
def reminderRemoveText = "Google has deprecated Reminders. Click here to learn more about the switch from Reminders to Google Tasks."
paragraph "${parent.getFormat("warning", "${reminderRemoveText}")}"
def taskDetailText = '
This Search Trigger will no longer function:
An error will appear in the log if an automation is in place trying to invoke a reminder search refresh
'
taskDetailText += '
Please either remove this child app or change it to a Task search trigger
Please note (from Google documentation): The Task due date only records date information; the time portion of the timestamp is discarded when setting the due date. It isn\'t possible to read or write the time that a task is due via the API.
'
taskDetailText += '
As a result the task due date will default to midnight of the date selected
'
paragraph "${taskDetailText}"
}
if (settings.searchType == "Task") {
paragraph "Please note (from Google documentation): The Task due date only records date information; the time portion of the timestamp is discarded when setting the due date. It isn\'t possible to read or write the time that a task is due via the API. As a result the task due date will default to midnight of the date selected. Please see this Issue Tracker from 2019 and vote to have due date time added to the Task API."
}
def watchListOptions, mailLabels
input name: "searchType", title:"Do you want to search Google Calendar Event, Task, or Gmail?", type: "enum", required:true, multiple:false, options:scopesAuthorized, defaultValue: "Calendar", submitOnChange: true
//Default value above isn't working so set a default to prevent app errors
if ((settings.searchType == null || settings.searchType == "Calendar") && scopesAuthorized != null) {
if (scopesAuthorized.indexOf("Calendar") > -1) {
settings.searchType = "Calendar Event"
app.updateSetting("searchType", [value:"Calendar Event", type:"enum"])
} else {
settings.searchType = scopesAuthorized[0]
app.updateSetting("searchType", [value:"${scopesAuthorized[0]}", type:"enum"])
}
}
logDebug("mainPage - settings.searchType: ${settings.searchType}, scopesAuthorized: ${scopesAuthorized}")
if (settings.searchType == "Calendar Event") {
watchListOptions = parent.getCalendarList()
if (watchListOptions == "error") {
paragraph "${parent.getFormat("warning", "The Google Calendar API has not been enabled in your Google project. Please click here to add it the in Google Console. Then refresh this page.")}"
} else {
//logDebug("Calendar list = ${watchListOptions}")
input name: "watchList", title:"Which calendar do you want to search?", type: "enum", required:true, multiple:false, options:watchListOptions, submitOnChange: true
input name: "includeAllDay", type: "bool", title: "Include All Day Events?", defaultValue: true, required: false
input name: "timeZoneQuery", type: "bool", title: "Include hub's timezone in calendar query? By default the timezone set within the Google Calendar is used when querying for events. If your hub's timezone and calendar's timezone are different the matched events may have incorrect date/timestamps. Enable this setting to query for events based on your hub's timezone. You can check your Google calendars timezone by following the instructions in this article.", defaultValue: false
input name: "searchField", type: "enum", title: "Calendar field to search", required: true, defaultValue: "title", options:["title","location"]
input name: "GoogleMatching", type: "bool", title: "Use Google Query Matching? By default calendar event matching is done by the HE hub and it allows multiple search strings. If you prefer to use Google search features and special characters, toggle this setting. Caching of events is not supported when using Google query matching.", defaultValue: false, submitOnChange: true
}
} else if (settings.searchType == "Task") {
settings.GoogleMatching = false // Task API doesn't allow text searching
watchListOptions = parent.getTaskList()
if (watchListOptions == "error") {
paragraph "${parent.getFormat("warning", "The Google Tasks API has not been enabled in your Google project. Please click here to add it the in Google Console. Then refresh this page.")}"
} else {
logDebug("Task list = ${watchListOptions}")
input name: "watchList", title:"Which task list do you want to search?", type: "enum", required:true, multiple:false, options:watchListOptions, submitOnChange: true
}
} else if (settings.searchType == "Gmail") {
settings.GoogleMatching = true // Gmail must use Google Matching
settings.appName = "Gmail Search"
} else {
// Else here for deprecated Reminder API leaving just in case
logDebug("scopesAuthorized: ${scopesAuthorized}")
settings.GoogleMatching = false
}
if (settings.GoogleMatching == true) {
def searchHelp = ""
if (settings.searchType == "Gmail") {
searchHelp = "If not familiar with Google Search special characters, please visit Search operators you can use with Gmail for examples."
} else {
searchHelp = "If not familiar with Google Search special characters, please visit GoogleGuide for examples."
}
paragraph "${parent.getFormat("text", searchHelp)}"
} else {
paragraph '
Search String Options:
By default matches are CaSe sensitive, toggle \'Enable case sensitive matching\' to make search matching case insensitive.
By default the search string is matched to the ' + settings.searchType.toLowerCase() + ' title using a starts with search.
For exact match, prefix the search string with an = sign. For example enter =Kids No School to find items with the exact title/location of \'Kids No School\'.
For a contains search, include an * sign. For example to find any item with the word School, enter *School. This also works for multiple non consecutive words. For example to match both Kids No School and Kids Late School enter Kids*School.
Multiple search strings may be entered separated by commas.
To match any ' + settings.searchType.toLowerCase() + ' for that day, enter *
To exclude ' + settings.searchType.toLowerCase() + ' with specific words, prefix the word with a \'-\' (minus) sign. For example if you would like to match all items except ones with the words \'personal\' and \'lunch\' enter \'* -personal -lunch\'
'
input name: "caseSensitive", type: "bool", title: "Enable case sensitive matching?", defaultValue: true
}
if (settings.searchType == "Gmail") {
paragraph "${parent.getFormat("text", getGmailSearchDescription())}"
mailLabels = parent.getUserLabels()
if (settings.messageQueryLabels != null && settings.messageQueryLabels.indexOf("none") > -1 && settings.messageQueryLabels.size() > 1) {
app.updateSetting("messageQueryLabels", [value:["none"], type:"enum"])
}
input name: "messageQueryLabels", title:"Search for emails with the following labels:", type: "enum", required:true, multiple:true, options:mailLabels, defaultValue: ["INBOX", "UNREAD"], submitOnChange: true
input name: "messageQueryAfterLastRefresh", type: "bool", title: "Search for emails received since the last refresh of this app?", defaultValue: true, required: false, submitOnChange: true
}
def requireSearchString = (settings.searchType == "Gmail" && (settings.messageQueryLabels == null || settings.messageQueryLabels.indexOf("none") == -1)) ? false : true
input name: "search", type: "text", title: "Search String", required: requireSearchString, submitOnChange: true
if (settings.searchType == "Gmail") {
def mailQuery = getGmailQuery()
def mailURL = "https://mail.google.com/mail/u/1/#search/" + URLEncoder.encode(mailQuery.toString())
paragraph "${parent.getFormat("text", "When adjusting the Search Preferences of this app, you should test within the Gmail website to ensure the right messages are found. The following is the query that will be used to query for emails.")}"
paragraph "${parent.getFormat("code", "${mailQuery}")}"
paragraph "${parent.getFormat("text", "Add/Remove labels: Labels can be added and removed to emails matching the search criteria which may help designate that an email was processed. Enable this option to adjust the labels.")}"
input name: "messageApplyLabels", type: "bool", title: "Add/Remove labels on matching emails?", defaultValue: true, required: false, submitOnChange: true
if (settings.messageApplyLabels == null || settings.messageApplyLabels == true) {
input name: "messageSetLabels", title:"Matched Email Labels. Note: Labels selected will be added to the emails and labels unselected will be removed. Choose NONE to remove all labels.", type: "enum", required:true, multiple:true, options:mailLabels, defaultValue: ["INBOX"]
}
}
paragraph "${parent.getFormat("line")}"
}
}
if (settings.search || settings.searchType == "Gmail") {
section("${parent.getFormat("box", "Schedule Settings")}") {
paragraph "${parent.getFormat("text", "${settings.searchType} searches can be triggered once a day or periodically. Periodic options include every N hours, every N minutes, or you may enter a Cron expression.")}"
input name: "whenToRun", type: "enum", title: "When to Run", required: true, options:["Once Per Day", "Periodically"], submitOnChange: true
if (settings.whenToRun == "Once Per Day") {
input name: "timeToRun", type: "time", title: "Time to run", required: true
}
if (settings.whenToRun == "Periodically") {
input name: "frequency", type: "enum", title: "Frequency", required: true, options:["Hours", "Minutes", "Cron String"], submitOnChange: true
if (settings.frequency == "Hours") {
input name: "hours", type: "number", title: "Every N Hours: (range 1-12)", range: "1..12", required: true, submitOnChange: true
input name: "hourlyTimeToRun", type: "time", title: "Starting at", defaultValue: "08:00", required: true
}
if (settings.frequency == "Minutes") {
input name: "minutes", type: "enum", title: "Every N Minutes", required: true, options:["1", "2", "3", "4", "5", "6", "10", "12", "15", "20", "30"], submitOnChange: true
}
if (settings.frequency == "Cron String") {
paragraph "${parent.getFormat("text", "If not familiar with Cron Strings, please visit Cron Expression Generator")}"
input name: "cronString", type: "text", title: "Enter Cron string", required: true, submitOnChange: true
}
}
if (settings.searchType != "Gmail") {
paragraph "${parent.getFormat("text", "Search Range: By default, items from the time of search through the end of the current day are collected. Adjust this setting to expand the search to the end of the following day or a set number of hours from the time of search.")}"
input name: "endTimePref", type: "enum", title: "Search Range", defaultValue: "End of Current Day", options:["End of Current Day","End of Next Day", "Number of Hours from Current Time"], submitOnChange: true
if (settings.endTimePref == "Number of Hours from Current Time") {
input name: "endTimeHours", type: "number", title: "Number of Hours from Current Time (How many hours into the future at the time of the search, would you like to query for events?)", required: true
}
if (settings.searchType == "Calendar Event") {
paragraph "${parent.getFormat("text", "Sequential Event Preference: By default the Event End Time will be set to the end date of the last sequential event matching the search criteria. This prevents the switch from toggling and additonal actions triggering multiple times when using periodic searches. If this setting is set to false, it is recommended to set an Event End Offset in the optional setting below. If no Event End Offset is set, the scheduled trigger will be adjusted by -1 minute to ensure the switch has time to toggle.")}"
input name: "sequentialEvent", type: "bool", title: "Expand end date for sequential events?", defaultValue: true
}
paragraph "${parent.getFormat("text", "Delay to ${settings.searchType} Start Preference: By default the switch will toggle and additional actions will trigger based on the matching ${settings.searchType} Start Time. If this setting is set to false, the switch will toggle and additional actions will trigger at the run time of this search trigger if a match is found. The switch will continue to toggle and additional actions will trigger again based on the Event End Time (if applicable).")}"
input name: "delayToggle", type: "bool", title: "Delay to ${settings.searchType} start?", defaultValue: true
paragraph "${parent.getFormat("text", "Optional Offset Preferences: Based on the defined Search Range, if an item is found in the future from the current time, scheduled triggers will be created to toggle the switch and trigger additional actions based on the item's start and end times. Use the settings below to set an offset to firing of these triggers N number of minutes before/after the item date(s). For example, if you wish for the switch to toggle or additional actions to trigger 60 minutes prior to the start of the event, enter -60 in the Event Start Offset setting. This may be useful for reminder notifications where a message is sent/spoken in advance of a task. Again this is dependent on When to Run (how often the trigger is executed) and the Search Range of events.")}"
input name: "setOffset", type: "bool", title: "Set offset?", defaultValue: false, required: false, submitOnChange: true
if (settings.setOffset == true) {
if (settings.searchType == "Calendar Event") {
input name: "offsetStartFromReminder", type: "bool", title: "Start Offset from Event Reminder Value?", defaultValue: false, required: false, submitOnChange: true
}
if (settings.searchType != "Calendar Event" || (settings.searchType == "Calendar Event" && settings.offsetStartFromReminder != true)) {
input name: "offsetStart", type: "decimal", title: "Start Offset in minutes (+/-)", required: false
} else {
paragraph "${parent.getFormat("text", "Reminders are always stored in minutes even if hours or weeks is set when creating the event. By default offset will be X minutes before the event start time, however you may choose to delay the start time X minutes after the event start time too.")}"
input name: "offsetStartFromReminderWhen", type: "enum", title: "Before or After", required: true, options:["Before", "After"], defaultValue: "Before"
}
if (settings.searchType == "Calendar Event") {
input name: "offsetEnd", type: "decimal", title: "End Offset in minutes (+/-)", required: false
}
}
}
paragraph "${parent.getFormat("line")}"
}
}
if (settings.search || settings.searchType == "Gmail") {
section("${parent.getFormat("box", "Child Switch Preferences")}") {
paragraph "${parent.getFormat("text", "Create child switch: By default this app will create and toggle a child switch when an item matches the search criteria. Many inbuilt apps have restrictions based on the state of a switch where the rule won't fire if say for example a particular switch is turned on. In other use cases, a child switch may bring unnecessary overhead. Choose what you wish to happen when an item matches the search criteria.")}"
input name: "createChildSwitch", type: "bool", title: "Create child switch?", defaultValue: true, required: false, submitOnChange: true
if (settings.createChildSwitch != false) {
def defName = (settings.search) ? settings.search : settings.appName
defName = defName - "\"" - "\"" //.replaceAll(" \" [^a-zA-Z0-9]+","")
input name: "deviceName", type: "text", title: "Switch Device Name (Name of the Switch that gets created by this search trigger)", required: true, multiple: false, defaultValue: "${defName} Switch"
paragraph "${parent.getFormat("text", "Switch Default Value: Adjust this setting to the switch value preferred when there is no task. If a task is found, the switch will toggle from this value.")}"
input name: "switchValue", type: "enum", title: "Switch Default Value", required: true, defaultValue: "off", options:["on","off"]
paragraph "${parent.getFormat("text", "Date Format: Adjust this setting to your desired date format. By default time format will be based on the hub's time format setting. Choose other to enter your own custom date/time format. Please see this website for examples.")}"
input name: "dateFormat", type: "enum", title: "Date Format", required: true, defaultValue: "yyyy-MM-dd", options:["yyyy-MM-dd", "MM-dd-yyyy", "dd-MM-yyyy", "Other"], submitOnChange: true
if (settings.dateFormat == "Other") {
input name: "dateFormatOther", type: "text", title: "Enter custom date format", required: true
}
if (settings.searchType == "Calendar Event") {
paragraph "${parent.getFormat("text", "Set Next Event on Child Switch: Based on the defined Search Range, multiple items may be found matching the search criteria. Set this setting to true if you want the nextEvent attribute on the child switch to be set with details of other events found.")}"
input name: "setNextEvent", type: "bool", title: "Set Next Event on Child Switch", defaultValue: false, required: false, submitOnChange: true
if (settings.setNextEvent == true) {
paragraph "${parent.getFormat("text", "Next Event Detail: " + getNotificationMsgDescription(settings.searchType, false))}"
input name: "nextEventDetail", type: "text", title: "Next Event Detail", required: false, defaultValue: "%eventTitle%"
}
if (settings.setOffset == true) {
paragraph "${parent.getFormat("text", "Note: The child switch will be populated with the calendar event start and end timestamps in the eventStartTime and eventEndTime state attributes. By default these values will be the actual start and end timestamps. There is an optional preference on the child switch to set these to the offset values instead.")}"
}
}
}
paragraph "${parent.getFormat("line")}"
}
section("${parent.getFormat("box", "Additional Action Preferences")}") {
paragraph "${parent.getFormat("text", "Additional actions are optional and can be useful if you choose not to create a child switch to send notifications and run Rule Machine rules as items are found. Please be aware if you have rules set to trigger based on the child switch and you choose to run actions for those same rules problems may arise.")}"
paragraph "${parent.getFormat("text", "Notifications can be sent/spoken based on an item matching search criteria.")}"
input "sendNotification", "bool", title: "Send notifications?", defaultValue: false, submitOnChange: true
if (settings.sendNotification == true) {
def startMsg, endMsg
if (settings.searchType == "Calendar Event") {
startMsg = "Custom message to send at event scheduled start"
endMsg = "Custom message to send at event scheduled end"
} else if (settings.searchType == "Gmail") {
startMsg = "Custom message to send when message is found"
} else {
startMsg = "Custom message to send at " + settings.searchType.toLowerCase() + " due date"
endMsg = "Custom message to send when " + settings.searchType.toLowerCase() + " is completed"
}
paragraph "${parent.getFormat("text", "Custom message to send: " + getNotificationMsgDescription(settings.searchType))}"
if (settings.searchType == "Calendar Event") {
input "sendReminder", "bool", title: "Send reminder?", defaultValue: false, submitOnChange: true
if (settings.sendReminder == true) {
input name: "notificationReminderMsg", type: "textarea", title: "Custom reminder to send at event reminder setting", required: true, defaultValue: "%eventTitle% is starting at %eventStartTime%"
}
}
input name: "notificationStartMsg", type: "textarea", title: "${startMsg}", required: false
if (endMsg != null) {
input name: "notificationEndMsg", type: "textarea", title: "${endMsg}", required: false
}
paragraph "${parent.getFormat("text", "Include details from all matching items: Based on the defined Search Range, multiple items may be found matching the search criteria. By default only details from the first item will be included in notifications. Set this setting to true if you want details from all matching items to be included.")}"
input "includeAllItems", "bool", title: "Include details from all matching items?", defaultValue: false
input "notificationDevices", "capability.notification", title: "Send notification to device(s)?", required: false, multiple: true
input "speechDevices", "capability.speechSynthesis", title: "Speak notification on these device(s)?", required: false, multiple: true
}
paragraph "${parent.getFormat("line")}"
paragraph "${parent.getFormat("text", "Rule Machine rules can be evaluated based on an item matching search criteria.")}"
input "runRuleActions", "bool", title: "Run Rule Machine actions?", defaultValue: false, submitOnChange: true
if (settings.runRuleActions == true) {
def legacyRules = RMUtils.getRuleList()
if (legacyRules != null) {
input "legacyRule", "enum", title: "Select which Rule Machine Legacy rules to run", options: legacyRules, multiple: true
}
def currentRules = RMUtils.getRuleList('5.0')
input "currentRule", "enum", title: "Select which rules to run", options: currentRules, multiple: true
if (settings.searchType == "Calendar Event") {
paragraph "${parent.getFormat("text", "Set Rule Machine Private Boolean: Rule actions will be invoked at the event start and event end. If you wish to differentiate between the two times, set this setting to true and the Rule Private Boolean will be set to True at the event start and False at the event end. Then build your rule conditions around these values to have a different flow.")}"
input "updateRuleBoolean", "bool", title: "Set Rule Machine Private Boolean?", defaultValue: false
}
}
paragraph "${parent.getFormat("line")}"
def controlSwitchesDescription = getControlSwitchesDescription(settings.searchType)
paragraph "${parent.getFormat("text", controlSwitchesDescription.description)}"
input "controlSwitches", "bool", title: "${controlSwitchesDescription.title}", defaultValue: false, submitOnChange: true
if (settings.controlSwitches == true) {
paragraph "${parent.getFormat("text", controlSwitchesDescription.instructions)}"
input name: "itemField", type: "enum", title: "${settings.searchType} field to use for switch instructions", required: true, defaultValue: "title", options:controlSwitchesDescription.options, width: 4
input name: "offTranslation", type: "text", title: "Translation for Off", required: true, defaultValue: "off", width: 4
input name: "onTranslation", type: "text", title: "Translation for On", required: true, defaultValue: "on", width: 4
input name: "ignoreWords", type: "text", title: "Verbs within text to ignore (separate multiple words by comma)", required: false, defaultValue: "turn", width: 6
input name: "conjunctionWords", type: "text", title: "Conjunction words for multiple switches (separate multiple words by comma)", required: false, defaultValue: "and", width: 6
input "controlSwitchList", "capability.switch", title: "Control These Switches", multiple: true, showFilter: true, required: true
if (settings.searchType == "Calendar Event") {
paragraph "${parent.getFormat("text", "Toggle switches at the event scheduled end: Switches that were controlled at the event scheduled start can optionally be toggled at the scheduled end of the event.")}"
input "controlSwitchEndToggle", "bool", title: "Toggle switches at the event scheduled end?", defaultValue: false
}
}
paragraph "${parent.getFormat("line")}"
def parseFieldDescription = getParseFieldDescription(settings.searchType)
paragraph "${parent.getFormat("text", parseFieldDescription.description)}"
input "parseField", "bool", title: "${parseFieldDescription.title}", defaultValue: false, submitOnChange: true
if (settings.parseField == true) {
//paragraph "${parent.getFormat("text", parseFieldDescription.instructions)}"
input name: "fieldToParse", type: "enum", title: "${settings.searchType} field to search for text matches", required: true, defaultValue: "title", options:parseFieldDescription.options
paragraph parseFieldDescription.mappingsTable
}
paragraph "${parent.getFormat("line")}"
paragraph "${parent.getFormat("text", "Switches can be toggled if a ${searchType.toLowerCase()} is found. You can choose to turn it on or off when a matching item is found.")}"
//If you would like other existing switches to follow the switch state of the child GCal Switch, set the following list with those switch(es). Please keep in mind that this is one way from the GCal switch to these switches.
input name: "toggleOtherSwitches", type: "bool", title: "Toggle Switches?", defaultValue: false, required: false, submitOnChange: true
if (settings.toggleOtherSwitches == true) {
input "otherOnSwitches", "capability.switch", title: "Turn These Switches On", multiple: true, showFilter: true, required: false, width: 4
input "otherOffSwitches", "capability.switch", title: "Turn These Switches Off", multiple: true, showFilter: true, required: false, width: 4
if (settings.searchType == "Calendar Event") {
input "toggleOtherSwitchesEndToggle", "bool", title: "Toggle switches at the event scheduled end?", defaultValue: true
}
}
paragraph "${parent.getFormat("line")}"
def updateVariableDescription = getVariableUpdateDescription(settings.searchType)
paragraph "${parent.getFormat("text", updateVariableDescription.description)}"
input "updateVariable", "bool", title: "${updateVariableDescription.title}", defaultValue: false, submitOnChange: true
if (settings.updateVariable == true) {
//paragraph "${parent.getFormat("text", updateVariableDescription.instructions)}"
paragraph updateVariableDescription.mappingsTable
}
paragraph "${parent.getFormat("line")}"
if (settings.sendNotification == true || settings.runRuleActions == true || settings.controlSwitches == true || settings.parseField == true || settings.toggleOtherSwitches == true) {
paragraph "${parent.getFormat("text", "Toggle 'Enable descriptionText logging' below if you want this app to create an event log entry when the additional actions are executed with details on that action.")}"
input name: "txtEnable", type: "bool", title: "Enable descriptionText logging?", defaultValue: false, required: false
paragraph "${parent.getFormat("line")}"
}
}
}
if (settings.search || settings.searchType == "Gmail") {
section("${parent.getFormat("box", "App Preferences")}") {
def defName = (settings.search) ? settings.search : settings.appName
defName = defName - "\"" - "\"" //.replaceAll(" \" [^a-zA-Z0-9]+","")
input name: "appName", type: "text", title: "Name this child app", required: true, defaultValue: "${defName}", submitOnChange: true
input name: "isDebugEnabled", type: "bool", title: "Enable debug logging?", defaultValue: false, required: false
paragraph "${parent.getFormat("line")}"
}
}
if (state.installed) {
section ("Remove Trigger and Corresponding Device") {
paragraph "ATTENTION: The only way to uninstall this trigger and the corresponding child switch device is by clicking the Remove button below. Trying to uninstall the corresponding device from within that device's preferences will NOT work."
}
}
}
}
String buttonLink(String btnName, String linkText, color = "#1A77C9", font = "15px") {
"
$linkText
"
}
def getNotificationMsgDescription(searchType, forNotification=true) {
def answer = ""
if (forNotification == true) {
answer = "Use %now% to include current date/time of when notification is sent, "
}
if (searchType == "Calendar Event") {
answer += "%eventTitle% to include event title, %eventLocation% to include event location, %eventDescription% to include event description, %eventStartTime% to include event start time, %eventEndTime% to include event end time, and %eventAllDay% to include event all day."
if (settings.setOffset) {
answer += " Offset values can be be added by using %scheduleStartTime% and %scheduleEndTime%."
}
if (settings.sendReminder) {
answer += " Reminder minutes can be be added by using %eventReminderMin%."
}
} else if (searchType == "Gmail") {
answer += "%messageTitle% to include message title, %messageBody% to include message body, %messageTo% to include message to, %messageFrom% to include message from, and %messageReceived% to include message received date."
} else {
answer += "%taskTitle% to include task title and %taskDueDate% to include task due date."
if (settings.setOffset) {
answer += " Offset values can be be added by using %scheduleStartTime%."
}
}
if (settings.controlSwitches == true && forNotification == true) {
answer += " With 'Control switches from ${searchType.toLowerCase()} details' enabled switches that will be turned on/off can be added by using %onSwitches% and %offSwitches%."
}
if (settings.parseField == true && forNotification == true) {
answer += " With 'Parse data from ${searchType.toLowerCase()} details and update hub variables' enabled variables that will be updated can be added by using %variableUpdates%."
}
return answer
}
def getNextItemDescription() {
def answer
if (state.item) {
def item = (state.item instanceof ArrayList) ? state.item[0] : state.item
def itemTitle
if (searchType == "Calendar Event") {
itemTitle = item.eventTitle
} else if (searchType == "Gmail") {
itemTitle = item.messageTitle
} else {
itemTitle = item.taskTitle
}
if (itemTitle != " ") {
def searchType = (settings.searchType) ? settings.searchType : "Item"
def itemDetails = ""
if (searchType == "Calendar Event") {
if (item.eventDescription != null) {
itemDetails += "Description: ${item.eventDescription}\n"
}
itemDetails += "Start Time: ${formatDateTime(item.eventStartTime)}"
itemDetails += (item.eventStartTime != item.scheduleStartTime) ? " (Start Offset: ${formatDateTime(item.scheduleStartTime)}) " : ""
itemDetails += ", "
itemDetails += "End Time: ${formatDateTime(item.eventEndTime)}"
itemDetails += (item.eventEndTime != item.scheduleEndTime) ? " (End Offset: ${formatDateTime(item.scheduleEndTime)}) " : " "
itemDetails += "\nLocation: ${item.eventLocation}"
itemDetails += "\nEvent All Day: ${item.eventAllDay}, Reminder: ${item.eventReminderMin} Minutes"
} else if (searchType == "Gmail") {
itemDetails += "Body: ${item.messageBody}\n"
itemDetails += "Received: ${formatDateTime(item.messageReceived)}\n"
itemDetails += "Messages found: ${state.item.size()}\n"
} else {
itemDetails += "Due Date: ${formatDateTime(item.taskDueDate)}"
itemDetails += (item.taskDueDate != item.scheduleStartTime) ? " (Due Date Offset: ${formatDateTime(item.scheduleStartTime)}) " : ""
}
if (item.containsKey("additionalActions") && item.additionalActions.containsKey("triggerStartSwitchControl") && !item.additionalActions.triggerStartSwitchControl.matchSwitches.isEmpty()) {
if (item.additionalActions.triggerStartSwitchControl.matchSwitches.on) {
itemDetails += "\nSwitches to turn on: "
itemDetails += gatherSwitchNames(item, "on")
}
if (item.additionalActions.triggerStartSwitchControl.matchSwitches.off) {
itemDetails += "\nSwitches to turn off: "
itemDetails += gatherSwitchNames(item, "off")
}
}
if (item.containsKey("additionalActions") && item.additionalActions.containsKey("triggerStartVariableUpdate") && !item.additionalActions.triggerStartVariableUpdate.isEmpty()) {
itemDetails += "\nVariables to update: "
itemDetails += gatherVariableUpdates(item)
}
if (searchType == "Gmail") {
answer = "Last Message:\n"
answer += "Title: ${itemTitle}"
} else {
answer = "Next ${searchType}: ${itemTitle}"
}
answer += "\n${itemDetails}"
}
}
return answer
}
def getControlSwitchesDescription(searchType) {
def answer = [
title: "Control switches from ${searchType.toLowerCase()} details?",
options: ["title"]
]
answer.description = "Switches can be toggled dynamically based on text/instructions found within the ${searchType.toLowerCase()}. For confirmation purposes, the matched switches will appear at the top of this app above the Search Preferences section when a maching ${searchType.toLowerCase()} is found. "
answer.description += "The same search matching options described above in the Search Preferences section are available to match switch names. Examples:\n"
answer.description += "
"
answer.description += "
'Turn off bedroom fan and turn on bedroom overhead lights, bedroom lamps, and bedroom tv' to turn off and on several switches
"
answer.description += "
'Turn off bedroom*' to turn off all switches that start with bedroom
${searchType.toLowerCase()} field to use...: Field within the the ${searchType.toLowerCase()} to look for instructions. Tasks only have the title field available.
"
answer.instructions += "
Translation for Off: Defaults to the word 'off' but international users may enter their translation for 'off'
"
answer.instructions += "
Translation for On: Defaults to the word 'on' but international users may enter their translation for 'on'
"
answer.instructions += "
Verbs within text to ignore: Optional: Enter word(s) that might be included within the instructions that you want to ignore during the matching process. In English you might say 'turn on the overhead lights' but the word 'turn' isn't really necessary and should be ignored.
"
answer.instructions += "
Conjunction words...: Optional: Words that might be used to separate multiple switches such as 'and' or 'along with'. By default multiple switches can be separated by a comma or semicolon.
"
answer.instructions += "
Control These Switches: Choose all switches that you would ever want to control from a ${searchType.toLowerCase()}. Text within the ${searchType.toLowerCase()} will be matched to the switch name/label set witin the device. Note: apostrophe's will be removed prior to matching since they can be problematic.
"
answer.instructions += "
"
return answer
}
def getParseFieldDescription(searchType) {
def answer = [
title: "Parse data from ${searchType.toLowerCase()} details and update hub variables",
options: ["title"]
]
answer.description = "Text found within the ${searchType.toLowerCase()} can be mapped to Hub Variables for additional rule processing. For example with an AirBnB rental calendar event, the last 4 digits of the renter's phone number can be mapped to a hub variable that will fire a rule to add these digits as a code to the lock on the property."
answer.description += "
"
answer.description += "
Each line of the chosen field will be processed looking for specific text entered in the Text to Find input
"
answer.description += "
If a match is found, the remaining text on that line (or next line) will become the value of the selected Hub Variable
"
if (searchType == "Calendar Event") {
answer.description += "
At the end of the ${searchType.toLowerCase()}, the variables can be set to a specified value unless left blank and the previous value parsed will remain
By default this will query for unread emails in the inbox received after the last refreshed time (in Unix epoch time)
"
answer += "
The default search labels (INBOX and UNREAD) can be adjusted in the selection below or manually in the Search String via 'label:' search
"
answer += "
The received query can be removed by toggling the 'Search for emails received...' setting below or manually in the Search String via 'after:' search
"
answer += "
By default any email matching the search criteria will remain in the inbox but the unread label will be removed. This can be adjusted with the Matched Email Labels setting below.
"
answer += "
"
return answer
}
def getVariableUpdateDescription(searchType) {
def answer = [
title: "Update Hub Variables from ${searchType.toLowerCase()} details"
]
answer.description = "Attributes from the ${searchType.toLowerCase()} can be mapped to Hub Variables for additional rule and notification processing. After completing a ${searchType.toLowerCase()} refresh, the selected hub variables will be populated with the matching attribute values. Note: Option is only utilized for date/time attributes and will be ignored for others."
if (settings.updateVariable == true) {
def variableMappings = (atomicState.variableMappings) ? atomicState.variableMappings : [[attribute:"None",variable:"None",dateOption:"None"]]
HashMap globalVars = getAllGlobalVars()
def globalVarNames = globalVars.keySet().sort()
String attributeOptions = ""
if (searchType == "Calendar Event") {
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
if (settings.setOffset) {
attributeOptions += ""
attributeOptions += ""
}
if (settings.setNextEvent == true) {
attributeOptions += ""
}
} else if (searchType == "Gmail") {
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
attributeOptions += ""
} else {
attributeOptions += ""
attributeOptions += ""
if (settings.setOffset) {
attributeOptions += ""
}
}
String variableOptions = ""
for (int i = 0; i < globalVarNames.size(); i++) {
def optionVal = globalVarNames[i]
variableOptions += ""
}
String dateOptions = ""
dateOptions += ""
dateOptions += ""
dateOptions += ""
String str = ""
str += "
" +
"
" +
"
Attribute
" +
"
Variable
" +
"
Option
" +
"
"
for (int m = 0; m < variableMappings.size(); m++) {
def mapping = variableMappings[m]
String deleteRow = buttonLink("deleteRowA" + m, "", "#FF0000", "20px")
str += "