/** * Homebridge Hubitat Interface * App footer inspired from Hubitat Package Manager (Thanks @dman2306) * * Copyright 2018-2023 Anthony Santilli * Contributions by @nh.schottfam */ //file:noinspection GroovySillyAssignment //file:noinspection GroovyUnusedAssignment //file:noinspection unused //file:noinspection GroovyPointlessBoolean //file:noinspection GroovyFallthrough //file:noinspection SpellCheckingInspection import groovy.json.JsonOutput import groovy.transform.Field import groovy.transform.CompileStatic import java.text.SimpleDateFormat import java.util.concurrent.Semaphore import java.util.zip.GZIPOutputStream definition( name: 'Homebridge v2', namespace: 'tonesto7', author: 'Anthony Santilli', description: 'Provides the API interface between Homebridge (HomeKit) and ' + platformFLD, category: 'My Apps', iconUrl: 'https://raw.githubusercontent.com/tonesto7/homebridge-hubitat-tonesto7/master/images/hb_tonesto7.png', iconX2Url: 'https://raw.githubusercontent.com/tonesto7/homebridge-hubitat-tonesto7/master/images/hb_tonesto7.png', iconX3Url: 'https://raw.githubusercontent.com/tonesto7/homebridge-hubitat-tonesto7/master/images/hb_tonesto7.png', importUrl: 'https://raw.githubusercontent.com/tonesto7/homebridge-hubitat-tonesto7/master/apps/homebridge-v2.groovy', oauth: true) preferences { page(name: 'startPage') page(name: 'mainPage') page(name: 'deviceSelectPage') page(name: 'changeLogPage') page(name: 'capFilterPage') page(name: "attrFilterPage") page(name: 'developmentPage') page(name: 'pluginConfigPage') page(name: 'donationPage') page(name: 'historyPage') page(name: 'deviceDebugPage') page(name: 'settingsPage') page(name: 'confirmPage') } // STATICALLY DEFINED VARIABLES @Field static final String appVersionFLD = '2.9.2' //@Field static final String appModifiedFLD = '09-20-2023' @Field static final String branchFLD = 'master' @Field static final String platformFLD = 'Hubitat' @Field static final String pluginNameFLD = 'Hubitat-v2' @Field static final Boolean devModeFLD = false @Field static final Map minVersionsFLD = [plugin: 291] @Field static final String sNULL = (String) null @Field static final String sBLANK = '' @Field static final String sSPACE = ' ' @Field static final String sBULLET = '\u2022' //@Field static final String sFRNFACE = '\u2639' //@Field static final String okSymFLD = '\u2713' //@Field static final String notOkSymFLD = '\u2715' //@Field static final String sPAUSESymFLD = '\u275A\u275A' @Field static final String sEXTNRL = 'external' @Field static final String sLINEBR = '
' @Field static final String sFALSE = 'false' @Field static final String sTRUE = 'true' @Field static final String sBOOL = 'bool' @Field static final String sENUM = 'enum' //@Field static final String sTIME = 'time' @Field static final String sSVR = 'svraddr' @Field static final String sCLN = ':' @Field static final String sNLCLN = 'null:null' @Field static final String sEVT = 'evt' @Field static final String sEVTLOGEN = 'evtLogEn' @Field static final String sDBGLOGEN = 'dbgLogEn' @Field static final String sDBG = 'debug' @Field static final String sUPD = 'update' @Field static final String sEVTUPD = 'EventUpdate' @Field static final String sAPPJSON = 'application/json' @Field static final String sSUCC = 'Success' @Field static final String sATK = 'accessToken' @Field static final String sMEDIUM = 'medium' @Field static final String sSMALL = 'small' @Field static final String sCLR4D9 = '#2784D9' @Field static final String sCLR9B1 = '#0299B1' @Field static final String sCLRRED = 'red' //@Field static final String sCLRRED2 = '#cc2d3b' @Field static final String sCLRGRY = 'gray' @Field static final String sCLRGRN = 'green' @Field static final String sCLRGRN2 = '#43d843' @Field static final String sCLRORG = 'orange' @Field static final String sTTM = 'Tap to modify...' @Field static final String sTTC = 'Tap to configure...' //@Field static final String sTTP = 'Tap to proceed...' @Field static final String sTTV = 'Tap to view...' @Field static final String sTTS = 'Tap to select...' // @Field static final String sPOST = 'Post' @Field static final String sASYNCCR = 'asyncHttpCmdResp' @Field static final String sLASTWU = 'lastwebCoREUpdDt' @Field static final String sINFO = 'info' @Field static final String sCMD = 'command' @Field static final String sCAP_SW = 'capability.switch' @Field static final String sSW = 'switch' @Field static final Integer i0 = 0 @Field static final Integer i1 = 1 // IN-MEMORY VARIABLES (Cleared only on HUB REBOOT) @Field static final Map> allowedListFLD = [ attributes: [ "acceleration", "airQualityIndex", "alarmSystemStatus", "battery", "button", "carbonDioxideMeasurement", "carbonMonoxide", "colorTemperature", "contact", "coolingSetpoint", "door", "doubleTapped", "energy", "fanMode", "fanState", "fanTargetState", "heatingSetpoint", "held", "hue", "humidity", "illuminance", "level", "level", "lock", "motion", "mute", "outlet", "pm25", "position", "power", "powerSource", "presence", "pushed", "saturation", "smoke", "speed", "switch", "tamper", "temperature", "thermostatFanMode", "thermostatMode", "thermostatOperatingState", "thermostatSetPoint", "valve", "volume", "water", "waterLevel", "windowShade", ], capabilities: [ "AccelerationSensor", "Actuator", "AirQuality", "Alarm", "AlarmSystemStatus", "Audio Mute", "Audio Volume", "Battery", "Bulb", "Button", "CarbonDioxideMeasurement", "CarbonMonoxideDetector", "ColorControl", "ColorTemperature", "ContactSensor", "Door", "DoorControl", "DoubleTapableButton", "EnergyMeter", "Fan", "FanControl", "FanLight", "GarageDoorControl", "HoldableButton", "HumidityControl", "Humidifier", "Dehumidifier", "IlluminanceMeasurement", "Light", "LightBulb", "Lock", "LockCodes", "Mode", "MotionSensor", "Outlet", "Piston", "PowerMeter", "PowerSource", "PresenceSensor", "PushableButton", "RelativeHumidityMeasurement", // "ReleasableButton", "Routine", "Sensor", "SmokeDetector", "Speaker", "Switch", "SwitchLevel", "TamperAlert", "TemperatureMeasurement", "Thermostat", "ThermostatCoolingSetpoint", "ThermostatFanMode", "ThermostatHeatingSetpoint", "ThermostatMode", "ThermostatOperatingState", "ThermostatSetpoint", "Valve", "WaterSensor", "WindowBlind", "WindowShade" ], commands: [ "armAway", "armHome", "disarm", "auto","heat","cool", "channelDown", "channelUp", "nextTrack", "previousTrack", "emergencyHeat", "fanAuto", "fanCirculate", "fanOn", "flip", "mute", "on", "off", "open", "close", "pause", "push", "hold", "doubleTap", "setColorTemperature", "setHue", "setSaturation", "setCoolingSetpoint", "setFanSpeed", "setHeatingSetpoint", "setLevel", "setPosition", "setSchedule", "setSpeed", "setThermostatFanMode", "setThermostatMode","setThermostatSetpoint", "setTargetHumidity", "setTiltLevel", "setVolume", "start", "stop", "unmute", "volumeDown", "volumeUp" ], ] @Field static final Map attMapFLD = [ 'acceleration': 'Acceleration', 'battery': 'Battery', 'contact': 'Contact', 'energy': 'Energy', 'humidity': 'Humidity', 'illuminance': 'Illuminance', 'level': 'Level', 'lock': 'Lock', 'motion': 'Motion', 'power': 'Power', 'presence': 'Presence', 'securityKeypad' : 'SecurityKeypad', 'speed': 'FanSpeed', 'switch': 'Switch', 'tamper': 'Tamper', 'temperature': 'Temp', 'valve': 'Valve', 'pushed': 'PushableButton', 'held': 'HoldableButton', 'doubleTapped': 'DoubleTapableButton' ] @Field static final Map capFilterFLD = [ 'Acceleration': 'AccelerationSensor', 'Battery': 'Battery', 'Button': 'Button', 'ColorControl': 'ColorControl', 'ColorTemperature': 'ColorTemperature', 'Contact': 'ContactSensor', 'Energy': 'EnergyMeter', 'Humidity': 'RelativeHumidityMeasurement', 'Illuminance': 'IlluminanceMeasurement', 'Level': 'SwitchLevel', 'Lock': 'Lock', 'Motion': 'MotionSensor', 'Power': 'PowerMeter', 'Presence': 'PresenceSensor', 'SecurityKeypad' : 'SecurityKeypad', 'Switch': 'Switch', 'Water': 'WaterSensor', 'Thermostat': 'Thermostat', 'ThermostatFanMode': 'ThermostatFanMode', 'ThermostatOperatingState': 'ThermostatOperatingState', 'ThermostatSetpoint': 'ThermostatSetpoint', 'ThermostatCoolingSetpoint': 'ThermostatCoolingSetpoint', 'ThermostatHeatingSetpoint': 'ThermostatHeatingSetpoint', 'Tamper': 'TamperAlert', 'Temp': 'TemperatureMeasurement', 'Valve': 'Valve', 'PushableButton': 'PushableButton', 'HoldableButton': 'HoldableButton', 'DoubleTapableButton': 'DoubleTapableButton', ] def startPage() { if (!getAccessToken()) { return dynamicPage(name: 'mainPage', install: false, uninstall: true) { section() { paragraph spanSmBldBr('OAuth Error', sCLRRED) + spanSmBld("OAuth is not Enabled for ${app?.getName()}!.

Please click remove and Enable Oauth under the Hubitat App Settings in the App Code page.") } } } else { if (!state.installData) { state.installData = [initVer: appVersionFLD, dt: getDtNow(), updatedDt: getDtNow(), shownDonation: false] } healthCheck(true) if (showChgLogOk()) { return changeLogPage() } if (showDonationOk()) { return donationPage() } return mainPage() } } @Field static List ListVars static void FILL_ListVars() { if(!ListVars){ List items items = deviceSettingKeys().collect { (String)it.key } items = items + virtSettingKeys().collect { (String)it.key } ListVars = items } } def mainPage() { // Fill up the ListVars array FILL_ListVars() Boolean isInst = ((Boolean)state.isInstalled == true) if (getBoolSetting('enableWebCoRE') && !webCoREFLD) { webCoRE_init() } // return dynamicPage(name: 'mainPage', nextPage: (isInst ? 'confirmPage' : sBLANK), install: !isInst, uninstall: true) { return dynamicPage(name: 'mainPage', nextPage: sBLANK, install: true, uninstall: true) { appInfoSect() section(sectHead('Device Configuration:')) { String deviceDesc = getSelectedDeviceDescs() href 'deviceSelectPage', title: inTS1('Device Selection', 'devices2'), required: false, description: (deviceDesc ? spanSm(deviceDesc, sCLR4D9) : inputFooter('Tap to select devices...', sCLRGRY, true)) } inputDupeValidation() section(sectHead('Attribute/Capability Filtering:')) { String aFilterDesc = getCustAttrFilterDesc() String cFilterDesc = getCapFilterDesc() href 'attrFilterPage', title: inTS1('Filter out attributes from your devices', 'filter'), description: aFilterDesc + (attrFiltersSelected() ? inputFooter(sTTM, sCLR4D9) : inputFooter(sTTC, sCLRGRY, true)), required: false href 'capFilterPage', title: inTS1('Filter out capabilities from your devices', 'filter'), description: cFilterDesc + (capFiltersSelected() ? inputFooter(sTTM, sCLR4D9) : inputFooter(sTTC, sCLRGRY, true)), required: false } section(sectHead('Location Options:')) { input 'addSecurityDevice', sBOOL, title: inTS1("Allow ${getAlarmSystemName()} Control in HomeKit?", 'alarm_home'), required: false, defaultValue: true, submitOnChange: true } section(sectHead('HomeBridge Plugin:')) { String pluginStatus = getPluginStatusDesc() href 'pluginConfigPage', style: 'embedded', required: false, title: inTS1('Generate Config for HomeBridge', sINFO), description: pluginStatus + inputFooter(sTTV, sCLRGRY, true) } section(sectHead('History Data & Device Debug:')) { href 'historyPage', title: inTS1('View Command and Event History', 'backup'), description: inputFooter(sTTV, sCLRGRY, true) href 'deviceDebugPage', title: inTS1('View Device Debug Data', sDBG), description: inputFooter(sTTV, sCLRGRY, true) } section(sectHead("Feature Requests/Issue Reporting"), hideable: true, hidden: true) { String issueUrl = "https://github.com/tonesto7/homebridge-hubitat-tonesto7/issues/new?assignees=tonesto7&labels=bug&template=bug_report.md&title=%28BUG%29+&projects=homebridge-hubitat-tonesto7%2F6" String featUrl = "https://github.com/tonesto7/homebridge-hubitat-tonesto7/issues/new?assignees=tonesto7&labels=enhancement&template=feature_request.md&title=%5BFeature+Request%5D&projects=homebridge-hubitat-tonesto7%2F6" String devUrl = "https://github.com/tonesto7/homebridge-hubitat-tonesto7/issues/new?assignees=tonesto7&labels=device_support&template=device_support.md&title=%5BDevice+Support%5D&projects=homebridge-hubitat-tonesto7%2F6" href url: featUrl, style: sEXTNRL, required: false, title: inTS1("New Feature Request", "info"), description: inputFooter("Tap to open browser", sCLRGRY, true) href url: devUrl, style: sEXTNRL, required: false, title: inTS1("Device Support", "info"), description: inputFooter("Tap to open browser", sCLRGRY, true) href url: issueUrl, style: sEXTNRL, required: false, title: inTS1("Report an Issue", "info"), description: inputFooter("Tap to open browser", sCLRGRY, true) } section(sectHead('App Preferences:')) { String sDesc = getSetDesc() href 'settingsPage', title: inTS1('App Settings', 'settings'), description: sDesc href 'changeLogPage', title: inTS1('View Changelog', 'change_log'), description: inputFooter(sTTV, sCLRGRY, true) label title: inTS1('Label this Instance (optional)', 'name_tag'), description: 'Rename this App', defaultValue: app?.name, required: false } if (devMode()) { section(sectHead('Dev Mode Options')) { input 'sendViaNgrok', sBOOL, title: inTS1('Communicate with Plugin via Ngrok Http?', sCMD), defaultValue: false, submitOnChange: true if (getBoolSetting('sendViaNgrok')) { input 'ngrokHttpUrl', 'text', title: inTS1('Enter the ngrok code from the url'), required: true, submitOnChange: true } } section(sectHead('Other Settings:')) { input 'restartService', sBOOL, title: inTS1('Restart Homebridge plugin when you press Save?', 'reset'), required: false, defaultValue: false, submitOnChange: true } } if(isInst) { section(sectHead('Save Your Settings')) { paragraph spanSmBldBr('NOTICE:', sCLRGRY) + spanSm('Once you press Done the Homebridge plugin will refresh your device changes after 15-20 seconds.') } appFooter() } clearTestDeviceItems() } } def pluginConfigPage() { return dynamicPage(name: 'pluginConfigPage', title: sBLANK, install: false, uninstall: false) { section(sectHead('Plugin Communication Options:')) { input 'consider_fan_by_name', sBOOL, title: inTS1('Use the word Fan in device name to determine if device is a Fan?', sCMD), required: false, defaultValue: true, submitOnChange: true input 'consider_light_by_name', sBOOL, title: inTS1('Use the word Light in device name to determine if device is a Light?', sCMD), required: false, defaultValue: false, submitOnChange: true input 'use_cloud_endpoint', sBOOL, title: inTS1('Communicate with Plugin Using Cloud Endpoint?', sCMD), required: false, defaultValue: false, submitOnChange: true input 'validate_token', sBOOL, title: inTS1('Validate AppID & Token for All Communications?', sCMD), required: false, defaultValue: false, submitOnChange: true input 'round_levels', sBOOL, title: inTS1('Round Levels <5% to 0% and >95% to 100%?', sCMD), required: false, defaultValue: true, submitOnChange: true input 'temp_unit', sENUM, title: inTS1('Temperature Unit?', 'temp_unit'), required: true, defaultValue: location?.temperatureScale, options: ['F':'Fahrenheit', 'C':'Celcius'], submitOnChange: true // input 'polling_seconds', number, title: inTS1('Plugin Polls Hubitat for Updates (in Seconds)?', sCMD), required: false, defaultValue: 3600, submitOnChange: true } section(sectHead('HomeKit Adaptive Lighting')) { String url = 'https://www.howtogeek.com/712520/how-to-use-adaptive-lighting-with-apple-homekit-lights/#:~:text=The%20Adaptive%20Lighting%20feature%20was,home%20lights%20throughout%20the%20day.' href url: url, style: sEXTNRL, title: inTS1('What is Adaptive Lighting?', sINFO), description: inputFooter('Tap to open in browser', sCLRGRY, true) input 'adaptive_lighting', sBOOL, title: inTS1('Allow Supported Bulbs to Use HomeKit Adaptive Lighting?', sCMD), required: false, defaultValue: true, submitOnChange: true if (getBoolSetting('adaptive_lighting')) { input 'adaptive_lighting_offset', 'number', title: inTS1('Adaptive Lighting - Offset ColorTemp Conversions by +/- Mireds?', sCMD), range: '-100..100', required: false, defaultValue: 0, submitOnChange: true } } // section(sectHead("Plugin Device Options:")) { // input "round_up_99", sBOOL, title: inTS1("Round Up Devices with 9?", sCMD)), required: false, defaultValue: false, submitOnChange: true // input "temp_unit", sENUM, title: inTS1("Temperature Unit?", "temp_unit")), required: true, defaultValue: location?.temperatureScale, options: ["F":"Fahrenheit", "C":"Celcius"], submitOnChange: true // input "validate_token", sBOOL, title: inTS1("Validate AppID & Token for All Communications?", sCMD)), required: false, defaultValue: false, submitOnChange: true // } section(sectHead('Generated HomeBridge Plugin Platform Config')) { paragraph divSm("") } section(sectHead('Test Communication with Plugin')) { String url = "http://${getServerAddress()}/pluginTest" href url: url, style: sEXTNRL, title: inTS1('Test Plugin Communication?', sINFO), description: inputFooter('Tap to open in browser', sCLRGRY, true) } } } static Map deviceValidationErrors() { /* NOTE: Define what we require to determine the thermostat is a thermostat so we can support devices like Flair which are custom heat-only thermostats. */ Map reqs = [ tstat: [ c:['Thermostat Operating State'], a: [r: ['thermostatOperatingState'], o: ['heatingSetpoint', 'coolingSetpoint']] ], tstat_heat: [ c: ['Thermostat Operating State'], a: [ r: ['thermostatOperatingState', 'heatingSetpoint'], o: [] ] ] ] // if(tstatHeatList || tstatCoolList || tstatList || tstatFanList) {} return reqs } def deviceSelectPage() { return dynamicPage(name: 'deviceSelectPage', title: sBLANK, install: false, uninstall: false) { section(sectHead('Define Specific Categories:')) { paragraph spanSmBldBr('Description:', sCLR4D9) + spanSm('Each category below will adjust the device attributes to make sure they are recognized as the desired device type under HomeKit', sCLR4D9) paragraph spanSmBldBr('NOTE: ') + spanSmBldBr('Please do not select a device more than once in the inputs below') input 'lightList', sCAP_SW, title: inTS1("Lights: (${lightList ? lightList.size() : 0} Selected)", 'light_on'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'outletList', sCAP_SW, title: inTS1("Outlets: (${outletList ? outletList.size() : 0} Selected)", 'outlet'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'lightNoAlList', sCAP_SW, title: inTS1("Lights (No Adaptive Lighting): (${lightNoAlList ? lightNoAlList.size() : 0} Selected)", 'light_on'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'garageList', 'capability.garageDoorControl', title: inTS1("Garage Doors: (${garageList ? garageList.size() : 0} Selected)", 'garage_door'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'speakerList', sCAP_SW, title: inTS1("Speakers: (${speakerList ? speakerList.size() : 0} Selected)", 'media_player'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'shadesList', 'capability.windowShade', title: inTS1("Window Shades: (${shadesList ? shadesList.size() : 0} Selected)", 'window_shade'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'securityKeypadsList', 'capability.securityKeypad', title: inTS1("Security Keypads: (${securityKeypadsList ? securityKeypadsList.size() : 0} Selected)", 'devices2'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead("Buttons:")) { if(pushableButtonList || holdableButtonList || doubleTapableButtonList) { paragraph spanSmBldBr("NOTICE:", sCLRRED) + spanSmBr("Buttons are a weird device under HomeKit and don't allow any inbound action under Hubitat. They only allow you to execute actions/automations under HomeKit per button you've configured", sCLRRED) + spanSmBr("Once the remote device is created under HomeKit it will remain in the unconfigured state so find it and configure each button number and pushed/held/tapped event in the Home app.", sCLRRED) } input "pushableButtonList", "capability.pushableButton", title: inTS1("Pushable Buttons: (${pushableButtonList ? pushableButtonList.size() : 0} Selected)", "button"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input "holdableButtonList", "capability.holdableButton", title: inTS1("Holdable Buttons: (${holdableButtonList ? holdableButtonList.size() : 0} Selected)", "button"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input "doubleTapableButtonList", "capability.doubleTapableButton", title: inTS1("Double Tapable Buttons: (${doubleTapableButtonList ? doubleTapableButtonList.size() : 0} Selected)", "button"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead('Fans:')) { input 'fanList', sCAP_SW, title: inTS1("Fans: (${fanList ? fanList.size() : 0} Selected)", 'fan_on'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'fan3SpdList', sCAP_SW, title: inTS1("Fans (3 Speeds): (${fan3SpdList ? fan3SpdList.size() : 0} Selected)", 'fan_on'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'fan4SpdList', sCAP_SW, title: inTS1("Fans (4 Speeds): (${fan4SpdList ? fan4SpdList.size() : 0} Selected)", 'fan_on'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'fan5SpdList', sCAP_SW, title: inTS1("Fans (5 Speeds): (${fan5SpdList ? fan5SpdList.size() : 0} Selected)", 'fan_on'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead('Thermostats:')) { input 'tstatList', 'capability.thermostat', title: inTS1("Thermostats: (${tstatList ? tstatList.size() : 0} Selected)", 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'tstatFanList', 'capability.thermostat', title: inTS1("Thermostats + Fan: (${tstatFanList ? tstatFanList.size() : 0} Selected)", 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'tstatCoolList', 'capability.thermostat', title: inTS1("Cool Only Thermostats: (${tstatCoolList ? tstatCoolList.size() : 0} Selected)", 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'tstatHeatList', 'capability.thermostat', title: inTS1("Heat Only Thermostats: (${tstatHeatList ? tstatHeatList.size() : 0} Selected)", 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } // section(sectHead('Humidity Control:')) { // input 'humidifierList', 'capability.relativeHumidityMeasurement', title: inTS1("Humidifier: (${humidifierList ? humidifierList.size() : 0} Selected)", 'humidity'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false // input 'dehumidifierList', 'capability.relativeHumidityMeasurement', title: inTS1("Dehumidifier: (${dehumidifierList ? dehumidifierList.size() : 0} Selected)", 'humidity'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false // } section(sectHead('All Other Devices:')) { input 'sensorList', 'capability.sensor', title: inTS1("Sensors: (${sensorList ? sensorList.size() : 0} Selected)", 'sensors'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'switchList', sCAP_SW, title: inTS1("Switches: (${switchList ? switchList.size() : 0} Selected)", sSW), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'deviceList', 'capability.*', title: inTS1("Others: (${deviceList ? deviceList.size() : 0} Selected)", 'devices2'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead('Create Devices for Modes in HomeKit?')) { paragraph spanSmBldBr('What are these for?', sCLRGRY) + spanSm("Creates a virtual device for selected modes in HomeKit.
${sBULLET} The switch will be ON when that mode is active.", sCLRGRY) List modes = ((List)location.getModes())?.sort { (String)it?.name }?.collect { [("${it?.id}".toString()): (String)it?.name] } input 'modeList', sENUM, title: inTS1('Create Devices for these Modes', 'mode'), required: false, description: inputFooter(sTTS, sCLRGRY, true), multiple: true, options: modes, submitOnChange: true } section(sectHead('Create Devices for WebCoRE Pistons in HomeKit?')) { input 'enableWebCoRE', sBOOL, title: inTS1('Enable webCoRE Integration', webCore_icon()), required: false, defaultValue: false, submitOnChange: true if (getBoolSetting('enableWebCoRE')) { if (!webCoREFLD) { webCoRE_init() } paragraph spanSmBldBr('What are these for?', sCLRGRY) + spanSm("Creates a virtual device for selected pistons in HomeKit.
${sBULLET} These are useful for use in Home Kit scenes", sCLRGRY) List pistons = webCoRE_list() input 'pistonList', sENUM, title: inTS1('Create Devices for these Pistons', webCore_icon()), required: false, description: inputFooter(sTTS, sCLRGRY, true), multiple: true, options: pistons, submitOnChange: true } else { webCoREFLD = [:]; unsubscribe(webCoRE_handle()); remTsVal(sLASTWU) } } inputDupeValidation() } } def settingsPage() { return dynamicPage(name: 'settingsPage', title: sBLANK, install: false, uninstall: false) { section(sectHead('Logging:')) { input 'showCmdLogs', sBOOL, title: inTS1('Show Command Events?', sDBG), required: false, defaultValue: true, submitOnChange: true input 'showEventLogs', sBOOL, title: inTS1('Show Device/Location Events? (Turns Off After 2 hours)', sDBG), required: false, defaultValue: true, submitOnChange: true input 'showDebugLogs', sBOOL, title: inTS1('Show Detailed Logging? (Turns Off After 2 hours)', sDBG), required: false, defaultValue: false, submitOnChange: true } section(sectHead('Security:')) { paragraph spanSmBldBr('Description:', sCLRGRY) + spanSm('This will allow you to clear you existing app accessToken and force a new one to be created.
You will need to update the homebridge config with the new token in order to continue using hubitat with HomeKit', sCLRGRY) input 'resetAppToken', sBOOL, title: inTS1('Revoke and Recreate App Access Token?', 'reset'), defaultValue: false, submitOnChange: true if (getBoolSetting('resetAppToken')) { settingUpdate('resetAppToken', sFALSE, sBOOL); resetAppToken() } } } } private void resetAppToken() { logWarn('resetAppToken | Current Access Token Removed...') state.remove('accessToken') if (getAccessToken()) { logInfo('resetAppToken | New Access Token Created...') } remTsVal(sSVR) } private String getSelectedDeviceDescs() { List tl Boolean conf; conf=false for (String m in ListVars){ if(!conf){ tl = getListSetting(m) if(conf || tl.size()) { conf=true } } } String desc; desc= sNULL Integer devCnt = getDeviceCnt() if (conf) { //static Map fanSettingKeys() { Integer fansize; fansize = 0 List items; items = fanSettingKeys().collect { (String)it.key } items.each { String item -> fansize += getListSetting(item).size() } Integer sz String s Map akeys = deviceSettingKeys() + virtSettingKeys() items = [] + ListVars - items //fanSettingKeys() desc = sBLANK items.each { String item -> tl = getListSetting(item) sz = tl.size() s = tl.size() > 1 ? akeys[item] : akeys[item].replace('Devices','Device') desc += tl.size() > 0 ? spanSmBld(s) + spanSmBr(" (${sz})") : sBLANK } desc += fansize > 0 ? spanSmBld("Fan Device${fansize > 1 ? 's' : sBLANK}") + spanSmBr(" (${fansize})") : sBLANK desc += getBoolSetting('addSecurityDevice') ? spanSmBld('HSM') + spanSmBr(' (1)') : sBLANK desc += htmlLine(sCLR4D9, 150) desc += spanSmBld('Devices Selected:') + spanSmBr(" (${devCnt})") desc += (devCnt > 149) ? lineBr() + spanSmBld('NOTICE: ', sCLRRED) + spanSmBr('Homebridge only allows 149 Devices per HomeKit Bridge!!!', sCLRRED) : sBLANK desc += inputFooter(sTTM) } return desc } private void resetCapFilters() { List remKeys = ((Map)settings).findAll { ((String)it.key).startsWith('remove') }.collect { (String)it.key } if (remKeys?.size() > 0) { remKeys.each { String k-> settingRemove(k) } } settingRemove("customCapFilters") } private Boolean capFiltersSelected() { Map cFilters = parseCustomFilterStr(getStrSetting('customCapFilters') ?: sBLANK) Map perDev = cFilters?.perDevice ?: [:] List global = cFilters?.global ?: [] if (perDev && perDev?.keySet()?.size() || global && global?.size()) { return true } return ( ((Map)settings).findAll { ((String)it.key).startsWith('remove') && it.value }.collect { (String)it.key })?.size() > 0 } private String getCapFilterDesc() { String desc; desc = sBLANK List remKeys = ((Map)settings).findAll { ((String)it.key).startsWith('remove') && it.value != null }.collect { (String)it.key } if (remKeys?.size()) { remKeys.sort().each { String k-> String capName = k.replaceAll('remove', sBLANK) Integer capSize = settings[k]?.size() desc += spanSmBr("${capName}: (${capSize}) Device(s)", sCLR4D9) } } if(desc.size() > 0) { desc += spanSmBr("", sCLR4D9) } List capItems String s = getStrSetting('customCapFilters') capItems = s ? s.split(',').collect { String it -> it.trim() } : [] if (capItems?.size()) { capItems = capItems.unique().sort() desc += spanSmBr("Custom Capabilities: (${capItems.size()})", sCLR4D9) capItems.each { String cap-> desc += spanSmBr(" ${sBULLET} ${cap}", sCLR4D9) } } return desc } private List getCustCapFilters() { List capItems String s = getStrSetting('customCapFilters') capItems = s ? s.split(',').collect { String it -> it.trim() } : [] return capItems?.size() ? capItems.unique().sort() : [] } private void resetAttrFilters() { settingRemove('customAttrFilters') } private Boolean attrFiltersSelected() { Map cFilters = parseCustomFilterStr(getStrSetting('customAttrFilters') ?: sBLANK) Map perDev = cFilters?.perDevice ?: [:] List global = cFilters?.global ?: [] if (perDev && perDev.keySet()?.size() || global && global.size()) { return true } return false } private String getCustAttrFilterDesc() { String desc; desc = sBLANK Map cFilters = parseCustomFilterStr(getStrSetting('customAttrFilters') ?: sBLANK) // log.debug "getCustAttrFilterDesc | customFilters: ${cFilters}" Map perDev = cFilters?.perDevice ?: [:] List global = cFilters?.global ?: [] if (perDev && perDev.keySet()?.size()) { desc += spanSmBr("Per-Device Attributes: (${perDev.keySet().size()})", sCLR4D9) // perDev.each { String dev, Object attrsObj -> // List attrs = attrsObj instanceof List ? attrsObj : attrsObj.toList() // desc += spanSmBr("${dev}:", sCLR4D9) // attrs.each { String attr-> // desc += spanSmBr(" ${sBULLET} ${attr}", sCLR4D9) // } // } } if (global && global.size()) { List attrItems = global.unique().sort() desc += spanSmBr("Global Attributes: (${attrItems.size()})", sCLR4D9) attrItems.each { String attr-> desc += spanSmBr(" ${sBULLET} ${attr}", sCLR4D9) } } return desc } private void inputDupeValidation() { Map> clnUp = (['d': [:], 'o': [:]] as Map>) Map dMap dMap = [:] + deviceSettingKeys() dMap.remove('pushableButtonList'); dMap.remove('holdableButtonList'); dMap.remove('doubleTapableButtonList') dMap.remove('deviceList'); dMap.remove('sensorList'); dMap.remove('switchList') Map> items = [ d: dMap, o: ['deviceList': 'Other', 'sensorList': 'Sensor', 'switchList': 'Switch'] ] items.d.each { String k, String v-> List priItems = (settings."${k}"?.size()) ? settings."${k}"?.collect { (String)it?.getDisplayName() } : null if (priItems && priItems.size()) { items.d.each { String k2, String v2-> List secItems = (settings."${k2}"?.size()) ? settings."${k2}"?.collect { (String)it?.getDisplayName() } : null if (k != k2 && secItems) { secItems?.retainAll(priItems) if (secItems?.size()) { clnUp.d[k2] = clnUp?.d[k2] ?: [] clnUp.d[k2] = (clnUp?.d[k2] + secItems)?.unique() } } } items.o.each { String k2, String v2-> List secItems = (settings."${k2}"?.size()) ? settings."${k2}"?.collect { (String) it?.getDisplayName() } : null if (secItems) { secItems?.retainAll(priItems) if (secItems?.size()) { clnUp.o[k2] = clnUp?.o[k2] ?: [] clnUp.o[k2] = (clnUp?.o[k2] + secItems)?.unique() } } } } } String out; out = sBLANK Boolean show,first show = false first = true if (clnUp.d.size() > 0) { show = true clnUp.d.each { String k, List v-> out += (first ? sBLANK : lineBr()) + spanBldBr("${items?.d[k]}:") out += spanBr(v.collect { " ${sBULLET} " + it.toString() }?.join(sLINEBR)) first = false } } if (clnUp.o.size() > 0) { show = true clnUp.o.each { String k, List v-> out += (first ? sBLANK : lineBr()) + spanBldBr("${items?.o[k]}:") out += spanBr(v.collect { " ${sBULLET} " + it.toString() }?.join(sLINEBR)) first = false } } if (show && out) { section(sectHead('Duplicate Device Validation:')) { paragraph spanBldBr('These Inputs Contain the Same Devices:', sCLRRED) + spanSmBr(out, sCLRRED) + spanSmBld('Please remove these duplicate items!', sCLRRED) } } } String getSetDesc() { List s = [] if (getBoolSetting('showEventLogs') || getBoolDefSetting('showEventLogs',true)) s.push("${sBULLET} Device Event Logging") if (getBoolSetting('showCmdLogs') || getBoolDefSetting('showCmdLogs',true)) s.push("${sBULLET} Command Event Logging") if (getBoolSetting('showDebugLogs')) s.push("${sBULLET} Debug Logging") return s.size() > 0 ? spanSmBr("${s.join('
')}", sCLR4D9) + inputFooter(sTTM, sCLR4D9) : inputFooter(sTTC, sCLRGRY, true) } def historyPage() { return dynamicPage(name: 'historyPage', title: sBLANK, install: false, uninstall: false) { List cHist = getCmdHistory()?.sort { (Long)it.gt }?.reverse() List eHist = getEvtHistory()?.sort { (Long)it.gt }?.reverse() section() { paragraph spanSmBldBr('Notice:', sCLRGRY, sINFO) + spanSm('This history is only stored in memory. It is erased after every code update and Hub reboot.', sCLRGRY) } section(sectHead("Last (${cHist.size()}) Commands Received From HomeKit:")) { if (cHist.size() > 0) { cHist.each { Map c -> List hList = [] hList.push([name: 'Device:', val: c?.data?.device]) hList.push([name: 'Command:', val: c?.data?.cmd]) if (c?.data?.value1) hList.push([name: 'Value1:', val: c?.data?.value1]) if (c?.data?.value2) hList.push([name: 'Value2:', val: c?.data?.value2]) if (c?.data?.execTime) hList.push([name: 'ExecTime:', val: "${c?.data?.execTime}ms"]) hList.push([name: 'Date:', val: c.dt]) if (hList.size()) { paragraph kvListToHtmlTable(hList, sCLR4D9) } } } else { paragraph spanSm('No Command History Found...', sCLRGRY) } } section(sectHead("Last (${eHist.size()}) Events Sent to HomeKit:")) { if (eHist.size() > 0) { eHist.each { Map h-> List hList = [] hList.push([name: 'Device:', val: h?.data?.device]) hList.push([name: 'Event:', val: h?.data?.name]) if (h?.data?.value) hList.push([name: 'Value:', val: h?.data?.value]) if (h?.data?.execTime) hList.push([name: 'ExecTime:', val: "${h?.data?.execTime}ms"]) if (hList.size()) { paragraph spanSmBldBr((String)h.dt, sCLR4D9) + kvListToHtmlTable(hList, sCLR4D9) } } } else { paragraph spanSm('No Event History Found...', sCLRGRY) } } } } private static String kvListToHtmlTable(List tabList, String color=sCLRGRY) { String str; str = sBLANK if (tabList?.size()) { str += "" tabList.each { it-> str += "" } str += '
${spanSmBld((String)it.name)}${spanSmBr("${it.val}")}
' } return str } def capFilterPage() { return dynamicPage(name: 'capFilterPage', title: 'Capability Filtering', install: false, uninstall: false) { section(sectHead('Restrict Temp Device Creation')) { input 'noTemp', sBOOL, title: inTS1('Remove Temperature from All Contacts and Water Sensors?', 'temperature'), required: false, defaultValue: false, submitOnChange: true if (getBoolSetting('noTemp')) { input 'sensorAllowTemp', 'capability.sensor', title: inTS1('Allow Temps on these sensors', 'temperature'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } } section(sectHead('Remove Sensor Capabilities')) { paragraph spanSmBldBr('Description:', sCLRGRY) + spanSm('These inputs will remove specific capabilities from a device preventing the addition of unwanted characteristics in devices under HomeKit', sCLRGRY) input 'removeAcceleration', 'capability.accelerationSensor', title: inTS1('Remove Acceleration from these Devices', 'acceleration'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeBattery', 'capability.battery', title: inTS1('Remove Battery from these Devices', 'battery'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeContact', 'capability.contactSensor', title: inTS1('Remove Contact from these Devices', 'contact'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false // input "removeEnergy", "capability.energyMeter", title: inTS1("Remove Energy Meter from these Devices", "power"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeHumidity', 'capability.relativeHumidityMeasurement', title: inTS1('Remove Humidity from these Devices', 'humidity'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeIlluminance', 'capability.illuminanceMeasurement', title: inTS1('Remove Illuminance from these Devices', 'illuminance'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeMotion', 'capability.motionSensor', title: inTS1('Remove Motion from these Devices', 'motion'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false // input "removePower", "capability.powerMeter", title: inTS1("Remove Power Meter from these Devices", "power"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removePresence', 'capability.presenceSensor', title: inTS1('Remove Presence from these Devices', 'presence'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeTamper', 'capability.tamperAlert', title: inTS1('Remove Tamper from these Devices', 'tamper'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeTemp', 'capability.temperatureMeasurement', title: inTS1('Remove Temperature from these Devices', 'temperature'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeWater', 'capability.waterSensor', title: inTS1('Remove Water from these Devices', 'water'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead('Remove Actuator Capabilities')) { paragraph spanSmBldBr('Description:', sCLRGRY) + spanSm('These inputs will remove specific capabilities from a device preventing the addition of unwanted characteristics in devices under HomeKit', sCLRGRY) input 'removeColorControl', 'capability.colorControl', title: inTS1('Remove ColorControl from these Devices', 'color'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeColorTemperature', 'capability.colorTemperature', title: inTS1('Remove ColorTemperature from these Devices', 'color'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeLevel', 'capability.switchLevel', title: inTS1('Remove Level from these Devices', 'speed_knob'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeLock', 'capability.lock', title: inTS1('Remove Lock from these Devices', 'lock'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeSwitch', sCAP_SW, title: inTS1('Remove Switch from these Devices', sSW), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeValve', 'capability.valve', title: inTS1('Remove Valve from these Devices', 'valve'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead('Remove Thermostats Capabilities')) { paragraph spanSmBldBr('Description:', sCLRGRY) + spanSm('These inputs allow you to remove certain capabilities from thermostats allowing you to customize your experience in HomeKit (Certain Items may break the Thermostat under HomeKit)', sCLRGRY) input 'removeThermostat', 'capability.thermostat', title: inTS1('Remove Thermostat from these Devices', 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeThermostatFanMode', 'capability.thermostat', title: inTS1('Remove Thermostat Fan from these Devices', 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeThermostatCoolingSetpoint', 'capability.thermostat', title: inTS1('Remove Thermostat Cooling from these Devices', 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeThermostatHeatingSetpoint', 'capability.thermostat', title: inTS1('Remove Thermostat Heating from these Devices', 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input 'removeThermostatMode', 'capability.thermostat', title: inTS1('Remove Thermostat Modes from these Devices', 'thermostat'), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead('Remove Buttons Capabilities')) { input "removeHoldableButton", "capability.holdableButton", title: inTS1("Remove Holdable Buttons from these Devices", "button"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input "removeDoubleTapableButton", "capability.doubleTapableButton", title: inTS1("Remove Double Tapable Buttons from these Devices", "button"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false input "removePushableButton", "capability.pushableButton", title: inTS1("Remove Pushable Buttons from these Devices", "button"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, submitOnChange: true, required: false } section(sectHead('Custom Capabilities')) { paragraph spanSmBldBr('Description:', sCLRGRY) + spanSm('This input allows you to define custom capabilities per device and/or globally to prevent unwanted characteristics in devices under HomeKit', sCLRGRY) + spanSmBr("There are 2 ways to format the data:", sCLRGRY) + spanSmBr(" ${sBULLET} To filter out a specific device capabilities wrap the item in brackets like [device_id1:Battery,TemperatureMeasurement], [device_id2:AccelerationSensor,IlluminanceMeasurement]", sCLRGRY) + spanSmBr(" ${sBULLET} To filter out an capabilities from all devices don't use brackets", sCLRGRY) + spanSmBr(" ${sBULLET} Make sure to separate each type (per-device and global) with a comma (,)") + spanSmBr(" ${sBULLET} Use the Device Debug page to see if your filter is working...", sCLRGRY) + spanSmBr("Here is an example of mixing per-device and global filters: [device_id1:Battery,TemperatureMeasurement], [device_id2:AccelerationSensor,IlluminanceMeasurement], SwitchLevel", sCLRORG) input "customCapFilters", "textarea", title: inTS1("Enter custom capabilities", "filter"), description: "Enter the filters using the format mentioned above...", submitOnChange: true, required: false } section(sectHead('Reset Filters:'), hideable: true, hidden: true) { input 'resetCapFilters', sBOOL, title: inTS1('Clear All Capability Filters?', 'reset'), required: false, defaultValue: false, submitOnChange: true if (getBoolSetting('resetCapFilters')) { settingUpdate('resetCapFilters', sFALSE, sBOOL); resetCapFilters() } } } } def attrFilterPage() { return dynamicPage(name: 'attrFilterPage', title: 'Attribute Filtering', install: false, uninstall: false) { section(sectHead('Custom Attributes')) { paragraph spanSmBldBr('Description:', sCLRGRY) + spanSm('This input allows you to define custom attributes per device or globally to prevent subsciption events and/or the addition of unwanted characteristics in devices under HomeKit', sCLRGRY) + spanSmBr("There are 2 ways to format the data:", sCLRGRY) + spanSmBr(" ${sBULLET} To filter out a specific device attributes wrap the item in brackets like [device_id1:speed,switch], [device_id2:temperature,motion]", sCLRGRY) + spanSmBr(" ${sBULLET} To filter out an attribute from all devices don't use brackets", sCLRGRY) + spanSmBr(" ${sBULLET} Make sure to separate each type (per-device and global) with a comma (,)") + spanSmBr(" ${sBULLET} Use the Device Debug page to see if your filter is working...", sCLRGRY) + spanSmBr("Here is an example of mixing per-device and global filters: [device_id1:speed,switch], [device_id2:temperature,motion], temperature", sCLRORG) input "customAttrFilters", "textarea", title: inTS1("Enter custom attributes", "filter"), description: "Enter the filters using the format mentioned above...", submitOnChange: true, required: false } section(sectHead('Reset Filters:'), hideable: true, hidden: true) { input 'resetAttrFilters', sBOOL, title: inTS1('Clear All Attibute Filters?', 'reset'), required: false, defaultValue: false, submitOnChange: true if (getBoolSetting('resetAttrFilters')) { settingUpdate('resetAttrFilters', sFALSE, sBOOL); resetAttrFilters() } } } } def donationPage() { return dynamicPage(name: 'donationPage', title: sBLANK, nextPage: 'mainPage', install: false, uninstall: false) { section(sBLANK) { String str; str = sBLANK str += spanSmBldBr('Hello User,') + spanSmBr("Please forgive the interuption but it's been 30 days since you installed/updated this App and I wanted to present you with this one time reminder that donations are accepted (We do not require them).") str += spanSmBr("If you have been enjoying the software and devices please remember that we have spent thousand's of hours of our spare time working on features and stability for those applications and devices.") str += spanSmBr('If you have already donated, thank you very much for your support!') str += spanSmBr('If you are just not interested in donating please ignore this message') str += spanSm('Thanks again for using Homebridge Hubitat') paragraph divSm(str, sCLRRED) input 'sentDonation', sBOOL, title: inTS1('Already Donated?'), defaultValue: false, submitOnChange: true href url: textDonateLink(), style: sEXTNRL, required: false, title: inTS1('Donations', 'donations'), description: inputFooter('Tap to open in browser', sCLRGRY, true) } updInstData('shownDonation', true) } } def confirmPage() { return dynamicPage(name: 'confirmPage', title: sBLANK, install: true, uninstall:true) { section(sectHead('Confirmation Page')) { paragraph spanSmBldBr('NOTICE:', sCLRGRY) + spanSm('The plugin no longer requires a restart to apply device changes in this app to HomeKit.

Once you press Done the Homebridge plugin will refresh your device changes after 15-20 seconds.') } appFooter() } } def deviceDebugPage() { return dynamicPage(name: 'deviceDebugPage', title: sBLANK, install: false, uninstall: false) { section(sectHead('View All Device Data Sent to HomeBridge:')) { href url: getAppEndpointUrl('alldevices'), style: sEXTNRL, required: false, title: inTS1('View Device Data Sent to Homebridge...', sINFO), description: sBLANK, disabled: true } if (devMode()) { section(sectHead('TimeStamp Debug Data:')) { Map tsMap = tsDtMapFLD[gtAppId()] ?: [:] paragraph "${tsMap}" } } section(sectHead('View Individual Device Data:')) { paragraph spanSmBldBr('NOTICE:', sCLRGRY) + spanSm("Do you have a device that's not working under homekit like you want?
${sBULLET} Select a device from one of the inputs below and it will show you all data about the device.", sCLRGRY) input 'debug_device', 'capability.*', title: inTS1('All Devices:', 'devices2'), description: inputFooter(sTTS, sCLRGRY, true), multiple: false, submitOnChange: true, required: false } if (debug_device) { section(sectHead('Device Data:'), hideable: false, hidden: false) { String desc; desc = viewDeviceDebugPretty() if (desc) { // paragraph spanSmBld('Device Data:', sCLR4D9) paragraph divSm(desc, sCLRGRY) } else { paragraph spanSmBld('No Device Data Received', sCLRRED) } } section(sectHead('Device Data (JSON):'), hideable: false, hidden: false) { // paragraph spanSmBld('Device Data:', sCLR4D9) paragraph divSm("", sCLRGRY) } } } } void clearTestDeviceItems() { settingRemove('debug_device') } private String viewDeviceDebugPretty() { def sDev; sDev = null if (debug_device) { sDev = debug_device } Map devData = getDeviceDebugMap(sDev) String desc; desc = sNULL if(devData) { desc = spanMdBldBr(strUnder('MetaData:'), sCLR4D9) desc += spanSmBld('DisplayName:') + spanSmBr(" ${devData.name}", sCLRGRY) desc += spanSmBld('BaseName:') + spanSmBr(" ${devData.basename}", sCLRGRY) desc += spanSmBld('DeviceID:') + spanSmBr(" ${devData.deviceid}", sCLRGRY) desc += spanSmBld('Status:') + spanSmBr(" ${devData.status}", sCLRGRY) desc += spanSmBld('Manufacturer:') + spanSmBr(" ${devData.manufacturer}", sCLRGRY) desc += spanSmBld('Model:') + spanSmBr(" ${devData.model}", sCLRGRY) desc += spanSmBld('DeviceNetworkId:') + spanSmBr(" ${devData.deviceNetworkId}", sCLRGRY) desc += spanSmBld('LastActivity:') + spanSmBr(" ${devData.lastActivity}", sCLRGRY) // List device attributes desc += lineBr() + spanMdBldBr(strUnder('Attributes:'), sCLR4D9) Map tmp = (Map)devData.attributes if(tmp.size()) { tmp.keySet().sort().each { String att -> Boolean ck = ((List)devData.attributes_filtered).contains(att) def val = tmp[att] String clr = (ck ? sCLRGRY : sCLRGRN) String status = (ck ? 'Filtered' : 'Allowed') desc += spanSm(" ${sBULLET} ") + (ck ? spanSmBr(att + ": ${val.toString()}" + " (${status})", clr) : spanSmBldBr(att + ": ${val.toString()}" + " (${status})", clr)) } } else { desc += spanSmBldBr('No Attributes Found', sCLRRED) } // List device capabilities desc += lineBr() + spanMdBldBr(strUnder('Capabilities:'), sCLR4D9) List tmp1 tmp1 = (List)devData.capabilities if(tmp1?.size()) { tmp1.sort().each { String cap -> Boolean ck = ((List)devData.capabilities_filtered).contains(cap) String clr = (ck ? sCLRGRY : sCLRGRN) String status = (ck ? 'Filtered' : 'Allowed') desc += spanSm(" ${sBULLET} ") + (ck ? spanSmBr(cap + " (${status})", clr) : spanSmBldBr(cap + " (${status})", clr)) } } else { desc += spanSmBldBr('No Capabilities Found', sCLRRED) } // List device commands desc += lineBr() + spanMdBldBr(strUnder('Commands:'), sCLR4D9) tmp1 = (List)devData.commands if(tmp1?.size()) { tmp1.sort().each { String cmd -> Boolean ck = ((List)devData.commands_filtered).contains(cmd) String clr = (ck ? sCLRGRY : sCLRGRN) String status = (ck ? 'Filtered' : 'Allowed') desc += spanSm(" ${sBULLET} ") + (ck ? spanSmBr(cmd + " (${status})", clr) : spanSmBldBr(cmd + " (${status})", clr)) } } else { desc += spanSmBldBr('No Commands Found', sCLRRED) } // List event history desc += lineBr() + spanMdBldBr(strUnder('Event History:'), sCLR4D9) tmp1 = (List)devData.eventHistory if(tmp1?.size()) { tmp1.each { String evt -> desc += spanSmBr(" ${sBULLET} " + evt, sCLRGRY) } } else { desc += spanSmBldBr('No Events Found', sCLRRED) } } return desc } private String viewDeviceDebugAsJson() { def sDev; sDev = null if (debug_device) { sDev = debug_device } String json = new JsonOutput().toJson(getDeviceDebugMap(sDev)) String jsonStr = new JsonOutput().prettyPrint(json) return jsonStr } private Map getDeviceDebugMap(dev) { Map r; r = [result: 'No Data Returned'] if (dev) { def aa try { r = [:] r.name = dev.displayName?.toString()?.replaceAll("[#\$()!%&@^']", sBLANK) r.basename = dev.getName() r.deviceid = gtDevId(dev) r.status = dev.getStatus() r.manufacturer = dev.manufacturerName ?: 'Unknown' r.model = dev?.modelName ?: dev?.getTypeName() r.deviceNetworkId = dev.getDeviceNetworkId() aa = dev.getLastActivity() r.lastActivity = aa ?: null r.capabilities = dev.capabilities?.collect { (String)it.name }?.unique()?.sort() ?: [] aa = deviceCapabilityList(dev) r.capabilities_processed = aa ?: [:] r.capabilities_filtered = filteredOutCaps(dev) ?: [] r.commands = dev.supportedCommands?.collect { (String)it.name }?.unique()?.sort() ?: [] aa = deviceCommandList(dev).sort { it?.key } r.commands_processed = aa ?: [:] r.commands_filtered = filteredOutCommands(dev) ?: [] aa = getDeviceFlags(dev) r.customflags = aa ?: [:] r.attributes = [:] dev.supportedAttributes?.collect { (String)it.name }?.unique()?.sort()?.each { String it -> r.attributes[it] = dev.currentValue(it) } aa = deviceAttributeList(dev).sort { it?.key } r.attributes_processed = aa ?: [:] r.attributes_filtered = filteredOutAttrs(dev) ?: [] r.eventHistory = dev.eventsSince(new Date() - 1, [max: 20])?.collect { "${it?.date} | [${it?.name}] | (${it?.value}${it?.unit ? " ${it.unit}" : sBLANK})" } } catch (ex) { logError("Error while generating device data: ${ex}", ex) } } return r } private Integer getDeviceCnt(Boolean phyOnly=false) { List devices; devices = [] List items = deviceSettingKeys().collect { (String)it.key } items?.each { String item -> List si = getListSetting(item) if (si && si.size() > 0) devices = devices + si.collect { (String)"device_${gtDevId(it)}" } } if (!phyOnly) { virtSettingKeys().collect { (String)it.key }.each { String item-> List si = getListSetting(item) if (si && si.size() > 0) { String aa = item.replaceAll('List', sBLANK) devices = devices + si.collect { "${aa}_${it}".toString() } } } } Integer dSize; dSize = devices.unique().size() dSize = dSize ?: 0 if (getBoolSetting('addSecurityDevice')) { dSize = dSize + 1 } return dSize } def installed() { logDebug("${(String)app.name} | installed() has been called...") state.installData = [initVer: appVersionFLD, dt: getDtNow(), updatedDt: 'Not Set', showDonation: false, shownChgLog: true] initialize() } def updated() { logDebug("${(String)app.name} | updated() has been called...") if (!state.installData) { state.installData = [initVer: appVersionFLD, dt: getDtNow(), updatedDt: getDtNow(), shownDonation: false] } unsubscribe() appCleanup() initialize() remTsVal(sLASTWU) if (getBoolSetting('enableWebCoRE')) { webCoRE_poll(true) } } def initialize() { state.isInstalled = true remTsVal(sSVR) if (getAccessToken()) { subscribeToEvts() runEvery5Minutes('healthCheck') if (getBoolSetting('showEventLogs') && getLastTsValSecs(sEVTLOGEN, 0) == 0) { updTsVal(sEVTLOGEN) } if (getBoolSetting('showDebugLogs') && getLastTsValSecs(sDBGLOGEN, 0) == 0) { updTsVal(sDBGLOGEN) } } else { logError('initialize error: Unable to get or generate app access token') } } Boolean getAccessToken(Boolean disableRetry=false) { try { if (!state.accessToken) { state.accessToken = createAccessToken() remTsVal(sSVR) logWarn('App Access Token Missing... Generating New Token!!!') return true } return true } catch (ex) { if (!disableRetry) { enableOauth() // can fail depending on security settings return getAccessToken(true) } else { String msg = "Error: OAuth is not Enabled for ${app.getName()}!. Please click remove and Enable Oauth under in the HE console 'Apps Code'" logError("getAccessToken Exception: ${msg}",ex) return false } } } private void enableOauth() { Map params = [ uri: "http://localhost:8080/app/edit/update?_action_update=Update&oauthEnabled=true&id=${app.appTypeId}".toString(), headers: ['Content-Type':'text/html;charset=utf-8'] ] try { httpPost(params) { resp -> //LogTrace("response data: ${resp.data}") } } catch (ex) { logError("enableOauth something went wrong: ${ex}", ex) } } void subscribeToEvts() { runIn(6, 'registerDevices') logInfo('Starting Device Subscription Process...') if (getBoolSetting('addSecurityDevice')) { logInfo('Subscribed to (HSM AlarmSystem Events)') subscribe(location, 'hsmStatus', changeHandler) subscribe(location, 'hsmAlert', changeHandler) } if (getListSetting('modeList')) { logInfo("Subscribed to (${getListSetting('modeList').size() ?: 0} Location Modes)") subscribe(location, 'mode', changeHandler) } if (getBoolSetting('enableWebCoRE')) { webCoRE_init() } } @CompileStatic private void healthCheck(Boolean ui=false) { checkVersionData() if (checkIfCodeUpdated(ui)) { logWarn('Code Version Change Detected... Health Check will occur on next cycle.') updated() return } webCoRE_poll() Integer lastUpd = getLastTsValSecs('lastActTs') Integer evtLogSec = getLastTsValSecs(sEVTLOGEN, 0) Integer dbgLogSec = getLastTsValSecs(sDBGLOGEN, 0) // log.debug "evtLogSec: $evtLogSec | dbgLogSec: $dbgLogSec" if (!ui && lastUpd > 14400) { remTsVal(sSVR) } if (evtLogSec > 7200 && getBoolSetting('showEventLogs')) { logWarn("Turning OFF Event Logs | It's been (${evtLogSec} sec)"); remTsVal(sEVTLOGEN); settingUpdate('showEventLogs', sFALSE, sBOOL) } else if (evtLogSec == 0 && getBoolSetting('showEventLogs')) { updTsVal(sEVTLOGEN) } if (dbgLogSec > 7200 && getBoolSetting('showDebugLogs')) { logWarn("Turning OFF Debug Logs | It's been (${dbgLogSec} sec)"); remTsVal(sDBGLOGEN); settingUpdate('showDebugLogs', sFALSE, sBOOL) } else if (dbgLogSec == 0 && getBoolSetting('showDebugLogs')) { updTsVal(sDBGLOGEN) } } Boolean checkIfCodeUpdated(Boolean ui=false) { //if(!ui) logDebug("Code versions: ${state.codeVersions}") if (state.codeVersions?.mainApp != appVersionFLD) { updCodeVerMap('mainApp', appVersionFLD) Map iData = state.installData ?: [:] iData['updatedDt'] = getDtNow() iData['shownChgLog'] = false if (iData?.shownDonation == null) { iData['shownDonation'] = false } state.installData = iData logInfo('Code Version Change Detected... | Re-Initializing Homebridge App in 5 seconds') return true } return false } private void appCleanup() { List removeItems = ['hubPlatform', 'cmdHistory', 'evtHistory', 'tsDtMap', 'lastMode', 'pollBlocked', 'devchanges', 'subscriptionRenewed'] if (state.directIP && state.directPort) { // old cleanup state.pluginDetails = [ directIP: state.directIP, directPort: state.directPort ] removeItems.push('directIP') removeItems.push('directPort') } removeItems.each { String it -> if (state?.containsKey(it)) state.remove(it) } List removeSettings = ['removeColorTemp', 'hubitatQueryString'] removeSettings.each { String it -> if (settings.containsKey(it)) settingRemove(it) } } private List renderDevices() { Map devMap = [:] List devList = [] List items; items = deviceSettingKeys().collect { (String)it.key } items = items + virtSettingKeys().collect { (String)it.key } items.each { String item -> List tl= getListSetting(item) if (tl.size()) { tl.each { dev-> Map devObj try { devObj = getDeviceData(item, dev) devObj = devObj != null ? devObj : [:] if (devObj.size() > 0) { devMap[(String)devObj.deviceid] = devObj } } catch (ex) { logError("Setting key $item Device (${dev?.displayName}) | Render Exception: ${ex.message}", ex) } } } } if (getBoolSetting('addSecurityDevice')) { devList?.push(getSecurityDevice()) } if (devMap.size() > 0) { devMap.sort { (String)it.value.name }?.each { k, v-> devList.push(v) } } return devList } private Map performPluginTest(){ Map params = [ uri: "http://localhost:8080/app/edit/update?_action_update=Update&oauthEnabled=true&id=${app.appTypeId}".toString(), headers: ['Content-Type':'text/html;charset=utf-8'] ] try { httpPost(params) { resp -> //LogTrace("response data: ${resp.data}") } } catch (ex) { logError("enableOauth something went wrong: ${ex}", ex) null } } private Map getDeviceData(String type, sItem) { // log.debug "getDeviceData($type, $sItem)" String curType; curType= 'device' String devId; devId= sBLANK //= sItem.toString() Boolean isVirtual; isVirtual= false String firmware; firmware= sNULL String name; name= sNULL Map optFlags = [:] def attrVal; attrVal= null def obj //= null switch (type) { case 'pistonList': isVirtual = true curType = 'Piston' optFlags['virtual_piston'] = 1 devId = sItem.toString() obj = getPistonById(devId) if (obj) { name = 'Piston - ' + obj?.name attrVal = 'off' } break case 'modeList': isVirtual = true curType = 'Mode' optFlags['virtual_mode'] = 1 obj = fndMode(sItem.toString()) if (obj) { // BUGFIX for modes deviceId may not be unique vs. device.id devId = 'm_'+sItem.toString() name = 'Mode - ' + (String)obj.name attrVal = modeSwitchState((String)obj.name) } break case 'securityKeypadsList': curType = 'Security Keypad' default: obj = sItem // Define firmware variable and initialize it out of device handler attribute` if (sItem) { try { if (sItem.hasAttribute('firmware')) { firmware = sItem.currentValue('firmware')?.toString() } } catch (ignored) { firmware = sNULL } } break } if (curType && obj && sItem) { if (curType == 'Security Keypad') { return [ name: sItem.displayName?.toString()?.replaceAll("[#\$()!%&@^']", sBLANK), basename: sItem.name, deviceid: "securityKeypad_${sItem.id}", status: sItem.status, manufacturerName: sItem.manufacturerName ?: pluginNameFLD, modelName: sItem.modelName ?: sItem.getTypeName(), serialNumber: sItem.getDeviceNetworkId(), firmwareVersion: firmware ?: '1.0.0', lastTime: sItem?.getLastActivity() ?: null, capabilities: ['Alarm System Status': 1, 'Alarm': 1], commands: [], attributes: ['alarmSystemStatus': getSecurityKeypadMode((String)sItem.currentValue('securityKeypad')?.toString())] ] } else { return [ name: !isVirtual ? sItem.displayName?.toString()?.replaceAll("[#\$()!%&@^']", sBLANK) : name?.toString()?.replaceAll("[#\$()!%&@^']", sBLANK), basename: !isVirtual ? sItem.name : name, deviceid: !isVirtual ? sItem.id : devId, status: !isVirtual ? sItem.status : 'Online', manufacturerName: (!isVirtual ? sItem.manufacturerName : pluginNameFLD) ?: pluginNameFLD, modelName: !isVirtual ? (sItem.modelName ?: sItem.getTypeName()) : curType+" Device", serialNumber: !isVirtual ? sItem.getDeviceNetworkId() : curType+devId, firmwareVersion: firmware ?: '1.0.0', lastTime: !isVirtual ? (sItem.getLastActivity() ?: null) : wnow(), capabilities: !isVirtual ? deviceCapabilityList(sItem) : [(curType) : 1], commands: !isVirtual ? deviceCommandList(sItem) : [on: 1], deviceflags: !isVirtual ? getDeviceFlags(sItem) : optFlags, attributes: !isVirtual ? deviceAttributeList(sItem) : [(sSW): attrVal] ] } } return null } String modeSwitchState(String mode) { return ((String)location.getMode() == mode) ? 'on' : 'off' } def getSecurityDevice() { return [ name: getAlarmSystemName(), basename: getAlarmSystemName(), deviceid: "alarmSystemStatus_${location?.id}", status: 'ACTIVE', manufacturerName: pluginNameFLD, modelName: getAlarmSystemName(), serialNumber: getAlarmSystemName(true), firmwareVersion: '1.0.0', lastTime: null, capabilities: ['Alarm System Status': 1, 'Alarm': 1], commands: [], attributes: ['alarmSystemStatus': getSecurityStatus()] ] } @CompileStatic Map getDeviceFlags(device) { Map opts = [:] [fan3SpdList: "fan_3_spd", fan4SpdList: "fan_4_spd", fan5SpdList: "fan_5_spd", lightNoAlList: "light_no_al"].each { String k, String v -> if (isDeviceInInput(k, gtDevId(device))) { opts[v] = 1 } } // if(opts?.size()>0) log.debug "opts: ${opts}" return opts } def findDevice(String dev_id) { List allDevs; allDevs= [] deviceSettingKeys().collect { (String)it.key }?.each { String key-> List setL = getListSetting(key) allDevs = allDevs + (setL ?: []) } def aa = allDevs.find { gtDevId(it) == dev_id } return aa ?: null } static def authError() { return [error: 'Permission denied'] } static String getAlarmSystemName(Boolean abbr=false) { return (abbr ? 'HSM' : 'Hubitat Safety Monitor') } String getSecurityStatus(Boolean retInt=false) { String cur = (String)location.hsmStatus /*if (retInt) { switch (cur) { case 'armedHome': case 'stay': return 0 case 'armedAway': case 'away': return 1 case 'armedNight': case 'night': return 2 case 'disarmed': case 'off': return 3 case 'intrusion-home': case 'intrusion-away': case 'intrusion-night': return 4 } //} else { return cur ?: 'disarmed' } }*/ return cur ?: 'disarmed' } void setAlarmSystemMode(String mode) { String sMode; sMode= sNULL switch (mode) { case 'armAway': case 'away': sMode = 'armAway' break case 'armNight': sMode = 'armNight' break case 'armHome': case 'night': case 'stay': sMode = 'armHome' break case 'disarm': case 'off': sMode = 'disarm' break } logInfo("Setting the ${getAlarmSystemName()} Mode to (${sMode})...") sendLocationEvent(name: 'hsmSetArm', value: sMode) } static String mapSecurityKeypadMode(String cmd) { String kCmd; kCmd = sNULL switch (cmd) { case 'armAway': case 'away': kCmd = 'armAway' break case 'night': case 'armNight': kCmd = 'armNight' break case 'armHome': case 'home': case 'stay': kCmd = 'armHome' break case 'disarm': case 'off': case 'cancel': kCmd = 'disarm' break } // log.debug "mapSecurityKeypadMode | StatusIn: (${cmd}) | ModeOut: (${kCmd})" return kCmd } static String getSecurityKeypadMode(String status) { // log.debug "getSecurityKeypadMode: ${status}" String hStatus; hStatus = sNULL switch (status) { case 'armed away': hStatus = 'armedAway' break case 'intrusion-away': // accomodate custom drivers setting the securityKeypad attribute to custom values for intrusion hStatus = 'intrusion-away' break case 'armed night': hStatus = 'armedNight' break case 'intrusion-night': // accomodate custom drivers setting the securityKeypad attribute to custom values for intrusion hStatus = 'intrusion-night' break case 'armed home': hStatus = 'armedHome' break case 'intrusion-home': // accomodate custom drivers setting the securityKeypad attribute to custom values for intrusion hStatus = 'instrusion-home' break case 'disarmed': hStatus = 'disarmed' break } // log.debug "getSecurityKeypadMode | StatusIn: (${status}) | ModeOut: (${hStatus})" return hStatus } String getAppEndpointUrl(String subPath) { return "${getApiServerUrl()}/${getHubUID()}/apps/${app?.id}${subPath ? "/${subPath}" : sBLANK}?access_token=${(String)state.accessToken}".toString() } String getLocalEndpointUrl(String subPath) { return "${getLocalApiServerUrl()}/apps/${app?.id}${subPath ? "/${subPath}" : sBLANK}?access_token=${(String)state.accessToken}".toString() } String getLocalUrl(String subPath) { return "${getLocalApiServerUrl()}/apps/${app?.id}${subPath ? "/${subPath}" : sBLANK}?access_token=${(String)state.accessToken}".toString() } String renderConfig() { Map jsonMap = [ platform: pluginNameFLD, name: pluginNameFLD, app_url_local: "${getLocalApiServerUrl()}/".toString(), app_url_cloud: "${getApiServerUrl()}/${getHubUID()}/apps/".toString(), app_id: gtAppId(), app_platform: platformFLD, use_cloud: getBoolSetting('use_cloud_endpoint'), polling_seconds: (Integer)settings.polling_seconds ?: 3600, access_token: (String)state.accessToken, temperature_unit: getStrSetting('temp_unit') ?: (String)location.temperatureScale, validateTokenId: getBoolSetting('validate_token'), adaptive_lighting: getBoolDefSetting('adaptive_lighting',true), consider_fan_by_name: getBoolDefSetting('consider_fan_by_name',true), consider_light_by_name: getBoolSetting('consider_light_by_name'), adaptive_lighting_offset: (getBoolSetting('adaptive_lighting') && settings.adaptive_lighting_offset) ? settings.adaptive_lighting_offset.toInteger() : 0, round_levels: getBoolDefSetting('round_levels',true), logConfig: [ debug: false, showChanges: true ] ] String cj = new JsonOutput().toJson(jsonMap) String cs = new JsonOutput().prettyPrint(cj) return cs } Map renderLocation() { return [ latitude: location?.latitude, longitude: location?.longitude, mode: location?.mode, name: location?.name, temperature_scale: getStrSetting('temp_unit') ?: (String)location.temperatureScale, zip_code: location?.zipCode, hubIP: ((List)location?.hubs)[0]?.localIP, use_cloud: getBoolSetting('use_cloud_endpoint'), app_version: appVersionFLD ] } def CommandReply(Boolean shw, String statusOut, String messageOut, Integer code) { String replyJson = new JsonOutput().toJson([status: statusOut, message: messageOut]) if (shw) { logInfo(messageOut) } wrender contentType: sAPPJSON, data: replyJson, code: code } static Map getHttpHeaders(String headers) { Map obj = [:] new String(headers.decodeBase64()).split('\r\n')?.each { param -> List nameAndValue = param.split(sCLN) obj[(String)nameAndValue[0]] = (nameAndValue.length == 1) ? sBLANK : nameAndValue[1].trim() } return obj } def deviceCommand() { // log.info("Command Request: $params") def val1 = request?.JSON?.value1 ?: null def val2 = request?.JSON?.value2 ?: null return processCmd((String)params?.id, (String)params?.command, val1, val2) } private processCmd(String idevId, String cmd, value1, value2) { String devId; devId=idevId Long execDt = wnow() Boolean shw = getBoolSetting('showCmdLogs') if (shw) { logInfo("Plugin called Process Command | DeviceId: $devId | Command: ($cmd)${value1 ? " | Param1: ($value1)" : sBLANK}${value2 ? " | Param2: ($value2)" : sBLANK}") } if (!devId) { return } String command; command = cmd if (devId.contains("securityKeypad_") && getListSetting('securityKeypadsList')) { command = mapSecurityKeypadMode(command) devId = devId.replaceFirst("securityKeypad_", "") } if (devId == "alarmSystemStatus_${location?.id}" && getBoolSetting('addSecurityDevice')) { setAlarmSystemMode(command) Long pt = execDt ? (wnow() - execDt) : 0L logCmd([cmd: command, device: getAlarmSystemName(), value1: value1, value2: value2, execTime: pt]) return CommandReply(shw, sSUCC, "Security Alarm, Command: [$command]", 200) } else if (command == 'mode' && getListSetting('modeList')) { if (shw) { logDebug("Virtual Mode Received: ${devId}") } String mdevId = devId.replaceAll('m_', sBLANK) changeMode(mdevId, shw) Long pt = execDt ? (wnow() - execDt) : 0L logCmd([cmd: command, device: 'Mode Device', value1: value1, value2: value2, execTime: pt]) return CommandReply(shw, sSUCC, "Mode Device | Command: [$command] | Process Time: (${pt}ms)", 200) } else if (command == 'piston' && getListSetting('pistonList')) { if (shw) { logDebug("Virtual Piston Received: ${devId}") } String aa = runPiston(devId, shw) Long pt = execDt ? (wnow() - execDt) : 0L logCmd([cmd: command, device: 'Piston Device', value1: value1, value2: value2, execTime: pt]) return CommandReply(shw, sSUCC, "Piston | ${aa} | Command: [$command] | Process Time: (${pt}ms)", 200) } else { def device = findDevice(devId) String devN = device?.displayName if (!device) { logError("Device Not Found $devId") return CommandReply(shw, 'Failure', 'Device Not Found', 500) } if (!device?.hasCommand(command)) { logError("Device ${devN} does not have the command $command") return CommandReply(shw, 'Failure', "Device ${devN} does not have the command $command", 500) } if (command == "setColorTemperature" && device.currentValue("switch") != "on"){ return CommandReply(shw, sSUCC, 'Command was setColorTemperature but device is not on', 200) } String cmdS cmdS = shw ? "Command Successful for Device | Name: ${devN} | Command: [${command}(".toString() : sBLANK try { if (value2 != null) { device."$command"(value1, value2) if (shw) { cmdS = cmdS + "$value1, $value2)]".toString() } } else if (value1 != null) { device."$command"(value1) if (shw) { cmdS = cmdS + "$value1)]".toString() } } else { device."$command"() if (shw) { cmdS = cmdS + ')]' } } if (shw) { logInfo(cmdS) } Long pt = execDt ? (wnow() - execDt) : 0L logCmd([cmd: command, device: devN, value1: value1, value2: value2, execTime: pt]) return CommandReply(shw, sSUCC, "Name: ${devN} | Command: [${command}()] | Process Time: (${pt}ms)", 200) } catch (ex) { logError("Error Occurred for Device | Name: ${devN} | Command: [${command}()] ${ex}", ex) return CommandReply(shw, 'Failure', "Error Occurred For Device ${devN} | Command [${command}()]", 500) } } } private void changeMode(String modeId, Boolean shw) { if (modeId) { Map mode = fndMode(modeId) if (mode) { if (shw) { logInfo("Setting the Location Mode to (${mode.name})...") } setLocationMode(mode.name) } else { logError("Unable to find a matching mode for the id: ${modeId}") } } } private runPiston(rtId, Boolean shw) { if (rtId) { Map rt = findVirtPistonDevice(rtId) String name = (String)rt?.name if (name) { if (shw) { logInfo("Executing the (${name}) Piston...") } sendLocationEvent(name: rt.id, value:'homebridge', isStateChange: true, displayed: false, linkText: 'Execute Piston from homebridge', descriptionText: "Homebridge piston execute ${name}", data: [:]) runIn(2, 'endPiston', [data: [id:rtId, name:name]]) return name } else { logError("Unable to find a matching piston for the id: ${rtId}") } } return null } void endPiston(evt) { changeHandler([deviceId:evt.id , name: 'webCoRE', value: 'pistonExecuted', displayName: evt.name, date: new Date()]) } def deviceAttribute() { def device = findDevice((String)params?.id) String attribute = (String)params?.attribute Map res Integer code; code=200 if (!device) { code=404 res= [status: 'Failure', message: 'Device not found'] } else { res=[currentValue: device?.currentValue(attribute)] } String resultJson = new JsonOutput().toJson(res) wrender contentType: sAPPJSON, data: resultJson, code: code } static Map findVirtPistonDevice(id) { Map aa = getPistonById("${id}".toString()) return aa ?: null } Map deviceCapabilityList(device) { String devid = gtDevId(device) if (!device || !devid) { return [:] } Map capItems = ((List)device.getCapabilities())?.findAll { (String)it.name in allowedListFLD.capabilities }?.collectEntries { capability-> [ ((String)capability.name) :1 ] } if(isDeviceInInput("pushableButtonList", devid)) { capItems["Button"] = 1; capItems["PushableButton"] = 1 } else { capItems.remove("PushableButton") } if(isDeviceInInput("holdableButtonList", devid)) { capItems["Button"] = 1; capItems["HoldableButton"] = 1 } else { capItems.remove("HoldableButton") } if(isDeviceInInput("doubleTapableButtonList", devid)) { capItems["Button"] = 1; capItems["DoubleTapableButton"] = 1 } else { capItems.remove("DoubleTapableButton") } if (isDeviceInInput('lightList', devid)) { capItems['LightBulb'] = 1 } if (isDeviceInInput('outletList', devid)) { capItems['Outlet'] = 1 } if (isDeviceInInput('lightNoAlList', devid)) { capItems['LightBulb'] = 1 } if (isDeviceInInput('fanList', devid)) { capItems['Fan'] = 1 } if (isDeviceInInput('speakerList', devid)) { capItems['Speaker'] = 1 } if (isDeviceInInput('shadesList', devid)) { capItems['WindowShade'] = 1 } if (isDeviceInInput('securityKeypadsList', devid)) { capItems['SecurityKeypad'] = 1 } if (isDeviceInInput('garageList', devid)) { capItems['GarageDoorControl'] = 1 } if (isDeviceInInput('tstatList', devid)) { capItems['Thermostat'] = 1; capItems['ThermostatOperatingState'] = 1; capItems?.remove('ThermostatFanMode') } if (isDeviceInInput('tstatFanList', devid)) { capItems['Thermostat'] = 1; capItems['ThermostatOperatingState'] = 1 } if (isDeviceInInput('tstatCoolList', devid)) { capItems['Thermostat'] = 1; capItems['ThermostatOperatingState'] = 1; capItems.remove('ThermostatHeatingSetpoint') } if (isDeviceInInput('tstatHeatList', devid)) { capItems['Thermostat'] = 1; capItems['ThermostatOperatingState'] = 1; capItems.remove('ThermostatCoolingSetpoint') } // if (isDeviceInInput('humidifierList', devId)) { capItems['HumidityControl'] = 1; capItems['Humidifier'] = 1 } // if (isDeviceInInput('dehumidifierList', devId)) { capItems['HumidityControl'] = 1; capItems['Dehumidifier'] = 1 } //switchList, deviceList if (getBoolSetting('noTemp') && capItems['TemperatureMeasurement'] && (capItems['ContactSensor'] || capItems['WaterSensor'])) { Boolean remTemp; remTemp = true if (getListSetting('sensorAllowTemp') && isDeviceInInput('sensorAllowTemp', devid)) { remTemp = false } if (remTemp) { capItems.remove('TemperatureMeasurement') } } //This will filter out selected capabilities from the devices selected in filtering inputs. List remKeys remKeys = ((Map)settings).findAll { ((String)it.key).startsWith('remove') && it.value != null }.collect { (String)it.key } if (!remKeys) remKeys = [] Boolean sdl = getBoolSetting('showDebugLogs') remKeys.each { String k-> String capName = k.replaceAll('remove', sBLANK) String theCap = (String)capFilterFLD[capName] if (theCap && capItems[theCap] && isDeviceInInput(k, devid)) { capItems?.remove(theCap) if (sdl) { logDebug("Filtering ${capName}") } } } return capItems?.sort { (String)it.key } } private List filteredOutCaps(device) { List capsFiltered; capsFiltered = [] List remKeys remKeys = ((Map)settings).findAll { ((String)it.key).startsWith('remove') && it.value != null }.collect { (String)it.key } if (!remKeys) remKeys = [] remKeys.each { String k-> String capName = k.replaceAll('remove', sBLANK) String theCap = capFilterFLD[capName] if (theCap && isDeviceInInput(k, gtDevId(device))) { capsFiltered.push(theCap) } } // List custCaps = getCustCapFilters() capsFiltered = capsFiltered + device.capabilities?.findAll { ignoreCapability(device, (String)it.name, true) }?.collect { (String)it.name } return capsFiltered } private Boolean ignoreCapability(device, String icap, Boolean inclIgnoreFld=false) { String cap; cap = icap.toLowerCase() if (inclIgnoreFld && !(cap in allowedListFLD.capabilities.collect { it.toLowerCase() })) { return true } Map customFilters = parseCustomFilterStr(getStrSetting('customCapFilters') ?: sBLANK) List globalFilters = customFilters.global ?: [] Map> perDeviceFilters = customFilters.perDevice ?: [:] if (globalFilters.contains(cap)) { return true } String devid = gtDevId(device) if (perDeviceFilters[devid] && (((List)perDeviceFilters[devid]).collect { it.toLowerCase() }?.contains(cap)) ) { return true } return false } Map deviceCommandList(device) { String devid = gtDevId(device) if (!device || !devid) { return [:] } Map cmds = device.supportedCommands?.findAll { !ignoreCommand((String)it.name) }?.collectEntries { c-> [ ((String)c.name) : 1 ] } if (isDeviceInInput('tstatList', devid)) { cmds.remove('setThermostatFanMode'); cmds.remove('fanAuto'); cmds.remove('fanOn'); cmds.remove('fanCirculate') } if (isDeviceInInput('tstatCoolList', devid)) { cmds.remove('setHeatingSetpoint'); cmds.remove('auto'); cmds.remove('heat') } if (isDeviceInInput('tstatHeatList', devid)) { cmds.remove('setCoolingSetpoint'); cmds.remove('auto'); cmds.remove('cool') } if (isDeviceInInput('removeColorControl', devid)) { cmds.remove('setColor'); cmds.remove('setHue'); cmds.remove('setSaturation') } if (isDeviceInInput('removeColorTemperature', devid)) { cmds.remove('setColorTemperature') } if (isDeviceInInput('removeThermostatFanMode', devid)) { cmds.remove('setThermostatFanMode'); cmds.remove("setSupportedThermostatFanModes"); cmds.remove('fanAuto'); cmds.remove('fanOn'); cmds.remove('fanCirculate') } if (isDeviceInInput('removeThermostatMode', devid)) { cmds.remove('setThermostatMode'); cmds.remove("setSupportedThermostatModes"); cmds.remove('auto'); cmds.remove('cool'); cmds.remove('emergencyHeat'); cmds.remove('heat') } if (isDeviceInInput('removeThermostatCoolingSetpoint', devid)) { cmds.remove('setCoolingSetpoint'); cmds.remove('auto'); cmds.remove('cool') } if (isDeviceInInput('removeThermostatHeatingSetpoint', devid)) { cmds.remove('setHeatingSetpoint'); cmds.remove('heat'); cmds.remove('emergencyHeat'); cmds.remove('auto') } return cmds } private static Boolean ignoreCommand(String cmd) { return cmd && !(cmd in allowedListFLD.commands) } private List filteredOutCommands(device) { List filtered = device.supportedCommands?.findAll { ignoreCommand((String)it.name) }?.collect { c-> (String)c.name } ?: [] return filtered } private Map parseCustomFilterStr(String text) { Map result = [ perDevice: [:], global: [] ] // Match sections either inside [ ] or standalone without brackets. java.util.regex.Matcher matcher = text =~ /\[([^\]]+)\]|([^,\[]+)(?=\s*,|$)/ matcher.each { String section section = it[0].trim() if (section.startsWith("[") && section.endsWith("]")) { section = section[1..-2] // Remove the brackets String[] parts = section.split(":") if (parts.size() == 2) { String deviceId = parts[0].trim() String[] attributes = parts[1].split(",\\s*") result.perDevice[deviceId] = attributes } } else { // If no brackets, it's a global attribute result.global << section.trim() } } // log.debug "parseCustomFilterStr: ${result}" return result } private Boolean ignoreAttribute(device, String iattr, Boolean inclIgnoreFld=true) { String attr; attr = iattr.toLowerCase() if (inclIgnoreFld && !(attr in allowedListFLD.attributes.collect { it.toLowerCase() })) { return true } Map customFilters = parseCustomFilterStr(getStrSetting('customAttrFilters') ?: sBLANK) List globalFilters = customFilters.global ?: [] Map> perDeviceFilters = customFilters.perDevice ?: [:] if (globalFilters.contains(attr)) { return true } String devid = gtDevId(device) if (perDeviceFilters[devid] && (((List)perDeviceFilters[devid]).collect { it.toLowerCase() }?.contains(attr)) ) { return true } return false } private List filteredOutAttrs(device) { List filtered; filtered = [] filtered = device.supportedAttributes?.findAll { ignoreAttribute(device, (String)it.name) }?.collect { (String)it.name } return filtered } Map deviceAttributeList(device) { String devid= gtDevId(device) if (!device || !devid) { return [:] } Map atts = ((List)device.getSupportedAttributes())?.findAll { (String)it.name in allowedListFLD.attributes }?.collectEntries { attribute -> String attr=(String)attribute.name try { // if(attr == "speed") { // [(attr): getFanSpeedInteger(device.currentValue(attr))] // } else { [(attr): device.currentValue(attr)] // } } catch (ignored) { [(attr): null] } } if (isDeviceInInput('tstatCoolList', devid)) { atts.remove('heatingSetpoint'); atts.remove('heatingSetpointRange') } if (isDeviceInInput('tstatHeatList', devid)) { atts.remove('coolingSetpoint'); atts.remove('coolingSetpointRange') } if (isDeviceInInput('removeColorControl', devid)) { atts.remove('RGB'); atts.remove('color'); atts.remove('colorName'); atts.remove('hue'); atts.remove('saturation') } if (isDeviceInInput('removeColorTemperature', devid)) { atts.remove('colorTemperature') } if (isDeviceInInput('removeThermostatFanMode', devid)) { atts.remove('thermostatFanMode'); atts.remove('supportedThermostatFanModes') } if (isDeviceInInput('removeThermostatMode', devid)) { atts.remove('thermostatMode'); atts.remove('supportedThermostatModes') } if (isDeviceInInput('removeThermostatCoolingSetpoint', devid)) { atts.remove('thermostatCoolingSetpoint'); atts.remove('coolingSetpoint') } if (isDeviceInInput('removeThermostatHeatingSetpoint', devid)) { atts.remove('thermostatHeatingSetpoint'); atts.remove('heatingSetpoint') } return atts } def getAllData() { logTrace('Plugin called to Renew subscriptions') // state.subscriptionRenewed = wnow() String deviceJson = new JsonOutput().toJson([location: renderLocation(), deviceList: renderDevices()]) updTsVal('lastDeviceDataQueryDt') wrender contentType: sAPPJSON, data: deviceJson } static Map deviceSettingKeys() { return [ 'lightList': 'Light Devices', 'lightNoAlList': 'Light (Block Adaptive Lighting) Devices', 'outletList': 'Outlet Devices', 'pushableButtonList': 'Pushable Button Devices', 'holdableButtonList': 'Holdable Button Devices', 'doubleTapableButtonList': 'Double Tapable Button Devices', ] + fanSettingKeys() + [ 'speakerList': 'Speaker Devices', 'shadesList': 'Window Shade Devices', 'securityKeypadsList': 'Security Keypad Devices', 'garageList': 'Garage Door Devices', 'tstatList': 'T-Stat Devices', 'tstatFanList': 'T-Stat + Fan Devices', 'tstatHeatList': 'T-Stat (HeatOnly) Devices', 'tstatCoolList': 'T-Stat (CoolOnly) Devices', 'sensorList': 'Sensor Devices', 'switchList': 'Switch Devices', 'deviceList': 'Other Devices', // 'humidifierList': "Humidifiers", 'dehumidifierList': "Dehumidifiers", ] } static Map fanSettingKeys() { return [ 'fanList': 'Fan Devices', 'fan3SpdList': 'Fans (3Spd) Devices', 'fan4SpdList': 'Fans (4Spd) Devices', 'fan5SpdList': 'Fans (5Spd) Devices', ] } static Map virtSettingKeys() { return ['modeList': 'Mode Devices', 'pistonList': 'Piston Devices'] } void registerDevices() { //This has to be done at startup because it takes too long for a normal command. Boolean shw deviceSettingKeys().each { String k, String v-> shw = false //(k=='shadesList') List l = getListSetting(k) logDebug("Subscribed to (${l?.size() ?: 0}) ${v}") registerChangeHandler(l, shw) } logInfo("Subscribed to (${getDeviceCnt(true)} Physical Devices)") logDebug('-----------------------------------------------') if (getBoolSetting('restartService')) { logWarn('Sent Request to Homebridge Service to restart...') attemptServiceRestart() settingUpdate('restartService', sFALSE, sBOOL) } runIn(5, 'updateServicePrefs') runIn(8, 'sendDeviceRefreshCmd') } @CompileStatic Boolean isDeviceInInput(String setKey, String devId) { List l = getListSetting(setKey) if (l) { return (l.find { gtDevId(it) == devId }) } return false } void registerChangeHandler(List devices, Boolean showlog=false) { devices?.each { device -> String devid = gtDevId(device) List theAtts = ((List)device.getSupportedAttributes())?.collect { (String)it.name }?.unique() if (showlog) { log.debug "atts: ${theAtts}" } theAtts?.each { String att -> if (allowedListFLD.attributes.contains(att)) { if (getBoolSetting('noTemp') && att == 'temperature' && (device.hasAttribute('contact') || device.hasAttribute('water'))) { Boolean skipAtt; skipAtt = true if (getListSetting('sensorAllowTemp')) { skipAtt = isDeviceInInput('sensorAllowTemp', devid) } if (skipAtt) { return } } attMapFLD.each { String k, String v -> if (att == k && isDeviceInInput("remove${v}".toString(), devid)) { return } } if ( (att == 'pushed' && !isDeviceInInput('pushableButtonList', devid)) || (att == 'held' && !isDeviceInInput('holdableButtonList', devid)) || (att == 'doubleTapped' && !isDeviceInInput('doubleTapableButtonList', devid)) ) { return } subscribe(device, att, 'changeHandler') if (showlog || devMode()) { log.debug "Registering ${device.displayName} for ${att} events" } } //else if(devMode()) log.debug "ignoring attribute $att for ${device.displayName}" } } } String getAlarmIntrusionMode() { String curMode = getSecurityStatus() switch(curMode) { case 'armedAway': return 'intrusion-away' case 'armedHome': return 'intrusion-home' case 'armedNight': return 'intrusion-night' } return "disarmed" } def changeHandler(evt) { Long execDt = wnow() List sendItems = [] String src = evt.source String deviceid; deviceid = evt.getDeviceId()?.toString() String deviceName = (String)evt.displayName String attr; attr = (String)evt.name def value; value = evt.value Date dt = (Date)evt.date Boolean sendEvt; sendEvt = true Boolean evtLog = (getTsVal(sEVT) == sTRUE) // if(evt.name.startsWith('hsm')) { // log.debug "${evt.name}: [evtSource: ${src}, evtDeviceName: ${deviceName}, evtDeviceId: ${deviceid}, evtAttr: ${attr}, evtValue: ${value}, evtUnit: ${evt?.unit ?: sBLANK}, evtDate: ${dt}]" // } switch ((String)evt.name) { case 'hsmStatus': deviceid = "alarmSystemStatus_${location?.id}" attr = 'alarmSystemStatus' sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: sBLANK, evtDate: dt]) break case 'hsmAlert': deviceid = "alarmSystemStatus_${location?.id}" attr = 'alarmSystemStatus' if (value?.toString() == 'intrusion-delay') { // Handle 'intrusion-delay' case separately sendEvt = false } else if (value?.toString()?.startsWith('intrusion')) { sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: getAlarmIntrusionMode(), evtUnit: evt?.unit ?: sBLANK, evtDate: dt]) } else if (value?.toString() == 'cancel') { sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: getSecurityStatus(), evtUnit: evt?.unit ?: sBLANK, evtDate: dt]) } else { sendEvt = false } break case 'hsmRules': case 'hsmSetArm': sendEvt = false break case 'securityKeypad': deviceid = "securityKeypad_${deviceid}" attr = 'alarmSystemStatus' value = getSecurityKeypadMode("${value}") sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: sBLANK, evtDate: dt, evtData: null]) break case 'alarmSystemStatus': deviceid = "alarmSystemStatus_${location?.id}" sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: sBLANK, evtDate: dt]) break case 'mode': ((List)getListSetting('modeList'))?.each { id -> Map md = fndMode(id) if (md && md.id) { String devId = 'm_'+md.id sendItems.push([evtSource: 'MODE', evtDeviceName: "Mode - ${md.name}", evtDeviceId: devId, evtAttr: sSW, evtValue: modeSwitchState(md.name), evtUnit: sBLANK, evtDate: dt]) } } break case 'webCoRE': if (getBoolSetting('enableWebCoRE')) { sendEvt = false if ((String)evt.value == 'pistonList') { List p; p = (List)webCoREFLD?.pistons ?: [] Map d = evt.jsonData ?: [:] if (d.id && d.pistons && (d.pistons instanceof List)) { p.removeAll { it.iid == d.id } p += ((List)d.pistons).collect { [iid:d.id] + it }.sort { (String)it.name } def a = webCoREFLD?.cbk webCoREFLD = [cbk: a, updated: wnow(), pistons: p] updTsVal(sLASTWU) } if (evtLog) { logDebug("got webCoRE piston list event $webCoREFLD") } break } else if ((String)evt.value == 'pistonExecuted') { ((List)getListSetting('pistonList'))?.each { String id -> Map rt = getPistonById(id) if (rt && rt.id) { sendEvt = true sendItems.push([evtSource: 'PISTON', evtDeviceName: "Piston - ${rt.name}", evtDeviceId: rt.id, evtAttr: sSW, evtValue: 'off', evtUnit: sBLANK, evtDate: dt]) } } break } } logDebug("unknown webCoRE event $evt.value") break case 'held': case 'pushed': case 'doubleTapped': Map evtData = [buttonNumber: value] sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: "button", evtValue: attr, evtUnit: evt?.unit ?: sBLANK, evtDate: dt, evtData: evtData]) break // case 'speed': // if (isDeviceInInput('fanList', deviceid) || isDeviceInInput('fan3SpdList', deviceid) || isDeviceInInput('fan4SpdList', deviceid) || isDeviceInInput('fan5SpdList', deviceid)) { // // Convert the speed to a number for Homebridge based on it's speed 3,4,5 speed type // Integer fanSpd = 1 // if (isDeviceInInput('fan3SpdList', deviceid)) fanSpd = 3 // if (isDeviceInInput('fan4SpdList', deviceid)) fanSpd = 4 // if (isDeviceInInput('fan5SpdList', deviceid)) fanSpd = 5 // Integer newSpdVal = getFanSpeedInteger(value, fanSpd) // log.debug "Fan Speed: ${value} | New Speed: ${newSpdVal}" // sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: newSpdVal, evtUnit: evt?.unit ?: sBLANK, evtDate: dt]) // } // break default: sendItems.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: sBLANK, evtDate: dt, evtData: null]) break } if (sendEvt && sendItems.size() > 0) { String server = getServerAddress() if (server == sCLN || server == sNLCLN ) { // can be configured ngrok?? return } //Send Using the Direct Mechanism sendItems.each { Map send-> if (evtLog) { //if(getBoolSetting('showEventLogs')) { String unitStr switch ((String)send.evtAttr) { case 'temperature': unitStr = "${send?.evtUnit}" break case 'humidity': case 'level': case 'battery': unitStr = '%' break case 'power': unitStr = 'W' break case 'illuminance': unitStr = ' Lux' break default: unitStr = "${send.evtUnit}" break } logInfo("Sending ${send.evtSource ?: sBLANK} Event (${send.evtDeviceName} | ${((String)send.evtAttr).toUpperCase()}: ${send.evtValue}${unitStr}) ${send.evtData ? "Data: ${send.evtData}" : sBLANK} to Homebridge at (${server})") } sendHttpPost(sUPD, [ change_name : send.evtDeviceName, change_device : send.evtDeviceId, change_attribute: send.evtAttr, change_value : send.evtValue, change_data : send.evtData, change_date : send.evtDate, app_id : gtAppId(), access_token : getTsVal(sATK) ], sEVTUPD, evtLog) logEvt([name: send.evtAttr, value: send.evtValue, device: send.evtDeviceName, execTime: wnow() - execDt]) } } } private static Integer getFanSpeedInteger(String speed, Integer numberOfSpeeds = 3) { Map speedMappings = [ "low": 0, "medium-low": 25, "medium": 50, "medium-high": 75, "high": 100, "on": 100, "off": 0, "auto": 50 // You can adjust this based on your needs ] Integer speedPercentage speedPercentage = speedMappings[speed] ?: 0 // Adjust percentage based on the number of speeds // -1 because index is 0-based speedPercentage = Math.round( ((speedPercentage / 100.0) * (numberOfSpeeds - 1)) * 100).toInteger() return speedPercentage } void sendHttpPost(String path, Map body, String src=sBLANK, Boolean evtLog, String contentType = sAPPJSON) { String server = getServerAddress() Boolean sendVia = getBoolSetting('sendViaNgrok') String url = sendVia ? getStrSetting('ngrokHttpUrl') : sBLANK if (!devMode() || !(sendVia && url)) { if (server == sCLN || server == sNLCLN ) { logError("sendHttpPost: no plugin server configured src: $src path: $path $body"); return } } Map params = [ uri: (devMode() && sendVia && url) ? "https://${url}.ngrok.io/${path}".toString() : "http://${server}/${path}".toString(), requestContentType: contentType, contentType: contentType, body: body, timeout: 20 ] asynchttpPost(sASYNCCR, params, [execDt: wnow(), src: src, evtLog: evtLog]) } void asyncHttpCmdResp(response, Map data) { if (getTsVal(sDBG) == sTRUE && bIs(data,'evtLog')) { def resp = response?.getData() // || null String src = data?.src ? (String)data.src : 'Unknown' logDebug(sASYNCCR + " | Src: ${src} | Resp: ${resp} | Status: ${response?.getStatus()} | Data: ${data}") logDebug("Send to plugin Completed | Process Time: (${data?.execDt ? (wnow()-(Long)data.execDt) : 0}ms)") } } @CompileStatic String getServerAddress() { String sv; sv = getTsVal(sSVR) if (sv == sNULL) { Map pluginDetails = (Map)gtState('pluginDetails') ?: [:] sv = "${pluginDetails.directIP}:${pluginDetails.directPort}".toString() updTsVal(sSVR, sv) updTsVal(sDBG, getBoolSetting('showDebugLogs').toString()) updTsVal(sEVT, getBoolSetting('showEventLogs').toString()) updTsVal(sATK, (String)gtState('accessToken')) updTsVal('lastActTs') } return sv } String getPluginStatusDesc() { String out; out = sBLANK Map pluginDetails = state.pluginDetails ?: [:] if(pluginDetails && pluginDetails.keySet().size() > 0) { out += pluginDetails?.directIP && pluginDetails?.directPort ? spanSmBld('Plugin Server:', sCLRGRY) + spanSmBr(" ${pluginDetails?.directIP}:${pluginDetails?.directPort}", sCLRGRY) : sBLANK out += state?.pluginDetails?.version ? spanSmBld('Plugin Version:', sCLRGRY) + spanSmBr(" v${state?.pluginDetails?.version}", sCLRGRY) : sBLANK out += spanBr(" ") } return out } private Map fndMode(String m){ def mode= ((List)location.getModes())?.find{ it-> ((Long)it.getId()).toString()==m || (String)it.getName()==m } return mode ? [ id: ((Long)mode.getId()).toString(), name: (String)mode.getName()] :null } @Field volatile static Map webCoREFLD = [:] private static String webCoRE_handle() { return 'webCoRE' } static String webCore_icon() { return 'https://raw.githubusercontent.com/ady624/webCoRE/master/resources/icons/app-CoRE.png' } private webCoRE_init(pistonExecutedCbk=null) { if (getBoolSetting('enableWebCoRE')) { List tmp = getListSetting('pistonList') if (tmp) { logInfo("Subscribed to (${tmp.size()} WebCoRE Pistons)") } subscribe(location, webCoRE_handle(), changeHandler) if (!webCoREFLD) { webCoREFLD = [:] + [cbk:true] // pistonExecutedCbk] webCoRE_poll(true) } } } private void webCoRE_poll(Boolean now = false) { if (getBoolSetting('enableWebCoRE')) { Integer lastUpd = getLastTsValSecs(sLASTWU) if ((lastUpd > (3600 * 24)) || (now && lastUpd > 300)) { sendLocationEvent(name: 'webCoRE.poll', value: 'poll') // ask webCoRE for piston list updTsVal(sLASTWU) } } } static List webCoRE_list() { return ((List)webCoREFLD?.pistons)?.sort { it?.name }?.collect { [(it?.id): ((String)it?.aname)?.replaceAll('<[^>]*>', sBLANK)] } } static Map getPistonById(String rId) { return ((List)webCoREFLD?.pistons)?.find { it?.id == rId } } static Map getPistonByName(String name) { return ((List)webCoREFLD?.pistons)?.find { it?.name == name } } void settingUpdate(String name, String value, String type=sNULL) { if (name && type) { app.updateSetting(name, [type: type, value: value]) } else if (name && type == sNULL) { app.updateSetting(name, value) } } void settingRemove(String name) { // logTrace("settingRemove($name)...") if (name && settings.containsKey(name)) { app.removeSetting(name) } } static Boolean devMode() { return devModeFLD } void activateDirectUpdates(Boolean isLocal=false) { logTrace("activateDirectUpdates: ${getServerAddress()}${isLocal ? ' | (Local)' : sBLANK}") sendHttpPost('initial', [ app_id: gtAppId(), access_token: (String)state.accessToken ], 'activateDirectUpdates', getBoolSetting('showDebugLogs')) } void attemptServiceRestart(Boolean isLocal=false) { logTrace("attemptServiceRestart: ${getServerAddress()}${isLocal ? ' | (Local)' : sBLANK}") sendHttpPost('restart', [ app_id: gtAppId(), access_token: (String)state.accessToken ], 'attemptServiceRestart', getBoolSetting('showDebugLogs')) } void sendDeviceRefreshCmd(Boolean isLocal=false) { logTrace("sendDeviceRefreshCmd: ${getServerAddress()}${isLocal ? ' | (Local)' : sBLANK}") sendHttpPost('refreshDevices', [ app_id: gtAppId(), access_token: (String)state.accessToken ], 'sendDeviceRefreshCmd', getBoolSetting('showDebugLogs')) } void updateServicePrefs(Boolean isLocal=false) { logTrace("updateServicePrefs: ${getServerAddress()}${isLocal ? ' | (Local)' : sBLANK}") sendHttpPost('updateprefs', [ app_id: gtAppId(), access_token: (String)state.accessToken, use_cloud: getBoolSetting('use_cloud_endpoint'), validateTokenId: getBoolSetting('validate_token'), local_hub_ip: ((List)location?.hubs)[0]?.localIP ], 'updateServicePrefs', getBoolSetting('showDebugLogs')) } def pluginStatus() { logTrace('Plugin called... pluginStatus()') def body = request?.JSON state.pluginUpdates = [hasUpdate: (body?.hasUpdate == true), newVersion: (body?.newVersion ?: null)] if (body?.version) { updCodeVerMap('plugin', (String)body?.version) } String resultJson = new JsonOutput().toJson([status: 'OK']) wrender contentType: sAPPJSON, data: resultJson } def enableDirectUpdates() { logTrace('Plugin called enableDirectUpdates()') // log.trace "enableDirectUpdates: ($params)" state.pluginDetails = [ directIP: params?.ip, directPort: params?.port, version: params?.version ?: null ] remTsVal(sSVR) updCodeVerMap('plugin', (String)params?.version ?: sNULL) activateDirectUpdates() updTsVal('lastDirectUpdsEnabled') String resultJson = new JsonOutput().toJson([status: 'OK']) wrender contentType: sAPPJSON, data: resultJson } mappings { path('/devices') { action: [GET: 'getAllData'] } path('/alldevices') { action: [GET: 'renderDevices'] } path('/deviceDebug') { action: [GET: 'viewDeviceDebug'] } path('/location') { action: [GET: 'renderLocation'] } path('/pluginStatus') { action: [POST: 'pluginStatus'] } path('/:id/command/:command') { action: [POST: 'deviceCommand'] } path('/:id/attribute/:attribute') { action: [GET: 'deviceAttribute'] } path('/startDirect/:ip/:port/:version') { action: [POST: 'enableDirectUpdates'] } } def appInfoSect() { Boolean isNote; isNote = false String tStr tStr = spanSmBld('Version:', sCLRGRY) + spanSmBr(" v${appVersionFLD}", sCLRGRY) tStr += state?.pluginDetails?.version ? spanSmBld('Plugin:', sCLRGRY) + spanSmBr(" v${state?.pluginDetails?.version}", sCLRGRY) : sBLANK section (sectH3TS((String)app.name, tStr, getAppImg('hb_tonesto7'), 'orange')) { Map minUpdMap = getMinVerUpdsRequired() List codeUpdItems = codeUpdateItems(true) if (bIs(minUpdMap,'updRequired') && ((List)minUpdMap.updItems).size() > 0) { isNote = true String str3 str3 = spanSmBldBr('Updates Required:', sCLRRED) ((List)minUpdMap.updItems).each { item-> str3 += spanSmBr(" ${sBULLET} ${item}", sCLRRED) } str3 += lineBr() + spanSmBld('If you just updated the code please press Done/Next to let the app process the changes.', sCLRRED) paragraph str3 } else if (codeUpdItems?.size()) { isNote = true String str2 str2 = spanSmBldBr('Code Updates Available:', sCLRRED) codeUpdItems?.each { item-> str2 += spanSmBr(" ${sBULLET} ${item}", sCLRRED) } paragraph str2 } if (!isNote) { paragraph spanSm('No Issues to Report', sCLRGRY) } paragraph htmlLine(sCLRGRY) } } /********************************************** APP HELPER FUNCTIONS ***********************************************/ static String getAppImg(String imgName, String ext='.png') { return "https://raw.githubusercontent.com/tonesto7/homebridge-hubitat-tonesto7/${branchFLD}/images/${imgName}${ext}" } static String getPublicImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/SmartThings-tonesto7-public/master/resources/icons/${imgName}.png" } static String sectH3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return "

${i ? " " : sBLANK} ${t?.replaceAll("\\n", '
')}

${st ?: sBLANK}" } static String paraTS(String t, String i = sNULL, Boolean bold=true, String color=sNULL) { return "${color ? "
" : sBLANK}${bold ? '' : sBLANK}${i ? " " : sBLANK}${t?.replaceAll("\\n", '
')}${bold ? '
' : sBLANK}${color ? '
' : sBLANK}".toString() } static String sectHead(String str, String img = sNULL) { return str ? "

" + spanImgStr(img) + span(str, sCLR4D9, sNULL, true) + '

' + "
" : sBLANK } //static String sTS(String t, String i = sNULL, Boolean bold=false) { return "

${i ? " " : sBLANK} ${bold ? '' : sBLANK}${t?.replaceAll('\n', '
')}${bold ? '
' : sBLANK}

" } //static String s3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return "

${i ? " " : sBLANK} ${t?.replaceAll('\n', '
')}

${st ? "${st}" : sBLANK}" } //static String pTS(String t, String i = sNULL, Boolean bold=true, String color=sNULL) { return "${color ? "
" : sBLANK}${bold ? '' : sBLANK}${i ? " " : sBLANK}${t?.replaceAll('\n', '
')}${bold ? '
' : sBLANK}${color ? '
' : sBLANK}" } static String inTS1(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return spanSmBldUnd(str, clr, img) } //static String inTS(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return divSm(strUnder(str?.replaceAll('\n', sSPACE).replaceAll('
', sSPACE), und), clr, img) } // Root HTML Objects static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLANK}" : sBLANK } static String div(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "
${str}
${br ? sLINEBR : sBLANK}" : sBLANK } static String spanImgStr(String img=sNULL) { return img ? span(" ") : sBLANK } //static String divImgStr(String str, String img=sNULL) { return ((str) ? div(img ? spanImg(img) + span(str) : str) : sBLANK) } static String strUnder(String str, Boolean showUnd=true) { return str ? (showUnd ? "${str}" : str) : sBLANK } static String strStrkTh(String str) { return str ? "${str}" : sBLANK } //static String getOkOrNotSymHTML(Boolean ok) { return ok ? span("(${okSymFLD})", sCLRGRN2) : span("(${notOkSymFLD})", sCLRRED2) } static String htmlLine(String color=sCLR4D9, Integer width = null) { return "
" } static String lineBr(Boolean show=true) { return show ? sLINEBR : sBLANK } static String inputFooter(String str, String clr=sCLR4D9, Boolean noBr=false) { return str ? lineBr(!noBr) + divSmBld(str, clr) : sBLANK } //static String inactFoot(String str) { return str ? inputFooter(str, sCLRGRY, true) : sBLANK } //static String actFoot(String str) { return str ? inputFooter(str, sCLR4D9, false) : sBLANK } //static String optPrefix() { return spanSm(' (Optional)', 'violet') } // Custom versions of the root objects above //static String spanBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true) : sBLANK } static String spanBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true, true) : sBLANK } static String spanBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, false, true) : sBLANK } static String spanSm(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL) : sBLANK } static String spanSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, false, true) : sBLANK } static String spanSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true) : sBLANK } static String spanSmBldUnd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(strUnder(str), clr, sSMALL, true) : sBLANK } static String spanSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true, true) : sBLANK } static String spanStrkTh(String str, String clr=sNULL) { return str ? span(strStrkTh(str), clr, sNULL, false, false) : sBLANK } static String spanStrkThBr(String str, String clr=sNULL) { return str ? span(strStrkTh(str), clr, sNULL, false, true) : sBLANK } static String spanStrkThBld(String str, String clr=sNULL) { return str ? span(strStrkTh(str), clr, sNULL, true, false) : sBLANK } static String spanStrkThBldBr(String str, String clr=sNULL) { return str ? span(strStrkTh(str), clr, sNULL, true, true) : sBLANK } static String spanMd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM) : sBLANK } static String spanMdBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, false, true) : sBLANK } static String spanMdBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true) : sBLANK } static String spanMdBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true, true) : sBLANK } //static String divBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, false) : sBLANK } //static String divBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, true) : sBLANK } //static String divBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, false, true) : sBLANK } static String divSm(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL) : sBLANK } //static String divSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, false, true) : sBLANK } static String divSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true) : sBLANK } //static String divSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true, true) : sBLANK } def appFooter() { section() { // paragraph htmlLine('orange') paragraph spanSm("
Homebridge Hubitat


Please consider donating if you find this integration useful.
", sCLRORG) } } static String dashItem(String inStr, String strVal, newLine=false) { return "${(inStr == sBLANK && !newLine) ? sBLANK : '\n'} - ${strVal}".toString() } static String textDonateLink() { return 'https://www.paypal.com/donate?hosted_button_id=5GMA6C3RTLXH6' } static Integer versionStr2Int(String str) { return str ? str.tokenize('-')[0]?.replaceAll("\\.", sBLANK)?.toInteger() : null } static String versionCleanup(String str) { return str ? str.tokenize('-')[0] : sNULL } static Boolean codeUpdIsAvail(String inewVer, String icurVer, String type) { Boolean result; result = false String latestVer String newVer, curVer newVer=inewVer curVer=icurVer if (newVer && curVer) { newVer = versionCleanup(newVer) curVer = versionCleanup(curVer) List versions = [newVer, curVer] as List if (newVer != curVer) { latestVer = versions.max { a, b -> List verA = a?.tokenize('.'); List verB = b?.tokenize('.') Integer commonIndices = Math.min(verA?.size(), verB?.size()) for (Integer i = 0; i < commonIndices; ++i) { if (verA[i]?.toInteger() != verB[i]?.toInteger()) { return verA[i]?.toInteger() <=> verB[i]?.toInteger() } } verA?.size() <=> verB?.size() } result = (latestVer == newVer) } } return result } Boolean appUpdAvail() { return (state?.appData?.versions && state?.codeVersions?.mainApp && codeUpdIsAvail((String)state?.appData?.versions?.mainApp, appVersionFLD, 'main_app')) } Boolean pluginUpdAvail() { return (state?.appData?.versions && state?.codeVersions?.plugin && codeUpdIsAvail((String)state?.appData?.versions?.plugin, (String)state?.codeVersions?.plugin, 'plugin')) } private Map getMinVerUpdsRequired() { Boolean updRequired; updRequired = false List updItems = [] Map codeItems = [plugin: 'Homebridge Plugin'] Map codeVers = (Map)state.codeVersions ?: [:] codeVers?.each { String k, String v-> try { if (codeItems?.containsKey(k) && v != null && (versionStr2Int(v) < (Integer)minVersionsFLD[k])) { updRequired = true; updItems.push(codeItems[k]) } } catch (ex) { logError("getMinVerUpdsRequired Error: ${ex}", ex) } } return [updRequired: updRequired, updItems: updItems] } private List codeUpdateItems(Boolean shrt=false) { Boolean appUpd = appUpdAvail() Boolean plugUpd = pluginUpdAvail() List updItems = [] if (appUpd) updItems.push("${!shrt ? '\nHomebridge ' : sBLANK}App: (v${state?.appData?.versions?.mainApp?.toString()})") if (plugUpd) updItems.push("${!shrt ? '\n' : sBLANK}Plugin: (v${state?.appData?.versions?.plugin?.toString()})") return updItems } @Field volatile static Map tsDtMapFLD = [:] @CompileStatic Integer getLastTsValSecs(String val, Integer nullVal=1000000) { String appId = gtAppId() Map tsMap = tsDtMapFLD[appId] ?: [:] return (val && tsMap && tsMap[val]) ? GetTimeDiffSeconds((String)tsMap[val]).toInteger() : nullVal } @CompileStatic private void updTsVal(String key, String dt=sNULL) { String appId = gtAppId() Map data = tsDtMapFLD[appId] ?: [:] if (key) { data[key] = dt ?: getDtNow() } tsDtMapFLD[appId] = data tsDtMapFLD = tsDtMapFLD } private void remTsVal(key) { String appId = gtAppId() Map data = tsDtMapFLD[appId] ?: [:] if (key) { if( key instanceof List) { List klist = (List)key klist.each { k-> if (data.containsKey(k)) { data.remove(k) } } } else { String ks = key if (ks && data.containsKey(ks)) { data.remove(ks) } } tsDtMapFLD[appId] = data tsDtMapFLD = tsDtMapFLD } } @CompileStatic private String getTsVal(String val) { String appId = gtAppId() Map tsMap = tsDtMapFLD[appId] if (val && tsMap && tsMap[val]) { return (String)tsMap[val] } return sNULL } private void updCodeVerMap(String key, String val) { Map cv; cv = (Map)state.codeVersions if (cv == null) cv = [:] if (val && (!cv.containsKey(key) || (cv.containsKey(key) && (String)cv[key] != val))) { cv[key] = val } if (cv.containsKey(key) && val == sNULL) { cv.remove(key) } state.codeVersions = cv } private void updInstData(String key, val) { Map iData = (Map)state.installData ?: [:] iData[key] = val state.installData = iData } private getInstData(String key) { Map iMap = (Map)state.installData if (key && iMap && iMap[key]) { return iMap[key] } return null } @CompileStatic private void checkVersionData(Boolean now = false) { //This reads a JSON file from GitHub with version numbers Integer lastUpd = getLastTsValSecs('lastAppDataUpdDt') if (now || !gtState('appData') || (lastUpd > (3600 * 6))) { if (now && (lastUpd < 300)) { return } getConfigData() } } @CompileStatic void getConfigData() { Map params = [ uri: 'https://raw.githubusercontent.com/tonesto7/homebridge-hubitat-tonesto7/master/appData.json', contentType: sAPPJSON, timeout: 20 ] Map data = (Map)getWebData(params, 'appData', false) if (data) { assignSt('appData',data) updTsVal('lastAppDataUpdDt') logDebug("Successfully Retrieved (v${data.appDataVer}) of AppData Content from GitHub Repo...") } } private getWebData(Map params, String desc, Boolean text=true) { try { httpGet(params) { resp -> if (resp?.status != 200) { logWarn("${resp?.status} $params") } if (resp?.data) { if (text) { return resp.data.text?.toString() } return resp.data } } } catch (ex) { if (ex instanceof groovyx.net.http.HttpResponseException) { logWarn("${desc} file not found") } else { logError("getWebData Exception | params: $params, desc: $desc, text: $text | Error: ${ex}", ex) } if (text) { return "${desc} info not found" } return null } } /****************************************** | DATE | TIME HELPERS ******************************************/ @CompileStatic static String formatDt(Date dt, Boolean tzChg=false) { SimpleDateFormat tf = new SimpleDateFormat('E MMM dd HH:mm:ss z yyyy') if (tzChg && getDefTz()) { tf.setTimeZone(getDefTz()) } return tf.format(dt) } @CompileStatic static String getDtNow() { Date now = new Date() return formatDt(now) } @CompileStatic Long GetTimeDiffSeconds(String lastDate, String sender=sNULL) { if (lastDate) { if (!lastDate.contains('dtNow')) { //String stopVal = getDtNow() Long start = Date.parse('E MMM dd HH:mm:ss z yyyy', lastDate).getTime() //Long stop = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal).getTime() Long stop = wnow() Long diff = (Long)((stop - start) / 1000L) return diff } } else { logError("GetTimeDiffSeconds Exception: (${sender ? "$sender | " : sBLANK}lastDate: $lastDate)") } return 10000L } /****************************************** | Changelog Logic ******************************************/ Boolean showDonationOk() { return ((Boolean)state.isInstalled && !state?.installData?.shownDonation && getDaysSinceUpdated() >= 30 && !getBoolSetting('sentDonation')) } Integer getDaysSinceUpdated() { def t0 = state.installData?.updatedDt String updDt = t0 ? (String)t0 : sNULL if (updDt == sNULL || updDt == 'Not Set') { updInstData('updatedDt', getDtNow()) return 0 } Date start = Date.parse('E MMM dd HH:mm:ss z yyyy', updDt) Date stop = new Date() if (start && stop) { return (stop - start) } return 0 } String changeLogData() { String txt txt = (String)getWebData([uri: 'https://raw.githubusercontent.com/tonesto7/homebridge-hubitat-tonesto7/master/CHANGELOG-app.md', contentType: 'text/plain; charset=UTF-8', timeout: 20], 'changelog', true) txt = txt?.replaceAll('##', sBLANK)?.replaceAll(/(_\*\*)/, '')?.replaceAll(/(\*\*_)/, '') // Replaces header format txt = txt?.replaceAll(/(- )/, " ${sBULLET} ") txt = txt?.replaceAll(/(\[NEW])/, spanSmBld('[NEW]')) txt = txt?.replaceAll(/(\[UPDATE])/, spanSmBld('[FIX]')) txt = txt?.replaceAll(/(\[FIX])/, spanSmBld('[FIX]')) return txt // Replaces ## then **_ and _** in changelog data } Boolean showChgLogOk() { return ((Boolean)state.isInstalled && ((String)state.curAppVer != appVersionFLD || state?.installData?.shownChgLog != true)) } private changeLogPage() { return dynamicPage(name: 'changeLogPage', title: sBLANK, nextPage: 'mainPage', install: false) { section(sectHead('Release Notes:', 'change_log')) { paragraph spanSm(changeLogData()) } state.curAppVer = appVersionFLD updInstData('shownChgLog', true) } } @Field volatile static Map>> historyMapFLD = [:] private void addToHistory(String logKey, Map data, Integer max=10) { String appId = gtAppId() Boolean ssOk = true /* groovylint-disable-next-line UnusedVariable */ getTheLock(sHMLF, "addToHistory(${logKey})") // log.trace "lock wait: ${aa}" Map> memStore = historyMapFLD[appId] ?: [:] List eData; eData = (List)memStore[logKey] ?: [] if (eData.find { it?.data == data }) { releaseTheLock(sHMLF) return } eData.push([dt: getDtNow(), gt: wnow(), data: data]) Integer lsiz = eData.size() if (!ssOk || lsiz > max) { eData = eData.drop( (lsiz - max) ) } updMemStoreItem(logKey, eData) releaseTheLock(sHMLF) } private void logDebug(String msg) { if (getBoolSetting('showDebugLogs')) logPrefix(sDBG, msg, "purple") } private void logTrace(String msg) { if (getBoolSetting('showDebugLogs')) logPrefix('trace', msg, sCLRGRY) } private void logInfo(String msg) { logPrefix(sINFO, msg, sCLR9B1) } private void logWarn(String msg) { logPrefix('warn', msg, sCLRORG) } private void logError(String msg, ex=null) { logPrefix('error', msg, sCLRRED) String a try { if (ex) a = getExceptionMessageWithLine(ex) } catch (ignored) { } if(a) { logPrefix('error', a, sCLRRED) } } private void logPrefix(String lvl, String msg, String color = sNULL) { String pad; pad = sBLANK if (lvl in ['warn', sINFO]) { pad = sSPACE } log."$lvl" pad + span("Homebridge (v${appVersionFLD}) | ", sCLRGRY) + span(msg, color) } private List getCmdHistory() { getTheLock(sHMLF, 'getCmdHistory') // log.trace "lock wait: ${aa}" List his; his = getMemStoreItem('cmdHistory') if (his == null) his = [] List newHis = (List)[] + his releaseTheLock(sHMLF) return newHis } private List getEvtHistory() { getTheLock(sHMLF, 'getEvtHistory') // log.trace "lock wait: ${aa}" List his; his = getMemStoreItem('evtHistory') if (his == null) his = [] List newHis = (List)[] + his releaseTheLock(sHMLF) return newHis } private void clearHistory() { String appId = gtAppId() getTheLock(sHMLF, 'clearHistory') // log.trace "lock wait: ${aa}" historyMapFLD.put(appId, [:] as Map>) historyMapFLD = historyMapFLD releaseTheLock(sHMLF) } private void logEvt(Map evtData) { addToHistory('evtHistory', evtData, 25) } private void logCmd(Map cmdData) { addToHistory('cmdHistory', cmdData, 25) } // FIELD VARIABLE FUNCTIONS private void updMemStoreItem(String key, List val) { String appId = gtAppId() Map memStore = historyMapFLD[appId] ?: [:] memStore[key] = val historyMapFLD[appId] = memStore historyMapFLD = historyMapFLD // log.debug("updMemStoreItem(${key}): ${memStore[key]}") } private List getMemStoreItem(String key) { String appId = gtAppId() Map> memStore = historyMapFLD[appId] ?: [:] return (List)memStore[key] ?: null } // Memory Barrier @Field static Semaphore theMBLockFLD = new Semaphore(0) static void mb(String meth=sNULL) { if (theMBLockFLD.tryAcquire()) { theMBLockFLD.release() } } @Field static final String sHMLF = 'theHistMapLockFLD' @Field static Semaphore histMapLockFLD = new Semaphore(1) @CompileStatic static Integer getSemaNum(String name) { if (name == sHMLF) return 0 //log.warn 'unrecognized lock name...' return 0 // Integer stripes=22 // if(name.isNumber()) return name.toInteger()%stripes // Integer hash=smear(name.hashCode()) // return Math.abs(hash)%stripes // log.info "sema $name # $sema" } @CompileStatic static Semaphore getSema(Integer snum) { switch (snum) { case 0: return histMapLockFLD default: // log.error "bad hash result $snum" return null } } @Field volatile static Map lockTimesFLD = [:] @Field volatile static Map lockHolderFLD = [:] @CompileStatic void getTheLock(String qname,String meth=sNULL,Boolean longWait=false) { Boolean a = getTheLockW(qname,meth,longWait) } @CompileStatic Boolean getTheLockW(String qname, String meth=sNULL, Boolean longWait=false) { Long waitT = longWait ? 1000L : 60L Boolean wait; wait = false Integer semaNum = getSemaNum(qname) String semaSNum = semaNum.toString() Semaphore sema = getSema(semaNum) while (!sema.tryAcquire()) { // did not get the lock Long timeL; timeL = lockTimesFLD[semaSNum] if (timeL == null) { timeL = wnow() lockTimesFLD[semaSNum] = timeL lockTimesFLD = lockTimesFLD } if (devMode()) { logWarn "waiting for ${qname} ${semaSNum} lock access, $meth, long: $longWait, holder: ${(String)lockHolderFLD[semaSNum]}" } wpauseExecution(waitT) wait = true if ((wnow() - timeL) > 30000L) { releaseTheLock(qname) if (devMode()) { logWarn "overriding lock $meth" } } } lockTimesFLD[semaSNum] = wnow() lockTimesFLD = lockTimesFLD lockHolderFLD[semaSNum] = "${gtAppId()} ${meth}".toString() lockHolderFLD = lockHolderFLD return wait } @CompileStatic static void releaseTheLock(String qname) { Integer semaNum = getSemaNum(qname) String semaSNum = semaNum.toString() Semaphore sema = getSema(semaNum) lockTimesFLD[semaSNum] = (Long)null lockTimesFLD = lockTimesFLD lockHolderFLD[semaSNum] = sNULL lockHolderFLD = lockHolderFLD sema.release() } String gtDevId(dev){ return (String)dev.getId() } String gtAppId(){ return ((Long)app.getId()).toString() } private Long wnow(){ return (Long)now() } private static TimeZone getDefTz() { return TimeZone.getDefault() } private void wpauseExecution(Long t) { pauseExecution(t) } @CompileStatic private static Boolean bIs(Map m,String v) { (Boolean)m.get(v) } /** m.string */ @CompileStatic private static String sMs(Map m,String v) { (String)m[v] } private void assignSt(String name,v) { ((Map)state).put(name,v) } private gtState(String name) { return state.get(name) } private getSetting(String name) { return settings.get(name) } private String getStrSetting(String name) { return (String)settings.get(name) } private Boolean getBoolSetting(String name) { return (Boolean)settings.get(name) == true } private Boolean getBoolDefSetting(String name,Boolean defVal=true){ if(settings.get(name) == null) { settingUpdate(name,defVal.toString(),sBOOL) return defVal } return getBoolSetting(name) } private List getListSetting(String name) { return (List)settings.get(name) ?: [] } static String getObjType(obj) { if(obj instanceof String)return 'String' else if(obj instanceof Map)return 'Map' else if(obj instanceof List)return 'List' else if(obj instanceof ArrayList)return 'ArrayList' else if(obj instanceof BigInteger)return 'BigInt' else if(obj instanceof Long)return 'Long' else if(obj instanceof Integer)return 'Int' else if(obj instanceof Boolean)return 'Bool' else if(obj instanceof BigDecimal)return 'BigDec' else if(obj instanceof Double)return 'Double' else if(obj instanceof Float)return 'Float' else if(obj instanceof Byte)return 'Byte' else if(obj instanceof com.hubitat.app.DeviceWrapper)return 'Device' else{ // if(eric()) log.error "object: ${describeObject(obj)}" return 'unknown' } } @Field static final String sAE = 'Accept-encoding' @Field static final String sCE = 'Content-Encoding' @Field static final String sGZIP = 'gzip' @Field static final String sDATA = 'data' @Field static final String sUTF8 = 'UTF-8' private Map wrender(Map options=[:]){ //debug "wrender: options:: ${options} " //debug "request: ${request} " Map h=(Map)request?.headers if(h && sMs(h,sAE)?.contains(sGZIP)){ // debug "will accept gzip" String s=sMs(options,sDATA) Integer sz=s?.length() if(sz>256){ try{ String a= string2gzip(s) Integer nsz=a.size() if(devMode())logDebug "options.data is $sz after compression $nsz saving ${Math.round((1.0D-(nsz/sz))*1000.0D)/10.0D}%" // options[sDATA]=a // options[sCE]=sGZIP }catch(ignored){} } } render(options) } static String string2gzip(String s) { ByteArrayOutputStream baos= new ByteArrayOutputStream() GZIPOutputStream zipStream= new GZIPOutputStream(baos) zipStream.write(s.getBytes(sUTF8)) zipStream.close() byte[] result= baos.toByteArray() baos.close() return result.encodeBase64() }