/** * Copyright 2023 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.2.10 2023-01-07 update to object command to support color bulbs. thanks to @djgutheinz for the patch! * 1.2.11 2023-01-11 Fix for mirror rules config. Allow for replicaEvent, replicaStatus, replicaHealth to be sent to DH if command exists. * 1.2.12 2023-01-12 Fix for duplicate attributes(like TV). Removed debug. Update to all refresh() command to be used in rules and not captured. * 1.3.00 2023-01-13 Formal Release Candidate * 1.3.02 2023-01-26 Support for passing unit:'' and data:[:] structures from ST. Intial work to support ST Virtual device creation (not completed) * 1.3.03 2023-02-09 Support for SmartThings Virtual Devices. Major UI Button overhaul. Work to improve refresh. * 1.3.04 2023-02-16 Support for SmartThings Scene MVP. Not released. * 1.3.05 2023-02-18 Support for 200+ SmartThings devices. Increase OAuth maximum from 20 to 30. * 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. LINE 30 MAX */ public static String version() { return "1.3.07" } public static String copyright() { return "© 2023 ${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" } 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()=="PENDING") oauthStatus = "PENDING" else if(oauthStatus!="FAILURE" && 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() 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: getFormat("text","$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." paragraph( getFormat("comments",comments,null,"Gray") ) 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." paragraph( getFormat("comments",comments,null,"Gray") ) 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: getFormat("text","$sHubitatIcon Advanced Configuration"), defaultValue: false, submitOnChange: true) if(pageMainShowAdvanceConfiguration) { def deviceText = (deviceAuthCount<1 ? ": (Select to Authorize Devices to Mirror)" : (deviceAuthCount==1 ? ": ($deviceAuthCount Device Authorized)" : ": ($deviceAuthCount Devices Authorized)")) href "pageAuthDevice", title: getFormat("text","$sHubitatIcon Authorize Hubitat Devices $deviceText"), description: "Click to show" paragraph("") input(name: "pageMainPageAppLabel", type: "text", title: getFormat("text"," $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")) { 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() 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 += "" } } 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()}--$sNoStatusIcon $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" } 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 $sHubitatIconStatic $sSamsungIconStatic")) { input(name: "userAuthorizedDevices", type: "capability.*", title: "Hubitat Devices:", description: "Choose a Hubitat devices", multiple: true, submitOnChange: true) if(deviceAuthCount>0) { paragraph( getFormat("line") ) href "pageMirrorDevice", title: "Mirror Hubitat Device (Advanced)", 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 pageHubiThingDevice(){ app.removeSetting('pageCreateDeviceLabel') //1.3.03 app.removeSetting('pageCreateDeviceShowAllDevices') //1.3.03 app.removeSetting('pageCreateDeviceSmartDevice') //1.3.03 app.removeSetting('pageCreateDeviceSmartDeviceComponent') //1.3.03 app.removeSetting('pageCreateDeviceType') //1.3.03 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")) { paragraph( getFormat("comments","${(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.",null,"Gray") ) 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?:""}$sNoStatusIcon $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( "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( "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(250) // no need to hammer ST getSmartDeviceHealth(deviceId) pauseExecution(250) // 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: 3, 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( "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}" ]) } 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)" ]) } } } 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)" ]) } 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 ) } 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: hubitatType] ) } 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")) { if(getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.replicaType == 'location') paragraph( getFormat("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.",null,"Gray") ) if(getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.replicaType == 'scene') paragraph( getFormat("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.",null,"Gray") ) 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) 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(getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.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: "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%;") if(g_mAppDeviceSettings?.pageVirtualDeviceCreateButton) { paragraph( g_mAppDeviceSettings.pageVirtualDeviceCreateButton ) g_mAppDeviceSettings.pageVirtualDeviceCreateButton = null } } section(menuHeader("Modify Virtual Device, Mode or Scene")) { paragraph( getFormat("comments","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.",null,"Gray") ) 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" String name = getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.name String prototype = getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.typeId 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(getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.replicaType == 'scene' && !pageVirtualDeviceScene) g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = errorMsg("Error: SmartThings Scene selection is invalid") else { Map response = createVirtualDevice(pageVirtualDeviceLocation, pageVirtualDeviceRoom, name, prototype) if(response?.statusCode==200) { g_mVirtualDeviceListCache[app.getId()]=null app.updateSetting( "pageVirtualDeviceModify", [type:"enum", value: response?.data?.deviceId] ) g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = statusMsg("'$name' was created with deviceId: ${response?.data?.deviceId}") if(pageVirtualDeviceOauth) { getChildAppById(pageVirtualDeviceOauth.toLong())?.createSmartDevice(pageVirtualDeviceLocation, response?.data?.deviceId, true) } if(pageVirtualDeviceOauth&&pageVirtualDeviceHubitatType) { Map deviceType = getDeviceHandlers()?.items?.find{ it?.id==pageVirtualDeviceHubitatType } String label = name String deviceId = response?.data?.deviceId String componentId = "main" g_mAppDeviceSettings['pageVirtualDeviceCreateButton'] = createChildDevice(deviceType, name, label, deviceId, componentId) } if(getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.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(getVirtualDeviceTypes()?.find{ it?.id==pageVirtualDeviceType?.toInteger() }?.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-> if(replicaDevice?.getLabel()==label) 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) { 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 } 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 List devices = [ [id:1, name: 'Switch', typeId: 'VIRTUAL_SWITCH', replicaName: 'Replica Switch' ], [id:2, name: 'Dimmer Switch', typeId: 'VIRTUAL_DIMMER_SWITCH', replicaName: 'Replica Dimmer' ], //[id:3, name: 'Button', typeId: 'VIRTUAL_BUTTON' ], //[id:4, name: 'Camera', typeId: 'VIRTUAL_CAMERA' ], //[id:5, name: 'Color Bulb', typeId: 'VIRTUAL_COLOR_BULB' ], //[id:6, name: 'Contact Sensor', typeId: 'VIRTUAL_CONTACT_SENSOR' ], //[id:7, name: 'Dimmer (no switch)', typeId: 'VIRTUAL_DIMMER' ], //[id:8, name: 'Garage Door Opener', typeId: 'VIRTUAL_GARAGE_DOOR_OPENER' ], //[id:9, name: 'Lock', typeId: 'VIRTUAL_LOCK' ], //[id:10, name: 'Metered Switch', typeId: 'VIRTUAL_METERED_SWITCH' ], //[id:11, name: 'Motion Sensor', typeId: 'VIRTUAL_MOTION_SENSOR' ], //[id:12, name: 'Multi-Sensor', typeId: 'VIRTUAL_MULTI_SENSOR' ], //[id:13, name: 'Presence Sensor', typeId: 'VIRTUAL_PRESENCE_SENSOR' ], //[id:14, name: 'Refrigerator', typeId: 'VIRTUAL_REFRIGERATOR' ], //[id:15, name: 'RGBW Bulb', typeId: 'VIRTUAL_RGBW_BULB' ], //[id:16, name: 'Siren', typeId: 'VIRTUAL_SIREN' ], //[id:17, name: 'Thermostat', typeId: 'VIRTUAL_THERMOSTAT' ], [id:18, name: 'Location Mode Knob', typeId: 'VIRTUAL_SWITCH', replicaName: 'Replica Location Knob', replicaType: 'location' ], [id:19, name: 'Scene Knob', typeId: 'VIRTUAL_SWITCH', replicaName: 'Replica Scene Knob', replicaType: 'scene' ] ] return devices } Map createVirtualDevice(String locationId, String roomId, String name, String prototype) { logDebug "${app.getLabel()} executing 'createVirtualDevice($locationId, $prototype, $name)'" Map response = [statusCode:iHttpError] def device = [ name: name, roomId: roomId, prototype: prototype, owner: [ ownerType: "LOCATION", ownerId: locationId ] ] Map params = [ uri: sURI, body: JsonOutput.toJson(device), path: "/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] Boolean allowDuplicateAttribute = pageConfigureDeviceAllowDuplicateAttribute Boolean muteTriggerRuleInfo = pageConfigureDeviceMuteTriggerRuleInfo Boolean disableStatusUpdate = pageConfigureDeviceDisableStatusUpdate app.updateSetting("pageConfigureDeviceAllowDuplicateAttribute", false) app.updateSetting("pageConfigureDeviceMuteTriggerRuleInfo", false) app.updateSetting("pageConfigureDeviceDisableStatusUpdate", false) 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(muteTriggerRuleInfo) newRule['mute'] = true if(disableStatusUpdate) newRule['disableStatus'] = true if(action=='store' && (!replicaDeviceRules?.components?.find{ it?.type==type && it?.trigger?.label?.trim()==triggerKey } || allowDuplicateAttribute)) { 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) Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules" ) String replicaDeviceRulesList = "" replicaDeviceRulesList += "" replicaDeviceRules?.components?.sort{ a,b -> a?.type <=> b?.type ?: a?.trigger?.label <=> b?.trigger?.label ?: a?.command?.label <=> b?.command?.label }?.each { rule -> String muteflag = rule?.mute ? "$sLogMuteIcon" : "" String disableStatusFlag = rule?.disableStatus ? "$sNoStatusIcon" : "" String trigger = "${rule?.type=='hubitatTrigger' ? sHubitatIcon : sSamsungIcon} ${rule?.trigger?.label}" String command = "${rule?.type!='hubitatTrigger' ? sHubitatIcon : sSamsungIcon} ${rule?.command?.label} $muteflag $disableStatusFlag" trigger = checkTrigger(replicaDevice, rule?.type, rule?.trigger?.label) ? trigger : "$trigger" command = checkCommand(replicaDevice, rule?.type, rule?.command?.label) ? command : "$command" replicaDeviceRulesList += "" } replicaDeviceRulesList +="
TriggerAction
$trigger$command
" if (replicaDeviceRules?.components?.size){ section(menuHeader("Active Rules ➢ $replicaDevice")) { paragraph( replicaDeviceRulesList ) paragraph(rawHtml: true, """""") } } if(checkFirmwareVersion("2.3.4.132") && false) { section(menuHeader("Replica Handler Development")) { input(name: "pageConfigureDeviceFetchCapabilityFileName", type: "text", title: "Replica Capabilities Filename:", description: "Capability JSON Local Filename", width: 4, submitOnChange: true, newLineAfter:true) input(name: "dynamic::pageConfigureDevicefetchCapabilityButton", type: "button", title: "Fetch", width: 2, style:"width:75%;") input(name: "dynamic::pageConfigureDeviceStoreCapabilityButton", type: "button", title: "Store", width: 2, style:"width:75%;") } } } void pageConfigureDevicefetchCapabilityButton() { logDebug "${app.getLabel()} executing 'pageConfigureDevicefetchCapabilityButton()' $pageConfigureDeviceFetchCapabilityFileName" byte[] filebytes = downloadHubFile(pageConfigureDeviceFetchCapabilityFileName) def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) 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 pageConfigureDeviceStoreCapabilityButton() { logDebug "${app.getLabel()} executing 'pageConfigureDeviceStoreCapabilityButton()' $pageConfigureDeviceFetchCapabilityFileName" def replicaDevice = getDevice(pageConfigureDeviceReplicaDevice) Map capabilities = getReplicaDataJsonValue(replicaDevice, "capabilities") if(pageConfigureDeviceFetchCapabilityFileName && capabilities) { //logInfo capabilities byte[] filebytes =((String)JsonOutput.toJson(capabilities))?.getBytes() uploadHubFile(pageConfigureDeviceFetchCapabilityFileName, filebytes) } } 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, triggerLabel) { Map trigger = type=='hubitatTrigger' ? getHubitatAttributeOptions(replicaDevice) : getSmartAttributeOptions(replicaDevice) return trigger?.get(triggerLabel) } Boolean checkCommand(replicaDevice, type, commandLabel) { Map commands = type!='hubitatTrigger' ? getHubitatCommandOptions(replicaDevice) : getSmartCommandOptions(replicaDevice) return commands?.get(commandLabel) } 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(pageConfigureDeviceShowDetail && replicaDevice) { def hubitatStats = getHubitatDeviceStats(replicaDevice) paragraph( 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) 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:true) 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%;") if(pageConfigureDeviceShowDetail) { paragraph( hubitatAttribute ? "$sHubitatIcon $hubitatAttribute : ${JsonOutput.toJson(hubitatAttributeOptions?.get(hubitatAttribute))}" : "$sHubitatIcon No Selection" ) paragraph( smartCommand ? "$sSamsungIcon $smartCommand : ${JsonOutput.toJson(smartCommandOptions?.get(smartCommand))}" : "$sSamsungIcon No Selection" ) } 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%;") if(pageConfigureDeviceShowDetail) { paragraph( smartAttribute ? "$sSamsungIcon $smartAttribute : ${JsonOutput.toJson(smartAttributeOptions?.get(smartAttribute))}" : "$sSamsungIcon No Selection" ) paragraph( hubitatCommand ? "$sHubitatIcon $hubitatCommand : ${JsonOutput.toJson(hubitatCommandOptions?.get(hubitatCommand))}" : "$sHubitatIcon No Selection" ) } paragraph( getFormat("line") ) input(name: "pageConfigureDeviceAllowDuplicateAttribute", type: "bool", title: "Allow duplicate Attribute TRIGGER", defaultValue: false, submitOnChange: true, width: 3) input(name: "pageConfigureDeviceMuteTriggerRuleInfo", type: "bool", title: "Mute $sLogMuteIcon TRIGGER rule Logging", defaultValue: false, submitOnChange: true, width: 3) input(name: "pageConfigureDeviceDisableStatusUpdate", type: "bool", title: "Disable $sNoStatusIcon periodic device refresh", defaultValue: false, submitOnChange: true, width: 3) app.updateSetting("pageConfigureDeviceAllowActionAttribute", false) input(name: "pageConfigureDeviceShowDetail", type: "bool", title: "Show detail for attributes and commands", defaultValue: false, submitOnChange: true, width: 3, newLineAfter:true) // gather these all up so when user presses store - it uses this structure. g_mAppDeviceSettings['hubitatAttribute'] = hubitatAttributeOptions?.get(hubitatAttribute) ?: null g_mAppDeviceSettings['smartAttribute'] = smartAttributeOptions?.get(smartAttribute) ?: null g_mAppDeviceSettings['smartCommand'] = smartCommandOptions?.get(smartCommand) ?: null g_mAppDeviceSettings['hubitatCommand'] = hubitatCommandOptions?.get(hubitatCommand) ?: null } 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") 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 } } return smartCommandOptions } Map getSmartAttributeOptions(replicaDevice) { Map smartAttributeOptions = [:] Map capabilities = getReplicaDataJsonValue(replicaDevice, "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 } @Field volatile static Map g_bAppButtonHandlerLock = [:] void appButtonHandler(String btn) { logDebug "${app.getLabel()} executing 'appButtonHandler($btn)'" if(g_bAppButtonHandlerLock[app.id]) return appButtonHandlerLock() if(btn.contains("::")) { List items = btn.tokenize("::") if(items && items.size() > 1 && items[1]) { String k = (String)items[0] String v = (String)items[1] 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": this."$v"() break default: logInfo "Not supported" } } } appButtonHandlerUnLock() } void appButtonHandlerLock() { g_bAppButtonHandlerLock[app.id] = true runIn(10,appButtonHandlerUnLock) } void appButtonHandlerUnLock() { unschedule('appButtonHandlerUnLock') g_bAppButtonHandlerLock[app.id] = false } void allSmartDeviceRefresh() { // 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 Hubitat subscribed getAllReplicaDevices()?.each { replicaDevice -> replicaDeviceSubscribe(replicaDevice) } // lets get status on everything. should this be scheduled? allSmartDeviceStatus(10) allSmartDeviceHealth(20) allSmartDeviceDescription(30) } 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) } } } // called from subscribe HE devices void deviceTriggerHandler(def event) { //event.properties.each { logInfo "$it.key -> $it.value" } deviceTriggerHandler(event?.getDevice(), event?.name, event?.value, event?.unit, event?.getJsonData()) } // called from replica HE drivers void deviceTriggerHandler(def replicaDevice, Map event) { Boolean refresh = deviceTriggerHandler(replicaDevice, event?.name, event?.value, event?.unit, event?.data, event?.now) if(event?.name == "configure") { clearReplicaDataCache(replicaDevice) replicaDeviceRefresh(replicaDevice) } if(event?.name == "refresh") { String deviceId = getReplicaDeviceId(replicaDevice) if(deviceId&&refresh) getSmartDeviceStatus(deviceId) else replicaDeviceRefresh(replicaDevice) } } Boolean deviceTriggerHandler(def replicaDevice, String eventName, def eventValue, String eventUnit, Map eventJsonData, Long eventPostTime=null) { eventPostTime = eventPostTime ?: now() logDebug "${app.getLabel()} executing 'deviceTriggerHandler()' replicaDevice:'${replicaDevice.getDisplayName()}' name:'$eventName' value:'$eventValue' unit:'$eventUnit', data:'$eventJsonData'" Boolean response = false String deviceId = getReplicaDeviceId(replicaDevice) String componentId = getReplicaDataJsonValue(replicaDevice, "replica")?.componentId ?: "main" //componentId was not added until v1.2.06 Map replicaDeviceRules = getReplicaDataJsonValue(replicaDevice, "rules") replicaDeviceRules?.components?.findAll{ it?.type == "hubitatTrigger" }?.each { rule -> Map trigger = rule?.trigger Map command = rule?.command if(eventName==trigger?.name) { logTrace ">> trigger:'$trigger' eventValue:'$eventValue'" // simple enum case if(trigger?.type=="command" && trigger?.parameters==null || trigger?.value==eventValue) { // check if this was from ST and should not be sent back if(trigger?.type=="command" || !deviceTriggerHandlerCache(replicaDevice, eventName, eventValue)) { setSmartDeviceCommand(deviceId, componentId, command?.capability, command?.name) response = true if(!rule?.mute) logInfo "${app.getLabel()} sending '${replicaDevice?.getDisplayName()}' ● trigger:${trigger?.type=="command"?"command":eventName}:${trigger?.type=="command"?eventName:eventValue} ➣ command:${command?.name} ● delay:${now() - eventPostTime}ms" } } // non-enum case https://developer-preview.smartthings.com/docs/devices/capabilities/capabilities else if((trigger?.type=="command" && trigger?.parameters) || !trigger?.value) { String evtName = eventName String evtValue = eventValue.toString() logTrace "evtName:${evtName} evtValue:${evtValue}" // check if this was from ST and should not be sent back if(trigger?.type=="command" || !deviceTriggerHandlerCache(replicaDevice, evtName, evtValue)) { String type = command?.arguments?.getAt(0)?.schema?.type?.toLowerCase() def arguments = null switch(type) { case 'integer': // A whole number. Limits can be defined to constrain the range of possible values. arguments = [ (evtValue?.isNumber() ? (evtValue?.isFloat() ? (int)(Math.round(evtValue?.toFloat())) : evtValue?.toInteger()) : null) ] break case 'number': // A number that can have fractional values. Limits can be defined to constrain the range of possible values. arguments = [ evtValue?.toFloat() ] break case 'boolean': // Either true or false arguments = [ evtValue?.toBoolean() ] break case 'object': // A map of name value pairs, where the values can be of different types. Map map = new JsonSlurper().parseText(eventValue) arguments = [ map ] //updated version v1.2.10 break case 'array': // A list of values of a single type. List list = new JsonSlurper().parseText(eventValue) arguments = list break default: arguments = [ evtValue ] 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 }) } setSmartDeviceCommand(deviceId, componentId, command?.capability, command?.name, arguments) response = true if(!rule?.mute) logInfo "${app.getLabel()} sending '${replicaDevice?.getDisplayName()}' $type ● trigger:${evtName} ➣ command:${command?.name}:${arguments?.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(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(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"(*[[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 ? "" : "$sNoStatusIcon $sNoRules" String eventCount = (getReplicaDeviceEventsCache(replicaDevice)?.eventCount ?: 0).toString() value = (healthState=='offline' ? "$sNoStatusIcon $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(deviceId, component, capability, command, arguments = []) { logDebug "${app.getLabel()} executing 'setSmartDeviceCommand()'" Map response = [statusCode:iHttpError] Map commands = [ commands: [[ component: component, capability: capability, command: command, arguments: arguments ]] ] Map data = [ 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", data).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": 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} 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 @Field static final String sLogMuteIcon=""" """ @Field static final String sNoStatusIcon=""" """ @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 = """""" /* * 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", msg, null, sColorDarkRed) } String statusMsg(String msg) { getFormat("text", msg, null, sColorDarkBlue) } 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 { logInfo "${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 }