/** * Copyright 2024 Bloodtick * * 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 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. * * HubiThings Replica * * Update: Bloodtick Jones * Date: 2022-10-01 * * 1.0.00 2022-10-01 First pass. * ... Deleted * 1.3.06 2023-02-26 Natural order sorting. [patched 2023-02-28 for event sorting] * 1.3.07 2023-03-14 Bug fixes for possible Replica UI list nulls. C-8 hub migration OAuth warning. * 1.3.08 2023-04-23 Support for more SmartThings Virtual Devices. Refactor of deviceTriggerHandlerPrivate() to support. * 1.3.09 2023-06-05 Updated to support 'warning' for token refresh with still valid OAuth authorization. * 1.3.10 2023-06-17 Support SmartThings Virtual Lock, add default values to ST Virtuals, fix mirror/create flow logic * 1.3.11 2023-07-05 Support for building your own Virtual Devices, Mute logs/Disable periodic refresh buttons on rules. Updated to support schema.oneOf.type drivers. * 1.3.12 2023-08-06 Bug fix for dup event trigger to different command event (virtual only). GitHub issue ticket support for new devices requests. * 1.3.13 2024-02-17 Updated refresh support to allow for device (Location Knob) execution * 1.3.14 2024-03-08 Bug fix for capability check before attribute match in smartTriggerHandler(), checkCommand() && checkTrigger() * 1.3.15 2024-03-23 Update to OAuth to give easier callback identification. This will only take effect on new APIs, so old ones will still have generic name. (no Replica changes) * LINE 30 MAX */ public static String version() { return "1.3.15" } public static String copyright() { return "© 2024 ${author()}" } public static String author() { return "Bloodtick Jones" } import groovy.json.* import java.util.* import java.text.SimpleDateFormat import java.net.URLEncoder import hubitat.helper.RMUtils import com.hubitat.app.ChildDeviceWrapper import groovy.transform.CompileStatic import groovy.transform.Field @Field static final String sDefaultAppName="HubiThings Replica" @Field static final Integer iUseJqueryDataTables=25 @Field static final Integer iHttpSuccess=200 @Field static final Integer iHttpError=400 @Field static final String sURI="https://api.smartthings.com" @Field static final Integer iPageMainRefreshInterval=60*30 @Field static final String sColorDarkBlue="#1A77C9" @Field static final String sColorLightGrey="#DDDDDD" @Field static final String sColorDarkGrey="#696969" @Field static final String sColorDarkRed="DarkRed" @Field static final String sNotAuthorized="​Not Authorized" @Field static final String sNoRules="​No Rules" @Field static final String sOffline="​Offline" // IN-MEMORY VARIABLES (Cleared on HUB REBOOT or CODE UPDATES) @Field volatile static Map g_mSmartDeviceStatusMap = [:] @Field volatile static Map g_mSmartDeviceListCache = [:] @Field volatile static Map g_mVirtualDeviceListCache = [:] @Field volatile static Map g_mSmartLocationListCache = [:] @Field volatile static Map g_mSmartRoomListCache = [:] @Field volatile static Map g_mSmartSceneListCache = [:] @Field volatile static Map g_mReplicaDeviceCache = [:] @Field volatile static Map g_mAppDeviceSettings = [:] // don't clear void clearAllVolatileCache() { Long appId = app.getId() g_mSmartDeviceStatusMap[appId]=null g_mSmartDeviceListCache[appId]=null g_mVirtualDeviceListCache[appId]=null g_mSmartLocationListCache[appId]=null g_mSmartRoomListCache[appId]=null g_mSmartSceneListCache[appId]=null g_mReplicaDeviceCache[appId]=null } definition( name: sDefaultAppName, namespace: "replica", author: "bloodtick", description: "Hubitat Application to manage SmartThings Devices", category: "Convenience", importUrl:"https://raw.githubusercontent.com/bloodtick/Hubitat/main/hubiThingsReplica/hubiThingsReplica.groovy", iconUrl: "", iconX2Url: "", singleInstance: false ){} preferences { page name:"pageMain" page name:"pageAuthDevice" page name:"pageHubiThingDevice" page name:"pageMirrorDevice" page name:"pageConfigureDevice" page name:"pageVirtualDevice" page name:"pageSupportNewDevice" } def installed() { state.isInstalled = now() app.updateSetting("pageMainShowConfig", true) initialize() } def updated() { initialize() } def initialize() { logInfo "${app.getLabel()} executing 'initialize()'" if(pageMainPageAppLabel && pageMainPageAppLabel!=app.getLabel()) { app.updateLabel( pageMainPageAppLabel ) } subscribe(location, "mode", locationModeHandler) } def uninstalled() { logInfo "${app.getLabel()} executing 'uninstalled()'" unsubscribe() unschedule() getChildDevices().each { deleteChildDevice(it.deviceNetworkId) } getChildApps().each { deleteChildApp(it.id) } } /************************************** CHILD METHODS START *******************************************************/ public void childInitialize( childApp ) { logDebug "${app.getLabel()} executing 'childInitialize($childApp.id)'" } public void childUpdated( childApp ) { logDebug "${app.getLabel()} executing 'childUpdated($childApp.id)'" } public void childUninstalled( childApp ) { logDebug "${app.getLabel()} executing 'childUninstalled($childApp.id)'" runIn(2, allSmartDeviceRefresh) runIn(5, updateLocationSubscriptionSettings) // not the best place for this. not sure where is the best place. } public void childSubscriptionDeviceListChanged( childApp, data=null ) { logDebug "${app.getLabel()} executing 'childSubscriptionDeviceListChanged($childApp.id, $data)'" try { getSmartDevicesMap()?.items?.removeAll{ it.appId == childApp.getId() } getSmartDevicesMap()?.items?.addAll( childApp.getSmartSubscribedDevices()?.items ) } catch(e) { logInfo "${app.getLabel()} 'childSubscriptionDeviceListChanged($childApp.id)' did not complete successfully" } runIn(5, updateLocationSubscriptionSettings) // not the best place for this. not sure where is the best place. } public List childGetOtherSubscribedDeviceIds( childApp ) { logDebug "${app.getLabel()} executing 'childGetOtherSubscribedDeviceIds($childApp.id)'" List devices = [] getChildApps()?.each{ oauthApp -> if(oauthApp.getId() != childApp.getId()) { devices += oauthApp?.getSmartDeviceSelectList() } } return devices?.unique() } public void childSubscriptionEvent( childApp, event ) { oauthEventHandler( event?.eventData, now() ) } public void childHealthChanged( childApp ) { logDebug "${app.getLabel()} executing 'childHealthChanged($childApp.id)'" String locationId = childApp?.getLocationId() String oauthStatus = "UNKNOWN" getChildApps().each{ logTrace "${it?.getAuthStatus()?.toLowerCase()} ${it?.getLocationId()}" if(it?.getLocationId() == locationId) { if(it?.getAuthStatus()=="FAILURE") oauthStatus = "FAILURE" else if(oauthStatus!="FAILURE" && it?.getAuthStatus()=="WARNING") oauthStatus = "WARNING" else if(oauthStatus!="FAILURE" && oauthStatus!="WARNING" && it?.getAuthStatus()=="PENDING") oauthStatus = "PENDING" else if(oauthStatus!="FAILURE" && oauthStatus!="WARNING" && oauthStatus!="PENDING" && it?.getAuthStatus()=="AUTHORIZED") oauthStatus = "AUTHORIZED" } } getAllReplicaDevices()?.each { replicaDevice -> if(hasCommand(replicaDevice, 'setOauthStatusValue')) { Map description = getReplicaDataJsonValue(replicaDevice, "description") if(description?.locationId == locationId) { replicaDevice.setOauthStatusValue(oauthStatus?.toLowerCase()) } } } } public String getAuthToken() { return userSmartThingsPAT } public void setLocationMode(String mode) { logDebug "${app.getLabel()} executing 'setLocationMode($mode)'" app.setLocationMode(mode) } public void updateLocationSubscriptionSettings() { List primaryApps = getChildApps()?.clone()?.sort{ it?.getId() }?.unique{ a, b -> a.getLocationId() <=> b.getLocationId() } getChildApps()?.each{ ouathApp -> if( primaryApps?.find{ it?.getId()==ouathApp.getId() } ) { logDebug "${app.getLabel()} Leader**: location:${ouathApp?.getLocationId()} OAuthId:${ouathApp?.getOauthId()}" ouathApp.updateLocationSubscriptionSettings(true) } else { logDebug "${app.getLabel()} Follower: location:${ouathApp?.getLocationId()} OAuthId:${ouathApp?.getOauthId()}" ouathApp.updateLocationSubscriptionSettings(false) } ouathApp.setSmartDeviceSubscriptions() } } /************************************** CHILD METHODS STOP ********************************************************/ Map getSmartLocations() { return g_mSmartLocationListCache[app.getId()] ?: (g_mSmartLocationListCache[app.getId()]=getSmartLocationList()?.data) ?: [:] //blocking http } Map getSmartRooms(String locationId) { if(!g_mSmartRoomListCache[app.getId()]) g_mSmartRoomListCache[app.getId()] = [:] return g_mSmartRoomListCache[app.getId()]?."$locationId" ?: (g_mSmartRoomListCache[app.getId()]."$locationId"=getSmartRoomList(locationId)?.data) ?: [:] //blocking http } Map getSmartScenes(String locationId) { if(!g_mSmartSceneListCache[app.getId()]) g_mSmartSceneListCache[app.getId()] = [:] return g_mSmartSceneListCache[app.getId()]?."$locationId" ?: (g_mSmartSceneListCache[app.getId()]."$locationId"=getSmartSceneList(locationId)?.data) ?: [:] //blocking http } Map getVirtualDevices() { return g_mVirtualDeviceListCache[app.getId()] ?: (g_mVirtualDeviceListCache[app.getId()]=getVirtualDeviceList()?.data) ?: [:] //blocking http } String getSmartLocationName(String locationId) { getSmartLocations()?.items?.find{ it.locationId==locationId }?.name } Map getSmartDevicesMap() { Long appId = app.getId() if(g_mSmartDeviceListCache[appId]==null) { g_mSmartDeviceListCache[appId]=[:] allSmartDeviceRefresh() Integer count=0 while(count<40 && (g_mSmartDeviceListCache[appId])?.items==null ) { pauseExecution(250); count++ } // wait a max of 10 seconds } return g_mSmartDeviceListCache[appId]?:[:] } void setSmartDevicesMap(Map deviceList) { g_mSmartDeviceListCache[app.getId()] = deviceList clearReplicaDataCache() // lets clear the cache of any stale devices logInfo "${app.getLabel()} caching SmartThings device list" } def pageMain(){ if(!state?.isInstalled) { return dynamicPage(name: "pageMain", install: true, refreshInterval: 0) { displayHeader() section(menuHeader("Complete Install $sHubitatIconStatic $sSamsungIconStatic")) { paragraph("Please complete the install (click done) and then return to $sHubitatIcon SmartApp to continue configuration") input(name: "pageMainPageAppLabel", type: "text", title: getFormat("text","$sHubitatIcon Change ${app.getLabel()} SmartApp Name:"), width: 6, defaultValue:app.getLabel(), submitOnChange: true, newLineAfter:true) } } } Map smartDevices = getSmartDevicesMap()?.clone() Integer deviceAuthCount = getAuthorizedDevices()?.size() ?: 0 return dynamicPage(name: "pageMain", install: true, refreshInterval:iPageMainRefreshInterval) { displayHeader() state.pageMainLastRefresh = now() section(menuHeader("Replica Configuration $sHubitatIconStatic $sSamsungIconStatic")) { input(name: "pageMainShowConfig", type: "bool", title: "$sHubitatIcon Show Configuration", defaultValue: true, submitOnChange: true) paragraph( getFormat("line") ) if(pageMainShowConfig) { String comments = "This application utilizes the SmartThings Cloud API to create, delete and query devices. You must supply a SmartThings Personal Access Token (PAT) with all Authorized Scopes permissions to enable functionality. " comments+= "A PAT is valid for 50 years from creation date. Click the ${sSamsungIcon} SmartThings Personal Access Token link to be directed to the SmartThings website." paragraphComment(comments) input(name: "userSmartThingsPAT", type: "password", title: getFormat("hyperlink"," $sSamsungIcon SmartThings Personal Access Token:","https://account.smartthings.com/tokens"), description: "SmartThings UUID Token", width: 6, submitOnChange: true, newLineAfter:true) paragraph("") if(userSmartThingsPAT) { comments = "HubiThings OAuth Applications are required to enable SmartThings devices for replication. Each OAuth Application can subscribe up to 30 devices and is hub and location independent. " comments+= "HubiThings Replica allows for multiple OAuth Applications to be created for solution requirements beyond 30 devices. Click the '${sSamsungIcon} Authorize SmartThings Devices : Create OAuth Applications' link to create one or more OAuth Applications." paragraphComment(comments) app(name: "oauthChildApps", appName: "HubiThings OAuth", namespace: "replica", title: "${getFormat("text","$sSamsungIcon Authorize SmartThings Devices")} : Create OAuth Applications", multiple: true) paragraph( getFormat("line") ) input(name: "pageMainShowAdvanceConfiguration", type: "bool", title: "$sHubitatIcon Advanced Configuration", defaultValue: false, submitOnChange: true) if(pageMainShowAdvanceConfiguration) { pageAuthDeviceUserInputCtl() paragraph("") input(name: "pageMainPageAppLabel", type: "text", title: " $sHubitatIcon Hubitat SmartApp Name:", width: 6, submitOnChange: true, newLineAfter:true) input(name: "dynamic::pageMainChangeAppNameButton", type: "button", title: "Change Name", width: 3, style:"width:50%;", newLineAfter:true) } } } } if(pageMainShowConfig && pageMainShowAdvanceConfiguration) { section(menuHeader("Replica Application Logging")) { input(name: "appLogEventEnable", type: "bool", title: "Enable Event and Status Info logging", required: false, defaultValue: false, submitOnChange: true) if(appLogEventEnable) { List smartDevicesSelect = [] smartDevices?.items?.sort{ it.label }?.each { def device = [ "${it.deviceId}" : "${it.label}   (deviceId: ${it.deviceId})" ] smartDevicesSelect.add(device) } input(name: "appLogEventEnableDevice", type: "enum", title: getFormat("text"," $sSamsungIcon Selective SmartThings Info Logging:"), description: "Choose a SmartThings device", options: smartDevicesSelect, required: false, submitOnChange:true, width: 6) paragraph( getFormat("line") ) } else { app.removeSetting("appLogEventEnableDevice") } input(name: "appInfoDisable", type: "bool", title: "Disable info logging", required: false, defaultValue: false, submitOnChange: true) input(name: "appDebugEnable", type: "bool", title: "Enable debug logging", required: false, defaultValue: false, submitOnChange: true) input(name: "appTraceEnable", type: "bool", title: "Enable trace logging", required: false, defaultValue: false, submitOnChange: true) } } if(state?.user=="bloodtick") { section(menuHeader("Replica Application Development [${state.user}]")) { input(name: "dynamic::pageMainTestButton", type: "button", width: 2, title: "$sHubitatIcon Test", style:"width:75%;") input(name: "dynamic::allSmartDeviceStatus", type: "button", width: 2, title: "$sSamsungIcon Status", style:"width:75%;") input(name: "dynamic::allSmartDeviceDescription", type: "button", width: 2, title: "$sSamsungIcon Description", style:"width:75%;") input(name: "dynamic::allSmartDeviceHealth", type: "button", width: 2, title: "$sSamsungIcon Health", style:"width:75%;") } } if(userSmartThingsPAT&&getChildApps()?.size()) { section(menuHeader("HubiThings Device List")){ if (smartDevices) { String devicesTable = "" devicesTable += "" devicesTable += "" List deviceIds = getAllReplicaDeviceIds() try { smartDevices?.items?.sort{ it?.label }?.each { smartDevice -> deviceIds.remove(smartDevice?.deviceId) List hubitatDevices = getReplicaDevices(smartDevice?.deviceId) for (Integer i = 0; i ==0 || i < hubitatDevices.size(); i++) { def replicaDevice = hubitatDevices[i]?:null String deviceUrl = "http://${location.hub.getDataValue("localIP")}/device/edit/${replicaDevice?.getId()}" String oauthUrl = "http://${location.hub.getDataValue("localIP")}/installedapp/configure/${smartDevice?.appId}" devicesTable += "" devicesTable += smartDevice?.label ? "" : "" devicesTable += replicaDevice ? "" : "" devicesTable += smartDevice?.oauthId ? "" : "" devicesTable += replicaDevice ? "" : "" devicesTable += "" } } } catch(e) { } //noop. have a concurrency problem once and a while. logDebug "${app.getLabel()} deviceIds not found $deviceIds" deviceIds?.each { deviceId -> List hubitatDevices = getReplicaDevices(deviceId) for (Integer i = 0; i ==0 || i < hubitatDevices.size(); i++) { def replicaDevice = hubitatDevices[i]?:null String deviceUrl = "http://${location.hub.getDataValue("localIP")}/device/edit/${replicaDevice?.getId()}" devicesTable += "" devicesTable += "" devicesTable += "" devicesTable += "" devicesTable += "" devicesTable += "" } } devicesTable +="
$sSamsungIcon Device$sHubitatIcon Device$sHubitatIcon OAuth$sSamsungIcon Events
${smartDevice?.label}--${replicaDevice?.getDisplayName()}--${smartDevice?.locationName?:""} : ${smartDevice?.oauthId?:""}--${updateSmartDeviceEventsStatus(replicaDevice)}0
--${replicaDevice?.getDisplayName()}--$sWarningsIcon $sNotAuthorized
" devicesTable += """""" // Hubitat includes jquery DataTables in web code. https://datatables.net if(smartDevices?.items?.size()+deviceIds?.size() > iUseJqueryDataTables) { devicesTable += """""" //devicesTable += """""" devicesTable += """""" } else { devicesTable += """""" } paragraph( devicesTable ) String socketstatus = """""" socketstatus += """""" if(smartDevices?.items?.size()+deviceIds?.size() > iUseJqueryDataTables) socketstatus += """""" else socketstatus += """""" socketstatus += """""" paragraph( rawHtml: true, socketstatus ) } input(name: "dynamic::allSmartDeviceRefresh", type: "button", width: 2, title: "$sSamsungIcon Refresh", style:"width:75%;") } section(menuHeader("Replica Device Creation and Control")){ href "pageHubiThingDevice", title: "$sImgDevh Configure HubiThings Devices ", description: "Click to show" href "pageVirtualDevice", title: "$sImgDevv Configure Virtual Devices, Modes and Scenes", description: "Click to show" href "pageConfigureDevice", title: "$sImgRule Configure HubiThings Rules", description: "Click to show" if(deviceAuthCount>0) href "pageMirrorDevice", title: "$sImgMirr Mirror Hubitat Devices (Advanced)", description: "Click to show" href "pageSupportNewDevice", title: "$sImgGitH Support for New Device", description: "Click to show" } if(pageMainShowConfig || appDebugEnable || appTraceEnable) { runIn(1800, updatePageMain) } else { unschedule('updatePageMain') } } //if(userSmartThingsPAT&&getChildApps()?.size()) displayFooter() } } void updatePageMain() { logInfo "${app.getLabel()} disabling debug and trace logs" app.updateSetting("pageMainShowConfig", false) app.updateSetting("pageMainShowAdvanceConfiguration", false) app.updateSetting("appDebugEnable", false) app.updateSetting("appTraceEnable", false) } def pageAuthDevice() { // Integer deviceAuthCount = getAuthorizedDevices()?.size() ?: 0 return dynamicPage(name: "pageAuthDevice", uninstall: false) { // displayHeader() section(menuHeader("Authorize Hubitat Devices to Mirror $sHubitatIconStatic $sSamsungIconStatic")) { paragraphComment("Hubitat Security requires each local device to be authorized with internal controls before HubiThings Replica can access. Please select Hubitat devices below before attempting mirror functions.") input(name: "userAuthorizedDevices", type: "capability.*", title: "Hubitat Devices:", description: "Choose a Hubitat devices", multiple: true, offerAll:true, submitOnChange: true, newLineAfter:true) paragraph("""


""") } } } def pageAuthDeviceUserInputCtl() { //http://192.168.1.33/installedapp/configure/314/pageMain/pageAuthDevice String authorizeUrl = "http://${location.hub.getDataValue("localIP")}/installedapp/configure/${app.getId()}/pageAuthDevice" //String authorizeTitle = " $sHubitatIcon Select Hubitat Device:" //authorizeTitle = """$authorizeTitle""" Integer deviceAuthCount = getAuthorizedDevices()?.size() ?: 0 String deviceText = (deviceAuthCount<1 ? ": (Select to Authorize Devices to Mirror)" : (deviceAuthCount==1 ? ": ($deviceAuthCount Device Authorized to Mirror)" : ": ($deviceAuthCount Devices Authorized to Mirror)")) // this is a workaround for the form data submission on 'external' modal boxes. not sure why hubitat is failing. paragraph (rawHtml: true, """""") href url: authorizeUrl, style: "external", title: "$sHubitatIcon Authorize Hubitat Devices $deviceText", description: "Click to show" //href "pageAuthDevice", title: "$sHubitatIcon Authorize Hubitat Devices $deviceText", description: "Click to show" } void pageMainChangeAppNameButton() { logDebug "${app.getLabel()} executing 'pageMainChangeAppNameButton()' $pageMainPageAppLabel" if(pageMainPageAppLabel && pageMainPageAppLabel!=app.getLabel()) { logInfo "${app.getLabel()} changing Hubitat SmartApp from ${app.getLabel()} to $pageMainPageAppLabel" app.updateLabel( pageMainPageAppLabel ) } } Map getDeviceHandlers() { Map handlers = [ items: [ [id:"1", name:"Virtual Switch", namespace:"hubitat" ], [id:"2", name:"Virtual Contact Sensor", namespace:"hubitat" ], [id:"3", name:"Virtual Motion Sensor", namespace:"hubitat" ], [id:"4", name:"Virtual Temperature Sensor", namespace:"hubitat" ], [id:"5", name:"Virtual Humidity Sensor", namespace:"hubitat" ], [id:"6", name:"Virtual Presence", namespace:"hubitat" ], [id:"7", name:"Virtual Shade", namespace:"hubitat" ], [id:"8", name:"Virtual Thermostat", namespace:"hubitat" ] ]] getDriverList()?.items?.each{ if(it?.namespace=='replica') handlers.items.add(it) } return handlers } def pageSupportNewDevice() { List replicaDevicesSelect = [] getAllReplicaDevices()?.sort{ a,b -> naturalSort(a.getDisplayName(),b.getDisplayName()) }.each { Map device = [ "${it.deviceNetworkId}" : "${it.getDisplayName()}   (deviceNetworkId: ${it.deviceNetworkId})" ] replicaDevicesSelect.add(device) } return dynamicPage(name: "pageSupportNewDevice", uninstall: false) { displayHeader() section(menuHeader("Support for New Device $sHubitatIconStatic $sSamsungIconStatic")) { def replicaDevice = getDevice(pageSupportNewDeviceReplicaDevice) String deviceTitle = " $sSamsungIcon Select HubiThings Device:" if(replicaDevice) { String deviceUrl = "http://${location.hub.getDataValue("localIP")}/device/edit/${replicaDevice.getId()}" deviceTitle = "${deviceTitle}" } input(name: "pageSupportNewDeviceReplicaDevice", type: "enum", title: deviceTitle, description: "Choose a HubiThings device", options: replicaDevicesSelect, multiple: false, submitOnChange: true, width: 8, newLineAfter:true) paragraphComment("To request a new device or a modification of a Replica Driver, the device needs to be authorized in HubiThings OAuth. Then build a temporary device using any Replica Driver, such as 'Replica Switch' with the 'Configure HubiThings Device' page. This will populate the device's description, capabilities, and status data fields. Copy this metadata and open new request for support.") paragraph( getFormat("line") ) if(replicaDevice) { String title = "[NEW DEVICE SUPPORT] $replicaDevice" String body = "Requesting New Device support.\nComments: {{Add your Comments Here}}\n\n***COPY REPLICA METADATA BELOW THIS LINE***\n" // the GitHub url is limited in size to around 2048 char. We are beyond that so the user needs to cut & paste the data into the form. :( paragraphComment("
↓↓↓ INCLUDE METADATA BELOW ↓↓↓") // center doesn't work with HTML5. Don't care that much. paragraphComment( "[{ \"DESCRIPTION\":
" + getReplicaDeviceSafeDataString(replicaDevice, "description") + "
},") paragraphComment( "{ \"CAPABILITIES\":
" + getReplicaDeviceSafeDataString(replicaDevice, "capabilities") + "
}," ) paragraphComment( "{ \"STATUS\":
" + getReplicaDeviceSafeDataString(replicaDevice, "status") + "
}," ) paragraphComment( "{ \"RULES\":
" + getReplicaDeviceSafeDataString(replicaDevice, "rules") + "
}]" ) paragraphComment("
↑↑↑ INCLUDE METADATA ABOVE ↑↑↑") Map params = [ assignees:"bloodtick", labels:"new_device_support", title:title, body:body ] String featUrl = "https://github.com/bloodtick/Hubitat/issues/new?${urlParamBuilder(params)}" href url: featUrl, style: "external", title: "$sImgGitH Open Request for New Device Support", description: "Tap to open browser (Requires GitHub Account)" // this is a workaround for the form data submission on 'external' modal boxes. not sure why hubitat is failing. paragraph (rawHtml: true, """""") paragraph (rawHtml: true, """""") } } if(checkFirmwareVersion("2.3.4.132") && state?.user=="bloodtick") { section(menuHeader("Replica Handler Development [${state.user}]")) { input(name: "pageSupportNewDeviceCapabilityFileName", type: "text", title: "Replica Capabilities Filename:", description: "Capability JSON Local Filename", width: 4, submitOnChange: true, newLineAfter:true) input(name: "dynamic::pageSupportNewDeviceFetchCapabilityButton", type: "button", title: "Fetch", width: 2, style:"width:75%;") input(name: "dynamic::pageSupportNewDeviceStoreCapabilityButton", type: "button", title: "Store", width: 2, style:"width:75%;") } } } } String urlParamBuilder(Map items) { return items.collect { String k,v -> "${k}=${URLEncoder.encode(v.toString())}" }?.join("&")?.toString() } String getReplicaDeviceSafeDataString(def replicaDevice, String name) { Map data = getReplicaDataJsonValue(replicaDevice, name) changeKeyValue("deviceId", "hidden", data) changeKeyValue("locationId", "hidden", data) changeKeyValue("hubId", "hidden", data) changeKeyValue("parentDeviceId", "hidden", data) changeKeyValue("roomId", "hidden", data) changeKeyValue("pattern", "removed", data) String strData = data&&!data.isEmpty() ? JsonOutput.toJson(data) : """{"$name":"empty"}""" strData = strData.replace('$', '\\\\$') return strData } def changeKeyValue(String key, String value, def obj) { obj?.each { k, v -> if (v instanceof Map) { changeKeyValue(key, value, v) // recurse into maps } else if (v instanceof List) { v?.each { // recurse into lists if (it instanceof Map) changeKeyValue(key, value, it) } } if (k == key) { obj[k] = value } } } void pageSupportNewDeviceFetchCapabilityButton() { logDebug "${app.getLabel()} executing 'pageSupportNewDeviceFetchCapabilityButton()' $pageSupportNewDeviceCapabilityFileName" byte[] filebytes = downloadHubFile(pageSupportNewDeviceCapabilityFileName) def replicaDevice = getDevice(pageSupportNewDeviceReplicaDevice) if(filebytes && replicaDevice) { String strFile = (new String(filebytes))?.replaceAll('“','"')?.replaceAll('”','"') Map capabilities = strFile ? new JsonSlurper().parseText(strFile) : [components:[]] logInfo capabilities setReplicaDataJsonValue(replicaDevice, "capabilities", capabilities) } } void pageSupportNewDeviceStoreCapabilityButton() { logDebug "${app.getLabel()} executing 'pageSupportNewDeviceStoreCapabilityButton()' $pageSupportNewDeviceCapabilityFileName" def replicaDevice = getDevice(pageSupportNewDeviceReplicaDevice) Map capabilities = getReplicaDataJsonValue(replicaDevice, "capabilities") if(pageSupportNewDeviceCapabilityFileName && capabilities) { //logInfo capabilities byte[] filebytes =((String)JsonOutput.toJson(capabilities))?.getBytes() uploadHubFile(pageSupportNewDeviceCapabilityFileName, filebytes) } } def pageHubiThingDevice(){ Map smartDevices = getSmartDevicesMap() List smartDevicesSelect = [] smartDevices?.items?.sort{ a,b -> naturalSort(a.label,b.label) }?.each { smartDevice -> smartDevice?.components?.each { component -> if( !smartDevicesSelect?.find{ smartDevice.deviceId==it.keySet().getAt(0) } && (pageHubiThingDeviceShowAllDevices || !getReplicaDevices(smartDevice.deviceId, component?.id)) ) smartDevicesSelect.add([ "$smartDevice.deviceId" : "$smartDevice.label   (deviceId: $smartDevice.deviceId)" ]) } } List smartComponentsSelect = [] smartDevices?.items?.find{it.deviceId == pageHubiThingDeviceSmartDevice}?.components.each { component -> if(pageHubiThingDeviceShowAllDevices || !getReplicaDevices(pageHubiThingDeviceSmartDevice, component?.id)) smartComponentsSelect.add(component.id) } List hubitatDeviceTypes = [] getDeviceHandlers()?.items?.sort{ a,b -> b?.namespace <=> a?.namespace ?: a?.name <=> b?.name }?.each { hubitatDeviceTypes.add([ "$it.id" : "$it.name   (namespace: $it.namespace)" ]) } if(smartComponentsSelect?.size()==1) { app.updateSetting( "pageHubiThingDeviceSmartDeviceComponent", [type:"enum", value: smartComponentsSelect?.get(0)] ) } String smartStats = getSmartDeviceStats(pageHubiThingDeviceSmartDevice, pageHubiThingDeviceSmartDeviceComponent) if(pageHubiThingDeviceSmartDevice!=g_mAppDeviceSettings?.pageHubiThingDeviceSmartDevice || pageHubiThingDeviceSmartDeviceComponent!=g_mAppDeviceSettings?.pageHubiThingDeviceSmartDeviceComponent) { g_mAppDeviceSettings['pageHubiThingDeviceSmartDevice'] = pageHubiThingDeviceSmartDevice g_mAppDeviceSettings['pageHubiThingDeviceSmartDeviceComponent'] = pageHubiThingDeviceSmartDeviceComponent String deviceLabel = smartDevices?.items?.find{it.deviceId == pageHubiThingDeviceSmartDevice}?.label app.updateSetting( "pageHubiThingDeviceLabel", deviceLabel ? "$deviceLabel${smartComponentsSelect?.size()>1 ? " - $pageHubiThingDeviceSmartDeviceComponent" : ""}" : "" ) } Integer refreshInterval = (g_mAppDeviceSettings?.pageHubiThingDeviceCreateButton || g_mAppDeviceSettings?.pageHubiThingDeviceModifyButton) ? 15 : 0 return dynamicPage(name: "pageHubiThingDevice", uninstall: false, refreshInterval:refreshInterval) { displayHeader() section(menuHeader("Create HubiThings Device $sHubitatIconStatic $sSamsungIconStatic")) { input(name: "pageHubiThingDeviceSmartDevice", type: "enum", title: " $sSamsungIcon Select SmartThings Device:", description: "Choose a SmartThings device", options: smartDevicesSelect, required: false, submitOnChange:true, width: 6) if(pageHubiThingDeviceSmartDevice && smartComponentsSelect?.size()>1) input(name: "pageHubiThingDeviceSmartDeviceComponent", type: "enum", title: "$sSamsungIcon Select SmartThings Device Component:", description: "Choose a Device Component", options: smartComponentsSelect, required: false, submitOnChange:true, width: 6, newLineAfter:true) paragraph( smartStats ) input(name: "pageHubiThingDeviceShowAllDevices", type: "bool", title: "Show All Authorized SmartThings Devices", defaultValue: false, submitOnChange: true, newLineAfter:true) paragraph( getFormat("line") ) input(name: "pageHubiThingDeviceType", type: "enum", title: " $sHubitatIcon Select Hubitat Device Type:", description: "Choose a Hubitat device type", options: hubitatDeviceTypes, required: false, submitOnChange:true, width: 6, newLineAfter:true) input(name: "pageHubiThingDeviceLabel", type: "text", title: "$sHubitatIcon Set Hubitat Device Label:", submitOnChange: true, width: 6, newLineAfter:true) input(name: "dynamic::pageHubiThingDeviceCreateButton", type: "button", width: 2, title: "$sHubitatIcon Create", style:"width:75%;") if( g_mAppDeviceSettings?.pageHubiThingDeviceCreateButton) { paragraph( getFormat("line") ) paragraph( g_mAppDeviceSettings.pageHubiThingDeviceCreateButton ) g_mAppDeviceSettings.pageHubiThingDeviceCreateButton = null href "pageConfigureDevice", title: "$sImgRule Configure HubiThings Rules", description: "Click to show" } } commonReplicaDevicesSection("pageHubiThingDevice") } } def commonReplicaDevicesSection(String dynamicPageName) { List hubitatDevicesSelect = [] getAllReplicaDevices()?.sort{ a,b -> naturalSort(a.getDisplayName(),b.getDisplayName()) }?.findAll{ (dynamicPageName=="pageHubiThingDevice" && getChildDevice( it?.deviceNetworkId )) || (dynamicPageName!="pageHubiThingDevice" && !getChildDevice( it?.deviceNetworkId )) }?.each{ Map device = [ "${it.deviceNetworkId}" : "${it.getDisplayName()}   (deviceNetworkId: ${it.deviceNetworkId})" ] hubitatDevicesSelect.add(device) } section(menuHeader("Modify HubiThings Device")) { paragraphComment("${(dynamicPageName=="pageHubiThingDevice")?"Replace updates the 'Select SmartThings Device' to replica the 'Select Hubitat Device'. ":""}Remove deletes the 'Select Hubitat Device' child device from Hubitat, but will only decouple a mirror device and will not delete.") input(name: "pageHubiThingDeviceModify", type: "enum", title: " $sHubitatIcon Select HubiThings Device:", description: "Choose a HubiThings device", multiple: false, options: hubitatDevicesSelect, submitOnChange: true, width: 6, newLineAfter:true) if(dynamicPageName=="pageHubiThingDevice") input(name: "dynamic::pageHubiThingDeviceReplaceButton", type: "button", width: 2, title: "$sHubitatIcon Replace", style:"width:75%;") input(name: "dynamic::pageHubiThingDeviceRemoveButton", type: "button", width: 2, title: "$sHubitatIcon Remove", style:"width:75%;", newLineAfter:true) if(g_mAppDeviceSettings?.pageHubiThingDeviceModifyButton) { paragraph( g_mAppDeviceSettings?.pageHubiThingDeviceModifyButton ) g_mAppDeviceSettings.pageHubiThingDeviceModifyButton = null } } Map smartDevices = getSmartDevicesMap() String devicesTable = "" devicesTable += "" List devices = getAllReplicaDevices()?.sort{ it.getDisplayName() }?.findAll{ (dynamicPageName=="pageHubiThingDevice" && getChildDevice( it?.deviceNetworkId )) || (dynamicPageName!="pageHubiThingDevice" && !getChildDevice( it?.deviceNetworkId )) }?.each{ replicaDevice -> Boolean isChildDevice = (getChildDevice( replicaDevice?.deviceNetworkId ) != null) String deviceId = getReplicaDeviceId(replicaDevice) Map smartDevice = smartDevices?.items?.find{ it?.deviceId == deviceId } String deviceUrl = "http://${location.hub.getDataValue("localIP")}/device/edit/${replicaDevice.getId()}" String oauthUrl = "http://${location.hub.getDataValue("localIP")}/installedapp/configure/${smartDevice?.appId}" devicesTable += "" devicesTable += "" devicesTable += smartDevice?.oauthId ? "" : "" devicesTable += "" } devicesTable +="
$sHubitatIcon Device$sHubitatIcon Type$sHubitatIcon OAuth$sHubitatIcon Class
${replicaDevice.getDisplayName()}${replicaDevice.typeName}${smartDevice?.locationName?:""} : ${smartDevice?.oauthId?:""}$sWarningsIcon $sNotAuthorized${isChildDevice?'Child':'Mirror'}
" devicesTable += """""" if(devices?.size() > iUseJqueryDataTables) { devicesTable += """""" //devicesTable += """""" devicesTable += """""" } else { devicesTable += """""" } section(menuHeader("HubiThings Device List")) { if (devices?.size()) { paragraph( devicesTable ) } } } void pageHubiThingDeviceCreateButton() { logDebug "${app.getLabel()} executing 'pageHubiThingDeviceCreateButton()' $pageHubiThingDeviceSmartDevice $pageHubiThingDeviceSmartDeviceComponent $pageHubiThingDeviceType $pageHubiThingDeviceLabel" if(!pageHubiThingDeviceSmartDevice) g_mAppDeviceSettings['pageHubiThingDeviceCreateButton'] = errorMsg("Error: SmartThings Device selection is invalid") else if(!pageHubiThingDeviceSmartDeviceComponent) g_mAppDeviceSettings['pageHubiThingDeviceCreateButton'] = errorMsg("Error: SmartThings Device Component selection is invalid") else if(!pageHubiThingDeviceType) g_mAppDeviceSettings['pageHubiThingDeviceCreateButton'] = errorMsg("Error: Hubitat Device Type is invalid") else if(!pageHubiThingDeviceLabel) g_mAppDeviceSettings['pageHubiThingDeviceCreateButton'] = errorMsg("Error: Hubitat Device Label is invalid") else { Map deviceType = getDeviceHandlers()?.items?.find{ it?.id==pageHubiThingDeviceType } String name = (getSmartDevicesMap()?.items?.find{it?.deviceId == pageHubiThingDeviceSmartDevice}?.name) String label = pageHubiThingDeviceLabel String deviceId = pageHubiThingDeviceSmartDevice String componentId = pageHubiThingDeviceSmartDeviceComponent g_mAppDeviceSettings['pageHubiThingDeviceCreateButton'] = createChildDevice(deviceType, name, label, deviceId, componentId) } } String createChildDevice(Map deviceType, String name, String label, String deviceId, String componentId) { logDebug "${app.getLabel()} executing 'createChildDevice()' $deviceType $name $label $deviceId $componentId" String response = errorMsg("Error: '$label' was not created") String deviceNetworkId = "${UUID.randomUUID().toString()}" try { def replicaDevice = addChildDevice(deviceType.namespace, deviceType.name, deviceNetworkId, null, [name: name, label: label, completedSetup: true]) // the deviceId makes this a hubiThing // Needed for mirror function to prevent two SmartApps talking to same device. Map replica = [ deviceId:deviceId, componentId:componentId, replicaId:(app.getId()), type:'child'] setReplicaDataJsonValue(replicaDevice, "replica", replica) if(replicaDevice?.hasCommand('configure')) replicaDevice.configure() replicaDeviceRefresh(replicaDevice) logInfo "${app.getLabel()} created device '${replicaDevice.getDisplayName()}' with deviceId: $deviceId" app.updateSetting( "pageSupportNewDeviceReplicaDevice", [type:"enum", value: replicaDevice.deviceNetworkId] ) app.updateSetting( "pageConfigureDeviceReplicaDevice", [type:"enum", value: replicaDevice.deviceNetworkId] ) app.updateSetting( "pageHubiThingDeviceModify", [type:"enum", value: replicaDevice.deviceNetworkId] ) response = statusMsg("'$label' was created with deviceId: $deviceId and deviceNetworkId: $deviceNetworkId") } catch (e) { logWarn "${app.getLabel()} error creating $label: ${e}" logInfo pageHubiThingDeviceType logInfo getDeviceHandlers() } return response } void pageHubiThingDeviceRemoveButton() { logDebug "${app.getLabel()} executing 'pageHubiThingDeviceRemoveButton()' $pageHubiThingDeviceModify" if(pageHubiThingDeviceModify) { g_mAppDeviceSettings['pageHubiThingDeviceModifyButton'] = deleteChildDevice(pageHubiThingDeviceModify) } } String deleteChildDevice(String deviceNetworkId) { String response = errorMsg("Error: Could not find device to delete") def replicaDevice = getDevice( deviceNetworkId ) if(replicaDevice) { String label = replicaDevice?.getDisplayName() try { Boolean isChild = getChildDevice( deviceNetworkId ) app.removeSetting("pageHubiThingDeviceModify") if(isChild) { unsubscribe(replicaDevice) app.deleteChildDevice(replicaDevice?.deviceNetworkId) clearReplicaDataCache(replicaDevice) logInfo "${app.getLabel()} deleted '$label' with deviceNetworkId: ${replicaDevice?.deviceNetworkId}" response = statusMsg("'$label' was deleted with deviceNetworkId: ${replicaDevice?.deviceNetworkId}") } else { unsubscribe(replicaDevice) clearReplicaDataCache(replicaDevice, "capabilities", true) clearReplicaDataCache(replicaDevice, "description", true) clearReplicaDataCache(replicaDevice, "health", true) clearReplicaDataCache(replicaDevice, "replica", true) clearReplicaDataCache(replicaDevice, "rules", true) clearReplicaDataCache(replicaDevice, "status", true) logInfo "${app.getLabel()} detached '$label' with deviceNetworkId: ${replicaDevice?.deviceNetworkId}" response = statusMsg("'$label' was detached with deviceNetworkId: ${replicaDevice?.deviceNetworkId}") } } catch (e) { logWarn "${app.getLabel()} error deleting $label: ${e}" response = errorMsg("Error: '$label' was not detached or deleted") } } return response } void pageHubiThingDeviceReplaceButton() { logDebug "${app.getLabel()} executing 'pageHubiThingDeviceReplaceButton()' $pageHubiThingDeviceModify $pageHubiThingDeviceSmartDevice $pageHubiThingDeviceSmartDeviceComponent" if(!pageHubiThingDeviceSmartDevice) g_mAppDeviceSettings['pageHubiThingDeviceModifyButton'] = errorMsg("Error: SmartThings Device selection is invalid") else if(!pageHubiThingDeviceSmartDeviceComponent) g_mAppDeviceSettings['pageHubiThingDeviceModifyButton'] = errorMsg("Error: SmartThings Device Component selection is invalid") else if(!pageHubiThingDeviceModify) g_mAppDeviceSettings['pageHubiThingDeviceModifyButton'] = errorMsg("Error: Hubitat Device selection is invalid") else g_mAppDeviceSettings['pageHubiThingDeviceModifyButton'] = replaceChildDevice(pageHubiThingDeviceModify, pageHubiThingDeviceSmartDevice, pageHubiThingDeviceSmartDeviceComponent) } String replaceChildDevice(String deviceNetworkId, String deviceId, String componentId) { String response = errorMsg("Error: Could not find device to replace") def replicaDevice = getDevice(deviceNetworkId) if(replicaDevice) { String label = replicaDevice?.getDisplayName() try { Map replica = [ deviceId:deviceId, componentId:componentId, replicaId:(app.getId()), type:( getChildDevice( replicaDevice?.deviceNetworkId )!=null ? 'child' : 'mirror')] setReplicaDataJsonValue(replicaDevice, "replica", replica) clearReplicaDataCache(replicaDevice, "capabilities", true) clearReplicaDataCache(replicaDevice, "description", true) clearReplicaDataCache(replicaDevice, "health", true) clearReplicaDataCache(replicaDevice, "status", true) replicaDeviceRefresh(replicaDevice) logInfo "${app.getLabel()} replaced device'${replicaDevice.getDisplayName()}' with deviceId: $deviceId and deviceNetworkId: $replicaDevice.deviceNetworkId" app.updateSetting( "pageSupportNewDeviceReplicaDevice", [type:"enum", value: replicaDevice.deviceNetworkId] ) app.updateSetting( "pageConfigureDeviceReplicaDevice", [type:"enum", value: replicaDevice.deviceNetworkId] ) app.updateSetting( "pageHubiThingDeviceModify", [type:"enum", value: replicaDevice.deviceNetworkId] ) response = statusMsg("'$label' was replaced with deviceId: $deviceId and deviceNetworkId: $replicaDevice.deviceNetworkId") } catch (e) { logWarn "${app.getLabel()} error reassigning $label: ${e}" response = errorMsg("Error: '$label' was not replaced") } } return response } String getHubitatDeviceStats(hubitatDevice) { String hubitatStats = "" if(hubitatDevice) { hubitatStats += "Device Type: ${hubitatDevice?.getTypeName()}\n" hubitatStats += "Capabilities: ${hubitatDevice?.getCapabilities()?.collect{it?.toString()?.uncapitalize()}?.unique().sort()?.join(', ')}\n" hubitatStats += "Commands: ${hubitatDevice?.getSupportedCommands()?.sort{it.toString()}?.unique()?.join(', ')}\n" hubitatStats += "Attributes: ${hubitatDevice?.getSupportedAttributes()?.sort{it.toString()}?.unique()?.join(', ')}" } return hubitatStats } String getSmartDeviceStats(smartDeviceId, smartDeviceComponentId) { String smartStats = "" if(smartDeviceId) { Map smartDevices = getSmartDevicesMap() List smartCapabilities = [] smartDevices?.items?.find{it.deviceId == smartDeviceId}?.components?.each{ component -> if(component.id == smartDeviceComponentId) { component?.capabilities?.each { capability -> smartCapabilities.add("$capability.id") } } } smartStats += "Device Type: ${smartDevices?.items?.find{it.deviceId == smartDeviceId}?.deviceTypeName ?: (smartDevices?.items?.find{it.deviceId == smartDeviceId}?.name ?: "UNKNOWN")}\n" smartStats += "Component: ${smartDeviceComponentId}\n" smartStats += "Capabilities: ${smartCapabilities?.sort()?.join(', ')}" } return smartStats } void replicaDeviceRefresh(replicaDevice, delay=1) { logInfo "${app.getLabel()} refreshing '$replicaDevice' device" runIn(delay<1?:1, replicaDeviceRefreshHelper, [data: [deviceNetworkId:(replicaDevice.deviceNetworkId)]]) } void replicaDeviceRefreshHelper(data) { def replicaDevice = getDevice(data?.deviceNetworkId) getReplicaDeviceRefresh(replicaDevice) } void getReplicaDeviceRefresh(replicaDevice) { logDebug "${app.getLabel()} executing 'getReplicaDeviceRefresh($replicaDevice)'" String deviceId = getReplicaDeviceId(replicaDevice) if(deviceId) { replicaDeviceSubscribe(replicaDevice) getSmartDeviceStatus(deviceId) pauseExecution(200) // no need to hammer ST getSmartDeviceHealth(deviceId) pauseExecution(200) // no need to hammer ST getSmartDeviceDescription(deviceId) } else if(replicaDevice) { unsubscribe(replicaDevice) } } void replicaDeviceSubscribe(replicaDevice) { if(replicaDevice) { Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") List ruleAttributes = replicaDeviceRules?.components?.findAll{ it.type == "hubitatTrigger" && it?.trigger?.type == "attribute" }?.collect{ rule -> rule?.trigger?.name }?.unique() List appSubscriptions = app.getSubscriptions()?.findAll{ it?.deviceId?.toInteger() == replicaDevice?.id?.toInteger() }?.collect{ it?.data }?.unique() if(ruleAttributes) { appSubscriptions?.intersect(ruleAttributes)?.each{ appSubscriptions?.remove(it); ruleAttributes?.remove(it) } } appSubscriptions?.each{ attribute -> logInfo "${app.getLabel()} '$replicaDevice' unsubscribed to $attribute" unsubscribe(replicaDevice, attribute) } ruleAttributes?.each{ attribute -> logInfo "${app.getLabel()} '$replicaDevice' subscribed to $attribute" subscribe(replicaDevice, attribute, deviceTriggerHandler) } } } def pageMirrorDevice(){ Map smartDevices = getSmartDevicesMap() List smartDevicesSelect = [] smartDevices?.items?.sort{ a,b -> naturalSort(a.label,b.label) }?.each { smartDevice -> smartDevice?.components?.each { component -> if( !smartDevicesSelect?.find{ smartDevice.deviceId==it.keySet().getAt(0) } ) smartDevicesSelect.add([ "$smartDevice.deviceId" : "$smartDevice.label   (deviceId: $smartDevice.deviceId)" ]) } } List smartComponentsSelect = [] smartDevices?.items?.find{it.deviceId == pageMirrorDeviceSmartDevice}?.components.each { component -> smartComponentsSelect.add(component.id) } List hubitatDevicesSelect = [] getAuthorizedDevices().sort{ a,b -> naturalSort(a.getDisplayName(),b.getDisplayName()) }?.each { if(pageMirrorDeviceShowAllDevices || !getReplicaDataJsonValue(it, "replica")) hubitatDevicesSelect.add([ "$it.deviceNetworkId" : "${it.getDisplayName()}   (deviceNetworkId: $it.deviceNetworkId)" ]) } if(smartComponentsSelect?.size()==1) { app.updateSetting( "pageMirrorDeviceSmartDeviceComponent", [type:"enum", value: smartComponentsSelect?.get(0)] ) } def hubitatDevice = getDevice( pageMirrorDeviceHubitatDevice ) String hubitatStats = getHubitatDeviceStats(hubitatDevice) String smartStats = getSmartDeviceStats(pageMirrorDeviceSmartDevice, pageMirrorDeviceSmartDeviceComponent) Integer refreshInterval = (g_mAppDeviceSettings?.pageMirrorDeviceMirrorButton || g_mAppDeviceSettings?.pageHubiThingDeviceModifyButton) ? 15 : 0 return dynamicPage(name: "pageMirrorDevice", uninstall: false, refreshInterval:refreshInterval) { displayHeader() section(menuHeader("Mirror HubiThings Device $sHubitatIconStatic $sSamsungIconStatic")) { input(name: "pageMirrorDeviceSmartDevice", type: "enum", title: " $sSamsungIcon Select SmartThings Device:", description: "Choose a SmartThings device", options: smartDevicesSelect, required: false, submitOnChange:true, width: 6) if(pageMirrorDeviceSmartDevice && smartComponentsSelect?.size()>1) input(name: "pageMirrorDeviceSmartDeviceComponent", type: "enum", title: "$sSamsungIcon Select SmartThings Device Component:", description: "Choose a Device Component", options: smartComponentsSelect, required: false, submitOnChange:true, width: 6, newLineAfter:true) paragraph( smartStats ) paragraph( getFormat("line") ) input(name: "pageMirrorDeviceHubitatDevice", type: "enum", title: " $sHubitatIcon Select Hubitat Device:", description: "Choose a Hubitat device", options: hubitatDevicesSelect, required: false, submitOnChange:true, width: 6, newLineAfter:true) paragraph( hubitatStats ) input(name: "pageMirrorDeviceShowAllDevices", type: "bool", title: "Show All Authorized Hubitat Devices", defaultValue: false, submitOnChange: true, width: 6, newLineAfter:true) input(name: "dynamic::pageMirrorDeviceMirrorButton", type: "button", width: 2, title: "$sHubitatIcon Mirror", style:"width:75%;") if( g_mAppDeviceSettings?.pageMirrorDeviceMirrorButton) { paragraph( getFormat("line") ) paragraph( g_mAppDeviceSettings.pageMirrorDeviceMirrorButton ) g_mAppDeviceSettings.pageMirrorDeviceMirrorButton = null href "pageConfigureDevice", title: "$sImgRule Configure HubiThings Rules", description: "Click to show" } } commonReplicaDevicesSection("pageMirrorDevice") } } void pageMirrorDeviceMirrorButton() { logDebug "${app.getLabel()} executing 'pageMirrorDeviceMirrorButton()' $pageMirrorDeviceSmartDevice $pageMirrorDeviceSmartDeviceComponent $pageMirrorDeviceHubitatDevice" if(!pageMirrorDeviceSmartDevice) g_mAppDeviceSettings['pageMirrorDeviceMirrorButton'] = errorMsg("Error: SmartThings Device selection is invalid") else if(!pageMirrorDeviceSmartDeviceComponent) g_mAppDeviceSettings['pageMirrorDeviceMirrorButton'] = errorMsg("Error: SmartThings Device Component selection is invalid") else if(!pageMirrorDeviceHubitatDevice) g_mAppDeviceSettings['pageMirrorDeviceMirrorButton'] = errorMsg("Error: Hubitat Device selection is invalid") else g_mAppDeviceSettings['pageMirrorDeviceMirrorButton'] = createMirrorDevice(pageMirrorDeviceSmartDevice, pageMirrorDeviceSmartDeviceComponent, pageMirrorDeviceHubitatDevice) } String createMirrorDevice(String deviceId, String componentId, String deviceNetworkId) { String response = errorMsg("Error: Could not find Hubitat device to mirror") def replicaDevice = getDevice(deviceNetworkId) if(replicaDevice) { Map replica = [ deviceId:deviceId, componentId:componentId, replicaId:(app.getId()), type:( getChildDevice( replicaDevice?.deviceNetworkId )!=null ? 'child' : 'mirror')] setReplicaDataJsonValue(replicaDevice, "replica", replica) clearReplicaDataCache(replicaDevice, "capabilities", true) clearReplicaDataCache(replicaDevice, "description", true) clearReplicaDataCache(replicaDevice, "health", true) clearReplicaDataCache(replicaDevice, "status", true) replicaDeviceRefresh(replicaDevice) logInfo "${app.getLabel()} mirrored device'${replicaDevice.getDisplayName()}' with deviceId: $deviceId and deviceNetworkId: $replicaDevice.deviceNetworkId" app.updateSetting( "pageSupportNewDeviceReplicaDevice", [type:"enum", value: replicaDevice.deviceNetworkId] ) app.updateSetting( "pageConfigureDeviceReplicaDevice", [type:"enum", value: replicaDevice.deviceNetworkId] ) app.updateSetting( "pageHubiThingDeviceModify", [type:"enum", value: replicaDevice.deviceNetworkId] ) response = statusMsg("'${replicaDevice.getDisplayName()}' was mirrored with deviceId: $deviceId and deviceNetworkId: $replicaDevice.deviceNetworkId") } return response } def pageVirtualDevice() { List virtualDeviceTypesSelect = [] getVirtualDeviceTypes()?.sort{ it.id }?.each { virtualDeviceTypesSelect.add([ "${it.id}" : "${it.name} ${it?.attributes ? "   (${it.attributes.sort().join(', ')})" : ""}" ]) } Map allSmartLocations = getSmartLocations() List smartLocationSelect = [] allSmartLocations?.items?.sort{ it.name }.each { smartLocation -> smartLocationSelect.add([ "${smartLocation.locationId}" : "$smartLocation.name   (locationId: $smartLocation.locationId)" ]) } Map allSmartRooms = getSmartRooms(pageVirtualDeviceLocation) List smartRoomSelect = [] allSmartRooms?.items?.sort{ it.name }.each { smartRoom -> smartRoomSelect.add([ "${smartRoom.roomId}" : "$smartRoom.name   (roomId: $smartRoom.roomId)" ]) } Map allSmartScenes = getSmartScenes(pageVirtualDeviceLocation) List smartSceneSelect = [] allSmartScenes?.items?.sort{ it.name }.each { smartScene -> smartSceneSelect.add([ "${smartScene.sceneId}" : "$smartScene.sceneName   (sceneId: $smartScene.sceneId)" ]) } List oauthSelect = [] getChildApps()?.each{ oauth -> if(pageVirtualDeviceLocation==oauth?.getLocationId()) { Integer deviceCount = oauth?.getSmartDeviceSelectList()?.size() ?: 0 String locationName = allSmartLocations?.items?.find{ it.locationId==oauth?.getLocationId() }?.name ?: oauth?.getLocationId() if(oauth?.getMaxDeviceLimit() - deviceCount > 0) { oauthSelect.add([ "${oauth?.getId()}" : "${oauth?.getLabel()} ${deviceCount} of ${oauth?.getMaxDeviceLimit()} ($locationName)" ]) } } } Map allVirtualDevices = getVirtualDevices() List virtualDeviceDeleteSelect = [] allVirtualDevices?.items?.sort{ a,b -> naturalSort(a.label,b.label) }?.each { smartDevice -> virtualDeviceDeleteSelect.add([ "${smartDevice.deviceId}" : "$smartDevice.label   (deviceId: $smartDevice.deviceId)" ]) } if(pageVirtualDeviceModify!=g_mAppDeviceSettings?.pageVirtualDeviceModify) { g_mAppDeviceSettings['pageVirtualDeviceModify'] = pageVirtualDeviceModify String deviceLabel = allVirtualDevices?.items?.find{it.deviceId == pageVirtualDeviceModify}?.label ?: "" app.updateSetting( "pageVirtualDeviceLabel", deviceLabel ) } Integer refreshInterval = (g_mAppDeviceSettings?.pageVirtualDeviceCreateButton || g_mAppDeviceSettings?.pageVirtualDeviceModifyButtons) ? 15 : 0 return dynamicPage(name: "pageVirtualDevice", uninstall: false, refreshInterval:refreshInterval) { displayHeader() section(menuHeader("Create Virtual Device, Mode or Scene $sHubitatIconStatic $sSamsungIconStatic")) { Map virtualDeviceType = getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() } Map virtualDeviceTypeConfig = getVirtualDeviceTypeConfig( virtualDeviceType?.replicaType ) if(virtualDeviceTypeConfig?.comments) paragraphComment(virtualDeviceTypeConfig.comments) if(virtualDeviceTypeConfig?.noMirror) app.updateSetting("pageVirtualDeviceEnableMirrorHubitatDevice", false) input(name: "pageVirtualDeviceType", type: "enum", title: " $sSamsungIcon Select SmartThings Virtual Device Type:", description: "Choose a SmartThings virtual device type", multiple: false, options: virtualDeviceTypesSelect, required: false, submitOnChange: true, width: 6, newLineAfter:true) if(virtualDeviceType?.replicaType == 'custom') { input(name: "pageConfigureDeviceShowDetail", type: "bool", title: "Show Virtual Device details", defaultValue: false, submitOnChange: true, width: 3, newLineAfter:true) virtualDeviceCustomSection() } input(name: "pageVirtualDeviceLocation", type: "enum", title: " $sSamsungIcon Select SmartThings Location:", description: "Choose a SmartThings location", multiple: false, options: smartLocationSelect, required: false, submitOnChange: true, width: 6, newLineAfter:true) input(name: "pageVirtualDeviceRoom", type: "enum", title: " $sSamsungIcon Select SmartThings Room:", description: "Choose a SmartThings room (not required)", multiple: false, options: smartRoomSelect, submitOnChange: true, width: 6, newLineAfter:true) if(virtualDeviceType?.replicaType == 'scene') input(name: "pageVirtualDeviceScene", type: "enum", title: " $sSamsungIcon Select SmartThings Scene:", description: "Choose a SmartThings scene", multiple: false, options: smartSceneSelect, required: false, submitOnChange: true, width: 6, newLineAfter:true) if(oauthSelect?.size()) input(name: "pageVirtualDeviceOauth", type: "enum", title: " $sHubitatIcon Select HubiThings OAuth:", description: "Choose a HubiThings OAuth (not required)", multiple: false, options: oauthSelect, submitOnChange: true, width: 6, newLineAfter:true) else { app.removeSetting('pageVirtualDeviceOauth') paragraph("No HubiThings OAuth authorized for this location or have reached maximum capacity of devices.") } if(oauthSelect?.size() && pageVirtualDeviceOauth) input(name: "pageVirtualDeviceEnableMirrorHubitatDevice", type: "bool", title: "Mirror Existing Hubitat Devices", defaultValue: false, submitOnChange: true, width: 6, newLineAfter:true) if(pageVirtualDeviceEnableMirrorHubitatDevice && oauthSelect?.size() && pageVirtualDeviceOauth) { List hubitatDevicesSelect = [] getAuthorizedDevices().sort{ a,b -> naturalSort(a.getDisplayName(),b.getDisplayName()) }?.each { if(pageMirrorDeviceShowAllDevices || !getReplicaDataJsonValue(it, "replica")) hubitatDevicesSelect.add([ "$it.deviceNetworkId" : "${it.getDisplayName()}   (deviceNetworkId: $it.deviceNetworkId)" ]) } pageAuthDeviceUserInputCtl() input(name: "pageVirtualDeviceHubitatDevice", type: "enum", title: " $sHubitatIcon Select Hubitat Device:", description: "Choose a Hubitat device", options: hubitatDevicesSelect, required: false, submitOnChange:true, width: 6, newLineAfter:true) paragraph( hubitatStats ) input(name: "pageMirrorDeviceShowAllDevices", type: "bool", title: "Show All Authorized Hubitat Devices", defaultValue: false, submitOnChange: true, width: 6, newLineAfter:true) input(name: "dynamic::pageVirtualDeviceCreateButton", type: "button", width: 2, title: "$sHubitatIcon Mirror", style:"width:75%;", newLineAfter:true) } else { List hubitatDeviceTypes = [] getDeviceHandlers()?.items?.sort{ a,b -> b?.namespace <=> a?.namespace ?: a?.name <=> b?.name }?.each { hubitatDeviceTypes.add([ "$it.id" : "$it.name   (namespace: $it.namespace)" ]) } if(pageVirtualDeviceType!=g_mAppDeviceSettings?.pageVirtualDeviceType) { g_mAppDeviceSettings['pageVirtualDeviceType'] = pageVirtualDeviceType String replicaName = getVirtualDeviceTypes()?.find{it.id.toString() == pageVirtualDeviceType}?.replicaName ?: "" String hubitatType = hubitatDeviceTypes?.find{ it?.find{ k,v -> v?.contains( replicaName ) } }?.keySet()?.getAt(0) app.updateSetting( "pageVirtualDeviceHubitatType", [type:"enum", value: (replicaName!=""?hubitatType:null)] ) } if(oauthSelect?.size() && pageVirtualDeviceOauth) input(name: "pageVirtualDeviceHubitatType", type: "enum", title: " $sHubitatIcon Select Hubitat Device Type:", description: "Choose a Hubitat device type (not required)", options: hubitatDeviceTypes, submitOnChange:true, width: 6, newLineAfter:true) input(name: "dynamic::pageVirtualDeviceCreateButton", type: "button", width: 2, title: "$sSamsungIcon Create", style:"width:75%;", newLineAfter:true) } if(g_mAppDeviceSettings?.pageVirtualDeviceCreateButton) { paragraph( getFormat("line") ) paragraph( g_mAppDeviceSettings.pageVirtualDeviceCreateButton ) g_mAppDeviceSettings.pageVirtualDeviceCreateButton = null href "pageConfigureDevice", title: "$sImgRule Configure HubiThings Rules", description: "Click to show" } } section(menuHeader("Modify Virtual Device, Mode or Scene")) { paragraphComment("Create and Remove utilize the SmartThings Cloud API to create and delete subscriptions. SmartThings enforces a rate limit of 15 requests per 15 minutes to query the subscription API for status updates.") input(name: "pageVirtualDeviceModify", type: "enum", title: " $sSamsungIcon Select SmartThings Virtual Device:", description: "Choose a SmartThings virtual device", multiple: false, options: virtualDeviceDeleteSelect, submitOnChange: true, width: 6, newLineAfter:true) input(name: "pageVirtualDeviceLabel", type: "text", title: "$sSamsungIcon SmartThings Virtual Device Label:", submitOnChange: true, width: 6, newLineAfter:true) input(name: "dynamic::pageVirtualDeviceRenameButton", type: "button", width: 2, title: "$sSamsungIcon Rename", style:"width:75%;") input(name: "dynamic::pageVirtualDeviceRemoveButton", type: "button", width: 2, title: "$sSamsungIcon Remove", style:"width:75%;", newLineAfter:true) if(g_mAppDeviceSettings?.pageVirtualDeviceModifyButtons) { paragraph( g_mAppDeviceSettings?.pageVirtualDeviceModifyButtons ) g_mAppDeviceSettings.pageVirtualDeviceModifyButtons = null } } virtualDevicesSection(allVirtualDevices, allSmartLocations) } } String virtualDevicesSection(Map allVirtualDevices, Map allSmartLocations) { String devicesTable = "" devicesTable += "" allVirtualDevices?.items?.sort{ a,b -> naturalSort(a.label,b.label) }.each { virtualDevice -> List hubitatDevices = getReplicaDevices(virtualDevice.deviceId) for (Integer i = 0; i ==0 || i < hubitatDevices.size(); i++) { def replicaDevice = hubitatDevices[i]?:null String deviceUrl = "http://${location.hub.getDataValue("localIP")}/device/edit/${replicaDevice?.getId()}" String location = allSmartLocations?.items?.find{ it.locationId==virtualDevice.locationId }?.name ?: virtualDevice.locationId devicesTable += "" devicesTable += replicaDevice ? "" : "" devicesTable += "" devicesTable += "" } } devicesTable +="
$sSamsungIcon Device$sHubitatIcon Device$sSamsungIcon Type$sSamsungIcon Location
${virtualDevice.label}${replicaDevice?.getDisplayName()}--${virtualDevice.name}$location
" if(allVirtualDevices?.items?.size() > iUseJqueryDataTables) { devicesTable += """""" //devicesTable += """""" devicesTable += """""" } else { devicesTable += """""" } devicesTable += """""" section(menuHeader("SmartThings Virtual Device List")) { if(allVirtualDevices?.items?.size()) { paragraph( devicesTable ) } input(name: "dynamic::pageVirtualDeviceButtonRefresh", type: "button", width: 2, title: "$sSamsungIcon Refresh", style:"width:75%;") } } void pageVirtualDeviceCreateButton() { logDebug "${app.getLabel()} executing 'pageVirtualDeviceCreateButton()' $pageVirtualDeviceType $pageVirtualDeviceLocation $pageVirtualDeviceRoom $pageVirtualDeviceOauth $pageHubiThingDeviceType $pageVirtualDeviceHubitatDevice" Map virtualDeviceType = getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() } if(!pageVirtualDeviceType) g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = errorMsg("Error: SmartThings Virtual Device Type selection is invalid") else if(!pageVirtualDeviceLocation) g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = errorMsg("Error: SmartThings Location selection is invalid") else if(virtualDeviceType?.replicaType == 'scene' && !pageVirtualDeviceScene) g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = errorMsg("Error: SmartThings Scene selection is invalid") else { Map response = createVirtualDevice(pageVirtualDeviceLocation, pageVirtualDeviceRoom, virtualDeviceType?.name, virtualDeviceType?.typeId) if(response?.statusCode==200) { g_mVirtualDeviceListCache[app.getId()]=null app.updateSetting( "pageVirtualDeviceModify", [type:"enum", value: response?.data?.deviceId] ) g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = statusMsg("'${virtualDeviceType?.name}' was created with deviceId: ${response?.data?.deviceId}") setVirtualDeviceDefaults(response?.data?.deviceId, virtualDeviceType?.typeId) if(pageVirtualDeviceOauth) { getChildAppById(pageVirtualDeviceOauth.toLong())?.createSmartDevice(pageVirtualDeviceLocation, response?.data?.deviceId, true) } if(pageVirtualDeviceOauth&&(pageVirtualDeviceHubitatType&&!pageVirtualDeviceEnableMirrorHubitatDevice || pageVirtualDeviceHubitatDevice&&pageVirtualDeviceEnableMirrorHubitatDevice)) { String deviceId = response?.data?.deviceId String componentId = "main" if(pageVirtualDeviceEnableMirrorHubitatDevice) { if(getVirtualDeviceRules(virtualDeviceType?.typeId)) setReplicaDataValue(getDevice(pageVirtualDeviceHubitatDevice), "rules", getVirtualDeviceRules(virtualDeviceType?.typeId)) g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = createMirrorDevice(deviceId, componentId, pageVirtualDeviceHubitatDevice) if(renameVirtualDevice(deviceId, getDevice(pageVirtualDeviceHubitatDevice)?.getDisplayName())?.statusCode==200) getSmartDeviceDescription(deviceId) } else { Map deviceType = getDeviceHandlers()?.items?.find{ it?.id==pageVirtualDeviceHubitatType } g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = createChildDevice(deviceType, virtualDeviceType?.name, virtualDeviceType?.name, deviceId, componentId) } } if(virtualDeviceType?.replicaType == 'scene') { Map allSmartScenes = getSmartScenes(pageVirtualDeviceLocation) String sceneName = "Scene - ${allSmartScenes?.items?.find{ it?.sceneId==pageVirtualDeviceScene }?.sceneName}" app.updateSetting( "pageVirtualDeviceLabel", [type:"enum", value: sceneName] ) pageVirtualDeviceRenameButton() getReplicaDevices(pageVirtualDeviceModify)?.each{ replicaDevice-> replicaDevice?.setSceneIdValue( pageVirtualDeviceScene ) } } if(virtualDeviceType?.replicaType == 'location') { Map allSmartLocations = getSmartLocations() String locationName = "Location - ${allSmartLocations?.items?.find{ it?.locationId==pageVirtualDeviceLocation }?.name}" app.updateSetting( "pageVirtualDeviceLabel", [type:"enum", value: locationName] ) pageVirtualDeviceRenameButton() } } else g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = errorMsg("Error: '$name' was not created") } } void pageVirtualDeviceRemoveButton() { logDebug "${app.getLabel()} executing 'pageVirtualDeviceRemoveButton()' $pageVirtualDeviceModify" if(pageVirtualDeviceModify) { Map allVirtualDevices = getVirtualDevices() String label = allVirtualDevices?.items?.find{ it?.deviceId==pageVirtualDeviceModify }?.label ?: "unknown device" if(deleteSmartDevice(pageVirtualDeviceModify)?.statusCode==200) { g_mVirtualDeviceListCache[app.getId()]=null g_mAppDeviceSettings['pageVirtualDeviceModifyButtons'] = statusMsg("'$label' was removed with deviceId: $pageVirtualDeviceModify") getReplicaDevices(pageVirtualDeviceModify)?.each{ replicaDevice-> deleteChildDevice( replicaDevice?.deviceNetworkId ) } app.removeSetting("pageVirtualDeviceModify") } else g_mAppDeviceSettings['pageVirtualDeviceModifyButtons'] = errorMsg("Error: '$label' was not removed") } } void pageVirtualDeviceRenameButton() { logDebug "${app.getLabel()} executing 'pageVirtualDeviceRenameButton()' $pageVirtualDeviceModify $pageVirtualDeviceLabel" if(pageVirtualDeviceModify && pageVirtualDeviceLabel) { Map allVirtualDevices = getVirtualDevices() String label = allVirtualDevices?.items?.find{ it?.deviceId==pageVirtualDeviceModify }?.label if(renameVirtualDevice(pageVirtualDeviceModify, pageVirtualDeviceLabel)?.statusCode==200) { getSmartDeviceDescription(pageVirtualDeviceModify) g_mVirtualDeviceListCache[app.getId()]=null g_mAppDeviceSettings['pageVirtualDeviceModifyButtons'] = statusMsg("'$label' was renamed to '$pageVirtualDeviceLabel'") getReplicaDevices(pageVirtualDeviceModify)?.each{ replicaDevice-> if(replicaDevice?.getLabel()==label) replicaDevice?.setLabel( pageVirtualDeviceLabel ) } } else g_mAppDeviceSettings['pageVirtualDeviceModifyButtons'] = errorMsg("Error: '$label' was not renamed to '$pageVirtualDeviceLabel'") } } void pageVirtualDeviceButtonRefresh() { logDebug "${app.getLabel()} executing 'pageVirtualDeviceButtonRefresh()'" g_mVirtualDeviceListCache[app.getId()] = null g_mSmartLocationListCache[app.getId()] = null g_mSmartRoomListCache[app.getId()] = null g_mSmartSceneListCache[app.getId()] = null } def virtualDeviceCustomSection() { //components: [ // [ id: 'main', label:"Switch One", capabilities: [ [id: 'switch', version: 1], [id: 'contactSensor', version: 1] ], categories: [ [name:"Switch", categoryType:"manufacturer"] ] ], // [ id: 'main2', label:"Switch Two", capabilities: [ [id: 'switch', version: 1] ], categories: [ [name:"Switch", categoryType:"manufacturer"] ] ], // [ id: 'main3', label:"Switch Three", capabilities: [ [id: 'switch', version: 1] ], categories: [ [name:"Switch", categoryType:"manufacturer"] ] ] //] // https://community.smartthings.com/t/smartthings-virtual-devices-using-cli/244347 List virtualDeviceCapabilitiesSelect = [] getVirtualDeviceComponents()?.sort{ it.capability.id }?.each { virtualDeviceCapabilitiesSelect.add([ "${it.id}" : "${it.label}" ]) } Integer mainCount=1, n=0, maxComponent=20 while(++n<500 && mainCount<=maxComponent) { // using 500 to just make sure we don't have runaway. loop will break out earlier. max 20 components per device as per ST. https://github.com/SmartThingsCommunity/smartthings-core-sdk/blob/main/src/endpoint/deviceprofiles.ts#L48 String componentId = mainCount==1 ? "main" : "main$mainCount" Boolean componentNA = (settings["pageVirtualDeviceCustomCapability_${n-1}"]=="0"||mainCount==maxComponent) virtualDeviceCapabilitiesSelect[0] = [0: "[ componentId: $componentId ]"] List virtualDeviceCustomSelect = n==1 ? [virtualDeviceCapabilitiesSelect[0]] : componentNA ? virtualDeviceCapabilitiesSelect.tail() : virtualDeviceCapabilitiesSelect.clone() // walk the current selection poping them out until you see a new component id. Integer m=n while(--m>0) { virtualDeviceCustomSelect.removeAll{ it.find{ k,v -> k!="0" && k==settings["pageVirtualDeviceCustomCapability_$m"] } } if(settings["pageVirtualDeviceCustomCapability_$m"]=="0") break } Map virtualDeviceComponent = getVirtualDeviceComponents().find{ it.id == settings["pageVirtualDeviceCustomCapability_$n"]?.toInteger() } Boolean newLineAfter = (settings["pageVirtualDeviceCustomCapability_$n"]=="0" || virtualDeviceComponent?.capability?.config) ? false : true String error = virtualDeviceComponent?.error ? "
 ${errorMsg(virtualDeviceComponent?.error)}" : "" String title = " $sSamsungIcon ${virtualDeviceCustomSelect?.size()==1 ? "Component" : componentNA ? "Capability" : "Component or Capability"}:$error" String description = "${virtualDeviceCustomSelect?.size()==1 ? "Start new component" : componentNA ? "Add new capability" : "Start new component or add capability"}" input(name: "pageVirtualDeviceCustomCapability_$n", type: "enum", title: title, description: description, multiple: false, options: virtualDeviceCustomSelect, required: false, submitOnChange: true, width: 3, newLineAfter:newLineAfter) if(settings["pageVirtualDeviceCustomCapability_$n"]=="0") { input(name: "pageVirtualDeviceCustomCategories_$n", type: "enum", title: "Component Category (Optional):", description: "Add optional component category", multiple: false, options: getVirtualDeviceCategories().collect{ it.category }.sort(), required: false, submitOnChange: true, width: 3) input(name: "pageVirtualDeviceCustomLabel_$n", type: "text", title: "Component Label (Optional):", submitOnChange: true, width: 3, newLineAfter:true) mainCount++ } else if(getVirtualDeviceComponents().find{ it.id == settings["pageVirtualDeviceCustomCapability_$n"]?.toInteger() }?.capability?.config) { input(name: "pageVirtualDeviceCustomRangeLow_$n", type: "decimal", title: "Range Low (Optional):", submitOnChange: true, width: 2) input(name: "pageVirtualDeviceCustomRangeHigh_$n", type: "decimal", title: "Range High (Optional):", submitOnChange: true, width: 2) input(name: "pageVirtualDeviceCustomRangeStep_$n", type: "decimal", title: "Range Step (Optional):", submitOnChange: true, width: 2, newLineAfter:true) } if(settings["pageVirtualDeviceCustomCapability_$n"]==null) { while(settings["pageVirtualDeviceCustomCapability_$n"]!=null || settings["pageVirtualDeviceCustomCapability_${n+1}"]!=null) { app.removeSetting("pageVirtualDeviceCustomCapability_$n") app.removeSetting("pageVirtualDeviceCustomLabel_$n") app.removeSetting("pageVirtualDeviceCustomCategories_$n") app.removeSetting("pageVirtualDeviceCustomRangeLow_$n") app.removeSetting("pageVirtualDeviceCustomRangeHigh_$n") app.removeSetting("pageVirtualDeviceCustomRangeStep_$n") //logInfo "remove x:$n" n++ } break } } List components = [], defaultEvents = [] Map component = [:] mainCount = 1 n=1 while(n<500) { // using 500 to just make sure we don't have runaway. loop will break out earlier. if(settings["pageVirtualDeviceCustomCapability_$n"] == "0" && component.isEmpty()) { component.id = mainCount==1 ? "main" : "main$mainCount" component.capabilities = [] component.categories = [] if(settings["pageVirtualDeviceCustomLabel_$n"]) component.label = settings["pageVirtualDeviceCustomLabel_$n"] else component.remove('label') if(settings["pageVirtualDeviceCustomCategories_$n"]) { component.categories.add( [ name: settings["pageVirtualDeviceCustomCategories_$n"], categoryType:"manufacturer"] ) } n++ mainCount++ } else if(settings["pageVirtualDeviceCustomCapability_$n"] && settings["pageVirtualDeviceCustomCapability_$n"] != "0") { Map virtualDeviceComponent = getVirtualDeviceComponents().find{ it.id == settings["pageVirtualDeviceCustomCapability_$n"].toInteger() } Map capability = virtualDeviceComponent?.capability if(capability?.config && (settings["pageVirtualDeviceCustomRangeLow_$n"]!=null || settings["pageVirtualDeviceCustomRangeHigh_$n"]!=null || settings["pageVirtualDeviceCustomRangeStep_$n"]!=null)) { Map value = [ key:capability.config, enabledValues:[] ] if(settings["pageVirtualDeviceCustomRangeLow_$n"]!=null && settings["pageVirtualDeviceCustomRangeHigh_$n"]!=null && settings["pageVirtualDeviceCustomRangeHigh_$n"].toInteger() > settings["pageVirtualDeviceCustomRangeLow_$n"].toInteger()) value.range = [ settings["pageVirtualDeviceCustomRangeLow_$n"].toInteger(), settings["pageVirtualDeviceCustomRangeHigh_$n"].toInteger() ] if(settings["pageVirtualDeviceCustomRangeStep_$n"]?.toFloat() > 0) value.step = settings["pageVirtualDeviceCustomRangeStep_$n"].toFloat() capability.config = [ values:[ value ] ] } else capability.remove("config") component.capabilities.add( capability ) virtualDeviceComponent?.defaults.each{ event -> event.componentId = component.id defaultEvents.add( event ) } n++ } else if(component?.capabilities && !component.capabilities.isEmpty()) { Map tempClone = component.clone() components.add( tempClone ) component.clear() } else { //logInfo "break y:$n" break } } String ocfDeviceType = getVirtualDeviceCategories().find{ it.category == settings["pageVirtualDeviceCustomCategories_1"] }?.ocf Map metadata = ocfDeviceType ? [ ocfDeviceType:ocfDeviceType ] : [:] g_mAppDeviceSettings['pageVirtualDeviceCustomComponents']?.clear() g_mAppDeviceSettings['pageVirtualDeviceCustomComponentDefaults']?.clear() if(!components.isEmpty()) { g_mAppDeviceSettings['pageVirtualDeviceCustomComponents'] = [ components:components, metadata:metadata ] g_mAppDeviceSettings['pageVirtualDeviceCustomComponentDefaults'] = defaultEvents if(pageConfigureDeviceShowDetail) paragraphComment("deviceProfile=${g_mAppDeviceSettings['pageVirtualDeviceCustomComponents']?.toString()}
deviceDefaults=${g_mAppDeviceSettings['pageVirtualDeviceCustomComponentDefaults']?.toString()}") } } static final List getVirtualDeviceComponents() { return [ [ id:0, label:"Component ID", capability:[id: "a", version: 1], ], // 'a' to sort to top. otherwise not used. [ id:1, label:"Switch", capability:[id: "switch", version: 1], defaults:[ [componentId:"main", capability:"switch", attribute:"switch", value:"off"] ], rules:[] ], [ id:2, label:"Switch Level", capability:[id: "switchLevel", version: 1], defaults:[ [componentId:"main", capability:"switchLevel", attribute:"level", value:50] ], rules:[] ], [ id:3, label:"Lock", capability:[id: "lock", version: 1], defaults:[ [componentId:"main", capability:"lock", attribute:"lock", value:"unknown"] ], rules:[] ], [ id:4, label:"Contact Sensor", capability:[id: "contactSensor", version: 1], defaults:[ [componentId:"main", capability:"contactSensor", attribute:"contact", value:"closed"] ], rules:[] ], [ id:5, label:"Temperature", capability:[id: "temperatureMeasurement", version: 1, config:"temperature.value"], defaults:[ [componentId:"main", capability:"temperatureMeasurement", attribute:"temperature", value:70, unit:"F"] ], rules:[] ], [ id:6, label:"Humidity", capability:[id: "relativeHumidityMeasurement", version: 1], defaults:[ [componentId:"main", capability:"relativeHumidityMeasurement", attribute:"humidity", value:50, unit:"%"] ], rules:[] ], [ id:7, label:"Battery", capability:[id: "battery", version: 1], defaults:[ [componentId:"main", capability:"battery", attribute:"battery", value:100, unit:"%"] ], rules:[] ], [ id:8, label:"Motion Sensor", capability:[id: "motionSensor", version: 1], defaults:[ [componentId:"main", capability:"motionSensor", attribute:"motion", value:"inactive"] ], rules:[] ], [ id:9, label:"Alarm", capability:[id: "alarm", version: 1], defaults:[ [componentId:"main", capability:"alarm", attribute:"alarm", value:"off"] ], rules:[] ], [ id:10, label:"Acceleration Sensor", capability:[id: "accelerationSensor", version: 1], defaults:[ [componentId:"main", capability:"accelerationSensor", attribute:"acceleration", value:"inactive"] ], rules:[] ], [ id:11, label:"Audio Volume", capability:[id: "audioVolume", version: 1], defaults:[ [componentId:"main", capability:"audioVolume", attribute:"volume", value:50] ], rules:[] ], [ id:12, label:"Presence Sensor", capability:[id: "presenceSensor", version: 1], defaults:[ [componentId:"main", capability:"presenceSensor", attribute:"presence", value:"not present"] ], rules:[] ], [ id:13, label:"Smoke Detector", capability:[id: "smokeDetector", version: 1], defaults:[ [componentId:"main", capability:"smokeDetector", attribute:"smoke", value:"clear"] ], rules:[] ], [ id:14, label:"Power Source", capability:[id: "powerSource", version: 1], defaults:[ [componentId:"main", capability:"powerSource", attribute:"powerSource", value:"unknown"] ], rules:[] ], [ id:15, label:"Power Meter", capability:[id: "powerMeter", version: 1], defaults:[ [componentId:"main", capability:"powerMeter", attribute:"power", value:0] ], rules:[] ], [ id:16, label:"Button", capability:[id: "button", version: 1], defaults:[ [componentId:"main", capability:"button", attribute:"numberOfButtons", value:1], [componentId:"main", capability:"button", attribute:"supportedButtonValues", value:["pushed","held","double","down"]] ], rules:[] ], [ id:17, label:"Audio Mute", capability:[id: "audioMute", version: 1], defaults:[ [componentId:"main", capability:"audioMute", attribute:"mute", value:"unmuted"] ], rules:[] ], [ id:18, label:"Door Control", capability:[id: "doorControl", version: 1], defaults:[ [componentId:"main", capability:"doorControl", attribute:"door", value:"unknown"] ], rules:[] ], [ id:19, label:"Energy Meter", capability:[id: "energyMeter", version: 1], defaults:[ [componentId:"main", capability:"energyMeter", attribute:"energy", value:0] ], rules:[] ], [ id:20, label:"Fan Speed", capability:[id: "fanSpeed", version: 1], defaults:[ [componentId:"main", capability:"fanSpeed", attribute:"fanSpeed", value:0] ], rules:[] ], [ id:21, label:"Illuminance", capability:[id: "illuminanceMeasurement", version: 1], defaults:[ [componentId:"main", capability:"illuminanceMeasurement", attribute:"illuminance", value:0] ], rules:[] ], // doesn't show [ id:22, label:"Location Mode", capability:[id: "locationMode", version: 1], defaults:[ [componentId:"main", capability:"locationMode", attribute:"mode", value:"unknown"] ], rules:[] ], [ id:23, label:"Occupancy Sensor", capability:[id: "occupancySensor", version: 1], defaults:[ [componentId:"main", capability:"occupancySensor", attribute:"occupancy", value:"unoccupied"] ], rules:[] ], // doesn't show [ id:24, label:"Samsung TV", capability:[id: "samsungTV", version: 1], defaults:[], rules:[] ], [ id:25, label:"Security System", capability:[id: "securitySystem", version: 1], defaults:[ [componentId:"main", capability:"securitySystem", attribute:"alarm", value:"unknown"], [componentId:"main", capability:"securitySystem", attribute:"securitySystemStatus", value:"disarmed"] ], rules:[] ], [ id:26, label:"Ultraviolet Index", capability:[id: "ultravioletIndex", version: 1], defaults:[ [componentId:"main", capability:"ultravioletIndex", attribute:"ultravioletIndex", value:0] ], rules:[] ], [ id:27, label:"Valve", capability:[id: "valve", version: 1], defaults:[ [componentId:"main", capability:"valve", attribute:"valve", value:"closed"] ], rules:[] ], [ id:28, label:"Voltage", capability:[id: "voltageMeasurement", version: 1], defaults:[ [componentId:"main", capability:"voltageMeasurement", attribute:"voltage", value:0] ], rules:[] ], [ id:29, label:"Water Sensor", capability:[id: "waterSensor", version: 1], defaults:[ [componentId:"main", capability:"waterSensor", attribute:"water", value:"dry"] ], rules:[] ], // doesnt show [ id:30, label:"Lock Codes", capability:[id: "lockCodes", version: 1], defaults:[], rules:[] ], [ id:31, error: "Problem using UI to control", label:"Momentary", capability:[id: "momentary", version: 1], defaults:[], rules:[] ], [ id:32, error: "Problem changing setpoint from UI", label:"Thermostat Heating Setpoint", capability:[id: "thermostatHeatingSetpoint", version: 1, config:"heatingSetpoint.value"], defaults:[ [componentId:"main", capability:"thermostatHeatingSetpoint", attribute:"heatingSetpoint", value:70, unit:"F"] ], rules:[] ], [ id:33, error: "Problem changing setpoint from UI", label:"Thermostat Cooling Setpoint", capability:[id: "thermostatCoolingSetpoint", version: 1, config:"coolingSetpoint.value"], defaults:[ [componentId:"main", capability:"thermostatCoolingSetpoint", attribute:"coolingSetpoint", value:75, unit:"F"] ], rules:[] ], [ id:34, error: "AQI scale stops at 100 on UI", label:"Air Quality Sensor", capability:[id: "airQualitySensor", version: 1, config:"airQuality.value"], defaults:[ [componentId:"main", capability:"airQualitySensor", attribute:"airQuality", value:0] ], rules:[] ], ] } // https://developer.smartthings.com/docs/devices/device-profiles // then used ChatGPT4 to match https://community.smartthings.com/t/editing-dh-where-is-the-icons/204805 static final List getVirtualDeviceCategories() { return [ [category:"AirConditioner", ocf:"oic.d.airconditioner"], [category:"AirPurifier", ocf:"oic.d.airpurifier"], [category:"AirQualityDetector", ocf:"x.com.st.d.airqualitysensor"], [category:"Battery", ocf:"x.com.st.d.battery"], [category:"Blind", ocf:"oic.d.blind"], [category:"BluRayPlayer", ocf:"x.com.st.d.blurayplayer"], [category:"BluetoothCarSpeaker", ocf:null], [category:"BluetoothTracker", ocf:null], [category:"Bridges", ocf:null], [category:"Button", ocf:null], [category:"Camera", ocf:"oic.d.camera"], [category:"Car", ocf:null], [category:"Charger", ocf:null], [category:"ClothingCareMachine", ocf:null], [category:"CoffeeMaker", ocf:null], [category:"ContactSensor", ocf:"x.com.st.d.sensor.contact"], [category:"Cooktop", ocf:"x.com.st.d.cooktop"], [category:"CubeRefrigerator", ocf:null], [category:"CurbPowerMeter", ocf:null], [category:"Dehumidifier", ocf:null], [category:"Dishwasher", ocf:"oic.d.dishwasher"], [category:"Door", ocf:null], [category:"DoorBell", ocf:"x.com.st.d.doorbell"], [category:"Dryer", ocf:"oic.d.dryer"], [category:"Earbuds", ocf:null], [category:"ElectricVehicleCharger", ocf:"oic.d.electricvehiclecharger"], [category:"Elevator", ocf:"x.com.st.d.elevator"], [category:"Fan", ocf:"oic.d.fan"], [category:"Feeder", ocf:"x.com.st.d.feeder"], [category:"Flashlight", ocf:null], [category:"GarageDoor", ocf:"oic.d.garagedoor"], [category:"GasValve", ocf:"x.com.st.d.gasvalve"], [category:"GasMeter", ocf:null], [category:"GenericSensor", ocf:null], [category:"HealthTracker", ocf:"x.com.st.d.healthtracker"], [category:"Heatedmattresspad", ocf:null], [category:"HomeTheater", ocf:null], [category:"Hub", ocf:"x.com.st.d.hub"], [category:"Humidifier", ocf:"x.com.st.d.humidifier"], [category:"HumiditySensor", ocf:null], [category:"IrRemote", ocf:"x.com.st.d.irblaster"], [category:"Irrigation", ocf:"x.com.st.d.irrigation"], [category:"KimchiRefrigerator", ocf:null], [category:"KitchenHood", ocf:null], [category:"LeakSensor", ocf:"x.com.st.d.sensor.moisture"], [category:"Light", ocf:"oic.d.light"], [category:"LightSensor", ocf:"x.com.st.d.sensor.light"], [category:"MicroFiberFilter", ocf:null], [category:"Microwave", ocf:null], [category:"MobilePresence", ocf:null], [category:"MotionSensor", ocf:"x.com.st.d.sensor.motion"], [category:"MultiFunctionalSensor", ocf:"x.com.st.d.sensor.multifunction"], [category:"NetworkAudio", ocf:"oic.d.networkaudio"], [category:"Networking", ocf:null], [category:"Others", ocf:"oic.wk.d"], [category:"Oven", ocf:"oic.d.oven"], [category:"PresenceSensor", ocf:"x.com.st.d.sensor.presence"], [category:"Printer", ocf:null], [category:"PrinterMultiFunction", ocf:null], [category:"Projector", ocf:null], [category:"Range", ocf:null], [category:"Receiver", ocf:null], [category:"Refrigerator", ocf:"oic.d.refrigerator"], [category:"RemoteController", ocf:"x.com.st.d.remotecontroller"], [category:"RiceCooker", ocf:null], [category:"RobotCleaner", ocf:"oic.d.robotcleaner"], [category:"ScaleToMeasureMassOfHumanBody", ocf:null], [category:"Scanner", ocf:null], [category:"SecurityPanel", ocf:null], [category:"SetTop", ocf:null], [category:"Shade", ocf:null], [category:"ShoesCareMachine", ocf:null], [category:"Siren", ocf:"x.com.st.d.siren"], [category:"SmartLock", ocf:"oic.d.smartlock"], [category:"SmartPlug", ocf:"oic.d.smartplug"], [category:"SmokeDetector", ocf:"x.com.st.d.sensor.smoke"], [category:"SolarPanel", ocf:"x.com.st.d.solarPanel"], [category:"SoundSensor", ocf:"x.com.st.d.sensor.sound"], [category:"SoundMachine", ocf:null], [category:"Speaker", ocf:null], [category:"StickVacuumCleaner", ocf:null], [category:"Stove", ocf:"x.com.st.d.stove"], [category:"Switch", ocf:"oic.d.switch"], [category:"Television", ocf:"oic.d.tv"], [category:"TempSensor", ocf:null], [category:"Thermostat", ocf:"oic.d.thermostat"], [category:"Tracker", ocf:null], [category:"UPnPMediaRenderer", ocf:null], [category:"Vent", ocf:"x.com.st.d.vent"], [category:"VisionSensor", ocf:null], [category:"VoiceAssistance", ocf:"x.com.st.d.voiceassistance"], [category:"Washer", ocf:"oic.d.washer"], [category:"WaterHeater", ocf:"x.com.st.d.waterheater"], [category:"WaterValve", ocf:"oic.d.watervalve"], [category:"WaterPurifier", ocf:null], [category:"WiFiRouter", ocf:"oic.d.wirelessrouter"], [category:"Window", ocf:null], [category:"WineCellar", ocf:"x.com.st.d.winecellar"] ] } static final String getVirtualDeviceRules(String typeId) { switch(typeId) { case 'VIRTUAL_SWITCH': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"switch","type":"attribute","label":"attribute: switch.off","value":"off"},"command":{"name":"off","type":"command","capability":"switch","label":"command: off()"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"switch","type":"attribute","label":"attribute: switch.on","value":"on"},"command":{"name":"on","type":"command","capability":"switch","label":"command: on()"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"SwitchState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"switch","attribute":"switch","label":"attribute: switch.off","value":"off","dataType":"ENUM"},"command":{"name":"off","type":"command","label":"command: off()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"SwitchState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"switch","attribute":"switch","label":"attribute: switch.on","value":"on","dataType":"ENUM"},"command":{"name":"on","type":"command","label":"command: on()"},"type":"smartTrigger"}]}""" case 'VIRTUAL_DIMMER': return """{"version":1,"components":[{"trigger":{"dataType":"NUMBER","name":"level","type":"attribute","label":"attribute: level.*"},"command":{"name":"setLevel","arguments":[{"name":"level","optional":false,"schema":{"type":"integer","minimum":0,"maximum":100}},{"name":"rate","optional":true,"schema":{"title":"PositiveInteger","type":"integer","minimum":0}}],"type":"command","capability":"switchLevel","label":"command: setLevel(level*, rate)"},"type":"hubitatTrigger"},{"trigger":{"title":"IntegerPercent","type":"attribute","properties":{"value":{"type":"integer","minimum":0,"maximum":100},"unit":{"type":"string","enum":["%"],"default":"%"}},"additionalProperties":false,"required":["value"],"capability":"switchLevel","attribute":"level","label":"attribute: level.*"},"command":{"arguments":["NUMBER","NUMBER"],"parameters":[{"name":"Level*","type":"NUMBER","constraints":["NUMBER"]},{"name":"Duration","type":"NUMBER","constraints":["NUMBER"]}],"name":"setLevel","type":"command","label":"command: setLevel(level*, duration)"},"type":"smartTrigger"}]}""" case 'VIRTUAL_DIMMER_SWITCH': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"switch","type":"attribute","label":"attribute: switch.on","value":"on"},"command":{"name":"on","type":"command","capability":"switch","label":"command: on()"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"switch","type":"attribute","label":"attribute: switch.off","value":"off"},"command":{"name":"off","type":"command","capability":"switch","label":"command: off()"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"level","type":"attribute","label":"attribute: level.*"},"command":{"name":"setLevel","arguments":[{"name":"level","optional":false,"schema":{"type":"integer","minimum":0,"maximum":100}},{"name":"rate","optional":true,"schema":{"title":"PositiveInteger","type":"integer","minimum":0}}],"type":"command","capability":"switchLevel","label":"command: setLevel(level*, rate)"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"SwitchState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"switch","attribute":"switch","label":"attribute: switch.off","value":"off","dataType":"ENUM"},"command":{"name":"off","type":"command","label":"command: off()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"SwitchState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"switch","attribute":"switch","label":"attribute: switch.on","value":"on","dataType":"ENUM"},"command":{"name":"on","type":"command","label":"command: on()"},"type":"smartTrigger"},{"trigger":{"title":"IntegerPercent","type":"attribute","properties":{"value":{"type":"integer","minimum":0,"maximum":100},"unit":{"type":"string","enum":["%"],"default":"%"}},"additionalProperties":false,"required":["value"],"capability":"switchLevel","attribute":"level","label":"attribute: level.*"},"command":{"arguments":["NUMBER","NUMBER"],"parameters":[{"name":"Level*","type":"NUMBER","constraints":["NUMBER"]},{"name":"Duration","type":"NUMBER","constraints":["NUMBER"]}],"name":"setLevel","type":"command","label":"command: setLevel(level*, duration)"},"type":"smartTrigger","disableStatus":true}]}""" case 'VIRTUAL_BUTTON': return """{"version":1,"components":[{"trigger":{"dataType":"NUMBER","name":"pushed","type":"attribute","label":"attribute: pushed.*"},"command":{"type":"attribute","properties":{"value":{"title":"ButtonState","type":"string","enum":["pushed","held","double","pushed_2x","pushed_3x","pushed_4x","pushed_5x","pushed_6x","down","down_2x","down_3x","down_4x","down_5x","down_6x","down_hold","up","up_2x","up_3x","up_4x","up_5x","up_6x","up_hold","swipe_up","swipe_down","swipe_left","swipe_right"]}},"additionalProperties":false,"required":["value"],"capability":"button","attribute":"button","label":"attribute: button.pushed","value":"pushed","dataType":"ENUM"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"numberOfButtons","type":"attribute","label":"attribute: numberOfButtons.*"},"command":{"type":"attribute","properties":{"value":{"title":"PositiveInteger","type":"integer","minimum":0}},"additionalProperties":false,"required":["value"],"capability":"button","attribute":"numberOfButtons","label":"attribute: numberOfButtons.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"held","type":"attribute","label":"attribute: held.*"},"command":{"type":"attribute","properties":{"value":{"title":"ButtonState","type":"string","enum":["pushed","held","double","pushed_2x","pushed_3x","pushed_4x","pushed_5x","pushed_6x","down","down_2x","down_3x","down_4x","down_5x","down_6x","down_hold","up","up_2x","up_3x","up_4x","up_5x","up_6x","up_hold","swipe_up","swipe_down","swipe_left","swipe_right"]}},"additionalProperties":false,"required":["value"],"capability":"button","attribute":"button","label":"attribute: button.held","value":"held","dataType":"ENUM"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"doubleTapped","type":"attribute","label":"attribute: doubleTapped.*"},"command":{"type":"attribute","properties":{"value":{"title":"ButtonState","type":"string","enum":["pushed","held","double","pushed_2x","pushed_3x","pushed_4x","pushed_5x","pushed_6x","down","down_2x","down_3x","down_4x","down_5x","down_6x","down_hold","up","up_2x","up_3x","up_4x","up_5x","up_6x","up_hold","swipe_up","swipe_down","swipe_left","swipe_right"]}},"additionalProperties":false,"required":["value"],"capability":"button","attribute":"button","label":"attribute: button.double","value":"double","dataType":"ENUM"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"released","type":"attribute","label":"attribute: released.*"},"command":{"type":"attribute","properties":{"value":{"title":"ButtonState","type":"string","enum":["pushed","held","double","pushed_2x","pushed_3x","pushed_4x","pushed_5x","pushed_6x","down","down_2x","down_3x","down_4x","down_5x","down_6x","down_hold","up","up_2x","up_3x","up_4x","up_5x","up_6x","up_hold","swipe_up","swipe_down","swipe_left","swipe_right"]}},"additionalProperties":false,"required":["value"],"capability":"button","attribute":"button","label":"attribute: button.up","value":"up","dataType":"ENUM"},"type":"hubitatTrigger"}]}""" case 'VIRTUAL_CONTACT_SENSOR': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"contact","type":"attribute","label":"attribute: contact.*"},"command":{"type":"attribute","properties":{"value":{"title":"ContactState","type":"string","enum":["closed","open"]}},"additionalProperties":false,"required":["value"],"capability":"contactSensor","attribute":"contact","label":"attribute: contact.*"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"ContactState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"contactSensor","attribute":"contact","label":"attribute: contact.closed","value":"closed","dataType":"ENUM"},"command":{"name":"close","type":"command","label":"command: close()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"ContactState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"contactSensor","attribute":"contact","label":"attribute: contact.open","value":"open","dataType":"ENUM"},"command":{"name":"open","type":"command","label":"command: open()"},"type":"smartTrigger"}]}""" case 'VIRTUAL_GARAGE_DOOR_OPENER': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"contact","type":"attribute","label":"attribute: contact.*"},"command":{"type":"attribute","properties":{"value":{"title":"ContactState","type":"string","enum":["closed","open"]}},"additionalProperties":false,"required":["value"],"capability":"contactSensor","attribute":"contact","label":"attribute: contact.*"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"type":"string"}},"additionalProperties":false,"required":["value"],"capability":"doorControl","attribute":"door","label":"attribute: door.closed","value":"closed","dataType":"ENUM"},"command":{"name":"close","type":"command","label":"command: close()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"type":"string"}},"additionalProperties":false,"required":["value"],"capability":"doorControl","attribute":"door","label":"attribute: door.open","value":"open","dataType":"ENUM"},"command":{"name":"open","type":"command","label":"command: open()"},"type":"smartTrigger"},{"trigger":{"dataType":"ENUM","name":"door","type":"attribute","label":"attribute: door.closed","value":"closed"},"command":{"type":"attribute","properties":{"value":{"type":"string","enum":["closed","closing","open","opening","unknown"]}},"additionalProperties":false,"required":["value"],"capability":"doorControl","attribute":"door","label":"attribute: door.closed","value":"closed","dataType":"ENUM"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"door","type":"attribute","label":"attribute: door.open","value":"open"},"command":{"type":"attribute","properties":{"value":{"type":"string","enum":["closed","closing","open","opening","unknown"]}},"additionalProperties":false,"required":["value"],"capability":"doorControl","attribute":"door","label":"attribute: door.open","value":"open","dataType":"ENUM"},"type":"hubitatTrigger"}]}""" case 'VIRTUAL_LOCK': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"lock","type":"attribute","label":"attribute: lock.*"},"command":{"type":"attribute","properties":{"value":{"title":"LockState","type":"string","enum":["locked","unknown","unlocked","unlocked with timeout"]},"data":{"type":"object","additionalProperties":false,"required":[],"properties":{"method":{"type":"string","enum":["manual","keypad","auto","command","rfid","fingerprint","bluetooth"]},"codeId":{"type":"string"},"codeName":{"type":"string"},"timeout":{"title":"Iso8601Date","type":"string"}}}},"additionalProperties":false,"required":["value"],"capability":"lock","attribute":"lock","label":"attribute: lock.*"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"LockState","type":"string"},"data":{"type":"object","additionalProperties":false,"required":[],"properties":{"method":{"type":"string","enum":["manual","keypad","auto","command","rfid","fingerprint","bluetooth"]},"codeId":{"type":"string"},"codeName":{"type":"string"},"timeout":{"title":"Iso8601Date","type":"string"}}}},"additionalProperties":false,"required":["value"],"capability":"lock","attribute":"lock","label":"attribute: lock.locked","value":"locked","dataType":"ENUM"},"command":{"name":"lock","type":"command","label":"command: lock()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"LockState","type":"string"},"data":{"type":"object","additionalProperties":false,"required":[],"properties":{"method":{"type":"string","enum":["manual","keypad","auto","command","rfid","fingerprint","bluetooth"]},"codeId":{"type":"string"},"codeName":{"type":"string"},"timeout":{"title":"Iso8601Date","type":"string"}}}},"additionalProperties":false,"required":["value"],"capability":"lock","attribute":"lock","label":"attribute: lock.unlocked","value":"unlocked","dataType":"ENUM"},"command":{"name":"unlock","type":"command","label":"command: unlock()"},"type":"smartTrigger"},{"trigger":{"dataType":"NUMBER","name":"battery","type":"attribute","label":"attribute: battery.*"},"command":{"title":"IntegerPercent","type":"attribute","properties":{"value":{"type":"integer","minimum":0,"maximum":100},"unit":{"type":"string","enum":["%"],"default":"%"}},"additionalProperties":false,"required":["value"],"capability":"battery","attribute":"battery","label":"attribute: battery.*"},"type":"hubitatTrigger","mute":true}]}""" case 'VIRTUAL_METERED_SWITCH': return """{"version":1,"components":[{"trigger":{"dataType":"NUMBER","name":"energy","type":"attribute","label":"attribute: energy.*"},"command":{"type":"attribute","properties":{"value":{"type":"number"},"unit":{"type":"string","enum":["Wh","kWh","mWh","kVAh"],"default":"kWh"}},"additionalProperties":false,"required":["value"],"capability":"energyMeter","attribute":"energy","label":"attribute: energy.*"},"type":"hubitatTrigger","mute":true},{"trigger":{"dataType":"NUMBER","name":"power","type":"attribute","label":"attribute: power.*"},"command":{"type":"attribute","properties":{"value":{"type":"number"},"unit":{"type":"string","enum":["W"],"default":"W"}},"additionalProperties":false,"required":["value"],"capability":"powerMeter","attribute":"power","label":"attribute: power.*"},"type":"hubitatTrigger","mute":true},{"trigger":{"dataType":"ENUM","name":"switch","type":"attribute","label":"attribute: switch.off","value":"off"},"command":{"name":"off","type":"command","capability":"switch","label":"command: switch:off()"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"switch","type":"attribute","label":"attribute: switch.on","value":"on"},"command":{"name":"on","type":"command","capability":"switch","label":"command: switch:on()"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"SwitchState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"switch","attribute":"switch","label":"attribute: switch.off","value":"off","dataType":"ENUM"},"command":{"name":"off","type":"command","label":"command: off()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"SwitchState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"switch","attribute":"switch","label":"attribute: switch.on","value":"on","dataType":"ENUM"},"command":{"name":"on","type":"command","label":"command: on()"},"type":"smartTrigger"}]}""" case 'VIRTUAL_MOTION_SENSOR': return """{"version":1,"components":[{"trigger":{"type":"attribute","properties":{"value":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000},"unit":{"type":"string","enum":["F","C"]}},"additionalProperties":false,"required":["value","unit"],"capability":"temperatureMeasurement","attribute":"temperature","label":"attribute: temperature.*"},"command":{"arguments":["Number"],"parameters":[{"type":"Number"}],"name":"setTemperature","type":"command","label":"command: setTemperature(number*)"},"type":"smartTrigger","mute":true},{"trigger":{"type":"attribute","properties":{"value":{"title":"ActivityState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"motionSensor","attribute":"motion","label":"attribute: motion.active","value":"active","dataType":"ENUM"},"command":{"name":"active","type":"command","label":"command: active()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"ActivityState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"motionSensor","attribute":"motion","label":"attribute: motion.inactive","value":"inactive","dataType":"ENUM"},"command":{"name":"inactive","type":"command","label":"command: inactive()"},"type":"smartTrigger"},{"trigger":{"dataType":"ENUM","name":"motion","type":"attribute","label":"attribute: motion.*"},"command":{"type":"attribute","properties":{"value":{"title":"ActivityState","type":"string","enum":["active","inactive"]}},"additionalProperties":false,"required":["value"],"capability":"motionSensor","attribute":"motion","label":"attribute: motion.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"battery","type":"attribute","label":"attribute: battery.*"},"command":{"title":"IntegerPercent","type":"attribute","properties":{"value":{"type":"integer","minimum":0,"maximum":100},"unit":{"type":"string","enum":["%"],"default":"%"}},"additionalProperties":false,"required":["value"],"capability":"battery","attribute":"battery","label":"attribute: battery.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"temperature","type":"attribute","label":"attribute: temperature.*"},"command":{"type":"attribute","properties":{"value":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000},"unit":{"type":"string","enum":["F","C"]}},"additionalProperties":false,"required":["value","unit"],"capability":"temperatureMeasurement","attribute":"temperature","label":"attribute: temperature.*"},"type":"hubitatTrigger","mute":true}]}""" case 'VIRTUAL_MULTI_SENSOR': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"acceleration","type":"attribute","label":"attribute: acceleration.*"},"command":{"type":"attribute","properties":{"value":{"title":"ActivityState","type":"string","enum":["active","inactive"]}},"additionalProperties":false,"required":["value"],"capability":"accelerationSensor","attribute":"acceleration","label":"attribute: acceleration.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"battery","type":"attribute","label":"attribute: battery.*"},"command":{"title":"IntegerPercent","type":"attribute","properties":{"value":{"type":"integer","minimum":0,"maximum":100},"unit":{"type":"string","enum":["%"],"default":"%"}},"additionalProperties":false,"required":["value"],"capability":"battery","attribute":"battery","label":"attribute: battery.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"contact","type":"attribute","label":"attribute: contact.*"},"command":{"type":"attribute","properties":{"value":{"title":"ContactState","type":"string","enum":["closed","open"]}},"additionalProperties":false,"required":["value"],"capability":"contactSensor","attribute":"contact","label":"attribute: contact.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"temperature","type":"attribute","label":"attribute: temperature.*"},"command":{"type":"attribute","properties":{"value":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000},"unit":{"type":"string","enum":["F","C"]}},"additionalProperties":false,"required":["value","unit"],"capability":"temperatureMeasurement","attribute":"temperature","label":"attribute: temperature.*"},"type":"hubitatTrigger"}]}""" case 'VIRTUAL_PRESENCE_SENSOR': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"presence","type":"attribute","label":"attribute: presence.*"},"command":{"type":"attribute","properties":{"value":{"title":"PresenceState","type":"string","enum":["present","not present"]}},"additionalProperties":false,"required":["value"],"capability":"presenceSensor","attribute":"presence","label":"attribute: presence.*"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"PresenceState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"presenceSensor","attribute":"presence","label":"attribute: presence.present","value":"present","dataType":"ENUM"},"command":{"name":"arrived","type":"command","label":"command: arrived()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"PresenceState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"presenceSensor","attribute":"presence","label":"attribute: presence.not present","value":"not present","dataType":"ENUM"},"command":{"name":"departed","type":"command","label":"command: departed()"},"type":"smartTrigger"}]}""" case 'VIRTUAL_THERMOSTAT': return """{"version":1,"components":[{"trigger":{"dataType":"NUMBER","name":"coolingSetpoint","type":"attribute","label":"attribute: coolingSetpoint.*"},"command":{"name":"setCoolingSetpoint","arguments":[{"name":"setpoint","optional":false,"schema":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000}}],"type":"command","capability":"thermostatCoolingSetpoint","label":"command: setCoolingSetpoint(setpoint*)"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"heatingSetpoint","type":"attribute","label":"attribute: heatingSetpoint.*"},"command":{"name":"setHeatingSetpoint","arguments":[{"name":"setpoint","optional":false,"schema":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000}}],"type":"command","capability":"thermostatHeatingSetpoint","label":"command: setHeatingSetpoint(setpoint*)"},"type":"hubitatTrigger"},{"trigger":{"dataType":"JSON_OBJECT","name":"supportedThermostatFanModes","type":"attribute","label":"attribute: supportedThermostatFanModes.*"},"command":{"type":"attribute","properties":{"value":{"type":"array","items":{"title":"ThermostatFanMode","type":"string","enum":["auto","circulate","followschedule","on"]}}},"additionalProperties":false,"required":[],"capability":"thermostatFanMode","attribute":"supportedThermostatFanModes","label":"attribute: supportedThermostatFanModes.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"JSON_OBJECT","name":"supportedThermostatModes","type":"attribute","label":"attribute: supportedThermostatModes.*"},"command":{"type":"attribute","properties":{"value":{"type":"array","items":{"title":"ThermostatMode","type":"string","enum":["asleep","auto","autowitheco","autowithreset","autochangeover","autochangeoveractive","autocool","autoheat","auxheatonly","auxiliaryemergencyheat","away","cool","custom","dayoff","dryair","eco","emergency heat","emergencyheat","emergencyheatactive","energysavecool","energysaveheat","fanonly","frostguard","furnace","heat","heatingoff","home","hotwateronly","in","manual","moistair","off","out","resume","rush hour","rushhour","schedule","southernaway"]}}},"additionalProperties":false,"required":[],"capability":"thermostatMode","attribute":"supportedThermostatModes","label":"attribute: supportedThermostatModes.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"NUMBER","name":"temperature","type":"attribute","label":"attribute: temperature.*"},"command":{"type":"attribute","properties":{"value":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000},"unit":{"type":"string","enum":["F","C"]}},"additionalProperties":false,"required":["value","unit"],"capability":"temperatureMeasurement","attribute":"temperature","label":"attribute: temperature.*"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"thermostatFanMode","type":"attribute","label":"attribute: thermostatFanMode.*"},"command":{"name":"setThermostatFanMode","arguments":[{"name":"mode","optional":false,"schema":{"title":"ThermostatFanMode","type":"string","enum":["auto","circulate","followschedule","on"]}}],"type":"command","capability":"thermostatFanMode","label":"command: setThermostatFanMode(mode*)"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"thermostatMode","type":"attribute","label":"attribute: thermostatMode.*"},"command":{"name":"setThermostatMode","arguments":[{"name":"mode","optional":false,"schema":{"title":"ThermostatMode","type":"string","enum":["asleep","auto","autowitheco","autowithreset","autochangeover","autochangeoveractive","autocool","autoheat","auxheatonly","auxiliaryemergencyheat","away","cool","custom","dayoff","dryair","eco","emergency heat","emergencyheat","emergencyheatactive","energysavecool","energysaveheat","fanonly","frostguard","furnace","heat","heatingoff","home","hotwateronly","in","manual","moistair","off","out","resume","rush hour","rushhour","schedule","southernaway"]}}],"type":"command","capability":"thermostatMode","label":"command: setThermostatMode(mode*)"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"thermostatOperatingState","type":"attribute","label":"attribute: thermostatOperatingState.*"},"command":{"type":"attribute","properties":{"value":{"title":"ThermostatOperatingState","type":"string","enum":["cooling","fan only","heating","idle","pending cool","pending heat","vent economizer"]}},"additionalProperties":false,"required":["value"],"capability":"thermostatOperatingState","attribute":"thermostatOperatingState","label":"attribute: thermostatOperatingState.*"},"type":"hubitatTrigger"},{"trigger":{"title":"Temperature","type":"attribute","properties":{"value":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000},"unit":{"type":"string","enum":["F","C"]}},"additionalProperties":false,"required":["value","unit"],"capability":"thermostatHeatingSetpoint","attribute":"heatingSetpoint","label":"attribute: heatingSetpoint.*"},"command":{"arguments":["NUMBER"],"parameters":[{"name":"Temperature*","type":"NUMBER","constraints":["NUMBER"]}],"name":"setHeatingSetpoint","type":"command","label":"command: setHeatingSetpoint(temperature*)"},"type":"smartTrigger"},{"trigger":{"title":"Temperature","type":"attribute","properties":{"value":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000},"unit":{"type":"string","enum":["F","C"]}},"additionalProperties":false,"required":["value","unit"],"capability":"thermostatCoolingSetpoint","attribute":"coolingSetpoint","label":"attribute: coolingSetpoint.*"},"command":{"arguments":["NUMBER"],"parameters":[{"name":"Temperature*","type":"NUMBER","constraints":["NUMBER"]}],"name":"setCoolingSetpoint","type":"command","label":"command: setCoolingSetpoint(temperature*)"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"ThermostatMode","type":"string"},"data":{"type":"object","additionalProperties":false,"required":[],"properties":{"supportedThermostatModes":{"type":"array","items":{"title":"ThermostatMode","type":"string","enum":["asleep","auto","autowitheco","autowithreset","autochangeover","autochangeoveractive","autocool","autoheat","auxheatonly","auxiliaryemergencyheat","away","cool","custom","dayoff","dryair","eco","emergency heat","emergencyheat","emergencyheatactive","energysavecool","energysaveheat","fanonly","frostguard","furnace","heat","heatingoff","home","hotwateronly","in","manual","moistair","off","out","resume","rush hour","rushhour","schedule","southernaway"]}}}}},"additionalProperties":false,"required":["value"],"capability":"thermostatMode","attribute":"thermostatMode","label":"attribute: thermostatMode.*"},"command":{"arguments":["ENUM"],"parameters":[{"name":"Thermostat mode*","type":"ENUM","constraints":["auto","off","heat","emergency heat","cool"]}],"name":"setThermostatMode","type":"command","label":"command: setThermostatMode(thermostat mode*)"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"ThermostatFanMode","type":"string"},"data":{"type":"object","additionalProperties":false,"required":[],"properties":{"supportedThermostatFanModes":{"type":"array","items":{"title":"ThermostatFanMode","type":"string","enum":["auto","circulate","followschedule","on"]}}}}},"additionalProperties":false,"required":["value"],"capability":"thermostatFanMode","attribute":"thermostatFanMode","label":"attribute: thermostatFanMode.*"},"command":{"arguments":["ENUM"],"parameters":[{"name":"Fan mode*","type":"ENUM","constraints":["on","circulate","auto"]}],"name":"setThermostatFanMode","type":"command","label":"command: setThermostatFanMode(fan mode*)"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"TemperatureValue","type":"number","minimum":-460,"maximum":10000},"unit":{"type":"string","enum":["F","C"]}},"additionalProperties":false,"required":["value","unit"],"capability":"temperatureMeasurement","attribute":"temperature","label":"attribute: temperature.*"},"command":{"arguments":["NUMBER"],"parameters":[{"type":"NUMBER"}],"name":"setTemperature","type":"command","label":"command: setTemperature(number*)"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"ThermostatOperatingState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"thermostatOperatingState","attribute":"thermostatOperatingState","label":"attribute: thermostatOperatingState.*"},"command":{"arguments":["ENUM"],"parameters":[{"type":"ENUM"}],"name":"setThermostatOperatingState","type":"command","label":"command: setThermostatOperatingState(enum*)"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"type":"array","items":{"title":"ThermostatMode","type":"string","enum":["asleep","auto","autowitheco","autowithreset","autochangeover","autochangeoveractive","autocool","autoheat","auxheatonly","auxiliaryemergencyheat","away","cool","custom","dayoff","dryair","eco","emergency heat","emergencyheat","emergencyheatactive","energysavecool","energysaveheat","fanonly","frostguard","furnace","heat","heatingoff","home","hotwateronly","in","manual","moistair","off","out","resume","rush hour","rushhour","schedule","southernaway"]}}},"additionalProperties":false,"required":[],"capability":"thermostatMode","attribute":"supportedThermostatModes","label":"attribute: supportedThermostatModes.*"},"command":{"arguments":["JSON_OBJECT"],"parameters":[{"type":"JSON_OBJECT"}],"name":"setSupportedThermostatModes","type":"command","label":"command: setSupportedThermostatModes(json_object*)"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"type":"array","items":{"title":"ThermostatFanMode","type":"string","enum":["auto","circulate","followschedule","on"]}}},"additionalProperties":false,"required":[],"capability":"thermostatFanMode","attribute":"supportedThermostatFanModes","label":"attribute: supportedThermostatFanModes.*"},"command":{"arguments":["JSON_OBJECT"],"parameters":[{"type":"JSON_OBJECT"}],"name":"setSupportedThermostatFanModes","type":"command","label":"command: setSupportedThermostatFanModes(json_object*)"},"type":"smartTrigger"},{"trigger":{"dataType":"NUMBER","name":"battery","type":"attribute","label":"attribute: battery.*"},"command":{"title":"IntegerPercent","type":"attribute","properties":{"value":{"type":"integer","minimum":0,"maximum":100},"unit":{"type":"string","enum":["%"],"default":"%"}},"additionalProperties":false,"required":["value"],"capability":"battery","attribute":"battery","label":"attribute: battery.*"},"type":"hubitatTrigger"}]}""" case 'VIRTUAL_SIREN': return """{"version":1,"components":[{"trigger":{"dataType":"ENUM","name":"alarm","type":"attribute","label":"attribute: alarm.both","value":"both"},"command":{"name":"both","type":"command","capability":"alarm","label":"command: both()"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"alarm","type":"attribute","label":"attribute: alarm.off","value":"off"},"command":{"name":"off","type":"command","capability":"alarm","label":"command: off()"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"alarm","type":"attribute","label":"attribute: alarm.siren","value":"siren"},"command":{"name":"siren","type":"command","capability":"alarm","label":"command: siren()"},"type":"hubitatTrigger"},{"trigger":{"dataType":"ENUM","name":"alarm","type":"attribute","label":"attribute: alarm.strobe","value":"strobe"},"command":{"name":"strobe","type":"command","capability":"alarm","label":"command: strobe()"},"type":"hubitatTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"AlertState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"alarm","attribute":"alarm","label":"attribute: alarm.both","value":"both","dataType":"ENUM"},"command":{"name":"both","type":"command","label":"command: both()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"AlertState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"alarm","attribute":"alarm","label":"attribute: alarm.off","value":"off","dataType":"ENUM"},"command":{"name":"off","type":"command","label":"command: off()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"AlertState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"alarm","attribute":"alarm","label":"attribute: alarm.siren","value":"siren","dataType":"ENUM"},"command":{"name":"siren","type":"command","label":"command: siren()"},"type":"smartTrigger"},{"trigger":{"type":"attribute","properties":{"value":{"title":"AlertState","type":"string"}},"additionalProperties":false,"required":["value"],"capability":"alarm","attribute":"alarm","label":"attribute: alarm.strobe","value":"strobe","dataType":"ENUM"},"command":{"name":"strobe","type":"command","label":"command: strobe()"},"type":"smartTrigger"}]}""" default: return null } } static final List getVirtualDeviceDefaults(String typeId) { switch(typeId) { case 'VIRTUAL_SWITCH': return [ [componentId:"main", capability:"switch", attribute:"switch", value:"off"] ] case 'VIRTUAL_DIMMER': return [ [componentId:"main", capability:"switchLevel", attribute:"level", value:100] ] case 'VIRTUAL_DIMMER_SWITCH': return [ [componentId:"main", capability:"switch", attribute:"switch", value:"off"], [componentId:"main", capability:"switchLevel", attribute:"level", value:100] ] case 'VIRTUAL_BUTTON': return [ [componentId:"main", capability:"button", attribute:"numberOfButtons", value:1] ] case 'VIRTUAL_CONTACT_SENSOR': return [ [componentId:"main", capability:"contactSensor", attribute:"contact", value:"closed"] ] case 'VIRTUAL_GARAGE_DOOR_OPENER': return [ [componentId:"main", capability:"contactSensor", attribute:"contact", value:"closed"], [componentId:"main", capability:"doorControl", attribute:"door", value:"closed"] ] case 'VIRTUAL_LOCK': return [ [componentId:"main", capability:"lock", attribute:"lock", value:"unknown"], [componentId:"main", capability:"battery", attribute:"battery", value:100, unit:"%"] ] case 'VIRTUAL_METERED_SWITCH': return [ [componentId:"main", capability:"healthCheck", attribute:"DeviceWatch-DeviceStatus", value:"online"], [componentId:"main", capability:"energyMeter", attribute:"energy", value:0, unit:"kWh"], [componentId:"main", capability:"switch", attribute:"switch", value:"off"], [componentId:"main", capability:"powerMeter", attribute:"power", value:0, unit:"W"] ] case 'VIRTUAL_MOTION_SENSOR': return [ [componentId:"main", capability:"temperatureMeasurement", attribute:"temperature", value:70, unit:"F"], [componentId:"main", capability:"motionSensor", attribute:"motion", value:"inactive"], [componentId:"main", capability:"battery", attribute:"battery", value:100, unit:"%"] ] case 'VIRTUAL_MULTI_SENSOR': return [ [componentId:"main", capability:"accelerationSensor", attribute:"acceleration", value:"inactive"], [componentId:"main", capability:"threeAxis", attribute:"threeAxis", value:[0,0,0]], [componentId:"main", capability:"battery", attribute:"battery", value:100, unit:"%"], [componentId:"main", capability:"contactSensor", attribute:"contact", value:"closed"], [componentId:"main", capability:"temperatureMeasurement", attribute:"temperature", value:70, unit:"F"] ] case 'VIRTUAL_PRESENCE_SENSOR': return [ [componentId:"main", capability:"presenceSensor", attribute:"presence", value:"not present"] ] case 'VIRTUAL_THERMOSTAT': return [ [componentId:"main", capability:"thermostatHeatingSetpoint", attribute:"heatingSetpoint", value:70, unit:"F"], [componentId:"main", capability:"thermostatCoolingSetpoint", attribute:"coolingSetpoint", value:75, unit:"F"], [componentId:"main", capability:"thermostatOperatingState", attribute:"thermostatOperatingState", value:"idle"], [componentId:"main", capability:"temperatureMeasurement", attribute:"temperature", value:70, unit:"F"], [componentId:"main", capability:"thermostatMode", attribute:"thermostatMode", value:"off"], [componentId:"main", capability:"thermostatMode", attribute:"supportedThermostatModes", value:["off","cool","heat","emergency heat","auto"]], [componentId:"main", capability:"thermostatFanMode", attribute:"thermostatFanMode", value:"auto"], [componentId:"main", capability:"thermostatFanMode", attribute:"supportedThermostatFanModes", value:["auto","circulate","on"]], [componentId:"main", capability:"battery", attribute:"battery", value:100, unit:"%"] ] case 'VIRTUAL_SIREN': return [ [componentId:"main", capability:"alarm", attribute:"alarm", value:"off"] ] case 'VIRTUAL_CUSTOM': return (g_mAppDeviceSettings['pageVirtualDeviceCustomComponentDefaults'])?:[] default: return null } } static final List getVirtualDeviceTypes() { // https://community.smartthings.com/t/smartthings-virtual-devices-using-cli/244347 // https://community.smartthings.com/t/smartthings-cli-create-virtual-device/249199 // https://raw.githubusercontent.com/SmartThingsCommunity/smartthings-cli/eb1aab896d4248d293c662317056097aad777438/packages/cli/src/lib/commands/virtualdevices-util.ts return [ [id:1, name: "Virtual Switch", typeId: "VIRTUAL_SWITCH", replicaName: "Replica Switch", attributes:["rw:switch"] ], [id:2, name: "Virtual Dimmer Switch", typeId: "VIRTUAL_DIMMER_SWITCH", replicaName: "Replica Dimmer", attributes:["rw:switch","rw:level"] ], [id:3, name: "Virtual Button", typeId: "VIRTUAL_BUTTON", replicaName: "Replica Button", attributes:["r:held","r:pushed","r:doubleTapped","r:released"] ], //[id:4, name: "Virtual Camera", typeId: "VIRTUAL_CAMERA" ], //[id:5, name: "Virtual Color Bulb", typeId: "VIRTUAL_COLOR_BULB" ], [id:6, name: "Virtual Contact Sensor", typeId: "VIRTUAL_CONTACT_SENSOR", replicaName: "Replica Multipurpose Sensor", attributes:["r:contact"] ], //[id:7, name: "Virtual Dimmer (no switch)", typeId: "VIRTUAL_DIMMER", replicaName: "Replica Dimmer" ], // why does this exist? [id:8, name: "Virtual Garage Door Opener", typeId: "VIRTUAL_GARAGE_DOOR_OPENER", replicaName: "Replica Garage Door", attributes:["r:contact","rw:door"] ], [id:9, name: "Virtual Lock", typeId: "VIRTUAL_LOCK", replicaName: "Replica Lock", attributes:["rw:lock","r:battery"] ], [id:10, name: "Virtual Metered Switch", typeId: "VIRTUAL_METERED_SWITCH", attributes:["r:energy","r:power","rw:switch"] ], [id:11, name: "Virtual Motion Sensor", typeId: "VIRTUAL_MOTION_SENSOR", replicaName: "Replica Motion Sensor", attributes:["r:temperature","r:motion","r:battery"] ], [id:12, name: "Virtual Multipurpose Sensor", typeId: "VIRTUAL_MULTI_SENSOR", replicaName: "Replica Multipurpose Sensor", attributes:["r:acceleration","r:contact","r:temperature","r:battery"] ], [id:13, name: "Virtual Presence Sensor", typeId: "VIRTUAL_PRESENCE_SENSOR", replicaName: "Replica Presence", attributes:["r:presence"] ], //[id:14, name: "Virtual Refrigerator", typeId: "VIRTUAL_REFRIGERATOR" ], //[id:15, name: "Virtual RGBW Bulb", typeId: "VIRTUAL_RGBW_BULB" ], [id:16, name: "Virtual Siren", typeId: "VIRTUAL_SIREN", replicaName: "Replica Alarm", attributes:["rw:alarm"] ], //[id:17, name: "Virtual Thermostat", typeId: "VIRTUAL_THERMOSTAT", attributes:["r:battery","rw:coolingSetpoint","rw:heatingSetpoint","r:supportedThermostatFanModes","r:supportedThermostatModes","r:temperature","rw:thermostatFanMode","rw:thermostatFanMode","rw:thermostatMode","r:thermostatOperatingState"] ], [id:18, name: "Virtual Custom Device", typeId: "VIRTUAL_CUSTOM", replicaType: "custom" ], [id:19, name: "Location Mode Knob", typeId: "VIRTUAL_SWITCH", replicaName: "Replica Location Knob", replicaType: "location" ], [id:20, name: "Scene Knob", typeId: "VIRTUAL_SWITCH", replicaName: "Replica Scene Knob", replicaType: "scene" ] ] } static final Map getVirtualDeviceTypeConfig(String replicaType) { switch(replicaType) { case 'location': return [ replicaType:"location", noMirror: true, comments:"Location Mode Knob allows for creation, deletion, updating and mirroring the SmartThings mode within a Hubitat Device. Similar to Hubitat, each SmartThings location only supports a singular mode." ] case 'scene': return [ replicaType:"scene", noMirror: true, comments:"Scene Knob allows for triggering and mirroring a SmartThings scene within a Hubitat Device. NOTE for proper usage: A SmartThings virtual switch will be created, and it must be added to the SmartThings Scene 'actions' to update the switch to 'Turn on' when the scene is triggered." ] case 'custom': return [ replicaType:"custom", noMirror: false, comments:"Virtual Custom allows for the design of custom SmartThings virtual cloud devices. NOTE for proper usage: The combinations are endless, and this interface allows you to make mistakes that the SmartThings UI might not display properly. You can select only one similar capability type per component, so if you want more than one switch, you will need several components. With HubiThings Replica, each component acts as a separate device, so you can have many HE devices connected to one ST device. There is a maximum of 20 components per virtual device." ] default: return null } } void setVirtualDeviceDefaults(String deviceId, String prototype) { logDebug "${app.getLabel()} executing 'setVirtualDeviceDefaults($deviceId, $prototype)'" runIn(5, setVirtualDeviceDefaultsHelper, [data: [deviceId:deviceId, prototype:prototype]]) } void setVirtualDeviceDefaultsHelper(data) { setVirtualDeviceDefaultsPrivate(data.deviceId, data.prototype) } private void setVirtualDeviceDefaultsPrivate(String deviceId, String prototype) { logDebug "${app.getLabel()} executing 'setVirtualDeviceDefaultsPrivate($deviceId, $prototype)'" getVirtualDeviceDefaults(prototype)?.each{ logInfo "${app.getLabel()} sending $prototype default:$it" setVirtualDeviceAttribute(deviceId, it.componentId, it.capability, it.attribute, it.value, it?.unit?:null, it?.data?:null) } } Map createVirtualDevice(String locationId, String roomId, String name, String prototype) { logDebug "${app.getLabel()} executing 'createVirtualDevice($locationId, $name, $prototype)'" Map response = [statusCode:iHttpError] Map device = [ name: name, roomId: roomId, owner: [ ownerType: "LOCATION", ownerId: locationId ] ] if(prototype=="VIRTUAL_CUSTOM") device.deviceProfile = g_mAppDeviceSettings['pageVirtualDeviceCustomComponents'] else device.prototype = prototype logDebug "${app.getLabel()} building device:$device" Map params = [ uri: sURI, body: JsonOutput.toJson(device), path: ((prototype=="VIRTUAL_CUSTOM") ? "/virtualdevices" : "/virtualdevices/prototypes"), contentType: "application/json", requestContentType: "application/json", headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { httpPost(params) { resp -> logDebug "response data: ${resp.data}" response.data = resp.data response.statusCode = resp.status logInfo "${app.getLabel()} created SmartThings Virtual Device '${resp.data.label}'" } } catch (e) { logWarn "${app.getLabel()} has createVirtualDevice() error: $e" } return response } Map getVirtualDeviceList() { logDebug "${app.getLabel()} executing 'getVirtualDeviceList()'" Map response = [statusCode:iHttpError] Map params = [ uri: sURI, path: "/virtualdevices", headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { httpGet(params) { resp -> logDebug "response data: ${resp.data}" response.data = resp.data response.statusCode = resp.status } } catch (e) { logWarn "${app.getLabel()} has getVirtualDeviceList() error: $e" } return response } Map renameVirtualDevice(String deviceId, String label) { logDebug "${app.getLabel()} executing 'renameVirtualDevice()'" Map response = [statusCode:iHttpError] Map params = [ uri: sURI, body: JsonOutput.toJson([ label: label ]), path: "/devices/$deviceId", contentType: "application/json", requestContentType: "application/json", headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { httpPut(params) { resp -> logDebug "response data: ${resp.data}" response.data = resp.data response.statusCode = resp.status logInfo "${app.getLabel()} renamed SmartThings Virtual Device '${resp.data.label}'" } } catch (e) { logWarn "${app.getLabel()} has renameVirtualDevice() error: $e" } return response } Map deleteSmartDevice(String deviceId) { logDebug "${app.getLabel()} executing 'deleteSmartDevice()'" Map response = [statusCode:iHttpError] Map params = [ uri: sURI, path: "/devices/$deviceId", headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { httpDelete(params) { resp -> logDebug "response data: ${resp.data}" response.data = resp.data response.statusCode = resp.status logInfo "${app.getLabel()} deleted SmartThings Device with deviceId: $deviceId" } } catch (e) { logWarn "${app.getLabel()} has deleteSmartDevice() error: $e" } return response } Map getSmartRoomList(String locationId) { logDebug "${app.getLabel()} executing 'getSmartRoomList($locationId)'" Map response = [statusCode:iHttpError] Map params = [ uri: sURI, path: "/locations/${locationId}/rooms", headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { httpGet(params) { resp -> logDebug "response data: ${resp.data}" response.data = resp.data response.statusCode = resp.status } } catch (e) { logWarn "${app.getLabel()} has getSmartRoomList() error: $e" } return response } Map getSmartLocationList() { logDebug "${app.getLabel()} executing 'getSmartLocationList()'" Map response = [statusCode:iHttpError] Map params = [ uri: sURI, path: "/locations", headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { httpGet(params) { resp -> logDebug "response data: ${resp.data}" response.data = resp.data response.statusCode = resp.status } } catch (e) { logWarn "${app.getLabel()} has getSmartLocationList() error: $e" } return response } Map getSmartSceneList(String locationId) { logDebug "${app.getLabel()} executing 'getSmartSceneList($locationId)'" Map response = [statusCode:iHttpError] Map params = [ uri: sURI, path: "/scenes", query: [ locationId:locationId ], headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { httpGet(params) { resp -> logDebug "response data: ${resp.data}" response.data = resp.data response.statusCode = resp.status } } catch (e) { logWarn "${app.getLabel()} has getSmartSceneList() error: $e" } return response } void updateRuleList(action, type) { Map trigger = g_mAppDeviceSettings?.hubitatAttribute Map command = g_mAppDeviceSettings?.smartCommand if(type!='hubitatTrigger') { trigger = g_mAppDeviceSettings?.smartAttribute command = g_mAppDeviceSettings?.hubitatCommand } String triggerKey = trigger?.label?.trim() String commandKey = command?.label?.trim() def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) logDebug "${app.getLabel()} executing 'updateRuleList()' hubitatDevice:'${replicaDevice}' trigger:'${triggerKey}' command:'${commandKey}' action:'${action}'" Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") ?: [components:[],version:1] if(action=='delete') { replicaDeviceRules?.components?.removeAll{ it?.type==type && it?.trigger?.label?.trim()==triggerKey && it?.command?.label?.trim()==commandKey } replicaDeviceRules?.components?.removeAll{ it?.type==type && it?.trigger?.label?.trim()==triggerKey && commandKey==null } replicaDeviceRules?.components?.removeAll{ it?.type==type && it?.command?.label?.trim()==commandKey && triggerKey==null } } else if(triggerKey && commandKey && !replicaDeviceRules?.components?.find{ it?.type==type && it?.trigger?.label?.trim()==triggerKey && it?.command?.label?.trim()==commandKey }) { Map newRule = [ trigger:trigger, command:command, type:type] newRule?.command?.parameters?.each{ parameter -> if(parameter?.description) { parameter.remove('description') } } //junk try { newRule?.command?.arguments?.each{ arguments -> if(arguments?.schema?.pattern) { arguments?.schema.remove('pattern') } } } catch(e) {} //junk if(newRule?.trigger?.properties?.value?.enum) newRule.trigger.properties.value.remove('enum') //junk if(pageConfigureDeviceMuteTriggerRuleInfo) newRule['mute'] = true if(pageConfigureDeviceDisableStatusUpdate) newRule['disableStatus'] = true if(action=='store' && (!replicaDeviceRules?.components?.find{ it?.type==type && it?.trigger?.label?.trim()==triggerKey } || pageConfigureDeviceAllowDuplicateAttribute)) { replicaDeviceRules.components.add(newRule) } } setReplicaDataJsonValue(replicaDevice, "rules", replicaDeviceRules?.sort{ a, b -> b.key <=> a.key }) if(type=='hubitatTrigger') replicaDeviceSubscribe(replicaDevice) } void replicaDevicesRuleSection(){ def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) if(!replicaDevice) return Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") String X = "" String O = "" if(g_mAppDeviceSettings['replicaDevicesRuleSection']) g_mAppDeviceSettings['replicaDevicesRuleSection'].clear(); else g_mAppDeviceSettings['replicaDevicesRuleSection'] = [:] String replicaDeviceRulesList = "" replicaDeviceRulesList += "" replicaDeviceRules?.components?.sort{ a,b -> a?.type <=> b?.type ?: a?.trigger?.label <=> b?.trigger?.label ?: a?.command?.label <=> b?.command?.label }?.eachWithIndex { rule, index -> String trigger = "${rule?.type=='hubitatTrigger' ? sHubitatIcon : sSamsungIcon} ${rule?.trigger?.label}" String command = "${rule?.type!='hubitatTrigger' ? sHubitatIcon : sSamsungIcon} ${rule?.command?.label}" trigger = checkTrigger(replicaDevice, rule?.type, rule?.trigger) ? trigger : "$trigger" command = checkCommand(replicaDevice, rule?.type, rule?.command) ? command : "$command" g_mAppDeviceSettings['replicaDevicesRuleSection'].put( index.toString(), rule ) replicaDeviceRulesList += "" replicaDeviceRulesList += "" replicaDeviceRulesList += "" replicaDeviceRulesList += "" } replicaDeviceRulesList +="
$XTriggerAction$sInfoLogsIcon$sPeriodicIcon
${buttonLink("dynamic::pageConfigureDeviceSelectButton::$index", g_mAppDeviceSettings['replicaDevicesRuleSectionSelect']?.get(index.toString())?X:O)}$trigger$command${buttonLink("dynamic::pageConfigureDeviceMuteButton::$index", rule?.mute?O:X)}${buttonLink("dynamic::pageConfigureDeviceStatusButton::$index", rule?.disableStatus?O:X)}
" if (replicaDeviceRules?.components?.size){ section(menuHeader("Active Rules")) { paragraph( replicaDeviceRulesList ) if(g_mAppDeviceSettings['replicaDevicesRuleSectionSelect']?.find { key, value -> value==true }) input(name: "dynamic::pageConfigureDeviceDeleteSelected", type: "button", title: "Delete Selected", width: 2, style:"width:75%;") paragraph(rawHtml: true, """""") paragraph(rawHtml: true, """""") } } } String buttonLink(String btnName, String linkText) { return "
$linkText
" } void pageConfigureDeviceDeleteSelected() { logDebug "${app.getLabel()} executing 'pageConfigureDeviceDeleteSelected()'" def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") g_mAppDeviceSettings['replicaDevicesRuleSectionSelect']?.each { index, value -> Map match = value ? g_mAppDeviceSettings['replicaDevicesRuleSection']?.get(index) : [:] replicaDeviceRules?.components?.removeAll{ it?.trigger?.label?.trim()==match?.trigger?.label && it?.command?.label?.trim()==match?.command?.label } } g_mAppDeviceSettings['replicaDevicesRuleSectionSelect'].clear() setReplicaDataJsonValue(replicaDevice, "rules", replicaDeviceRules?.sort{ a, b -> b.key <=> a.key }) } void pageConfigureDeviceSelectButton(String index) { logDebug "${app.getLabel()} executing 'pageConfigureDeviceSelectButton($index)'" if(!g_mAppDeviceSettings['replicaDevicesRuleSectionSelect']) g_mAppDeviceSettings['replicaDevicesRuleSectionSelect'] = [:] g_mAppDeviceSettings['replicaDevicesRuleSectionSelect'].put( index, g_mAppDeviceSettings['replicaDevicesRuleSectionSelect']?.get(index) ? false : true ) } void pageConfigureDeviceMuteButton(String index) { logDebug "${app.getLabel()} executing 'pageConfigureDeviceMuteButton($index)'" def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") Map match = g_mAppDeviceSettings['replicaDevicesRuleSection']?.get(index) Map rule = replicaDeviceRules?.components?.find{ it?.type==match?.type && it?.trigger?.label?.trim()==match?.trigger?.label && it?.command?.label?.trim()==match?.command?.label } if(rule) { rule['mute'] = !rule?.mute setReplicaDataJsonValue(replicaDevice, "rules", replicaDeviceRules?.sort{ a, b -> b.key <=> a.key }) } } void pageConfigureDeviceStatusButton(String index) { logDebug "${app.getLabel()} executing 'pageConfigureDeviceStatusButton($index)'" def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") Map match = g_mAppDeviceSettings['replicaDevicesRuleSection']?.get(index) Map rule = replicaDeviceRules?.components?.find{ it?.type==match?.type && it?.trigger?.label?.trim()==match?.trigger?.label && it?.command?.label?.trim()==match?.command?.label } if(rule) { rule['disableStatus'] = !rule?.disableStatus setReplicaDataJsonValue(replicaDevice, "rules", replicaDeviceRules?.sort{ a, b -> b.key <=> a.key }) } } String getTimestampSmartFormat() { return ((new Date().format("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone('UTC'))).toString()) } def naturalSort( def a, def b ) { def aParts = a.replaceAll(/(\d+)/, '#$1#').split('#') def bParts = b.replaceAll(/(\d+)/, '#$1#').split('#') int i = 0 while(i < aParts.size() && i < bParts.size()) { if (aParts[i] != bParts[i]) { if (aParts[i].isNumber() && bParts[i].isNumber()) return aParts[i].toInteger() <=> bParts[i].toInteger() else return aParts[i] <=> bParts[i] } i++ } return aParts.size() <=> bParts.size() } def checkFirmwareVersion(versionString) { def (a1,b1,c1,d1) = location.hub.firmwareVersionString.split("\\.").collect { it.toInteger() } def (a2,b2,c2,d2) = versionString.split("\\.").collect { it.toInteger() } return (a1>=a2 || (a1>=a2 && b1>=b2) || (a1>=a2 && b1>=b2 && c1>=c2) || (a1>=a2 && b1>=b2 && c1>=c2 && d1>=d2)) } Boolean checkTrigger(replicaDevice, type, ruleTrigger) { Map trigger = type=='hubitatTrigger' ? getHubitatAttributeOptions(replicaDevice) : getSmartAttributeOptions(replicaDevice) return (type=='hubitatTrigger' ? !!trigger?.get(ruleTrigger?.label) : !!trigger?.find{ k,v -> v?.capability==ruleTrigger?.capability && v?.attribute==ruleTrigger?.attribute }) //return trigger?.get(ruleTrigger?.label) } Boolean checkCommand(replicaDevice, type, ruleCommand) { Map commands = type!='hubitatTrigger' ? getHubitatCommandOptions(replicaDevice) : getSmartCommandOptions(replicaDevice) return (type=='hubitatTrigger' ? !!commands?.get(ruleCommand?.label) : !!commands?.find{ k,v -> v?.capability==ruleCommand?.capability && v?.name==ruleCommand?.name }) //return commands?.get(ruleCommand?.label) } def pageConfigureDevice() { List replicaDevicesSelect = [] getAllReplicaDevices()?.sort{ a,b -> naturalSort(a.getDisplayName(),b.getDisplayName()) }.each { Map device = [ "${it.deviceNetworkId}" : "${it.getDisplayName()}   (deviceNetworkId: ${it.deviceNetworkId})" ] replicaDevicesSelect.add(device) } return dynamicPage(name: "pageConfigureDevice", uninstall: false) { displayHeader() section(menuHeader("Configure HubiThings Rules $sHubitatIconStatic $sSamsungIconStatic")) { def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) String deviceTitle = " $sSamsungIcon Select HubiThings Device:" if(replicaDevice) { String deviceUrl = "http://${location.hub.getDataValue("localIP")}/device/edit/${replicaDevice.getId()}" deviceTitle = "${deviceTitle}" } input(name: "pageConfigureDeviceReplicaDevice", type: "enum", title: deviceTitle, description: "Choose a HubiThings device", options: replicaDevicesSelect, multiple: false, submitOnChange: true, width: 8, newLineAfter:true) if(g_mAppDeviceSettings['pageConfigureDeviceReplicaDevice']!=pageConfigureDeviceReplicaDevice) { g_mAppDeviceSettings['pageConfigureDeviceReplicaDevice']=pageConfigureDeviceReplicaDevice g_mAppDeviceSettings['replicaDevicesRuleSectionSelect']?.clear() app.updateSetting("pageConfigureDeviceAllowDuplicateAttribute", false) app.updateSetting("pageConfigureDeviceMuteTriggerRuleInfo", false) app.updateSetting("pageConfigureDeviceDisableStatusUpdate", (replicaDevice && replicaDevice?.device?.deviceType?.namespace!="replica")) //logWarn JsonOutput.toJson(app) } if(pageConfigureDeviceShowDetail && replicaDevice) { def hubitatStats = getHubitatDeviceStats(replicaDevice) paragraphComment( hubitatStats ) } input(name: "pageConfigureDevice::refreshDevice", type: "button", title: "Refresh", width: 2, style:"width:75%;") input(name: "pageConfigureDevice::clearDeviceRules", type: "button", title: "Clear Rules", width: 2, style:"width:75%;") if(replicaDevice?.hasCommand('configure') && !getReplicaDataJsonValue(replicaDevice, "rules" )?.components) input(name: "pageConfigureDevice::configDeviceRules", type: "button", title: "Configure", width: 2, style:"width:75%;") paragraph( getFormat("line") ) Map hubitatAttributeOptions = getHubitatAttributeOptions(replicaDevice) Map smartCommandOptions = getSmartCommandOptions(replicaDevice) input(name: "hubitatAttribute", type: "enum", title: " $sHubitatIcon If Hubitat Attribute TRIGGER changes:", description: "Choose a Hubitat Attribute", options: hubitatAttributeOptions.keySet().sort(), required: false, submitOnChange:true, width: 4) List schemaOneOfType = smartCommandOptions?.get(smartCommand)?.arguments?.getAt(0)?.schema?.oneOf?.collect{ it?.type } ?: [] input(name: "smartCommand", type: "enum", title: " $sSamsungIcon Then ACTION SmartThings Command:", description: "Choose a SmartThings Command", options: smartCommandOptions.keySet().sort(), required: false, submitOnChange:true, width: 4, newLineAfter: schemaOneOfType.isEmpty()) if(!schemaOneOfType.isEmpty()) { input(name: "smartCommandSchemaOneOf", type: "enum", title: " $sSamsungIcon Argument Type:", description: "Choose an Argument Type", options: schemaOneOfType.sort(), required: true, submitOnChange:true, width: 4, newLineAfter:true) } else { app.removeSetting("smartCommandSchemaOneOf") } input(name: "pageConfigureDevice::hubitatAttributeStore", type: "button", title: "Store Rule", width: 2, style:"width:75%;") input(name: "pageConfigureDevice::hubitatAttributeDelete", type: "button", title: "Delete Rule", width: 2, style:"width:75%;") g_mAppDeviceSettings['hubitatAttribute']?.clear() g_mAppDeviceSettings['hubitatAttribute'] = hubitatAttributeOptions?.get(hubitatAttribute) ?: [:] g_mAppDeviceSettings['smartCommand']?.clear() g_mAppDeviceSettings['smartCommand'] = smartCommandOptions?.get(smartCommand) ?: [:] if(smartCommandSchemaOneOf) g_mAppDeviceSettings['smartCommand'].schemaOneOfType = smartCommandSchemaOneOf if(pageConfigureDeviceShowDetail) { String comments = hubitatAttribute&&!g_mAppDeviceSettings['hubitatAttribute'].isEmpty() ? "$sHubitatIcon $hubitatAttribute : ${JsonOutput.toJson(g_mAppDeviceSettings['hubitatAttribute'])}" : "$sHubitatIcon No Selection" comments += smartCommand&&!g_mAppDeviceSettings['smartCommand'].isEmpty() ? "
$sSamsungIcon $smartCommand : ${JsonOutput.toJson(g_mAppDeviceSettings['smartCommand'])}" : "
$sSamsungIcon No Selection" paragraphComment(comments) } paragraph( getFormat("line") ) Map smartAttributeOptions = getSmartAttributeOptions(replicaDevice) Map hubitatCommandOptions = getHubitatCommandOptions(replicaDevice) input(name: "smartAttribute", type: "enum", title: " $sSamsungIcon If SmartThings Attribute TRIGGER changes:", description: "Choose a SmartThings Attribute", options: smartAttributeOptions.keySet().sort(), required: false, submitOnChange:true, width: 4) input(name: "hubitatCommand", type: "enum", title: " $sHubitatIcon Then ACTION Hubitat Command${pageConfigureDeviceAllowActionAttribute?'/Attribute':''}:", description: "Choose a Hubitat Command", options: hubitatCommandOptions.keySet().sort(), required: false, submitOnChange:true, width: 4, newLineAfter:true) input(name: "pageConfigureDevice::smartAttributeStore", type: "button", title: "Store Rule", width: 2, style:"width:75%;") input(name: "pageConfigureDevice::smartAttributeDelete", type: "button", title: "Delete Rule", width: 2, style:"width:75%;") g_mAppDeviceSettings['smartAttribute']?.clear() g_mAppDeviceSettings['smartAttribute'] = smartAttributeOptions?.get(smartAttribute) ?: [:] g_mAppDeviceSettings['hubitatCommand']?.clear() g_mAppDeviceSettings['hubitatCommand'] = hubitatCommandOptions?.get(hubitatCommand) ?: [:] if(pageConfigureDeviceShowDetail) { String comments = smartAttribute&&!g_mAppDeviceSettings['smartAttribute'].isEmpty() ? "$sSamsungIcon $smartAttribute : ${JsonOutput.toJson(g_mAppDeviceSettings['smartAttribute'])}" : "$sSamsungIcon No Selection" comments += hubitatCommand&&!g_mAppDeviceSettings['hubitatCommand'].isEmpty() ? "
$sHubitatIcon $hubitatCommand : ${JsonOutput.toJson(g_mAppDeviceSettings['hubitatCommand'])}" : "
$sHubitatIcon No Selection" paragraphComment(comments) } paragraph( getFormat("line") ) String comments = "Allowing duplicate attribute triggers enables setting more than one command per triggering event. " comments += "Disabling the periodic refresh will stop HubiThings Replica from regular confidence updates which could be problematic with devices that send update events regardless of their current state. " paragraphComment(comments) input(name: "pageConfigureDeviceAllowDuplicateAttribute", type: "bool", title: "Allow duplicate Attribute TRIGGER", defaultValue: false, submitOnChange: true, width: 3) input(name: "pageConfigureDeviceMuteTriggerRuleInfo", type: "bool", title: "Disable TRIGGER $sInfoLogsIcon rule logs", defaultValue: false, submitOnChange: true, width: 3) input(name: "pageConfigureDeviceDisableStatusUpdate", type: "bool", title: "Disable $sPeriodicIcon periodic device refresh", defaultValue: false, submitOnChange: true, width: 3) app.updateSetting("pageConfigureDeviceAllowActionAttribute", false) input(name: "pageConfigureDeviceShowDetail", type: "bool", title: "Show attribute and command detail", defaultValue: false, submitOnChange: true, width: 3, newLineAfter:true) } replicaDevicesRuleSection() } } Map getHubitatCommandOptions(replicaDevice) { // check if this is a replica DH. return if so. Map hubitatCommandOptions = getReplicaCommandOptions(replicaDevice) if(hubitatCommandOptions.size()) return hubitatCommandOptions replicaDevice?.getSupportedCommands()?.each{ command -> Map commandJson = new JsonSlurper().parseText(JsonOutput.toJson(command)) //could not figure out how to convert command object to json. this works. commandJson.remove('id') commandJson.remove('version') commandJson.remove('capability') commandJson["type"] = "command" String parameterText = "(" commandJson?.parameters?.eachWithIndex{ parameter, index -> String parameterName = parameter?.name ? "${parameter?.name?.uncapitalize()}" : "${parameter?.type?.toLowerCase()}*" parameterText += (index ? ", $parameterName" : "$parameterName") } parameterText +=")" commandJson['label'] = "command: ${command?.name}$parameterText" if(commandJson?.arguments==null) commandJson.remove('arguments') if(commandJson?.parameters==null) commandJson.remove('parameters') if(commandJson?.values==null) commandJson.remove('values') hubitatCommandOptions[commandJson.label] = commandJson } if(pageConfigureDeviceAllowActionAttribute) { hubitatCommandOptions += getHubitatAttributeOptions(replicaDevice) } return hubitatCommandOptions } Map getReplicaCommandOptions(replicaDevice) { Map commands = getReplicaDataJsonValue(replicaDevice, "commands") Map replicaCommandOptions = [:] commands?.each{ command, parameters -> String parameterText = "(" parameters?.eachWithIndex{ parameter, index -> parameterText += (index ? ", ${parameter?.name}" : "${parameter?.name}") } parameterText +=")" def label = "command: ${command}$parameterText" replicaCommandOptions[label] = [name:command, label:label, type:'command'] if(parameters.size()) replicaCommandOptions[label].parameters = parameters } return replicaCommandOptions } Map getHubitatAttributeOptions(replicaDevice) { Map hubitatAttributeOptions = getReplicaTriggerOptions(replicaDevice) // might be a replica DH replicaDevice?.getSupportedAttributes()?.each{ attribute -> Map attributeJson = new JsonSlurper().parseText(JsonOutput.toJson(attribute)) attributeJson.remove('possibleValueJson') attributeJson.remove('possibleValues') attributeJson.remove('id') attributeJson.remove('version') attributeJson.remove('deviceTypeId') attributeJson.remove('capability') attributeJson["type"] = "attribute" if(attributeJson?.dataType=="ENUM") { def label = "attribute: ${attributeJson?.name}.*" hubitatAttributeOptions[label] = attributeJson.clone() hubitatAttributeOptions[label].label = label hubitatAttributeOptions[label].remove('values') attributeJson?.values?.each{ enumValue -> label = "attribute: ${attributeJson?.name}.${enumValue}" hubitatAttributeOptions[label] = attributeJson.clone() hubitatAttributeOptions[label].remove('values') hubitatAttributeOptions[label].label = label hubitatAttributeOptions[label].value = enumValue } } else { attributeJson['label'] = "attribute: ${attributeJson?.name}.*" hubitatAttributeOptions[attributeJson.label] = attributeJson hubitatAttributeOptions[attributeJson.label].remove('values') } } return hubitatAttributeOptions } Map getReplicaTriggerOptions(replicaDevice) { Map triggers = getReplicaDataJsonValue(replicaDevice, "triggers") Map replicaTriggerOptions = [:] triggers?.each{ command, parameters -> String parameterText = "(" parameters?.eachWithIndex{ parameter, index -> parameterText += (index ? ", ${parameter?.name}" : "${parameter?.name}") } parameterText +=")" def label = "command: ${command}$parameterText" replicaTriggerOptions[label] = [name:command, label:label, type:'command'] if(parameters.size()) replicaTriggerOptions[label].parameters = parameters } return replicaTriggerOptions } Map getSmartCommandOptions(replicaDevice) { Map smartCommandOptions = [:] Map capabilities = getReplicaDataJsonValue(replicaDevice, "capabilities") changeKeyValue("pattern", "removed", capabilities) capabilities?.components?.each{ capability -> capability?.commands?.each{ command, value -> def parameterText = "(" value?.arguments?.eachWithIndex{ parameter, index -> def parameterName = parameter?.optional ? "${parameter?.name?.uncapitalize()}" : "${parameter?.name?.uncapitalize()}*" parameterText += (index ? ", $parameterName" : "$parameterName") } parameterText +=")" value["type"] = "command" value["capability"] = capability.id def label = "command: ${command}$parameterText" if(smartCommandOptions[label]) { // this device has conflicting commands from different capablities. like alarm & switch def newLabel = "command: ${smartCommandOptions[label].capability}:${command}$parameterText" smartCommandOptions[newLabel] = smartCommandOptions[label] smartCommandOptions[newLabel].label = newLabel smartCommandOptions?.remove(label.toString()) label = "command: ${capability.id}:${command}$parameterText" } value["label"] = label if(value?.arguments==[]) value.remove('arguments') smartCommandOptions[value.label] = value } } if(getReplicaDataJsonValue(replicaDevice, "description")?.type=="VIRTUAL") smartCommandOptions+=getSmartAttributeOptions(replicaDevice) return smartCommandOptions } Map getSmartAttributeOptions(replicaDevice) { Map smartAttributeOptions = [:] Map capabilities = getReplicaDataJsonValue(replicaDevice, "capabilities") changeKeyValue("pattern", "removed", capabilities) capabilities?.components?.each{ capability -> capability?.attributes?.each{ attribute, value -> Map schema = value?.schema ?: [:] schema["capability"] = capability.id schema["attribute"] = attribute schema["type"] = "attribute" if(schema?.properties?.value?.enum) { def label = "attribute: ${attribute}.*" if(smartAttributeOptions[label]) { label = "attribute: ${capability.id}:${attribute}.*" } // duplicate attribute. rare case like TV. smartAttributeOptions[label] = schema.clone() smartAttributeOptions[label].label = label schema?.properties?.value?.enum?.each{ enumValue -> label = "attribute: ${attribute}.${enumValue}" if(smartAttributeOptions[label]) { label = "attribute: ${capability.id}:${attribute}.${enumValue}" } // duplicate attribute. rare case like TV. smartAttributeOptions[label] = schema.clone() smartAttributeOptions[label].label = label smartAttributeOptions[label].value = enumValue smartAttributeOptions[label].dataType = "ENUM" //match hubitat } } else { def type = schema?.properties?.value?.type schema["label"] = "attribute: ${attribute}.*" if(smartAttributeOptions[schema.label]) { schema.label = "attribute: ${capability.id}:${attribute}.*" } // duplicate attribute. rare case like TV. smartAttributeOptions[schema.label] = schema } } } // not sure why SmartThings treats health different. But everything reports healthStatus. So gonna make it look the same to the user configuration page. if(capabilities?.size()) { smartAttributeOptions["attribute: healthStatus.*"] = new JsonSlurper().parseText("""{"type":"attribute","properties":{"value":{"title":"HealthState","type":"string","enum":["offline","online"]}},"additionalProperties":false,"required":["value"],"capability":"healthCheck","attribute":"healthStatus","label":"attribute: healthStatus.*"}""") smartAttributeOptions["attribute: healthStatus.offline"] = new JsonSlurper().parseText("""{"type":"attribute","properties":{"value":{"title":"HealthState","type":"string","enum":["offline","online"]}},"additionalProperties":false,"required":["value"],"capability":"healthCheck","value":"offline","attribute":"healthStatus","dataType":"ENUM","label":"attribute: healthStatus.offline"}""") smartAttributeOptions["attribute: healthStatus.online"] = new JsonSlurper().parseText("""{"type":"attribute","properties":{"value":{"title":"HealthState","type":"string","enum":["offline","online"]}},"additionalProperties":false,"required":["value"],"capability":"healthCheck","value":"online","attribute":"healthStatus","dataType":"ENUM","label":"attribute: healthStatus.online"}""") } return smartAttributeOptions } void appButtonHandler(String btn) { logDebug "${app.getLabel()} executing 'appButtonHandler($btn)'" if(!appButtonHandlerLock()) return if(btn.contains("::")) { List items = btn.tokenize("::") if(items && items.size() > 1 && items[1]) { String k = (String)items[0] String v = (String)items[1] String a = (String)items[2] logTrace "Button [$k] [$v] pressed" switch(k) { case "pageConfigureDevice": switch(v) { case "hubitatAttributeStore": updateRuleList('store','hubitatTrigger') break case "hubitatAttributeDelete": updateRuleList('delete','hubitatTrigger') break case "smartAttributeStore": updateRuleList('store','smartTrigger') break case "smartAttributeDelete": updateRuleList('delete','smartTrigger') break case "configDeviceRules": def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) if(replicaDevice?.hasCommand('configure')) { replicaDevice.configure() } break case "clearDeviceRules": def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) clearReplicaDataCache(replicaDevice, "rules", true) unsubscribe(replicaDevice) break case "refreshDevice": def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) replicaDeviceRefresh(replicaDevice) break } break case "dynamic": if(a) this."$v"(a) else this."$v"() break default: logInfo "Not supported" } } } appButtonHandlerUnLock() } @Field volatile static Map g_bAppButtonHandlerLock = [:] Boolean appButtonHandlerLock() { if(g_bAppButtonHandlerLock[app.id]) { logInfo "${app.getLabel()} appButtonHandlerLock is locked"; return false } g_bAppButtonHandlerLock[app.id] = true runIn(10,appButtonHandlerUnLock) return true } void appButtonHandlerUnLock() { unschedule('appButtonHandlerUnLock') g_bAppButtonHandlerLock[app.id] = false } void allSmartDeviceRefresh(delay=1) { // brute force grabbing all devices in my OAuths. // smartLocationQuery is async so will not be available for first refresh Map smartDevices = [items:[]] getChildApps()?.each{ smartDevices.items.addAll( it.getSmartSubscribedDevices()?.items ) it.smartLocationQuery() } setSmartDevicesMap( smartDevices ) // check that everything is happy. This blocks for a second per device and will not allow concur runs. So thread it. runIn(delay<1?:1, getSmartDeviceRefresh) } void locationModeHandler(def event) { //subscribe(location, "mode", locationModeHandler) logDebug "${app.getLabel()} executing 'locationModeHandler($event.value)'" getAllReplicaDevices()?.each { replicaDevice -> if(replicaDevice?.hasCommand('setLocationMode')) { replicaDevice.setLocationMode(event.value,true) } } } Boolean locationKnobExecute(Map event, delay=1) { logDebug "${app.getLabel()} executing 'locationKnobExecute($event)'" if(event?.name=="execute" && event?.value=="command" && event?.data?.command) { runIn(delay<1?:1, locationKnobExecuteHelper, [data: event]) return true } return false } void locationKnobExecuteHelper(Map event) { logDebug "${app.getLabel()} executing 'locationKnobExecuteHelper($event)'" getSmartDeviceRefresh( event?.data?.command=="REFRESH_LOCATION_DEVICES" ? event?.data?.locationId : null ) } void getSmartDeviceRefresh(String locationId=null) { logDebug "${app.getLabel()} executing 'getSmartDeviceRefresh(locationId=$locationId)'" if(!appGetSmartDeviceRefreshLock()) return getAllReplicaDevices()?.each { replicaDevice -> if(locationId==null || (locationId==getReplicaDataJsonValue(replicaDevice, "description")?.locationId)) { getReplicaDeviceRefresh(replicaDevice) // this blocks for a couple seconds per device } } appGetSmartDeviceRefreshUnLock() } @Field volatile static Map g_bAppGetSmartDeviceRefreshLock = [:] Boolean appGetSmartDeviceRefreshLock() { if(g_bAppGetSmartDeviceRefreshLock[app.id]) { logInfo "${app.getLabel()} appGetSmartDeviceRefreshLock is locked"; return false } g_bAppGetSmartDeviceRefreshLock[app.id] = true runIn(30,appGetSmartDeviceRefreshUnLock) return true } void appGetSmartDeviceRefreshUnLock() { unschedule('appGetSmartDeviceRefreshUnLock') g_bAppGetSmartDeviceRefreshLock[app.id] = false } void deviceTriggerHandler(def event) { // called from subscribe HE device events. value is always string, but just converting anyway to be sure. //event.properties.each { logInfo "$it.key -> $it.value" } deviceTriggerHandlerPrivate(event?.getDevice(), event?.name, event?.value.toString(), event?.unit, event?.getJsonData()) } void deviceTriggerHandler(def replicaDevice, Map event) { // called from replica HE drivers. value might not be a string, converting it to normalize with events above. Boolean result = deviceTriggerHandlerPrivate(replicaDevice, event?.name, event?.value.toString(), event?.unit, event?.data, event?.now) if(locationKnobExecute(event)) { // noop } else if(event?.name == "configure" || event?.name == "refresh") { clearReplicaDataCache(replicaDevice) replicaDeviceRefresh(replicaDevice) } else if(!result) { logInfo "${app.getLabel()} executing 'deviceTriggerHandler()' replicaDevice:'${replicaDevice.getDisplayName()}' event:'${event?.name}' is not rule configured" } } private Boolean deviceTriggerHandlerPrivate(def replicaDevice, String eventName, String eventValue, String eventUnit, Map eventJsonData, Long eventPostTime=null) { eventPostTime = eventPostTime ?: now() logDebug "${app.getLabel()} executing 'deviceTriggerHandlerPrivate()' replicaDevice:'${replicaDevice.getDisplayName()}' name:'$eventName' value:'$eventValue' unit:'$eventUnit', data:'$eventJsonData'" Boolean response = false getReplicaDataJsonValue(replicaDevice, "rules")?.components?.findAll{ it?.type=="hubitatTrigger" && it?.trigger?.name==eventName && (!it?.trigger?.value || it?.trigger?.value==eventValue) }?.each { rule -> Map trigger = rule?.trigger Map command = rule?.command // commands are always passed. // this allows for cross setting switch.off->contact.closed // attempt to block sending anything duplicate and store for reflections if(trigger?.type=="command" || command?.attribute&&command?.attribute!=eventName || !deviceTriggerHandlerCache(replicaDevice, eventName, eventValue)) { String type = (command?.type!="attribute") ? (command?.arguments?.getAt(0)?.schema?.type?.toLowerCase() ?: command?.schemaOneOfType?.toLowerCase()) : command?.properties?.value?.type?.toLowerCase() def arguments = null switch(type) { case 'integer': // A whole number. Limits can be defined to constrain the range of possible values. arguments = [ (eventValue?.isNumber() ? (eventValue?.isFloat() ? (int)(Math.round(eventValue?.toFloat())) : eventValue?.toInteger()) : null) ] break case 'number': // A number that can have fractional values. Limits can be defined to constrain the range of possible values. arguments = [ eventValue?.toFloat() ] break case 'boolean': // Either true or false arguments = [ eventValue?.toBoolean() ] break case 'object': // A map of name value pairs, where the values can be of different types. try { Map map = new JsonSlurper().parseText(eventValue) arguments = [ map ] //updated version v1.2.10 } catch (e) { logWarn "${app.getLabel()} deviceTriggerHandlerPrivate() received $eventValue and not type 'object' as expected" } break case 'array': // A list of values of a single type. try { List list = new JsonSlurper().parseText(eventValue) arguments = (command?.type!="attribute") ? list : [ list ] } catch (e) { logWarn "${app.getLabel()} deviceTriggerHandlerPrivate() received $eventValue and not type 'array' as expected" } break case 'string': // enum cases arguments = [ (command?.value?:eventValue) ] break case null: // commands with no arguments will be type null arguments = [] break default: logWarn "${app.getLabel()} deviceTriggerHandlerPrivate() ${trigger?.type?:"event"}:'$eventName' has unknown argument type: $type" arguments = [] break } if(trigger?.parameters) { // add any additonal arguments. (these should be evaluated correct type since they are not a event 'value' ^^ which is defined as string) arguments = arguments?.plus( trigger?.parameters?.findResults{ parameter -> parameter?.data ? eventJsonData?.get(parameter?.data) : null }) } String deviceId = getReplicaDeviceId(replicaDevice) String componentId = getReplicaDataJsonValue(replicaDevice, "replica")?.componentId ?: "main" //componentId was not added until v1.2.06 if(command?.type!="attribute" && arguments!=null) { response = setSmartDeviceCommand(deviceId, componentId, command?.capability, command?.name, arguments)?.statusCode==iHttpSuccess if(!rule?.mute) logInfo "${app.getLabel()} sending '${replicaDevice?.getDisplayName()}' ${type?:""} ● trigger:${trigger?.type=="command"?"command":"event"}:${eventName} ➣ command:${command?.name}${arguments?.size()?":"+arguments?.toString():""} ● delay:${now() - eventPostTime}ms" } else if(arguments!=null) { // fix some brokes eventUnit = eventUnit?.replaceAll('°','') eventJsonData?.remove("version") if( command?.required?.contains("unit") && !command?.properties?.unit?.enum?.contains(eventUnit) ) logWarn "${app.getLabel()} deviceTriggerHandlerPrivate() requires unit value defined as ${command?.properties?.unit?.enum?:""} but found $eventUnit" response = setVirtualDeviceAttribute(deviceId, componentId, command?.capability, command?.attribute, arguments?.getAt(0), eventUnit, eventJsonData)?.statusCode==iHttpSuccess if(!rule?.mute) logInfo "${app.getLabel()} sending '${replicaDevice?.getDisplayName()}' ${type?:""} ○ trigger:${trigger?.type=="command"?"command":"event"}:${eventName} ➣ command:${command?.attribute}:${arguments?.getAt(0)?.toString()} ● delay:${now() - eventPostTime}ms" } } } return response } Boolean deviceTriggerHandlerCache(replicaDevice, attribute, value) { logDebug "${app.getLabel()} executing 'deviceTriggerHandlerCache()' replicaDevice:'${replicaDevice?.getDisplayName()}'" Boolean response = false Map device = getReplicaDeviceEventsCache(replicaDevice) if (device) { String a = device?.eventCache?.get(attribute).toString() String b = value.toString() if(a.isBigInteger() && b.isBigInteger()) { response = a==b logTrace "a:$a == b:$b match integer is $response" } else if(a.isNumber() && b.isNumber()) { response = Float.valueOf(a).round(4)==Float.valueOf(b).round(4) logTrace "a:$a == b:$b match float is $response" } else { response = a==b logTrace "a:$a == b:$b match string is $response" } logDebug "${app.getLabel()} cache <= ${device?.eventCache} match string:$response" } return response } void smartTriggerHandlerCache(replicaDevice, attribute, value) { logDebug "${app.getLabel()} executing 'smartTriggerHandlerCache()' replicaDevice:'${replicaDevice?.getDisplayName()}'" Map device = getReplicaDeviceEventsCache(replicaDevice) if (device!=null) { device?.eventCache[attribute] = value logDebug "${app.getLabel()} cache => ${device?.eventCache}" } } Map smartTriggerHandler(replicaDevice, Map event, String type, Long eventPostTime=null) { logDebug "${app.getLabel()} executing 'smartTriggerHandler()' replicaDevice:'${replicaDevice?.getDisplayName()}'" Map response = [statusCode:iHttpError] Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") event?.each { capability, attributes -> attributes?.each{ attribute, value -> logTrace "smartEvent: capability:'$capability' attribute:'$attribute' value:'$value'" replicaDeviceRules?.components?.findAll{ it?.type == "smartTrigger" }?.each { rule -> Map trigger = rule?.trigger Map command = rule?.command // simple enum case if(capability==trigger?.capability && attribute==trigger?.attribute && value?.value==trigger?.value) { smartTriggerHandlerCache(replicaDevice, attribute, value?.value) List args = [] String method = command?.name if(hasCommand(replicaDevice, method) && !(type=="status" && rule?.disableStatus)) { replicaDevice."$method"(*args) if(!rule?.mute) logInfo "${app.getLabel()} received '${replicaDevice?.getDisplayName()}' $type ○ trigger:$attribute:${value?.value} ➢ command:${command?.name} ${(eventPostTime ? "● delay:${now() - eventPostTime}ms" : "")}" } } // non-enum case else if(capability==trigger?.capability && attribute==trigger?.attribute && !trigger?.value) { smartTriggerHandlerCache(replicaDevice, attribute, value?.value) String method = command?.name String argType = hasCommand(replicaDevice, method) ? hasCommandType(replicaDevice, method) : null if(argType && !(type=="status" && rule?.disableStatus)) { if(argType!="JSON_OBJECT") replicaDevice."$method"(*[value.value]) else replicaDevice."$method"(*[[capability:capability, attribute:attribute, value:(value.value), unit:(value?.unit?:""), data:(value?.data?:[:]), stateChange:(value?.stateChange?:false), timestamp:(value?.timestamp)]]); if(!rule?.mute) logInfo "${app.getLabel()} received '${replicaDevice?.getDisplayName()}' $type ● trigger:$attribute ➢ command:${command?.name}:${value?.value} ${(eventPostTime ? "● delay:${now() - eventPostTime}ms" : "")}" } } } } response.statusCode = iHttpSuccess } return [statusCode:response.statusCode] } Boolean hasCommand(def replicaDevice, String method) { Boolean response = replicaDevice.hasCommand(method) if(!response) { response = (getReplicaDataJsonValue(replicaDevice, "commands")?.keySet()?.find{ it==method } != null) } return response } String hasCommandType(def replicaDevice, String method) { String response = replicaDevice.hasCommand(method) ? "STRING" : null if(!response) { List value = (getReplicaDataJsonValue(replicaDevice, "commands")?.find{ key, value -> key==method }?.value) response = (value?.size() && value?.get(0)?.type) ? value?.get(0)?.type : "STRING" logTrace "custom command: $value -> $response" } return response } Map getReplicaDeviceEventsCache(replicaDevice) { String deviceId = getReplicaDeviceId(replicaDevice) return (deviceId ? getSmartDeviceEventsCache(deviceId) : [:]) } Map getSmartDeviceEventsCache(deviceId) { Map response = [:] try { Long appId = app.getId() if(g_mSmartDeviceStatusMap[appId]==null) { g_mSmartDeviceStatusMap[appId] = [:] } if(!g_mSmartDeviceStatusMap[appId]?.get(deviceId)) { g_mSmartDeviceStatusMap[appId][deviceId] = [ eventCount:0, eventCache:[:] ] } response = g_mSmartDeviceStatusMap[appId]?.get(deviceId) } catch(e) { //we don't care. //logWarn "getSmartDeviceEventsCache $e" } return response } String updateSmartDeviceEventsStatus(replicaDevice) { String value = "--" if(replicaDevice) { String healthState = getReplicaDataJsonValue(replicaDevice, "health")?.state?.toLowerCase() String noRules = getReplicaDataJsonValue(replicaDevice, "rules")?.components ? "" : "$sWarningsIcon $sNoRules" String eventCount = (getReplicaDeviceEventsCache(replicaDevice)?.eventCount ?: 0).toString() value = (healthState=='offline' ? "$sWarningsIcon $sOffline" : noRules ?: eventCount) if(state.pageMainLastRefresh && (state.pageMainLastRefresh + (iPageMainRefreshInterval*1000)) > now()) { //only send if someone is watching sendEvent(name:'smartEvent', value:value, descriptionText: JsonOutput.toJson([ deviceNetworkId:(replicaDevice?.deviceNetworkId), debug: appLogEnable ])) } } return value } Map smartStatusHandler(replicaDevice, String deviceId, Map statusEvent, Long eventPostTime=null) { logDebug "${app.getLabel()} executing 'smartStatusHandler()' replicaDevice:'${replicaDevice?.getDisplayName()}'" Map response = [statusCode:iHttpError] if(appLogEventEnable && statusEvent && (!appLogEventEnableDevice || appLogEventEnableDevice==deviceId)) { log.info "Status: ${JsonOutput.toJson(statusEvent)}" } if(hasCommand(replicaDevice, 'replicaStatus')) { statusEvent.deviceId = deviceId replicaDevice.replicaStatus(app, statusEvent) } else { setReplicaDataJsonValue(replicaDevice, "status", statusEvent) } String componentId = getReplicaDataJsonValue(replicaDevice, "replica")?.componentId ?: "main" //componentId was not added until v1.2.06 statusEvent?.components?.get(componentId)?.each { capability, attributes -> response.statusCode = smartTriggerHandler(replicaDevice, [ "$capability":attributes ], "status", eventPostTime).statusCode } if( updateSmartDeviceEventsStatus(replicaDevice) == 'offline' ) { getSmartDeviceHealth( getReplicaDeviceId(replicaDevice) ) } return [statusCode:response.statusCode] } Map smartEventHandler(replicaDevice, Map deviceEvent, Long eventPostTime=null){ logDebug "${app.getLabel()} executing 'smartEventHandler()' replicaDevice:'${replicaDevice.getDisplayName()}'" Map response = [statusCode:iHttpSuccess] if(appLogEventEnable && deviceEvent && (!appLogEventEnableDevice || appLogEventEnableDevice==deviceEvent?.deviceId)) { log.info "Event: ${JsonOutput.toJson(deviceEvent)}" } //setReplicaDataJsonValue(replicaDevice, "event", deviceEvent) try { // events do not carry units. so get it from status. yeah smartthings is great! String unit = getReplicaDataJsonValue(replicaDevice, "status")?.components?.get(deviceEvent.componentId)?.get(deviceEvent.capability)?.get(deviceEvent.attribute)?.unit // status {"switchLevel": {"level": {"value":30, "unit":"%", "timestamp":"2022-09-07T21:16:59.576Z" }}} Map event = [ (deviceEvent.capability): [ (deviceEvent.attribute): [ value:(deviceEvent.value), unit:(deviceEvent?.unit ?: unit), data:(deviceEvent?.data), stateChange:(deviceEvent?.stateChange), timestamp: getTimestampSmartFormat() ]]] logTrace JsonOutput.toJson(event) response.statusCode = smartTriggerHandler(replicaDevice, event, "event", eventPostTime).statusCode if( updateSmartDeviceEventsStatus(replicaDevice) == 'offline' ) { getSmartDeviceHealth( getReplicaDeviceId(replicaDevice) ) } } catch (e) { logWarn "${app.getLabel()} smartEventHandler() error: $e : $deviceEvent" } return [statusCode:response.statusCode] } Map smartHealthHandler(replicaDevice, String deviceId, Map healthEvent, Long eventPostTime=null){ logDebug "${app.getLabel()} executing 'smartHealthHandler()' replicaDevice:'${replicaDevice?.getDisplayName()}'" Map response = [statusCode:iHttpError] if(appLogEventEnable && healthEvent && (!appLogEventEnableDevice || appLogEventEnableDevice==deviceId)) { log.info "Health: ${JsonOutput.toJson(healthEvent)}" } if(hasCommand(replicaDevice, 'replicaHealth')) { healthEvent.deviceId = deviceId replicaDevice.replicaHealth(app, healthEvent) } else { setReplicaDataJsonValue(replicaDevice, "health", healthEvent) } try { //{"deviceId":"2c80c1d7-d05e-430a-9ddb-1630ee457afb","state":"ONLINE","lastUpdatedDate":"2022-09-07T16:47:06.859Z"} // status {"switchLevel":{"level": {"value":30, "unit":"","timestamp":"2022-09-07T21:16:59.576Z" }}} Map event = [ healthCheck: [ healthStatus: [ value:(healthEvent?.state?.toLowerCase()), timestamp: healthEvent?.lastUpdatedDate, reason:(healthEvent?.reason ?: 'poll') ]]] logTrace JsonOutput.toJson(event) response.statusCode = smartTriggerHandler(replicaDevice, event, "health", eventPostTime).statusCode updateSmartDeviceEventsStatus(replicaDevice) } catch (e) { logWarn "${app.getLabel()} smartHealthHandler() error: $e : $healthEvent" } return [statusCode:response.statusCode] } Map smartDescriptionHandler(replicaDevice, Map descriptionEvent){ logDebug "${app.getLabel()} executing 'smartDescriptionHandler()' replicaDevice:'${replicaDevice.getDisplayName()}'" Map response = [statusCode:iHttpError] setReplicaDataJsonValue(replicaDevice, "description", descriptionEvent) try { getReplicaCapabilities(replicaDevice) // This should probably be threaded since it could be a large amount of calls. response.statusCode = iHttpSuccess } catch (e) { logWarn "${app.getLabel()} smartDescriptionHandler() error: $e : $descriptionEvent" } return [statusCode:response.statusCode] } Map smartCapabilityHandler(replicaDevice, Map capabilityEvent){ logDebug "${app.getLabel()} executing 'smartCapabilityHandler()' replicaDevice:'${replicaDevice.getDisplayName()}'" Map response = [statusCode:iHttpError] Map capabilities = getReplicaDataJsonValue(replicaDevice, "capabilities") try { if (capabilities?.components) { if (capabilities.components?.find { components -> components?.id==capabilityEvent.id && components?.version==capabilityEvent.version }) { logInfo "${app.getLabel()} '${replicaDevice.getDisplayName()}' attribute ${capabilityEvent.id} FOUND" } else { logInfo "${app.getLabel()} '${replicaDevice.getDisplayName()}' attribute ${capabilityEvent.id} ADDED" capabilities.components.add(capabilityEvent) setReplicaDataJsonValue(replicaDevice, "capabilities", capabilities) } } else { // first time. So just store. logInfo "${app.getLabel()} '${replicaDevice.getDisplayName()}' attribute ${capabilityEvent.id} ADDED*" setReplicaDataJsonValue(replicaDevice, "capabilities", ([components : [ capabilityEvent ]])) } response.statusCode = iHttpSuccess } catch (e) { logWarn "${app.getLabel()} smartCapabilityHandler() error: $e : $capabilityEvent" } return [statusCode:response.statusCode] } Map replicaHasSmartCapability(replicaDevice, String capabilityId, Integer capabilityVersion=1) { Map response = [:] Map capability = getReplicaDataJsonValue(replicaDevice, "capabilities") if (capability?.components?.find { components -> components?.id == capabilityId && components?.version == capabilityVersion }) { logDebug "Capability ${capabilityId} is cached" response = capability } return response } void getAllReplicaCapabilities(replicaDevice, Integer delay=1) { logInfo "${app.getLabel()} refreshing all '$replicaDevice' device capabilities" runIn(delay<1?:1, getAllReplicaCapabilitiesHelper, [data: [deviceNetworkId:(replicaDevice.deviceNetworkId)]]) } void getAllReplicaCapabilitiesHelper(Map data) { def replicaDevice = getDevice(data?.deviceNetworkId) getReplicaCapabilities(replicaDevice) } void getReplicaCapabilities(replicaDevice) { logDebug "${app.getLabel()} executing 'getReplicaCapabilities($replicaDevice)'" String deviceId = getReplicaDeviceId(replicaDevice) Map description = getReplicaDataJsonValue(replicaDevice, "description") description?.components.each { components -> components?.capabilities.each { capabilities -> if (replicaHasSmartCapability(replicaDevice, capabilities.id, capabilities.version) == [:]) { getSmartDeviceCapability(deviceId, capabilities.id, capabilities.version) pauseExecution(250) // no need to hammer ST } } } } Map getSmartDeviceCapability(String deviceId, String capabilityId, Integer capabilityVersion=1) { logDebug "${app.getLabel()} executing 'getSmartDeviceCapability()'" Map response = [statusCode:iHttpError] Map data = [ uri: sURI, path: "/capabilities/${capabilityId}/${capabilityVersion}", deviceId: deviceId, method: "getSmartDeviceCapability" ] response.statusCode = asyncHttpGet("asyncHttpGetCallback", data).statusCode return response } void allSmartDeviceHealth(Integer delay=1) { logInfo "${app.getLabel()} refreshing all SmartThings device health" runIn(delay<1?:1, getAllSmartDeviceHealth) } void getAllSmartDeviceHealth() { logDebug "${app.getLabel()} executing 'getAllSmartDeviceHealth()'" runIn(60, setAllSmartDeviceHealthOffline) // if no reponse in 60 seconds, declare everything offline getAllReplicaDeviceIds()?.each { deviceId -> getSmartDeviceHealth(deviceId) pauseExecution(250) // no need to hammer ST } } void setAllSmartDeviceHealthOffline() { logDebug "${app.getLabel()} executing 'setAllSmartDeviceHealthOffline()'" getAllReplicaDeviceIds()?.each { deviceId -> getReplicaDevices(deviceId)?.each { replicaDevice -> Map healthEvent = ["deviceId":deviceId, "state":"OFFLINE","lastUpdatedDate":getTimestampSmartFormat()] smartHealthHandler(replicaDevice, deviceId, healthEvent) } } } Map getSmartDeviceHealth(String deviceId) { logDebug "${app.getLabel()} executing 'getSmartDeviceHealth($deviceId)'" Map response = [statusCode:iHttpError] Map data = [ uri: sURI, path: "/devices/${deviceId}/health", deviceId: deviceId, method: "getSmartDeviceHealth" ] response.statusCode = asyncHttpGet("asyncHttpGetCallback", data).statusCode return response } void allSmartDeviceDescription(Integer delay=1) { logInfo "${app.getLabel()} refreshing all SmartThings device descriptions" runIn(delay<1?:1, getAllDeviceDescription) } void getAllDeviceDescription() { logDebug "${app.getLabel()} executing 'getAllDeviceDescription()'" getAllReplicaDeviceIds()?.each { deviceId -> getSmartDeviceDescription(deviceId) pauseExecution(250) // no need to hammer ST } } Map getSmartDeviceDescription(String deviceId) { logDebug "${app.getLabel()} executing 'getSmartDeviceDescription($deviceId)'" Map response = [statusCode:iHttpError] Map data = [ uri: sURI, path: "/devices/${deviceId}", deviceId: deviceId, method: "getSmartDeviceDescription" ] response.statusCode = asyncHttpGet("asyncHttpGetCallback", data).statusCode return response } void allSmartDeviceStatus(Integer delay=1) { logInfo "${app.getLabel()} refreshing all SmartThings device status" runIn(delay<1?:1, getAllSmartDeviceStatus) } void getAllSmartDeviceStatus() { logDebug "${app.getLabel()} executing 'getAllSmartDeviceStatus()'" getAllReplicaDeviceIds()?.each { deviceId -> getSmartDeviceStatus(deviceId) pauseExecution(250) // no need to hammer ST } } Map getSmartDeviceStatus(String deviceId) { logDebug "${app.getLabel()} executing 'getSmartDeviceStatus($deviceId)'" Map response = [statusCode:iHttpError] Map data = [ uri: sURI, path: "/devices/${deviceId}/status", deviceId: deviceId, method: "getSmartDeviceStatus" ] response.statusCode = asyncHttpGet("asyncHttpGetCallback", data).statusCode return response } private Map asyncHttpGet(String callbackMethod, Map data) { logDebug "${app.getLabel()} executing 'asyncHttpGet()'" Map response = [statusCode:iHttpError] Map params = [ uri: data.uri, path: data.path, headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { asynchttpGet(callbackMethod, params, data) response.statusCode = iHttpSuccess } catch (e) { logWarn "${app.getLabel()} asyncHttpGet() error: $e" } return response } void asyncHttpGetCallback(resp, data) { logDebug "${app.getLabel()} executing 'asyncHttpGetCallback()' status: ${resp.status} method: ${data?.method}" if (resp.status == iHttpSuccess) { resp.headers.each { logTrace "${it.key} : ${it.value}" } logTrace "response data: ${resp.data}" switch(data?.method) { case "getSmartDeviceHealth": def health = new JsonSlurper().parseText(resp.data) getReplicaDevices(data.deviceId)?.each { replicaDevice -> smartHealthHandler(replicaDevice, data.deviceId, health) } unschedule('setAllSmartDeviceHealthOffline') health = null break case "getSmartDeviceCapability": def capability = new JsonSlurper().parseText(resp.data) getReplicaDevices(data.deviceId)?.each { replicaDevice -> smartCapabilityHandler(replicaDevice, capability) } capability = null break case "getSmartDeviceDescription": def description = new JsonSlurper().parseText(resp.data) getReplicaDevices(data.deviceId)?.each { replicaDevice -> smartDescriptionHandler(replicaDevice, description); } description = null break case "getSmartDeviceStatus": Map status = new JsonSlurper().parseText(resp.data) getReplicaDevices(data.deviceId)?.each { replicaDevice -> smartStatusHandler(replicaDevice, data.deviceId, status) } status = null break default: logWarn "${app.getLabel()} asyncHttpGetCallback() ${data?.method} not supported" if (resp?.data) { logInfo resp.data } } } else { logWarn("${app.getLabel()} asyncHttpGetCallback() ${data?.method} ${data?.deviceId ? getReplicaDevices(data.deviceId) : ""} status:${resp.status} reason:${resp.errorMessage}") } } Map setSmartDeviceCommand(String deviceId, String component, String capability, String command, def arguments = []) { logDebug "${app.getLabel()} executing 'setSmartDeviceCommand()'" Map response = [statusCode:iHttpError] Map commands = [ commands: [[ component: component, capability: capability, command: command, arguments: arguments ]] ] Map params = [ uri: sURI, path: "/devices/$deviceId/commands", body: JsonOutput.toJson(commands), method: "setSmartDeviceCommand", authToken: getAuthToken() ] if(appLogEventEnable && (!appLogEventEnableDevice || appLogEventEnableDevice==deviceId)) { log.info "Device:${getReplicaDevices(deviceId)?.each{ it?.label }} commands:${JsonOutput.toJson(commands)}" } response.statusCode = asyncHttpPostJson("asyncHttpPostCallback", params).statusCode return response } Map setVirtualDeviceAttribute(String deviceId, String component, String capability, String attribute, def value, String unit=null, Map data=[:]) { logDebug "${app.getLabel()} executing 'setVirtualDeviceAttribute()'" Map response = [statusCode:iHttpError] Map deviceEvents = [ deviceEvents: [ [ component: component, capability: capability, attribute: attribute, value: value, unit: unit, data: data ] ] ] Map params = [ uri: sURI, path: "/virtualdevices/$deviceId/events", body: JsonOutput.toJson(deviceEvents), method: "setVirtualDeviceAttribute", authToken: getAuthToken() ] if(appLogEventEnable && (!appLogEventEnableDevice || appLogEventEnableDevice==deviceId)) { log.info "Device:${getReplicaDevices(deviceId)?.each{ it?.label }} deviceEvents:${JsonOutput.toJson(deviceEvents)}" } response.statusCode = asyncHttpPostJson("asyncHttpPostCallback", params).statusCode return response } private Map asyncHttpPostJson(String callbackMethod, Map data) { logDebug "${app.getLabel()} executing 'asyncHttpPostJson()'" Map response = [statusCode:iHttpError] Map params = [ uri: data.uri, path: data.path, body: data.body, contentType: "application/json", requestContentType: "application/json", headers: [ Authorization: "Bearer ${getAuthToken()}" ] ] try { asynchttpPost(callbackMethod, params, data) response.statusCode = iHttpSuccess } catch (e) { logWarn "${app.getLabel()} asyncHttpPostJson() error: $e" } return response } void asyncHttpPostCallback(resp, data) { logDebug "${app.getLabel()} executing 'asyncHttpPostCallback()' status: ${resp.status} method: ${data?.method}" if(resp.status==iHttpSuccess) { resp.headers.each { logTrace "${it.key} : ${it.value}" } logDebug "response data: ${resp.data}" switch(data?.method) { case "setSmartDeviceCommand": case "setVirtualDeviceAttribute": Map command = new JsonSlurper().parseText(resp.data) logDebug "${app.getLabel()} successful ${data?.method}:${command}" break default: logWarn "${app.getLabel()} asyncHttpPostCallback() ${data?.method} not supported" if (resp?.data) { logInfo resp.data } } } else { resp.headers.each { logTrace "${it.key} : ${it.value}" } logWarn("${app.getLabel()} asyncHttpPostCallback ${data?.method} body:${data?.body} status:${resp.status} reason:${resp.errorMessage}") } } Map oauthEventHandler(Map eventData, Long eventPostTime=null) { logDebug "${app.getLabel()} executing 'oauthEventHandler()'" Map response = [statusCode:iHttpSuccess] eventData?.events?.each { event -> switch(event?.eventType) { case 'DEVICE_EVENT': Map device = getSmartDeviceEventsCache(event?.deviceEvent?.deviceId) if(device?.eventCount!=null) device.eventCount += 1 // this needs to be done here and not the smartEventHandler to allow for componentId support getReplicaDevices(event?.deviceEvent?.deviceId)?.each{ replicaDevice -> if(hasCommand(replicaDevice, 'replicaEvent')) { replicaDevice.replicaEvent(app, event) } } getReplicaDevices(event?.deviceEvent?.deviceId, event?.deviceEvent?.componentId)?.each{ replicaDevice -> response.statusCode = smartEventHandler(replicaDevice, event?.deviceEvent, eventPostTime).statusCode } break case 'DEVICE_HEALTH_EVENT': String deviceId = event?.deviceHealthEvent?.deviceId Map healthEvent = [deviceId:deviceId, state:(event?.deviceHealthEvent?.status), lastUpdatedDate:(event?.eventTime), reason:'event'] getReplicaDevices(deviceId)?.each{ replicaDevice -> response.statusCode = smartHealthHandler(replicaDevice, deviceId, healthEvent, eventPostTime).statusCode logInfo "${app.getLabel()} health event $replicaDevice is ${event?.deviceHealthEvent?.status.toLowerCase()}" } break case 'MODE_EVENT': logDebug "${app.getLabel()} mode event: $event" getAllReplicaDevices()?.each { replicaDevice -> if(hasCommand(replicaDevice, 'setModeValue')) { Map description = getReplicaDataJsonValue(replicaDevice, "description") if(event?.modeEvent?.locationId==description?.locationId) { Map device = getReplicaDeviceEventsCache(replicaDevice) if(device?.eventCount!=null) device.eventCount += 1 updateSmartDeviceEventsStatus(replicaDevice) replicaDevice.setModeValue(event?.modeEvent?.modeId) } } } break case 'DEVICE_LIFECYCLE_EVENT': logTrace "${app.getLabel()} device lifecycle event: $event" switch(event?.deviceLifecycleEvent?.lifecycle) { case 'CREATE': logDebug "${app.getLabel()} CREATE locationId:${event?.deviceLifecycleEvent?.locationId} deviceId:${event?.deviceLifecycleEvent?.deviceId}" getChildApps()?.findAll{ it?.getLocationId()==event?.deviceLifecycleEvent?.locationId }.each{ it?.createSmartDevice(event?.deviceLifecycleEvent?.locationId, event?.deviceLifecycleEvent?.deviceId) } break case 'DELETE': logDebug "${app.getLabel()} DELETE locationId:${event?.deviceLifecycleEvent?.locationId} deviceId:${event?.deviceLifecycleEvent?.deviceId}" getChildApps()?.findAll{ it?.getLocationId()==event?.deviceLifecycleEvent?.locationId }.each{ it?.deleteSmartDevice(event?.deviceLifecycleEvent?.locationId, event?.deviceLifecycleEvent?.deviceId) } break case 'UPDATE': logDebug "${app.getLabel()} UPDATE locationId:${event?.deviceLifecycleEvent?.locationId} deviceId:${event?.deviceLifecycleEvent?.deviceId} update:${event?.deviceLifecycleEvent?.update}" getChildApps()?.findAll{ it?.getLocationId()==event?.deviceLifecycleEvent?.locationId }.each{ it?.getSmartDeviceList() } break case 'ROOM_MOVE': logDebug "${app.getLabel()} ROOM_MOVE locationId:${event?.deviceLifecycleEvent?.locationId} deviceId:${event?.deviceLifecycleEvent?.deviceId} roomMove:${event?.deviceLifecycleEvent?.roomMove}" getChildApps()?.findAll{ it?.getLocationId()==event?.deviceLifecycleEvent?.locationId }.each{ it?.getSmartDeviceList() } break default: logWarn "${app.getLabel()} oauthEventHandler() DEVICE_LIFECYCLE_EVENT did not handle $event" } break default: logInfo "${app.getLabel()} oauthEventHandler() did not handle $event" } } return [statusCode:response.statusCode, eventData:{}] } // https://fontawesomeicons.com/svg/icons // https://www.svgrepo.com/collection/coolicons-line-oval-icons/1 @Field static final String sWarningsIcon=""" """ @Field static final String sPeriodicIcon="""""" @Field static final String sInfoLogsIcon="""""" @Field static final String sSamsungIconStatic=""" """ @Field static final String sSamsungIcon="""""" @Field static final String sHubitatIconStatic=""" """ @Field static final String sHubitatIcon="""""" @Field static final String sImgRule = """""" @Field static final String sImgDevv = """""" @Field static final String sImgMirr = """""" @Field static final String sImgDevh = """""" @Field static final String sImgGitH = """ """ /* * Natural Sort algorithm for Javascript - Version 0.7 - Released under MIT license * Author: Jim Palmer (based on chunking idea from Dave Koelle) * Contributors: Mike Grier (mgrier.com), Clint Priest, Kyle Adams, guillermo * See: http://js-naturalsort.googlecode.com/svn/trunk/naturalSort.js * https://datatables.net/plug-ins/sorting/natural#Plug-in-code */ @Field static final String naturalSortFunction = """ (function() { function naturalSort (a, b, html) { var re = /(^-?[0-9]+(\\.?[0-9]*)[df]?e?[0-9]?%?\$|^0x[0-9a-f]+\$|[0-9]+)/gi, sre = /(^[ ]*|[ ]*\$)/g, hre = /^0x[0-9a-f]+\$/i, ore = /^0/, htmre = /(<([^>]+)>)/ig, dre = /(^([\\w ]+,?[\\w ]+)?[\\w ]+,?[\\w ]+\\d+:\\d+(:\\d+)?[\\w ]?|^\\d{1,4}[\\/\\-]\\d{1,4}[\\/\\-]\\d{1,4}|^\\w+, \\w+ \\d+, \\d{4})/, x = a.toString().replace(sre, '') || '', y = b.toString().replace(sre, '') || ''; if (!html) { x = x.replace(htmre, ''); y = y.replace(htmre, ''); } var xN = x.replace(re, '\0\$1\0').replace(/\0\$/,'').replace(/^\0/,'').split('\0'), yN = y.replace(re, '\0\$1\0').replace(/\0\$/,'').replace(/^\0/,'').split('\0'), xD = parseInt(x.match(hre), 10) || (xN.length !== 1 && x.match(dre) && Date.parse(x)), yD = parseInt(y.match(hre), 10) || xD && y.match(dre) && Date.parse(y) || null; if (yD) { if ( xD < yD ) { return -1; } else if ( xD > yD ) { return 1; } } for(var cLoc=0, numS=Math.max(xN.length, yN.length); cLoc < numS; cLoc++) { var oFxNcL = !(xN[cLoc] || '').match(ore) && parseFloat(xN[cLoc], 10) || xN[cLoc] || 0; var oFyNcL = !(yN[cLoc] || '').match(ore) && parseFloat(yN[cLoc], 10) || yN[cLoc] || 0; if (isNaN(oFxNcL) !== isNaN(oFyNcL)) { return (isNaN(oFxNcL)) ? 1 : -1; } else if (typeof oFxNcL !== typeof oFyNcL) { oFxNcL += ''; oFyNcL += ''; } if (oFxNcL < oFyNcL) { return -1; } if (oFxNcL > oFyNcL) { return 1; } } return 0; } jQuery.extend( jQuery.fn.dataTableExt.oSort, { "natural-asc": function ( a, b ) { return naturalSort(a,b,true); }, "natural-desc": function ( a, b ) { return naturalSort(a,b,true) * -1; }, "natural-nohtml-asc": function( a, b ) { return naturalSort(a,b,false); }, "natural-nohtml-desc": function( a, b ) { return naturalSort(a,b,false) * -1; }, "natural-ci-asc": function( a, b ) { a = a.toString().toLowerCase(); b = b.toString().toLowerCase(); return naturalSort(a,b,true); }, "natural-ci-desc": function( a, b ) { a = a.toString().toLowerCase(); b = b.toString().toLowerCase(); return naturalSort(a,b,true) * -1; } }); }()); """ // thanks to DCMeglio (Hubitat Package Manager) for a lot of formatting hints String getFormat(type, myText="", myHyperlink="", myColor=sColorDarkBlue){ if(type == "line") return "
" if(type == "title") return "

${myText}

" if(type == "text") return "${myText}" if(type == "hyperlink") return "${myText}" if(type == "comments") return "
${myText}
" } String errorMsg(String msg) { getFormat("text", "$sWarningsIcon $msg", null, sColorDarkRed) } String statusMsg(String msg) { getFormat("text", msg, null, sColorDarkBlue) } String paragraphComment(String msg) { paragraph( getFormat("comments", msg, null,"Gray") ) } def displayHeader() { section (getFormat("title", "${app.getLabel()}${sCodeRelease?.size() ? " : $sCodeRelease" : ""}" )) { paragraph "
Developed by: ${author()}
Current Version: v${version()} - ${copyright()}
" paragraph( getFormat("line") ) } } public static String paypal() { return "https://www.paypal.com/donate/?business=QHNE3ZVSRYWDA&no_recurring=1¤cy_code=USD" } def displayFooter(){ Long day14ms = 14*86400000 if(now()>(state.isInstalled+day14ms)) { section() { paragraph( getFormat("line") ) paragraph( rawHtml: true, """
$sDefaultAppName

PayPal Logo

Please consider donating. This application took a lot of work to make.
If you find it valuable, I'd certainly appreciate it!
""") } } } def menuHeader(titleText){"
${titleText}
"} private logInfo(msg) { if(!appInfoDisable) { log.info "${msg}" } } private logDebug(msg) { if(appDebugEnable) { log.debug "${msg}" } } private logTrace(msg) { if(appTraceEnable) { log.trace "${msg}" } } private logWarn(msg) { log.warn "${msg}" } private logError(msg) { log.error "${msg}" } // ******** Child and Mirror Device get Functions - Start ******** List getAllDevices() { List devices = getChildDevices() getAuthorizedDevices()?.each{ userAuthorizedDevice -> // don't repeat child devices that might be added if( !devices?.find{ it.deviceNetworkId == userAuthorizedDevice.deviceNetworkId } ) devices.add(userAuthorizedDevice) } return devices?.sort{ it.getDisplayName() } } List getAuthorizedDevices() { List devices = [] if( ((Map)settings).find{ it.key == "userAuthorizedDevices" } ) { userAuthorizedDevices?.each{ userAuthorizedDevice -> devices.add(userAuthorizedDevice) } } return devices?.sort{ it.getDisplayName() } } com.hubitat.app.DeviceWrapper getDevice(deviceNetworkId) { return getAllDevices()?.find{ it.deviceNetworkId == deviceNetworkId } // only one } List getAllReplicaDevices() { return getAllDevices()?.findAll{ getReplicaDeviceId(it) } // more than one } List getReplicaDevices(deviceId, componentId = null) { List response = getAllDevices()?.findAll{ getReplicaDeviceId(it) == deviceId } // could be more than one if (componentId) { response?.clone()?.each{ replicaDevice -> Map replica = getReplicaDataJsonValue(replicaDevice, "replica") if( replica?.componentId && replica?.componentId != componentId) //componentId was not added until v1.2.06 response.remove(replicaDevice) } } return response } List getAllReplicaDeviceIds() { return getAllReplicaDevices()?.collect{ getReplicaDeviceId(it) }?.unique() ?: [] } // ******** Child and Mirror Device get Functions - End ******** // ******** Volatile Memory Device Cache - Start ******** private String getReplicaDeviceId(def replicaDevice) { String deviceId = null Map replica = getReplicaDataJsonValue(replicaDevice, "replica") if(replica?.replicaId==app.getId()) { // just get MY app devices. could have a problem with mirror devices. deviceId = replica?.deviceId } return deviceId } private void setReplicaDataJsonValue(def replicaDevice, String dataKey, Map dataValue) { setReplicaDataValue(replicaDevice, dataKey, JsonOutput.toJson(dataValue)) } private void setReplicaDataValue(def replicaDevice, String dataKey, String dataValue) { getReplicaDataValue(replicaDevice, dataKey, dataValue) // update cache first replicaDevice?.updateDataValue(dataKey, dataValue) // STORE IT to device object } private Map getReplicaDataJsonValue(def replicaDevice, String dataKey) { def response = null try { def value = getReplicaDataValue(replicaDevice, dataKey) if (value) { response = new JsonSlurper().parseText(value) } } catch (e) { logInfo "${app.getLabel()} '$replicaDevice' getReplicaDataJsonValue($dataKey) did not complete." } return response } private String getReplicaDataValue(def replicaDevice, String dataKey, String setDataValue=null) { // setDataValue will directly update the cache without fetching from the object. Long appId = app.getId() if(g_mReplicaDeviceCache[appId]==null) { clearReplicaDataCache() } def cacheDevice = g_mReplicaDeviceCache[appId]?.get(replicaDevice?.deviceNetworkId) if(cacheDevice==null && replicaDevice?.deviceNetworkId) { cacheDevice = g_mReplicaDeviceCache[appId][replicaDevice?.deviceNetworkId] = [:] } String dataValue = setDataValue!=null ? null : cacheDevice?.get(dataKey) // this could be a setter, so don't grab cache if dataValue is present if(dataValue==null && replicaDevice?.deviceNetworkId) { dataValue = setDataValue ?: replicaDevice?.getDataValue(dataKey) // Use setter value if present or FETCH IT from device object if(dataValue) { cacheDevice[dataKey] = dataValue if(setDataValue!=null) logTrace "${app.getLabel()} '${replicaDevice?.getDisplayName()}' cache updated '$dataKey'" else logDebug "${app.getLabel()} '${replicaDevice?.getDisplayName()}' cached '$dataKey'" } else { logDebug "${app.getLabel()} '${replicaDevice?.getDisplayName()}' cannot find '$dataKey' setting cache to ignore" cacheDevice[dataKey] = "ignore" } } return (dataValue=="ignore" ? null : dataValue) } private void clearReplicaDataCache() { g_mReplicaDeviceCache[app.getId()] = [:] } private void clearReplicaDataCache(def replicaDevice) { Long appId = app.getId() if(g_mReplicaDeviceCache[appId] && replicaDevice?.deviceNetworkId ) { g_mReplicaDeviceCache[appId][replicaDevice?.deviceNetworkId] = null g_mReplicaDeviceCache[appId][replicaDevice?.deviceNetworkId] = [:] } } private void clearReplicaDataCache(def replicaDevice, String dataKey, Boolean delete=false) { if(delete) { replicaDevice?.removeDataValue(dataKey) } Long appId = app.getId() if(g_mReplicaDeviceCache[appId] && replicaDevice?.deviceNetworkId && dataKey) { if(g_mReplicaDeviceCache[appId]?.get(replicaDevice?.deviceNetworkId)?.get(dataKey)) { g_mReplicaDeviceCache[appId].get(replicaDevice?.deviceNetworkId).remove(dataKey) } } } // ******** Volatile Memory Device Cache - End ******** // ******** Thanks to Dominick Meglio for the code below! ******** Map getDriverList() { def params = [ uri: "http://127.0.0.1:8080/device/drivers", headers: [ Cookie: state.cookie //shouldn't need since we are using this only in the UI after the user logs in ] ] Map result = [items:[]] try { httpGet(params) { resp -> for (driver in resp.data.drivers) { result.items.add([id:driver.id.toString(),name:driver.name,namespace:driver.namespace]) } } } catch (e) { logWarn "Error retrieving installed drivers: ${e}" } return result } // ******** Thanks to Dominick Meglio for the code above! - End ******** void pageMainTestButton() { logWarn getAuthToken() return }