/**
 *  Copyright 2020 Markus Liljergren (https://oh-lalabs.com)
 *
 *  Version: v1.1.1.1123T
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 *  NOTE: This is an auto-generated file and most comments have been removed!
 *
 */

// BEGIN:getDefaultParentImports()
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
 
import java.security.MessageDigest
// END:  getDefaultParentImports()

metadata {
	definition (name: "Tasmota - Universal Parent", namespace: "tasmota", author: "Markus Liljergren", vid: "generic-switch", filename: "tasmota-universal-parent", importUrl: "https://raw.githubusercontent.com/markus-li/Hubitat/release/drivers/expanded/tasmota-universal-parent-expanded.groovy") {
        // BEGIN:getDefaultMetadataCapabilities()
        capability "Refresh"
        capability "Configuration"
        // END:  getDefaultMetadataCapabilities()
        capability "PresenceSensor"
        capability "Initialize"
        
        // BEGIN:getDefaultParentMetadataAttributes()
        attribute   "ip", "string"
        attribute   "ipLink", "string"
        attribute   "module", "string"
        attribute   "templateData", "string"
        attribute   "wifiSignal", "string"
        // END:  getDefaultParentMetadataAttributes()
        // BEGIN:getDefaultMetadataAttributes()
        attribute   "driver", "string"
        // END:  getDefaultMetadataAttributes()
        // BEGIN:getMetadataAttributesForLastCheckin()
        attribute "lastCheckin", "Date"
        attribute "lastCheckinEpoch", "number"
        attribute "notPresentCounter", "number"
        attribute "restoredCounter", "number"
        // END:  getMetadataAttributesForLastCheckin()
        attribute "commandSent", "string"
        attribute "commandResult", "string"

        // BEGIN:getMetadataCommandsForHandlingChildDevices()
        command "deleteChildren"
        // END:  getMetadataCommandsForHandlingChildDevices()
        // BEGIN:getDefaultMetadataCommands()
        command "reboot"
        // END:  getDefaultMetadataCommands()
        // BEGIN:getCommandsForPresence()
        command "resetRestoredCounter"
        // END:  getCommandsForPresence()
        command "toggle"
        command "sendCommand", [[name:"Command*", type: "STRING", description: "Tasmota Command"],
            [name:"Argument", type: "STRING", description: "Argument (optional)"]]
        
        command "parseJSON", [[name:"JSON*", type: "STRING", description: "Tasmota Status as JSON"]]
	}

	preferences {
        // BEGIN:getDefaultParentMetadataPreferences()
        input(name: "runReset", description: styling_addDescriptionDiv("For details and guidance, see the <a href=\"https://oh-lalabs.com\">Oh-La Labs website</a>. For settings marked as ADVANCED, make sure you understand what they do before activating them. If settings are not reflected on the device, press the Configure button in this driver. Also make sure all settings really are saved and correct."), title: styling_addTitleDiv("Settings"), displayDuringSetup: false, type: "paragraph", element: "paragraph")
        input(name: "debugLogging", type: "bool", title: styling_getLogo() + styling_addTitleDiv("Enable debug logging"), description: "" , defaultValue: false, submitOnChange: true, displayDuringSetup: false, required: false)
        input(name: "infoLogging", type: "bool", title: styling_addTitleDiv("Enable info logging"), description: "", defaultValue: true, submitOnChange: true, displayDuringSetup: false, required: false)
        // END:  getDefaultParentMetadataPreferences()
        // BEGIN:getMetadataPreferencesForLastCheckin()
        input(name: "lastCheckinEnable", type: "bool", title: styling_addTitleDiv("Enable Last Checkin Date"), description: styling_addDescriptionDiv("Records Date events if enabled"), defaultValue: true)
        input(name: "lastCheckinEpochEnable", type: "bool", title: styling_addTitleDiv("Enable Last Checkin Epoch"), description: styling_addDescriptionDiv("Records Epoch events if enabled"), defaultValue: false)
        input(name: "presenceEnable", type: "bool", title: styling_addTitleDiv("Enable Presence"), description: styling_addDescriptionDiv("Enables Presence to indicate if the device has sent data within the last 3 hours (REQUIRES at least one of the Checkin options to be enabled)"), defaultValue: true)
        input(name: "presenceWarningEnable", type: "bool", title: styling_addTitleDiv("Enable Presence Warning"), description: styling_addDescriptionDiv("Enables Presence Warnings in the Logs (default: true)"), defaultValue: true)
        // END:  getMetadataPreferencesForLastCheckin()
        input(name: "deviceConfig", type: "enum", title: styling_addTitleDiv("Device Configuration"), 
            description: styling_addDescriptionDiv("Select a Device Configuration (default: Generic Device)<br/>'Generic Device' doesn't configure device Template and/or Module on Tasmota. Child devices and types are auto-detected as well as auto-created and does NOT depend on this setting."), 
            options: getDeviceConfigurationsAsListOption(), defaultValue: "01generic-device", 
            displayDuringSetup: true, required: false)

        // BEGIN:getMetadataPreferencesForHiding()
        input(name: "hideExtended", type: "bool", title: styling_addTitleDiv("Hide Extended Settings"), description: styling_addDescriptionDiv("Hides extended settings, usually not needed."), defaultValue: true, displayDuringSetup: false, required: false)
        input(name: "hideAdvanced", type: "bool", title: styling_addTitleDiv("Hide Advanced Settings"), description: styling_addDescriptionDiv("Hides advanced settings, usually not needed anyway."), defaultValue: true, displayDuringSetup: false, required: false)
        // END:  getMetadataPreferencesForHiding()

        // BEGIN:getDefaultMetadataPreferencesForTasmota(True) # False = No TelePeriod setting
        input("password", "password", title: styling_addTitleDiv("Device Password"), description: styling_addDescriptionDiv("REQUIRED if set on the Device! Otherwise leave empty."))
        input(name: "ipAddress", type: "string", title: styling_addTitleDiv("Device IP Address"), description: styling_addDescriptionDiv("Set this as a default fallback for the auto-discovery feature."), displayDuringSetup: true, required: false)
        input(name: "port", type: "number", title: styling_addTitleDiv("Device Port"), description: styling_addDescriptionDiv("The http Port of the Device (default: 80)"), displayDuringSetup: true, required: false, defaultValue: 80)
        input(name: "override", type: "bool", title: styling_addTitleDiv("Override IP"), description: styling_addDescriptionDiv("Override the automatically discovered IP address and disable auto-discovery."), displayDuringSetup: true, required: false)
        input(name: "useIPAsID", type: "bool", title: styling_addTitleDiv("IP as Network ID"), description: styling_addDescriptionDiv("Not needed under normal circumstances. Setting this when not needed can break updates. This requires the IP to be static or set to not change in your DHCP server. It will force the use of IP as network ID. When in use, set Override IP to true and input the correct Device IP Address. See the release thread in the Hubitat forum for details and guidance."), displayDuringSetup: true, required: false)
        input(name: "telePeriod", type: "string", title: styling_addTitleDiv("Update Frequency"), description: styling_addDescriptionDiv("Tasmota sensor value update interval, set this to any value between 10 and 3600 seconds. See the Tasmota docs concerning telePeriod for details. This is NOT a poll frequency. Button/switch changes are immediate and are NOT affected by this. This ONLY affects SENSORS and reporting of data such as UPTIME. (default = 300)"), displayDuringSetup: true, required: false)
        input(name: "disableModuleSelection", type: "bool", title: styling_addTitleDiv("Disable Automatically Setting Module and Template"), description: "ADVANCED: " + styling_addDescriptionDiv("Disable automatically setting the Module Type and Template in Tasmota. Enable for using custom Module or Template settings directly on the device. With this disabled, you need to set these settings manually on the device."), displayDuringSetup: true, required: false)
        input(name: "moduleNumber", type: "number", title: styling_addTitleDiv("Module Number"), description: "ADVANCED: " + styling_addDescriptionDiv("Module Number used in Tasmota. If Device Template is set, this value is IGNORED. (default: -1 (use the default for the driver))"), displayDuringSetup: true, required: false, defaultValue: -1)
        input(name: "deviceTemplateInput", type: "string", title: styling_addTitleDiv("Device Template"), description: "ADVANCED: " + styling_addDescriptionDiv("Set this to a Device Template for Tasmota, leave it EMPTY to use the Device Configuration Default. Set it to 0 to NOT use a Template. NAME can be maximum 14 characters! (Example: {\"NAME\":\"S120\",\"GPIO\":[0,0,0,0,0,21,0,0,0,52,90,0,0],\"FLAG\":0,\"BASE\":18})"), displayDuringSetup: true, required: false)
        // END:  getDefaultMetadataPreferencesForTasmota(True) # False = No TelePeriod setting
        input(name: "invertPowerNumber", type: "bool", title: styling_addTitleDiv("Send POWER1 events to POWER2, and vice versa"), description: styling_addDescriptionDiv("Use this if you have a dimmer AND a switch in the same device and on/off is not sent/received correctly. Normally this is NOT needed."), defaultValue: false, displayDuringSetup: false, required: false)
        input(name: "useAlternateColorCommand", type: "bool", title: styling_addTitleDiv("Use Alternate Color command in Tasmota"), description: styling_addDescriptionDiv("When enabled, this will use \"Var1\" instead of \"Color1\" in order to be able to catch the command in rules. Normally this is NOT needed."), defaultValue: false, displayDuringSetup: false, required: false)
        // BEGIN:getDefaultMetadataPreferencesLast()
        input(name: "hideDangerousCommands", type: "bool", title: styling_addTitleDiv("Hide Dangerous Commands"), description: styling_addDescriptionDiv("Hides Dangerous Commands, such as 'Delete Children'."), defaultValue: true, displayDuringSetup: false, required: false)
        input(name: "disableCSS", type: "bool", title: styling_addTitleDiv("Disable CSS"), description: styling_addDescriptionDiv("CSS makes the driver more user friendly. Disable the use of CSS in the driver by enabling this. Does NOT affect HE resource usage either way."), defaultValue: false, displayDuringSetup: false, required: false)
        // END:  getDefaultMetadataPreferencesLast()
        
	}

    // BEGIN:getMetadataCustomizationMethods()
    metaDataExporter()
    if(isCSSDisabled() == false) {
        preferences {
            input(name: "hiddenSetting", description: "" + getDriverCSSWrapper(), title: "None", displayDuringSetup: false, type: "paragraph", element: "paragraph")
        }
    }
    // END:  getMetadataCustomizationMethods()
}

// BEGIN:getDeviceInfoFunction()
String getDeviceInfoByName(infoName) { 
     
    Map deviceInfo = ['name': 'Tasmota - Universal Parent', 'namespace': 'tasmota', 'author': 'Markus Liljergren', 'vid': 'generic-switch', 'filename': 'tasmota-universal-parent', 'importUrl': 'https://raw.githubusercontent.com/markus-li/Hubitat/release/drivers/expanded/tasmota-universal-parent-expanded.groovy']
     
    return(deviceInfo[infoName])
}
// END:  getDeviceInfoFunction()

// BEGIN:getHelperFunctions('device-configurations')
TreeMap getDeviceConfigurations() {
    List deviceConfigurations = [
        [typeId: 'sonoff-basic-r3', 
         name: 'Sonoff Basic R3',
         module: 1,
         installCommands: [["SetOption81", "0"]],
         deviceLink: 'https://templates.blakadder.com/sonoff_basic_R3.html'],

        [typeId: 'tuyamcu-ce-wf500d-dimmer',
         name: 'TuyaMCU CE Smart Home WF500D Dimmer',
         template: '{"NAME":"CE WF500D","GPIO":[255,255,255,255,255,255,0,0,255,108,255,107,255],"FLAG":0,"BASE":54}',
         installCommands: [["SetOption66", "0"],
         ],
         deviceLink: 'https://templates.blakadder.com/ce_smart_home-WF500D.html'],

        [typeId: 'ce-la-2-w3-wall-outlet',
         name: 'CE Smart Home LA-2-W3 Wall Outlet',
         template: '{"NAME":"CE LA-2-W3","GPIO":[255,255,255,255,157,17,0,0,21,255,255,255,255],"FLAG":15,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/ce_smart_home_LA-2-W3.html'],

        [typeId: 'ce-lq-2-w3-wall-outlet',
         name: 'CE Smart Home LQ-2-W3 Wall Outlet',
         template: '{"NAME":"CE LQ-2-W3","GPIO":[255,255,255,255,255,17,255,255,21,255,255,255,255],"FLAG":15,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/ce_smart_home_LQ-2-W3.html'],

        [typeId: 'ce-la-wf7-pm-plug',
         name: 'CE Smart Home LA-WF7 Power Monitor Plug',
         template: '{"NAME":"CESmartHLA-WF7","GPIO":[0,56,0,17,134,132,0,0,131,57,21,0,0],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/ce_smart_home_LA-WF7.html'],

        [typeId: 'mirebella-genio-i002741-light',
         name: 'Mirabella Genio I002741 RGB+CCT Light',
         template: '{"NAME":"GenioDLightRGB","GPIO":[0,0,0,0,38,37,0,0,41,39,40,0,0],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/mirabella_genio_I002741.html'],

        [typeId: 'mirebella-genio-i002742-light',
         name: 'Mirabella Genio I002742 CCT Light',
         template: '{"NAME":"GenioDLightCCT","GPIO":[0,0,0,0,0,0,0,0,47,0,37,0,0],"FLAG":0,"BASE":48}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/mirabella_genio_I002742.html'],

        [typeId: 'awp02l-n-plug',
         name: 'AWP02L-N Plug',
         template: '{"NAME":"AWP02L-N","GPIO":[57,0,56,0,0,0,0,0,0,17,0,21,0],"FLAG":1,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/hugoai_awp02l-n.html'],

        [typeId: 'cyyltf-bifans-j23-plug',
         name: 'CYYLTF BIFANS J23 Plug',
         template: '{"NAME":"CYYLTF J23","GPIO":[56,0,0,0,0,0,0,0,21,17,0,0,0],"FLAG":1,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/cyyltd_bifans_J23.html'],

        [typeId: 'gosund-wp3-plug',
         name: 'Gosund WP3 Plug',
         template: '{"NAME":"Gosund WP3","GPIO":[0,0,0,0,17,0,0,0,56,57,21,0,0],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/gosund_wp3.html'],

        [typeId: 'sk03-pm-outdoor-plug',
         name: 'SK03 Power Monitor Outdoor Plug',
         template: '{"NAME":"SK03 Outdoor","GPIO":[17,0,0,0,133,132,0,0,131,57,56,21,0],"FLAG":0,"BASE":57}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/SK03_outdoor.html'],

        [typeId: 'aoycocr-x10s-pm-plug',
         name: 'Aoycocr X10S Power Monitor Plug',
         template: '{"NAME":"Aoycocr X10S","GPIO":[56,0,57,0,21,134,0,0,131,17,132,0,0],"FLAG":0,"BASE":45}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/aoycocr_X10S.html'],

        [typeId: 'brilliant-20699-rgbw-bulb',
         name: 'Brilliant 20699 800lm RGBW Bulb',
         template: '{"NAME":"Brilliant20699","GPIO":[0,0,0,0,141,140,0,0,37,142,0,0,0],"FLAG":0,"BASE":18}',
         installCommands: [["WebLog", "2"]],
         deviceLink: 'https://templates.blakadder.com/brilliant_20699.html'],

        [typeId: 'sonoff-sv',
         name: 'Sonoff SV',
         template: '{"NAME":"Sonoff SV","GPIO":[17,255,0,255,255,255,0,0,21,56,255,0,0],"FLAG":1,"BASE":3}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/sonoff_SV.html'],

        [typeId: 'sonoff-th',
         name: 'Sonoff TH',
         template: '{"NAME":"Sonoff TH","GPIO":[17,255,0,255,255,0,0,0,21,56,255,0,0],"FLAG":0,"BASE":4}',
         installCommands: [["TempRes", (tempRes == '' || tempRes == null ? "1" : tempRes)]],
         deviceLink: 'https://templates.blakadder.com/sonoff_TH.html'],

        [typeId: 'sonoff-pow',
         name: 'Sonoff POW',
         template: '{"NAME":"Sonoff Pow","GPIO":[17,0,0,0,0,130,0,0,21,132,133,52,0],"FLAG":0,"BASE":6}',
         installCommands: [["SetOption81", "1"], ["LedPower", "1"], ["LedState", "8"]],
         deviceLink: 'https://templates.blakadder.com/sonoff_Pow.html'],

        [typeId: 'sonoff-s31',
         name: 'Sonoff S31',
         template: '{"NAME":"Sonoff S31","GPIO":[17,145,0,146,0,0,0,0,21,56,0,0,0],"FLAG":0,"BASE":41}',
         installCommands: [["SetOption81", "1"], ["LedPower", "1"], ["LedState", "8"]],
         deviceLink: 'https://templates.blakadder.com/sonoff_S31.html'],
        
        [typeId: 'sonoff-ifan02',
         name: 'Sonoff iFan02',
         module: 44,
         installCommands: [['Rule1', '0']],
         deviceLink: 'https://templates.blakadder.com/sonoff_ifan02.html'],

        [typeId: 'sonoff-ifan03-no_beep-m71',
         name: 'Sonoff iFan03 (No Beep)',
         module: 71,
         installCommands: [["SetOption67", "0"], ['Rule1', '0']],
         deviceLink: 'https://templates.blakadder.com/sonoff_ifan03.html'],

        [typeId: 'sonoff-ifan03-beep-m71',
         name: 'Sonoff iFan03 (Beep)',
         module: 71,
         installCommands: [["SetOption67", "1"], 
                           ['Rule1', 'ON Fanspeed#Data>=1 DO Buzzer %value%; ENDON ON Fanspeed#Data==0 DO Buzzer 1; ENDON'],
                           ['Rule1', '1']],
         deviceLink: 'https://templates.blakadder.com/sonoff_ifan03.html'],

        [typeId: 'treatlife-ds01-dimmer',
         name: 'TreatLife DS01 Dimmer ',
         template: '{"NAME":"TL DS01 Dimmer","GPIO":[0,107,0,108,0,0,0,0,0,0,0,0,0],"FLAG":0,"BASE":54}',
         installCommands: [["TuyaMCU", "21,2"], 
                           ["DimmerRange", "150,1000"]],
         deviceLink: 'https://templates.blakadder.com/treatlife_DS01.html'],
        
        [typeId: 'deta-6911ha-switch',
         name: 'Deta 6911HA Switch',
         template: '{"NAME":"Deta 1G Switch","GPIO":[0,0,0,0,157,0,0,0,0,21,0,0,90],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/deta_6911HA.html'],

        [typeId: 'deta-6912ha-switch',
         name: 'Deta 6912HA Switch',
         template: '{"NAME":"DETA 2G Switch","GPIO":[0,0,0,0,157,0,0,0,91,21,22,0,90],"FLAG":0,"BASE":18}',
         installCommands: [['Rule1', 'on system#boot do Backlog LedPower 1; LedState 0; LedPower 1; LedState 8; endon'],
                           ['Rule1', '1']],
         deviceLink: 'https://templates.blakadder.com/deta_6912HA.html'],

        [typeId: 'deta-6903ha-switch',
         name: 'Deta 6903HA Switch',
         template: '{"NAME":"DETA 3G Switch","GPIO":[157,0,0,92,91,21,0,0,23,0,22,0,90],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/deta_6903HA.html'],

        [typeId: 'deta-6904ha-switch',
         name: 'Deta 6904HA Switch',
         template: '{"NAME":"Deta 4G Switch","GPIO":[157,0,0,19,18,21,0,0,23,20,22,24,17],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/deta_6904HA.html'],

        [typeId: 'deta-6922ha-outlet',
         name: 'Deta 6922HA Wall Outlet',
         template: '{"NAME":"DETA 2G GPO","GPIO":[0,0,0,17,157,0,0,0,91,21,22,0,90],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/deta_6922HA.html'],

        [typeId: 'lh-znb22-001-9w ',
         name: 'Lohas ZN033 9W 810lm RGBCCT Bulb ',
         template: '{"NAME":"Lohas RGBCW","GPIO":[0,0,0,0,38,37,0,0,41,39,40,0,0],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/lohas-ZN033-B22.html'],

        [typeId: 'kmc-4-pm-plug',
         name: 'KMC 4 Power Monitor Plug',
         template: '{"NAME":"KMC 4 Plug","GPIO":[0,56,0,0,133,132,0,0,130,22,23,21,17],"FLAG":0,"BASE":36}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/kmc-4.html'],

        [typeId: 'teckin-ss30-power-strip',
         name: 'Teckin SS30 Power Strip',
         template: '{"NAME":"Teckin SS30","GPIO":[52,255,255,57,29,17,0,0,31,30,32,255,25],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/teckin_ss30.html'],

        [typeId: 'teckin-sp10-plug',
         name: 'Teckin SP10 Plug',
         template: '{"NAME":"Teckin SP10","GPIO":[255,255,56,255,255,255,0,0,255,17,255,21,255],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/teckin_SP10.html'],

        [typeId: 'awp04l-pm-plug',
         name: 'AWP04L Power Monitor Plug',
         template: '{"NAME":"AWP04L","GPIO":[57,255,255,131,255,134,0,0,21,17,132,56,255],"FLAG":0,"BASE":18}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/awp04l.html'],

        [typeId: 'dd001-mini-ir-v08-rgb-led-controller-no-ir',
         name: 'DD001-MINI(G)-IR-V08 RGB LED Controller (no IR)',
         template: '{"NAME":"DD001-NOIR-RGB","GPIO":[0,0,0,0,37,0,0,0,38,0,39,0,0],"FLAG":0,"BASE":18}',
         installCommands: [["WebLog", "2"]],
         deviceLink: 'https://templates.blakadder.com/DD001-MINIG-IR-V08.html'],

        [typeId: 'sonoff-4ch-pro',
         name: 'Sonoff 4CH Pro',
         template: '{"NAME":"Sonoff 4CH Pro","GPIO":[17,255,255,255,23,22,18,19,21,56,20,24,0],"FLAG":0,"BASE":23}',
         installCommands: [],
         deviceLink: 'https://templates.blakadder.com/sonoff_4CH_Pro.html'],

        [typeId: 'unbranded-rgbwwcw-controller-type-1',
         name: 'Unbranded RGBWWCW Controller (Type 1)',
         template: '{"NAME":"CtrlRGBWWCW-T1","GPIO":[17,0,0,0,0,40,0,0,38,39,37,41,0],"FLAG":0,"BASE":18}',
         installCommands: [["WebLog", "2"]],
         deviceLink: ''],
        
        [typeId: 'tuyamcu-touch-switch-1-button',
        name: 'TuyaMCU Touch Switch - 1 button',
        module: 54,
        installCommands: [["TuyaMCU", "11,1"], ["TuyaMCU", "12,0"], 
                          ["TuyaMCU", "13,0"], ["TuyaMCU", "14,0"]],
        deviceLink: ''],

        [typeId: 'tuyamcu-lucci-connect-remote-as-switches',
        name: 'TuyaMCU Lucci Connect Remote',
        template: '{"NAME":"Lucci Fan","GPIO":[0,0,0,0,0,0,0,0,0,108,0,107,0],"FLAG":0,"BASE":54}',
        installCommands: [["TuyaMCU", "11,102"], ["TuyaMCU", "12,1"], ["TuyaMCU", "13,0"], 
                          ["TuyaMCU", "14,0"], ["TuyaMCU", "15,0"], ["TuyaMCU", "21,50"], 
                          ["Rule1", "on TuyaReceived#Data=55AA00070005020400010012 do dimmer 1 endon on TuyaReceived#Data=55AA00070005020400010113 do dimmer 2 endon on TuyaReceived#Data=55AA00070005020400010214 do dimmer 3 endon on Dimmer#State=1 do TuyaSend4 2,0 endon on Dimmer#State=2 do TuyaSend4 2,1 endon on Dimmer#State=3 do TuyaSend4 2,2 endon"], 
                          ["Rule1", "1"]],
        deviceLink: 'https://templates.blakadder.com/luci-connect-remote-control.html'],

        [typeId: 'tuyamcu-touch-switch-2-button',
        name: 'TuyaMCU Touch Switch - 2 buttons',
        module: 54,
        installCommands: [["TuyaMCU", "11,1"], ["TuyaMCU", "12,2"], 
                          ["TuyaMCU", "13,0"], ["TuyaMCU", "14,0"]],
        deviceLink: ''],

        [typeId: 'tuyamcu-touch-switch-3-button',
        name: 'TuyaMCU Touch Switch - 3 buttons',
        module: 54,
        installCommands: [["TuyaMCU", "11,1"], ["TuyaMCU", "12,2"], 
                          ["TuyaMCU", "13,3"], ["TuyaMCU", "14,0"]],
        deviceLink: ''],

        [typeId: 'tuyamcu-touch-switch-4-button',
        name: 'TuyaMCU Touch Switch - 4 buttons',
        module: 54,
        template: '',
        installCommands: [["TuyaMCU", "11,1"], ["TuyaMCU", "12,2"], 
                          ["TuyaMCU", "13,3"], ["TuyaMCU", "14,4"]],
        deviceLink: ''],

        [typeId: 'feit-electric-dimmer',
        name: 'Feit Electric Dimmer',
        module: 54,
        installCommands: [["TuyaMCU", "11,1"], ["TuyaMCU", "21,2"], 
                          ["DimmerRange", "10,1000"]],
        deviceLink: 'https://templates.blakadder.com/feit_electric-DIM-WIFI.html'],

        [typeId: 'sonoff-powr2', 
        name: 'Sonoff POW R2',
        template: '{"NAME":"Sonoff Pow R2","GPIO":[17,145,0,146,0,0,0,0,21,56,0,0,0],"FLAG":0,"BASE":43}',
        installCommands: [["SetOption81", "1"], ["LedPower", "1"], ["LedState", "8"]],
        deviceLink: 'https://templates.blakadder.com/sonoff_Pow_R2.html'],

        [typeId: 'sonoff-s20', 
        name: 'Sonoff S20',
        template: '{"NAME":"Sonoff S20","GPIO":[17,255,255,255,0,0,0,0,21,56,0,0,0],"FLAG":0,"BASE":8}',
        installCommands: [["SetOption81", "1"], ["LedPower", "1"], ["LedState", "8"]],
        deviceLink: 'https://templates.blakadder.com/sonoff_S20.html'],

        [typeId: 'sonoff-s26', 
        name: 'Sonoff S26',
        template: '{"NAME":"Sonoff S26","GPIO":[17,255,255,255,0,0,0,0,21,158,0,0,0],"FLAG":0,"BASE":8}',
        installCommands: [["SetOption81", "1"]],
        deviceLink: 'https://templates.blakadder.com/sonoff_S26.html'],

        [typeId: 'sonoff-mini', 
        name: 'Sonoff Mini',
        template: '{"NAME":"Sonoff Mini","GPIO":[17,0,0,0,9,0,0,0,21,56,0,0,255],"FLAG":0,"BASE":1}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/sonoff_mini.html'],

        [typeId: 'sonoff-basic',
        name: 'Sonoff Basic',
        module: 1,
        installCommands: [["SetOption81", "1"]],
        deviceLink: 'https://templates.blakadder.com/sonoff_basic.html'],

        [typeId: 's120-plug' ,
        name: 'S120 USB Charger Plug',
        template: '{"NAME":"S120 Plug","GPIO":[0,0,0,0,0,21,0,0,0,52,90,0,0],"FLAG":0,"BASE":18}',
        installCommands: [["SetOption81", "1"]],
        deviceLink: 'https://templates.blakadder.com/brilliantsmart_20676.html'],

        [typeId: 's120-plug-bmp280' ,
        name: 'S120 USB Charger Plug + BMP280',
        template: '{"NAME":"S120THPPlug","GPIO":[0,6,0,5,0,21,0,0,0,52,90,0,0],"FLAG":0,"BASE":18}',
        installCommands: [["SetOption81", "1"]],
        deviceLink: 'https://templates.blakadder.com/brilliantsmart_20676.html'],

        [typeId: 'globe-34207-bulb' ,
        name: 'Globe 34207 800lm RGBCCT Bulb',
        template: '{"NAME":"GlobeRGBWW","GPIO":[0,0,0,0,37,40,0,0,38,41,39,0,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/globe-34207.html'],

        [typeId: 'brilliantsmart-20676-plug' ,
        name: 'BrilliantSmart 20676 USB Charger Plug',
        template: '{"NAME":"Brilliant20676","GPIO":[0,0,0,0,0,21,0,0,0,52,90,0,0],"FLAG":0,"BASE":18}',
        installCommands: [["SetOption81", "1"]],
        deviceLink: 'https://templates.blakadder.com/brilliantsmart_20676.html'],

        [typeId: 'brilliantsmart-20741-bulb' ,
        name: 'BrilliantSmart 20741 9W 750lm RGBW Bulb',
        template: '{"NAME":"BS-20741-RGBW","GPIO":[0,0,0,0,37,40,0,0,38,0,39,0,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/brilliantsmart_20741.html'],

        [typeId: 'brilliant-bl20925-pm-plug', 
        name: 'Brilliant Lighting BL20925 PM Plug',
        template: '{"NAME":"BL20925","GPIO":[0,0,0,17,133,132,0,0,131,158,21,0,0],"FLAG":0,"BASE":52}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/brilliant_BL20925.html'],

        [typeId: 'deta-6930ha-plug', 
        name: 'Deta 6930HA Plug',
        template: '{"NAME":"Deta6930HAPlug","GPIO":[0,17,0,0,0,0,0,0,0,56,21,0,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/deta_6930HA.html'],

        [typeId: 'prime-ccrcwfii113pk-plug', 
        name: 'Prime CCRCWFII113PK Plug',
        template: '{"NAME":"PrimeCCRC13PK","GPIO":[0,0,0,0,57,56,0,0,21,122,0,0,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/prime_CCRCWFII113PK.html'],

        [typeId: 'ykyc-wj1y0-10a', 
        name: 'YKYC-WJ1Y0-10A PM Plug',
        template: '{"NAME":"YKYC-001PMPlug","GPIO":[0,17,0,57,133,132,0,0,130,56,21,0,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: ''],

        [typeId: 'merkury-mi-bw210-999w', 
        name: 'Merkury MI-BW210-999W',
        template: '{"NAME":"MI-BW210-999W","GPIO":[0,0,0,0,140,37,0,0,142,38,141,0,0],"FLAG":0,"BASE":48}',
        installCommands: [],
        deviceLink: ''],

        [typeId: 'lumary-rgbcct-led-strip', 
        name: 'Lumary RGBCCT LED Strip',
        template: '{"NAME":"Lumary LED","GPIO":[17,0,0,0,37,40,0,0,38,41,39,0,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/lumary_led_strip.html'],

        [typeId: 'xs-ssa06-plug', 
        name: 'XS-SSA06 Plug with RGB',
        template: '{"NAME":"XS-SSA06-RGB","GPIO":[37,0,38,0,0,39,0,0,0,90,0,21,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/XS-SSA06.html'],

        [typeId: 'tuyamcu-wifi-dimmer', 
        name: 'TuyaMCU Wifi Dimmer',
        module: 54,
        installCommands: [["SetOption66", "0"],
        ],
        deviceLink: ''],

        [typeId: 'zigbee-controller-default' ,
        name: 'Zigbee Controller (default pinout)',
        template: '{"NAME":"Zigbee","GPIO":[0,0,0,0,0,0,0,0,0,166,0,165,0],"FLAG":0,"BASE":18}',
        installCommands: [["SerialLog", "0"],
                          ],
        deviceLink: 'https://tasmota.github.io/docs/#/Zigbee'],

        [typeId: 'unbranded-rgb-controller-with-ir-type-1' ,
        name: 'Unbranded RGB Controller with IR (Type 1)',
        template: '{"NAME":"RGB Controller","GPIO":[0,0,0,0,0,38,0,0,39,51,0,37,0],"FLAG":15,"BASE":18}',
        installCommands: [["WebLog", "2"]],
        deviceLink: ''],

        [typeId: 'sonoff-4ch',
        name: 'Sonoff 4CH',
        template: '{"NAME":"Sonoff 4CH","GPIO":[17,255,255,255,23,22,18,19,21,56,20,24,0],"FLAG":0,"BASE":7}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/sonoff_4CH.html'],

        [typeId: 'sonoff-4ch-pro-r2',
        name: 'Sonoff 4CH Pro (R2)',
        template: '{"NAME":"Sonoff 4CH Pro","GPIO":[17,255,255,255,23,22,18,19,21,56,20,24,0],"FLAG":0,"BASE":23}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/sonoff_4CH_Pro.html'],

        [typeId: 'nedis-ir-bridge',
        name: 'Nedis IR Bridge',
        template: '{"NAME":"Nedis IR Bridge","GPIO":[255,255,255,255,56,51,0,0,0,17,8,0,0],"FLAG":0,"BASE":62}',
        installCommands: [['SerialLog', '0']],
        deviceLink: 'https://templates.blakadder.com/nedis_WIFIRC10CBK.html'],

        [typeId: 'luminea-zx-2844-rgbw-led-controller',
        name: 'Luminea ZX-2844 RGBW LED Controller ',
        template: '{"NAME":"Luminea ZX-284","GPIO":[40,0,0,0,0,39,0,0,38,17,37,0,0],"FLAG":0,"BASE":18}',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/luminea_zx-2844.html'],

        [typeId: 'tuyamcu-znsn-wifi-curtain-wall-panel',
        comment: 'NOT GENERIC - read the instructions',
        name: 'TuyaMCU ZNSN Wifi Curtain Wall Panel',
        template: '{"NAME":"ZNSN Curtain","GPIO":[0,107,0,108,21,0,0,0,0,0,0,0,0],"FLAG":0,"BASE":54}',
        installCommands: [["WebLog", "2"],
                        ['SetOption66', "1"],
                        ['SetOption80', "1"],
                        ["PulseTime1", "0"],
                        ["PulseTime2", "0"],
                        ["Interlock", "1,2"],
                        ["Interlock", "ON"],
                        ["ShutterMotorDelay", "4.5"],
                        ["ShutterOpenDuration", "10"],
                        ["ShutterCloseDuration", "11.2"],
                        ["Var1", "ShutterClose1"],
                        ["Var2", "ShutterStop1"],
                        ["Var3", "ShutterOpen1"],
                        ["setoption34", "50"],
                        ["Rule1", "ON Power1#state=1 DO Backlog var3 var3; var2 ShutterStop1; TuyaSend4 101,0 ENDON "],
                        ["Rule1", "+ ON Power1#state=0 DO Backlog var2 var2; TuyaSend4 101,1; var1 ShutterClose1; var3 ShutterOpen1; ENDON "],
                        ["Rule1", "+ ON Power2#state=0 DO Backlog var2 var2; TuyaSend4 101,1; var1 ShutterClose1; var3 ShutterOpen1; ENDON "],
                        ["Rule1", "+ ON Power2#state=1 DO Backlog var1 var1; var2 ShutterStop1; TuyaSend4 101,2 ENDON "],
                        ["Rule1", "+ ON ShutterStop#Data DO Backlog var2 var2; TuyaSend4 101,1; var1 ShutterClose1; var3 ShutterOpen1; ENDON "],
                        ["Rule1", "+ ON Shutter1#Position DO var4 %value% ENDON ON Event#Close0 DO Backlog var2 var2; TuyaSend4 101,1; ENDON "],
                        ["Rule1", "1"],
                        ["Rule2", "ON TuyaReceived#Data=55AA00070005650400010277 DO backlog var1 ShutterClose1; %var1%;  ENDON "],
                        ["Rule2", "+ ON System#Init DO Backlog setoption34 50; var1 ShutterClose1; var2 ShutterStop1; var3 ShutterOpen1; ENDON "],
                        ["Rule2", "+ ON TuyaReceived#Data=55AA00070005020400010214 DO backlog var2 ShutterStop1; %var2%; ENDON "],
                        ["Rule2", "+ ON TuyaReceived#Data=55AA00070005650400010176 DO backlog var3 ShutterOpen1; %var3%; ENDON "],
                        ["Rule2", "+ ON Event#Open100 DO Backlog var2 var2; TuyaSend4 101,1; ENDON "],
                        ["Rule2", "+ ON ShutterOpen#Data=100 DO Event Open%var4% ENDON ON ShutterClose#Data=0 DO Event Close%var4% ENDON "],
                        ["Rule2", "1"],
                        ],

        deviceLink: '',
        open: ["TuyaSend4", "101,0"],
        stop: ["TuyaSend4", "101,1"],
        close: ["TuyaSend4", "101,2"],],
        
        [typeId: 'mj-sd02-dimmer-switch',
        comment: 'WITHOUT power status LED active by design',
        name: 'Martin Jerry MJ-SD02 Dimmer Switch',
        template: '{"NAME":"MJ-SD02","GPIO":[19,18,0,33,34,32,255,255,31,37,30,126,29],"FLAG":15,"BASE":18}',
        installCommands: [["WebLog", "2"],
                        ['SerialLog', '0'],
                        ['setoption3', '1'],
                        ['setoption1', '1'],
                        ['setoption32', '8'],
                        ['buttontopic', '0'],
                        ['Rule1', 'on Button3#state=2 do dimmer + endon on Button2#state=2 do dimmer - endon '],
                        ['Rule1', '+ on Button2#state=3 do dimmer 20 endon on Button3#state=3 do dimmer 100 endon '],
                        ['Rule1', '+ on Button1#state=2 do power1 2 endon on Button1#state=3 do power1 0 endon'],
                        ['Rule1', '1']],
        deviceLink: ''],

        [typeId: 'mj-sd02-dimmer-switch-led',
        comment: 'WITH power status LED active by design',
        name: 'Martin Jerry MJ-SD02 Dimmer Switch',
        template: '{"NAME":"MJ-SD02-LED","GPIO":[19,18,0,33,56,32,255,255,31,37,30,126,29],"FLAG":15,"BASE":18}',
        installCommands: [["WebLog", "2"],
                        ['SerialLog', '0'],
                        ['setoption3', '1'],
                        ['setoption1', '1'],
                        ['setoption32', '8'],
                        ['buttontopic', '0'],
                        ['LedPower', '1'],
                        ['SetOption31', '0'],
                        ['Rule1', 'on Button3#state=2 do dimmer + endon on Button2#state=2 do dimmer - endon '],
                        ['Rule1', '+ on Button2#state=3 do dimmer 20 endon on Button3#state=3 do dimmer 100 endon '],
                        ['Rule1', '+ on Button1#state=2 do power1 2 endon on Button1#state=3 do power1 0 endon'],
                        ['Rule1', '1']],
        deviceLink: ''],

        [typeId: 'maxcio-diffuser-v1',
        comment: 'REQUIRES "Use Alternate Color command in Tasmota" to be set!',
        name: 'Maxcio Diffuser Wood Grain (v1)',
        template: '{"NAME":"MaxcioDiffuser","GPIO":[0,107,0,108,21,0,0,0,37,38,39,28,0],"FLAG":0,"BASE":54}',
        installCommands: [["WebLog", "2"],
                        ['SerialLog', '0'],
                        ['setoption20', '1'],
                        ['Rule1', 'ON Var1#State DO backlog tuyasend3 8,%value%00ffff00; color %value%; rule2 0; power1 1; rule2 1; ENDON ON Scheme#Data=0 DO TuyaSend4 6,0 ENDON ON Scheme#Data>0 DO TuyaSend4 6,1 ENDON ON TuyaReceived#Data=55AA03070005050100010116 DO power1 1 ENDON ON TuyaReceived#Data=55AA03070005010100010011 DO backlog rule2 0; power2 0; rule2 1; power3 %var2%; var2 1; ENDON ON TuyaReceived#Data=55AA03070005010100010112 DO backlog rule2 0; power2 1; rule2 1; var2 0; power3 0; ENDON'],
                        ['Rule2', 'ON Power1#State DO tuyasend1 5,%value% ENDON ON Power2#State=0 DO tuyasend1 1,0 ENDON ON Power2#State=1 DO backlog var2 1; tuyasend1 1,1; ENDON'],
                        ['Rule3', 'ON TuyaReceived#Data=55AA03070005050100010015 DO power1 0 ENDON'],
                        ['Rule1', '1'],
                        ['Rule2', '1'],
                        ['Rule3', '1']],
        deviceLink: 'https://templates.blakadder.com/maxcio_400ml_diffuser.html'],

        [typeId: 'sonoff-rf-bridge-parent' , 
        notForUniversal: true,
        comment: 'Functional - Need feedback',
        name: '',
        template: '',
        installCommands: [],
        deviceLink: 'https://templates.blakadder.com/sonoff_RF_bridge.html'],
        
        [typeId: 'rflink-parent' , 
        notForUniversal: true,
        comment: 'Functional - Need feedback',
        name: '',
        template: '',
        installCommands: [],
        deviceLink: 'http://www.rflink.nl/blog2/wiring'],
        
        [typeId: '01generic-device',
        comment: 'Works with most devices' ,
        name: 'Generic Device',
        installCommands: [],
        deviceLink: ''],

        [typeId: '01generic-rgb-rgbw-controller-bulb-dimmer', 
        comment: 'RGB+WW+CW should all work properly',
        name: 'Generic RGB/RGBW Controller/Bulb/Dimmer',
        template: '',
        installCommands: [["WebLog", "2"]],
        deviceLink: ''],

        [typeId: '01generic-thp-device' ,
        name: 'Generic Temperature/Humidity/Pressure Device',
        template: '',
        installCommands: [["TempRes", (tempRes == '' || tempRes == null ? "1" : tempRes)]],
        deviceLink: ''],

    ]

    TreeMap deviceConfigurationsMap = [:] as TreeMap
    deviceConfigurations.each{
        deviceConfigurationsMap[it["typeId"]] = it
    }
    return deviceConfigurationsMap
}

def getDeviceConfiguration(String typeId) {
    TreeMap deviceConfigurationsMap = getDeviceConfigurations()
    try{
        return deviceConfigurationsMap[typeId]
    } catch(e) {
        log.warn "Failed to retrieve Device Configuration '$typeId': $e"
        return null
    }
}

def getDeviceConfigurationsAsListOption() {
    TreeMap deviceConfigurationsMap = getDeviceConfigurations()
    def items = []
    deviceConfigurationsMap.sort({ a, b -> a.key <=> b.key }).each { k, v ->
        def label = v["name"]
        if(v.containsKey("comment") && v["comment"].length() > 0) {
            label += " (${v["comment"]})"
        }
        if(!(v.containsKey("notForUniversal") && v["notForUniversal"] == true)) {
            items << ["${v["typeId"]}":"$label"] 
        }
    }
    return items
}
// END:  getHelperFunctions('device-configurations')

/* These methods are unique to each driver */
def installed() {
	logging("installed()", 100)

    tasmota_installedPreConfigure()
    configurePresence()

}

def initialize() {
    logging("initialize()", 100)
    generalInitialize()
    configurePresence()
}

def updated() {
    logging("updated()", 100)
    setDisableCSS(disableCSS)
    configurePresence()
    unschedule("updatePresence")
    unschedule("tasmota_updatePresence")
    updateNeededSettings()
    refresh()
}

def configure() {
    generalInitialize()
    updateNeededSettings()
}

def getDriverCSS() {
    
    r = ""
    
    r += '''
    /*form[action*="preference"]::before {
        color: green;
        content: "Hi, this is my content"
    }
    form[action*="preference"] div[for^=preferences] {
        color: blue;
    }*/
    div#stateComment {
        display: inline;
    }
    /*div#stateComment:after {
        color: red;
        display: inline;
        visibility: visible;
        position: absolute;
        bottom: 150%;
        left: 400%;
        white-space: nowrap;
    }*/
    div#stateComment:after {
        color: #382e2b;
        visibility: visible;
        position: relative;
        white-space: nowrap;
        display: inline;
    }
    /*div#stateComment:after {
        color: #382e2b;
        display: inline;
        visibility: visible;
        position: fixed;
        left: 680px;
        white-space: nowrap;
        top: 95px;
    }*/
    /*
    div#stateComment:after {
        color: #5ea767;
        display: inline;
        visibility: visible;
        position: absolute;
        left: 120px;
        white-space: nowrap;
        bottom: -128px;
        height: 36px;
        vertical-align: middle;
    }*/
    div#stateCommentInside {
        display: none;
    }
    li[id*='stateCommentInside'] {
        /*visibility: hidden;*/
        /*position: absolute;*/
        display: list-item;
    }
    .property-value {
        overflow-wrap: break-word;
    }
    '''
    return r
}

def refresh() {
    def metaConfig = tasmota_refresh(metaConfig=null)

    metaConfig = setStateVariablesToHide(['mac'], metaConfig=metaConfig)
    logging("hideExtended=$hideExtended, hideAdvanced=$hideAdvanced", 1)
    if(hideExtended == null || hideExtended == true) {
        metaConfig = setPreferencesToHide(['hideAdvanced', 'ipAddress', 'override', 'useIPAsID', 'telePeriod', 'invertPowerNumber', 'useAlternateColorCommand'], metaConfig=metaConfig)
    }
    if(hideExtended == null || hideExtended == true || hideAdvanced == null || hideAdvanced == true) {
        metaConfig = setPreferencesToHide(['disableModuleSelection', 'port', 'disableCSS', 'moduleNumber'], metaConfig=metaConfig)
    }
    if(hideDangerousCommands == null || hideDangerousCommands == true) {
        metaConfig = setCommandsToHide(['deleteChildren', 'initialize'], metaConfig=metaConfig)
    } else {
        metaConfig = setCommandsToHide(['initialize'], metaConfig=metaConfig)
    }
    if(deviceConfig == null) deviceConfig = "01generic-device"
    deviceConfigMap = getDeviceConfiguration(deviceConfig)
    logging("deviceConfigMap=$deviceConfigMap", 1)
    try{
        if(deviceConfigMap.containsKey('comment') && 
           deviceConfigMap['comment'] != null &&
           deviceConfigMap['comment'].length() > 0) {
            logging("Settings state.comment...", 1)
            setStateCommentInCSS(deviceConfigMap['comment'], metaConfig=metaConfig) 
            state.comment = "<div id=\"stateComment\"><div id=\"stateCommentInside\"></div></div>"
        } else {
            logging("Hiding state.comment...", 1)
            state.comment = "<div id=\"stateComment\"><div id=\"stateCommentInside\"></div></div>"
            metaConfig = setStateVariablesToHide(['comment'], metaConfig=metaConfig)
        }
    } catch(e2) {
        log.warn e2
        metaConfig = setStateVariablesToHide(['comment'], metaConfig=metaConfig)
    }
    metaConfig = setDatasToHide(["preferences", "metaConfig"], metaConfig=metaConfig)
}

/* The parse(description) function is included and auto-expanded from external files */
void parse(String description) {
    // BEGIN:getGenericTasmotaNewParseHeader()
    Map descMap = tasmota_parseDescriptionAsMap(description)
    String body = null
    //logging("descMap: ${descMap}", 0)
    
    boolean missingChild = false
    
    if (state.mac != descMap["mac"]) {
        logging("Mac address of device found ${descMap["mac"]}", 10)
        state.mac = descMap["mac"]
    }
    
    sendlastCheckinEvent(minimumMinutesToRepeat=55)
    
    tasmota_prepareDNI()
    
    if (descMap["body"] && descMap["body"] != "T04=") body = new String(descMap["body"].decodeBase64())
    
    if (body && body != "") {
        if(body.startsWith("{") || body.startsWith("[")) {
            boolean log99 = logging("========== Parsing Report ==========", 99)
            JsonSlurper slurper = new JsonSlurper()
            Map result = slurper.parseText(body)
    
            //logging("result: ${result}",0)
    // END:  getGenericTasmotaNewParseHeader()
        missingChild = parseResult(result, missingChild)
    // BEGIN:getGenericTasmotaNewParseFooter()
        result = null
    } else {
    
        }
    }
    
    if(missingChild == true) {
        log.warn "Missing a child device, run the Refresh command from the device page!"
    
        if(state.installing != "1") {
            logging("Initiating child device installation...", 100)
            state.installing = "1"
            runIn(120, "clearInstalling")
            tasmota_getAction(tasmota_getCommandString("Status", "0"), callback="tasmota_parseConfigureChildDevices")
        }
    }
    if (device.currentValue("ip") == null) {
        String curIP = getDataValue("ip")
        logging("Setting IP from Data: $curIP", 1)
        sendEvent(name: 'ip', value: curIP, isStateChange: false)
        sendEvent(name: "ipLink", value: "<a target=\"device\" href=\"http://$curIP\">$curIP</a>", isStateChange: false)
    }
    descMap = null
    body = null
    // END:  getGenericTasmotaNewParseFooter()
}

boolean parseResult(Map result) {
    boolean missingChild = false
    missingChild = parseResult(result, missingChild)
    return missingChild
}

void parseJSON(String jsonData) {
    boolean missingChild = false
    JsonSlurper jsonSlurper = new JsonSlurper()
    parseResult(jsonSlurper.parseText(jsonData), missingChild)
    jsonSlurper = null
}

boolean parseResult(Map result, boolean missingChild) {
    boolean log99 = logging("parseResult: $result", 99)
    logging("parseResult: $result", 100)
    // BEGIN:getTasmotaNewParserForStatusSTS()
    if (result.containsKey("StatusSTS")) {
        logging("StatusSTS: $result.StatusSTS",99)
        result << result.StatusSTS
    }
    // END:  getTasmotaNewParserForStatusSTS()
    // BEGIN:getTasmotaNewParserForParentSwitch()
    if (result.containsKey("POWER")  == true && result.containsKey("POWER1") == false) {
        logging("parser: POWER (child): $result.POWER",1)
    
        missingChild = callChildParseByTypeId("POWER1", [[name:"switch", value: result.POWER.toLowerCase()]], missingChild)
    } else {
    
        String currentPower = ""
        (1..16).each {i->
            currentPower = "POWER$i"
    
            if(result.containsKey(currentPower) == true) {
                if(i < 3 && invertPowerNumber == true) {
    
                    if(i == 1) {
                        currentPower = "POWER2"
                    } else {
                        currentPower = "POWER1"
                    }
                }
                logging("parser: $currentPower (original: POWER$i): ${result."POWER$i"}",1)
                missingChild = callChildParseByTypeId("$currentPower", [[name:"switch", value: result."POWER$i".toLowerCase()]], missingChild)
    
            }
        }
    }
    // END:  getTasmotaNewParserForParentSwitch()
    // BEGIN:getTasmotaNewParserForDimmableDevice()
    if(true) {
        com.hubitat.app.ChildDeviceWrapper childDevice = tasmota_getChildDeviceByActionType("POWER1")
        if(result.containsKey("Dimmer")) {
            def dimmer = result.Dimmer
            logging("Dimmer: ${dimmer}", 1)
            state.level = dimmer
            if(childDevice?.currentValue('level') != dimmer ) missingChild = callChildParseByTypeId("POWER1", [[name: "level", value: dimmer]], missingChild)
        }
    
        if(result.containsKey("TuyaReceived") && result.TuyaReceived.containsKey("Data")) {
    
            if(result.TuyaReceived.Data != "55AA000000010101") {
                missingChild = callChildParseByTypeId("POWER1", [[name: "tuyaData", value: result.TuyaReceived.Data]], missingChild)
            }
        }
    
        if(log99 == true && result.containsKey("Wakeup")) {
            logging("Wakeup: ${result.Wakeup}", 99)
    
        }
    }
    // END:  getTasmotaNewParserForDimmableDevice()
    // BEGIN:getTasmotaNewParserForRGBWDevice()
    if(true) {
    
        com.hubitat.app.ChildDeviceWrapper childDevice = tasmota_getChildDeviceByActionType("POWER1")
        String mode = "RGB"
        if (result.containsKey("Color")) {
            String color = result.Color
            logging("Color: ${color}, size: ${result.Color.tokenize(",").size()}", 1)
            if((color.length() > 6 && color.startsWith("000000")) ||
               (result.Color.tokenize(",").size() > 3 && color.startsWith("0,0,0"))) {
                mode = "CT"
            }
            state.colorMode = mode
            if(childDevice?.currentValue('colorMode') != mode ) missingChild = callChildParseByTypeId("POWER1", [[name: "colorMode", value: mode]], missingChild)
        }
        if (result.containsKey("Scheme")) {
            if(childDevice?.currentValue('effectNumber') != result.Scheme ) missingChild = callChildParseByTypeId("POWER1", [[name: "effectNumber", value: result.Scheme]], missingChild)
        }
        if (mode == "RGB" && result.containsKey("HSBColor")) {
            List hsbColor = result.HSBColor.tokenize(",")
            hsbColor[0] = Math.round((hsbColor[0] as Integer) / 3.6) as Integer
            hsbColor[1] = hsbColor[1] as Integer
    
            logging("hsbColor: ${hsbColor}", 1)
            if(childDevice?.currentValue('hue') != hsbColor[0] ) missingChild = callChildParseByTypeId("POWER1", [[name: "hue", value: hsbColor[0]]], missingChild)
            if(childDevice?.currentValue('saturation') != hsbColor[1] ) missingChild = callChildParseByTypeId("POWER1", [[name: "saturation", value: hsbColor[1]]], missingChild)
            String colorName = rgbw_getColorNameFromHueSaturation(hsbColor[0], hsbColor[1])
            if(childDevice?.currentValue('colorName') != colorName ) {
                missingChild = callChildParseByTypeId("POWER1", [[name: "colorName", value: colorName]], missingChild)
            }
        } else if (result.containsKey("CT")) {
            Integer t = Math.round(1000000/result.CT)
            if(childDevice?.currentValue('colorTemperature') != t ) {
                missingChild = callChildParseByTypeId("POWER1", [[name: "colorTemperature", value: t]], missingChild)
            }
            String colorName = rgbw_getColorNameFromTemperature(t)
            if(childDevice?.currentValue('colorName') != colorName ) {
                missingChild = callChildParseByTypeId("POWER1", [[name: "colorName", value: colorName]], missingChild)
            }
            logging("CT: $result.CT ($t)",99)
        }
    
    }
    // END:  getTasmotaNewParserForRGBWDevice()
    // BEGIN:getTasmotaNewParserForFanMode()
    if (result.containsKey("FanSpeed")) {
        String speed = "off"
        switch(result.FanSpeed) {
            case "1":
                speed = "low"
                break
            case "2":
                speed = "medium"
                break
            case "3":
                speed = "high"
                break
        }
        logging("parser: FanSpeed: $result.FanSpeed, speed = $speed", 1)
        missingChild = callChildParseByTypeId("FAN", [[name:"speed", value: speed]], missingChild)
    }
    // END:  getTasmotaNewParserForFanMode()
    // BEGIN:getTasmotaNewParserForBasicData()
    if (result.containsKey("StatusNET")) {
        logging("StatusNET: $result.StatusNET",99)
        result << result.StatusNET
    }
    if (result.containsKey("StatusFWR")) {
        logging("StatusFWR: $result.StatusFWR",99)
        result << result.StatusFWR
    }
    if (result.containsKey("StatusPRM")) {
        logging("StatusPRM: $result.StatusPRM",99)
        result << result.StatusPRM
    }
    if (false && result.containsKey("Status")) {
    
        logging("Status: $result.Status",99)
        result << result.Status
    }
    if (result.containsKey("LoadAvg")) {
        logging("LoadAvg: $result.LoadAvg",99)
    
    }
    if (log99 == true && result.containsKey("Sleep")) {
        logging("Sleep: $result.Sleep",99)
    }
    if (log99 == true && result.containsKey("SleepMode")) {
        logging("SleepMode: $result.SleepMode",99)
    }
    if (log99 == true && result.containsKey("Vcc")) {
        logging("Vcc: $result.Vcc",99)
    }
    if (log99 == true && result.containsKey("Hostname")) {
        logging("Hostname: $result.Hostname",99)
    }
    if (result.containsKey("IPAddress") && (override == false || override == null)) {
        logging("IPAddress: $result.IPAddress",99)
        sendEvent(name: "ip", value: "$result.IPAddress", isStateChange: false)
    
        sendEvent(name: "ipLink", value: "<a target=\"device\" href=\"http://$result.IPAddress\">$result.IPAddress</a>", isStateChange: false)
        updateDataValue("ip", "$result.IPAddress")
    }
    if (log99 == true && result.containsKey("WebServerMode")) {
        logging("WebServerMode: $result.WebServerMode",99)
    }
    if (result.containsKey("Version")) {
        logging("Version: $result.Version",99)
        updateDataValue("firmware", result.Version)
    }
    if (result.containsKey("Module") && !result.containsKey("Version")) {
    
        logging("Module: $result.Module",50)
        sendEvent(name: "module", value: "$result.Module", isStateChange: false)
    }
    
    if (result.containsKey("NAME") && result.containsKey("GPIO") && result.containsKey("FLAG") && result.containsKey("BASE")) {
        def n = result.toMapString()
        n = n.replaceAll(', ',',')
        n = n.replaceAll('\\[','{').replaceAll('\\]','}')
        n = n.replaceAll('NAME:', '"NAME":"').replaceAll(',GPIO:\\{', '","GPIO":\\[')
        n = n.replaceAll('\\},FLAG', '\\],"FLAG"').replaceAll('BASE', '"BASE"')
    
        logging("Template: $n",50)
        sendEvent(name: "templateData", value: "${n}", isStateChange: false)
    }
    if (result.containsKey("RestartReason")) {
        log.warn("RestartReason: $result.RestartReason")
    }
    if (result.containsKey("TuyaMCU")) {
        logging("TuyaMCU: $result.TuyaMCU",99)
        sendEvent(name: "tuyaMCU", value: "$result.TuyaMCU", isStateChange: false)
    }
    if (log99 == true && result.containsKey("SetOption81")) {
        logging("SetOption81: $result.SetOption81",99)
    }
    if (log99 == true && result.containsKey("SetOption113")) {
        logging("SetOption113 (Hubitat enabled): $result.SetOption113",99)
    }
    if (result.containsKey("Uptime")) {
        logging("Uptime: $result.Uptime",99)
    
        state.uptime = result.Uptime
        updateDataValue('uptime', result.Uptime)
    }
    // END:  getTasmotaNewParserForBasicData()
    // BEGIN:getTasmotaNewParserForEnergyMonitor()
    if (result.containsKey("StatusSNS")) {
        result << result.StatusSNS
    }
    if (result.containsKey("ENERGY")) {
    
        if (result.ENERGY.containsKey("Total")) {
            logging("Total: $result.ENERGY.Total kWh",99)
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"energyTotal", value:"$result.ENERGY.Total kWh"]], missingChild)
        }
        if (result.ENERGY.containsKey("Today")) {
            logging("Today: $result.ENERGY.Today kWh",99)
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"energyToday", value:"$result.ENERGY.Today kWh"]], missingChild)
        }
        if (result.ENERGY.containsKey("Yesterday")) {
            logging("Yesterday: $result.ENERGY.Yesterday kWh",99)
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"energyYesterday", value:"$result.ENERGY.Yesterday kWh"]], missingChild)
        }
        if (result.ENERGY.containsKey("Current")) {
            logging("Current: $result.ENERGY.Current A",99)
            def r = (result.ENERGY.Current == null) ? 0 : result.ENERGY.Current
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"current", value:"$r A"]], missingChild)
        }
        if (result.ENERGY.containsKey("ApparentPower")) {
            logging("apparentPower: $result.ENERGY.ApparentPower VA",99)
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"apparentPower", value:"$result.ENERGY.ApparentPower VA"]], missingChild)
        }
        if (result.ENERGY.containsKey("ReactivePower")) {
            logging("reactivePower: $result.ENERGY.ReactivePower VAr",99)
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"reactivePower", value:"$result.ENERGY.ReactivePower VAr"]], missingChild)
        }
        if (result.ENERGY.containsKey("Factor")) {
            logging("powerFactor: $result.ENERGY.Factor",99)
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"powerFactor", value:"$result.ENERGY.Factor"]], missingChild)
        }
        if (result.ENERGY.containsKey("Voltage")) {
            logging("Voltage: $result.ENERGY.Voltage V",99)
            def r = (result.ENERGY.Voltage == null) ? 0 : result.ENERGY.Voltage
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"voltageWithUnit", value:"$r V"]], missingChild)
            missingChild = callChildParseByTypeId("POWER1", [[name:"voltage", value: r, unit: "V"]], missingChild)
        }
        if (result.ENERGY.containsKey("Power")) {
            logging("Power: $result.ENERGY.Power W",99)
            def r = (result.ENERGY.Power == null) ? 0 : result.ENERGY.Power
    
            missingChild = callChildParseByTypeId("POWER1", [[name:"powerWithUnit", value:"$r W"]], missingChild)
            missingChild = callChildParseByTypeId("POWER1", [[name:"power", value: r, unit: "W"]], missingChild)
    
        }
    }
    // END:  getTasmotaNewParserForEnergyMonitor()
    // BEGIN:getTasmotaNewParserForSensors()
    for ( r in result ) {
    
        if((r.key == 'StatusSNS' || r.key == 'SENSOR') && r.value instanceof Map) {
            result << r
        }
    }
    for ( r in result ) {
        if(r.value instanceof Map && (r.value.containsKey("Temperature") ||
            r.value.containsKey("ProbeTemperature") ||
            r.value.containsKey("Humidity") || r.value.containsKey("Pressure") ||
            r.value.containsKey("Distance") || r.value.containsKey("Illuminance") ||
            r.value.containsKey("Gas") || r.value.containsKey("DewPoint"))) {
            if (r.value.containsKey("Humidity")) {
                logging("Humidity: RH $r.value.Humidity%", 99)
                missingChild = callChildParseByTypeId(r.key, [[name: "humidity", value: r.value.Humidity, unit: "%"]], missingChild)
            }
            if (r.value.containsKey("Temperature")) {
    
                logging("Temperature: $r.value.Temperature", 99)
                String c = String.valueOf((char)(Integer.parseInt("00B0", 16)));
                missingChild = callChildParseByTypeId(r.key, [[name: "temperature", value: r.value.Temperature, unit: "$c${location.temperatureScale}"]], missingChild)
            } else if (r.value.containsKey("ProbeTemperature")) {
    
                logging("ProbeTemperature: $r.value.ProbeTemperature", 99)
                String c = String.valueOf((char)(Integer.parseInt("00B0", 16)));
                missingChild = callChildParseByTypeId(r.key, [[name: "temperature", value: r.value.ProbeTemperature, unit: "$c${location.temperatureScale}"]], missingChild)
            }
            if (r.value.containsKey("DewPoint")) {
    
                logging("DewPoint: $r.value.DewPoint", 99)
                String c = String.valueOf((char)(Integer.parseInt("00B0", 16)));
                missingChild = callChildParseByTypeId(r.key, [[name: "dewPoint", value: r.value.DewPoint, unit: "$c${location.temperatureScale}"]], missingChild)
            }
            if (r.value.containsKey("Pressure")) {
                logging("Pressure: $r.value.Pressure", 99)
                String pressureUnit = "mbar"
                missingChild = callChildParseByTypeId(r.key, [[name: "pressure", value: r.value.Pressure, unit: pressureUnit]], missingChild)
    
            }
            if (r.value.containsKey("Gas")) {
                logging("Pressure: $r.value.Gas", 99)
                String gasUnit = "ohm"
                missingChild = callChildParseByTypeId(r.key, [[name: "gas", value: r.value.Gas, unit: gasUnit]], missingChild)
            }
            if (r.value.containsKey("Distance")) {
                logging("Distance: $r.value.Distance cm", 99)
                def realDistance = Math.round((r.value.Distance as Double) * 100) / 100
    
                missingChild = callChildParseByTypeId(r.key, [[name: "distance", value: String.format("%.2f cm", realDistance), unit: "cm"]], missingChild)
            }
            if (r.value.containsKey("Illuminance")) {
                logging("Illuminance: $r.value.Illuminance lux", 99)
                def realIlluminance = Math.round(r.value.Illuminance as Double)
    
                missingChild = callChildParseByTypeId(r.key, [[name: "illuminance", value: realIlluminance, unit: "lux"]], missingChild)
            }
        }
    }
    // END:  getTasmotaNewParserForSensors()
    // BEGIN:getTasmotaNewParserForWifi()
    if (result.containsKey("Wifi")) {
        if (log99 == true && result.Wifi.containsKey("AP")) {
            logging("AP: $result.Wifi.AP",99)
        }
        if (log99 == true && result.Wifi.containsKey("BSSId")) {
            logging("BSSId: $result.Wifi.BSSId",99)
        }
        if (log99 == true && result.Wifi.containsKey("Channel")) {
            logging("Channel: $result.Wifi.Channel",99)
        }
        if (result.Wifi.containsKey("RSSI")) {
            logging("RSSI: $result.Wifi.RSSI",99)
    
            String quality = "${result.Wifi.RSSI}%"
            if(device.currentValue('wifiSignal') != quality) sendEvent(name: "wifiSignal", value: quality, isStateChange: false)
        }
        if (log99 == true && result.Wifi.containsKey("SSId")) {
            logging("SSId: $result.Wifi.SSId",99)
        }
    }
    // END:  getTasmotaNewParserForWifi()
    // BEGIN:getTasmotaNewParserForShutter()
    if (result.containsKey("Shutter1")) {
        logging("parser: Shutter1: $result.Shutter1", 1)
        missingChild = callChildParseByTypeId("SHUTTER", [[name:"shutter", value:result.Shutter1.clone()]], missingChild)
    }
    // END:  getTasmotaNewParserForShutter()
    tasmota_updatePresence("present")
    return missingChild
}

void updateNeededSettings() {
    // BEGIN:getUpdateNeededSettingsTasmotaHeader()
    Map currentProperties = state.currentProperties ?: [:]
    
    state.settings = settings
    
    String isUpdateNeeded = "NO"
    
    if(runReset != null && runReset == 'RESET') {
        for ( e in state.settings ) {
            logging("Deleting '${e.key}' with value = ${e.value} from Settings", 50)
    
            device.clearSetting("${e.key}")
            device.removeSetting("${e.key}")
            state?.settings?.remove("${e.key}")
        }
    }
    
    tasmota_prepareDNI()
    // END:  getUpdateNeededSettingsTasmotaHeader()

    if(deviceConfig == null) deviceConfig = "01generic-device"
    Map deviceConfigMap = getDeviceConfiguration(deviceConfig)
    
    String originalDeviceTemplateInput = deviceTemplateInput
    String deviceTemplateInput = deviceConfigMap?.template
    if(deviceTemplateInput == null) deviceTemplateInput = originalDeviceTemplateInput
    if(deviceTemplateInput == "") deviceTemplateInput = null

    String originalModuleNumber = moduleNumber
    String moduleNumber = deviceConfigMap?.module
    if(moduleNumber == null) moduleNumber = originalModuleNumber
    if(moduleNumber == "") moduleNumber = null

    if(deviceTemplateInput != null && moduleNumber == null) moduleNumber = 0

    logging("updateNeededSettings: deviceConfigMap=$deviceConfigMap, deviceTemplateInput=$deviceTemplateInput, moduleNumber=$moduleNumber", 1)
    
    // BEGIN:getUpdateNeededSettingsTasmotaDynamicModuleCommand()
    tasmota_getAction(tasmota_getCommandString("Module", null))
    tasmota_getAction(tasmota_getCommandString("Template", null))
    boolean disableModuleSelectionSetting = disableModuleSelection
    if(disableModuleSelectionSetting == null) disableModuleSelectionSetting = false
    
    Integer moduleNumberUsed = null
    if(moduleNumber == null || moduleNumber == '-1') {
        moduleNumberUsed = -1
    } else {
        moduleNumberUsed = moduleNumber.toInteger()
    }
    boolean useDefaultTemplate = false
    String defaultDeviceTemplate = ''
    if(deviceTemplateInput != null && deviceTemplateInput == "0") {
        useDefaultTemplate = true
        defaultDeviceTemplate = ''
    }
    if(deviceTemplateInput == null || deviceTemplateInput == "") {
    
        useDefaultTemplate = true
        defaultDeviceTemplate = ''
    }
    if(deviceTemplateInput != null) deviceTemplateInput = deviceTemplateInput.replaceAll(' ','')
    logging("disableModuleSelectionSetting=$disableModuleSelectionSetting, deviceTemplateInput=$deviceTemplateInput, moduleNumberUsed=$moduleNumberUsed, moduleNumber=$moduleNumber", 1)
    if(disableModuleSelectionSetting == false && ((deviceTemplateInput != null && deviceTemplateInput != "") ||
                                           (useDefaultTemplate && defaultDeviceTemplate != ""))) {
        def usedDeviceTemplate = defaultDeviceTemplate
        if(useDefaultTemplate == false && deviceTemplateInput != null && deviceTemplateInput != "") {
            usedDeviceTemplate = deviceTemplateInput
        }
        logging("Setting the Template (${usedDeviceTemplate}) soon...", 100)
        logging("templateData = ${device.currentValue('templateData')}", 10)
        if(usedDeviceTemplate != '') moduleNumberUsed = 0
    
        if(usedDeviceTemplate != null && device.currentValue('templateData') != usedDeviceTemplate) {
            logging("The template is currently NOT set to '${usedDeviceTemplate}', it is set to '${device.currentValue('templateData')}'", 100)
    
            tasmota_getAction(tasmota_getCommandString("Template", usedDeviceTemplate))
        } else if (device.currentValue('module') == null){
    
            tasmota_getAction(tasmota_getCommandString("Template", null))
        }else if (usedDeviceTemplate != null) {
            logging("The template is set to '${usedDeviceTemplate}' already!", 100)
        }
    } else {
        logging("Can't set the Template...", 10)
        logging(device.currentValue('templateData'), 10)
    
    }
    if(disableModuleSelectionSetting == false && moduleNumberUsed != null && moduleNumberUsed >= 0) {
        logging("Setting the Module (${moduleNumberUsed}) soon...", 100)
        logging("device.currentValue('module'): '${device.currentValue('module')}'", 10)
    
        if(moduleNumberUsed != null && (device.currentValue('module') == null || !(device.currentValue('module').startsWith("[${moduleNumberUsed}:") || device.currentValue('module') == '0'))) {
            logging("Currently not using module ${moduleNumberUsed}, using ${device.currentValue('module')}", 100)
            tasmota_getAction(tasmota_getCommandString("Module", "${moduleNumberUsed}"))
        } else if (moduleNumberUsed != null && device.currentValue('module') != null){
            logging("This starts with [${moduleNumberUsed} ${device.currentValue('module')}",10)
        } else if (device.currentValue('module') == null){
    
            tasmota_getAction(tasmota_getCommandString("Module", null))
        } else {
            logging("Module is set to '${device.currentValue('module')}', and it's set to be null, report this to the creator of this driver!",10)
        }
    } else {
        logging("Setting the Module has been disabled!", 10)
    }
    // END:  getUpdateNeededSettingsTasmotaDynamicModuleCommand()
    //logging("After getUpdateNeededSettingsTasmotaDynamicModuleCommand", 0)

    installCommands = deviceConfigMap?.installCommands
    if(installCommands == null || installCommands == '') installCommands = []
    //logging("Got to just before tasmota_runInstallCommands", 0)
    tasmota_runInstallCommands(installCommands)

    // BEGIN:getUpdateNeededSettingsTasmotaFooter()
    tasmota_getAction(tasmota_getCommandString("TelePeriod", "${tasmota_getTelePeriodValue()}"))
    
    tasmota_getAction(tasmota_getCommandString("SetOption113", "1"))
    
    tasmota_getAction(tasmota_getCommandString("Emulation", "2"))
    tasmota_getAction(tasmota_getCommandString("HttpHookHost", device.hub.getDataValue("localIP")))
    tasmota_getAction(tasmota_getCommandString("HubitatHost", device.hub.getDataValue("localIP")))
    logging("HubitatPort: ${device.hub.getDataValue("localSrvPortTCP")}", 1)
    tasmota_getAction(tasmota_getCommandString("HttpHookPort", device.hub.getDataValue("localSrvPortTCP")))
    tasmota_getAction(tasmota_getCommandString("HubitatPort", device.hub.getDataValue("localSrvPortTCP")))
    tasmota_getAction(tasmota_getCommandString("FriendlyName1", device.displayName.take(32)))
    
    tasmota_getAction(tasmota_getCommandString("SetOption34", "20"))
    
    int tzoffset = getLocation().timeZone.getOffset(now()) / 3600000
    String tzoffsetWithSign = tzoffset < 0 ? "${tzoffset}" : "+${tzoffset}"
    logging("Setting timezone to $tzoffsetWithSign", 10)
    tasmota_getAction(tasmota_getCommandString("Timezone", tzoffsetWithSign))
    
    logging("Scheduling tasmota_refreshChildren...", 1)
    runIn(30, "tasmota_refreshChildren")
    runIn(60, "tasmota_refreshChildrenAgain")
    logging("Done scheduling tasmota_refreshChildren...", 1)
    
    if(override == true) {
        tasmota_sync(ipAddress)
    }
    
    sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: false)
    // END:  getUpdateNeededSettingsTasmotaFooter()
}

/** Calls TO Child devices */
boolean callChildParseByTypeId(String deviceTypeId, List<Map> event, boolean missingChild) {
    event.each{
        if(it.containsKey("descriptionText") == false) {
            it["descriptionText"] = "'$it.name' set to '$it.value'"
        }
        it["isStateChange"] = false
    }
    com.hubitat.app.ChildDeviceWrapper cd = getChildDevice("$device.id-$deviceTypeId")
    if(cd == null && (deviceTypeId == "POWER1" || deviceTypeId == "POWER2")) {
        cd = getChildDevice("$device.id-SHUTTER")
    }
    if(cd != null) {
        cd.parse(event)
    } else {
        log.warn("callChildParseByTypeId() can't FIND the device type ${deviceTypeId}! (childId: ${"$device.id-$deviceTypeId"}) Did you delete something?")
        missingChild = true
    }
    return missingChild
}

void clearInstalling() {
    state.installing = "0"
}

void childParse(com.hubitat.app.DeviceWrapper cd, event) {
    try {
        getChildDevice(cd.deviceNetworkId).parse(event)
    } catch(e) {
        log.warn("childParse() can't send parse event to device ${cd?.displayName}! Error=$e")
    }
}

String getDeviceActionType(String childDeviceNetworkId) {
    return childDeviceNetworkId.tokenize("-")[1]
}

/** Calls FROM Child devices */
void componentRefresh(com.hubitat.app.DeviceWrapper cd) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentRefresh(cd=${cd.displayName} (${cd.deviceNetworkId})) actionType=$actionType", 1)
    refresh()
}

void componentOn(String deviceNetworkId) {
    String actionType = getDeviceActionType(deviceNetworkId)
    if(invertPowerNumber == true) {
        if(actionType == "POWER1") { 
            actionType = "POWER2"
        } else if(actionType == "POWER2"){
            actionType = "POWER1"
        }
    }
    logging("componentOn(deviceNetworkId=${deviceNetworkId}), actionType=$actionType", 1)
    tasmota_getAction(tasmota_getCommandString("$actionType", "1"))
}

void componentOn(com.hubitat.app.DeviceWrapper cd) {
    componentOn(cd.deviceNetworkId)
}

void componentOff(String deviceNetworkId) {
    String actionType = getDeviceActionType(deviceNetworkId)
    if(invertPowerNumber == true) {
        if(actionType == "POWER1") { 
            actionType = "POWER2"
        } else if(actionType == "POWER2"){
            actionType = "POWER1"
        }
    }
    logging("componentOff(deviceNetworkId=${deviceNetworkId}), actionType=$actionType", 1)
    tasmota_getAction(tasmota_getCommandString("$actionType", "0"))
}

void componentOff(com.hubitat.app.DeviceWrapper cd) {
    componentOff(cd.deviceNetworkId)
}

void componentSetLevel(com.hubitat.app.DeviceWrapper cd, BigDecimal level) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentSetLevel(cd=${cd.displayName} (${cd.deviceNetworkId}), level=${level}) actionType=$actionType", 1)
    tasmota_rgbw_setLevel(level)
}

void componentSetLevel(com.hubitat.app.DeviceWrapper cd, BigDecimal level, BigDecimal duration) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentSetLevel(cd=${cd.displayName} (${cd.deviceNetworkId}), level=${level}, duration=${duration}) actionType=$actionType", 1)
    tasmota_rgbw_setLevel(level, duration)
}

void componentStartLevelChange(com.hubitat.app.DeviceWrapper cd, String direction) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentStartLevelChange(cd=${cd.displayName} (${cd.deviceNetworkId}), direction=${direction}) actionType=$actionType", 1)
    tasmota_rgbw_startLevelChange(direction)
}

void componentStopLevelChange(com.hubitat.app.DeviceWrapper cd) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentStopLevelChange(cd=${cd.displayName} (${cd.deviceNetworkId})) actionType=$actionType", 1)
    tasmota_rgbw_stopLevelChange()
}

void componentSetColor(com.hubitat.app.DeviceWrapper cd, Map colormap) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentSetColor(cd=${cd.displayName} (${cd.deviceNetworkId}), colormap=${colormap}) actionType=$actionType", 1)
    rgbw_setColor(colormap)
}

void componentSetHue(com.hubitat.app.DeviceWrapper cd, BigDecimal hue) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentSetHue(cd=${cd.displayName} (${cd.deviceNetworkId}), hue=${hue}) actionType=$actionType", 1)
    rgbw_setHue(hue)
}

void componentWhite(com.hubitat.app.DeviceWrapper cd) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentWhite(cd=${cd.displayName} (${cd.deviceNetworkId})) actionType=$actionType", 1)
    rgbw_white()
}

void componentSetRGB(com.hubitat.app.DeviceWrapper cd, r, g, b) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentSetRGB(cd=${cd.displayName} (${cd.deviceNetworkId}), r=${r}, g=${g}, b=${b}) actionType=$actionType", 1)
    tasmota_rgbw_setRGB(r, g, b)
}

void componentSetSaturation(com.hubitat.app.DeviceWrapper cd, BigDecimal saturation) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentSetSaturation(cd=${cd.displayName} (${cd.deviceNetworkId}), saturation=${saturation}) actionType=$actionType", 1)
    rgbw_setSaturation(saturation)
}

void componentSetColorTemperature(com.hubitat.app.DeviceWrapper cd, BigDecimal colortemperature) {
    String actionType = getDeviceActionType(cd.deviceNetworkId)
    logging("componentSetColorTemperature(cd=${cd.displayName} (${cd.deviceNetworkId}), colortemperature=${colortemperature}) actionType=$actionType", 1)
    tasmota_rgbw_setColorTemperature(colortemperature)
}

void componentSetEffect(com.hubitat.app.DeviceWrapper cd, BigDecimal effectnumber, BigDecimal speed) {
    tasmota_rgbw_modeSet((Integer) effectnumber, speed)
}

void componentModeWakeUp(com.hubitat.app.DeviceWrapper cd, BigDecimal wakeUpDuration, BigDecimal level) {
    tasmota_rgbw_modeWakeUp(wakeUpDuration, level)
}

void componentSetSpeed(com.hubitat.app.DeviceWrapper cd, String fanspeed) {
    String fanCommand = "Dimmer"
    String cModule = device.currentValue('module')
    if(cModule != null && (cModule.startsWith('[44') == true || cModule.startsWith('[71') == true)) {
        fanCommand = "FanSpeed"
    }
    switch(fanspeed) {
        case "off":
            tasmota_getAction(tasmota_getCommandString(fanCommand, "0"))
            break
        case "on":
        case "low":
            tasmota_getAction(tasmota_getCommandString(fanCommand, "1"))
            break
        case "medium-low":
        case "medium":  
            tasmota_getAction(tasmota_getCommandString(fanCommand, "2"))
            break
        case "medium-high":
        case "high":
            tasmota_getAction(tasmota_getCommandString(fanCommand, "3"))
            break
    }  
}

void componentOpen(com.hubitat.app.DeviceWrapper cd) {
    tasmota_getAction(tasmota_getCommandString("ShutterOpen1", null))
}

void componentClose(com.hubitat.app.DeviceWrapper cd) {
    tasmota_getAction(tasmota_getCommandString("ShutterClose1", null))
}

void componentStop(com.hubitat.app.DeviceWrapper cd) {
    tasmota_getAction(tasmota_getCommandString("ShutterStop1", null))
}

void componentSetPosition(com.hubitat.app.DeviceWrapper cd, BigDecimal position) {
    position = position.setScale(0, BigDecimal.ROUND_HALF_UP)
    tasmota_getAction(tasmota_getCommandString("ShutterPosition", position.toString()))
}

void componentSetColorByRGBString(com.hubitat.app.DeviceWrapper cd, String colorRGB) {
    tasmota_rgbw_setColorByRGBString(colorRGB)
}

void componentSetPixelColor(com.hubitat.app.DeviceWrapper cd, String colorRGB, BigDecimal pixel) {
    tasmota_rgbw_setPixelColor(colorRGB, pixel)
}

void componentSetAddressablePixels(com.hubitat.app.DeviceWrapper cd, BigDecimal pixels) {
    tasmota_rgbw_setAddressablePixels(pixels)
}

void componentSetAddressableRotation(com.hubitat.app.DeviceWrapper cd, BigDecimal pixels) {
    tasmota_rgbw_setAddressableRotation(pixels)
}

void componentSetEffectWidth(com.hubitat.app.DeviceWrapper cd, BigDecimal pixels) {
    tasmota_rgbw_setEffectWidth(pixels)
}

/**
 * -----------------------------------------------------------------------------
 * Everything below here are LIBRARY includes and should NOT be edited manually!
 * -----------------------------------------------------------------------------
 * --- Nothings to edit here, move along! --------------------------------------
 * -----------------------------------------------------------------------------
 */

// BEGIN:getDefaultFunctions()
private String getDriverVersion() {
    comment = ""
    if(comment != "") state.comment = comment
    String version = "v1.1.1.1123T"
    logging("getDriverVersion() = ${version}", 100)
    sendEvent(name: "driver", value: version)
    updateDataValue('driver', version)
    return version
}
// END:  getDefaultFunctions()

// BEGIN:getGetChildDriverNameMethod()
String getChildDriverName() {
    String deviceDriverName = getDeviceInfoByName('name')
    if(deviceDriverName.toLowerCase().endsWith(' (parent)')) {
        deviceDriverName = deviceDriverName.substring(0, deviceDriverName.length()-9)
    }
    String childDriverName = "${deviceDriverName} (Child)"
    logging("childDriverName = '$childDriverName'", 1)
    return(childDriverName)
}
// END:  getGetChildDriverNameMethod()

// BEGIN:getLoggingFunction(specialDebugLevel=True)
private boolean logging(message, level) {
    boolean didLogging = false
     
    Integer logLevelLocal = 0
    if (infoLogging == null || infoLogging == true) {
        logLevelLocal = 100
    }
    if (debugLogging == true) {
        logLevelLocal = 1
    }
     
    if (logLevelLocal != 0){
        switch (logLevelLocal) {
        case 1:  
            if (level >= 1 && level < 99) {
                log.debug "$message"
                didLogging = true
            } else if (level == 100) {
                log.info "$message"
                didLogging = true
            }
        break
        case 100:  
            if (level == 100 ) {
                log.info "$message"
                didLogging = true
            }
        break
        }
    }
    return didLogging
}
// END:  getLoggingFunction(specialDebugLevel=True)

// BEGIN:getHelperFunctions('all-default')
boolean isDriver() {
    try {
        getDeviceDataByName('_unimportant')
        logging("This IS a driver!", 1)
        return true
    } catch (MissingMethodException e) {
        logging("This is NOT a driver!", 1)
        return false
    }
}

void deviceCommand(String cmd) {
    def jsonSlurper = new JsonSlurper()
    def cmds = jsonSlurper.parseText(cmd)
     
    r = this."${cmds['cmd']}"(*cmds['args'])
     
    updateDataValue('appReturn', JsonOutput.toJson(r))
}

void setLogsOffTask(boolean noLogWarning=false) {
	if (debugLogging == true) {
        if(noLogWarning==false) {
            if(runReset != "DEBUG") {
                log.warn "Debug logging will be disabled in 30 minutes..."
            } else {
                log.warn "Debug logging will NOT BE AUTOMATICALLY DISABLED!"
            }
        }
        runIn(1800, "logsOff")
    }
}

void toggle() {
    if(device.currentValue('switch') == 'on') {
        off()
    } else {
        on()
    }
}

void logsOff() {
    if(runReset != "DEBUG") {
        log.warn "Debug logging disabled... "
        if(isDriver()) {
            device.clearSetting("logLevel")
            device.removeSetting("logLevel")
            device.updateSetting("logLevel", "0")
            state?.settings?.remove("logLevel")
            device.clearSetting("debugLogging")
            device.removeSetting("debugLogging")
            device.updateSetting("debugLogging", "false")
            state?.settings?.remove("debugLogging")
            
        } else {
            app.removeSetting("logLevel")
            app.updateSetting("logLevel", "0")
            app.removeSetting("debugLogging")
            app.updateSetting("debugLogging", "false")
        }
    } else {
        log.warn "OVERRIDE: Disabling Debug logging will not execute with 'DEBUG' set..."
        if (logLevel != "0" && logLevel != "100") runIn(1800, "logsOff")
    }
}

boolean isDeveloperHub() {
    return generateMD5(location.hub.zigbeeId as String) == "125fceabd0413141e34bb859cd15e067_disabled"
}

def getEnvironmentObject() {
    if(isDriver()) {
        return device
    } else {
        return app
    }
}

private def getFilteredDeviceDriverName() {
    def deviceDriverName = getDeviceInfoByName('name')
    if(deviceDriverName.toLowerCase().endsWith(' (parent)')) {
        deviceDriverName = deviceDriverName.substring(0, deviceDriverName.length()-9)
    }
    return deviceDriverName
}

private def getFilteredDeviceDisplayName() {
    def deviceDisplayName = device.displayName.replace(' (parent)', '').replace(' (Parent)', '')
    return deviceDisplayName
}

BigDecimal round2(BigDecimal number, Integer scale) {
    Integer pow = 10;
    for (Integer i = 1; i < scale; i++)
        pow *= 10;
    BigDecimal tmp = number * pow;
    return ( (Float) ( (Integer) ((tmp - (Integer) tmp) >= 0.5f ? tmp + 1 : tmp) ) ) / pow;
}

String generateMD5(String s) {
    if(s != null) {
        return MessageDigest.getInstance("MD5").digest(s.bytes).encodeHex().toString()
    } else {
        return "null"
    }
}

Integer extractInt(String input) {
  return input.replaceAll("[^0-9]", "").toInteger()
}

String hexToASCII(String hexValue) {
    StringBuilder output = new StringBuilder("")
    for (int i = 0; i < hexValue.length(); i += 2) {
        String str = hexValue.substring(i, i + 2)
        output.append((char) Integer.parseInt(str, 16) + 30)
        logging("${Integer.parseInt(str, 16)}", 10)
    }
     
    return output.toString()
}
// END:  getHelperFunctions('all-default')

// BEGIN:getHelperFunctions('driver-metadata')
private Map getMetaConfig() {
    def metaConfig = getDataValue('metaConfig')
    if(metaConfig == null) {
        metaConfig = [:]
    } else {
        metaConfig = parseJson(metaConfig)
    }
    return metaConfig
}

boolean isCSSDisabled(Map metaConfig=null) {
    if(metaConfig==null) metaConfig = getMetaConfig()
    boolean disableCSS = false
    if(metaConfig.containsKey("disableCSS")) disableCSS = metaConfig["disableCSS"]
    return disableCSS
}

private void saveMetaConfig(Map metaConfig) {
    updateDataValue('metaConfig', JsonOutput.toJson(metaConfig))
}

private Map setSomethingToHide(String type, List something, Map metaConfig=null) {
    if(metaConfig==null) metaConfig = getMetaConfig()
    def oldData = []
    something = something.unique()
    if(!metaConfig.containsKey("hide")) {
        metaConfig["hide"] = [type:something]
    } else {
        if(metaConfig["hide"].containsKey(type)) {
            metaConfig["hide"][type].addAll(something)
        } else {
            metaConfig["hide"][type] = something
        }
    }
    saveMetaConfig(metaConfig)
    logging("setSomethingToHide() = ${metaConfig}", 1)
    return metaConfig
}

private Map clearTypeToHide(String type, Map metaConfig=null) {
    if(metaConfig==null) metaConfig = getMetaConfig()
    if(!metaConfig.containsKey("hide")) {
        metaConfig["hide"] = [(type):[]]
    } else {
        metaConfig["hide"][(type)] = []
    }
    saveMetaConfig(metaConfig)
    logging("clearTypeToHide() = ${metaConfig}", 1)
    return metaConfig
}

Map clearThingsToHide(Map metaConfig=null) {
    metaConfig = setSomethingToHide("other", [], metaConfig=metaConfig)
    metaConfig["hide"] = [:]
    saveMetaConfig(metaConfig)
    logging("clearThingsToHide() = ${metaConfig}", 1)
    return metaConfig
}

Map setDisableCSS(boolean value, Map metaConfig=null) {
    if(metaConfig==null) metaConfig = getMetaConfig()
    metaConfig["disableCSS"] = value
    saveMetaConfig(metaConfig)
    logging("setDisableCSS(value = $value) = ${metaConfig}", 1)
    return metaConfig
}

Map setStateCommentInCSS(String stateComment, Map metaConfig=null) {
    if(metaConfig==null) metaConfig = getMetaConfig()
    metaConfig["stateComment"] = stateComment
    saveMetaConfig(metaConfig)
    logging("setStateCommentInCSS(stateComment = $stateComment) = ${metaConfig}", 1)
    return metaConfig
}

Map setCommandsToHide(List commands, Map metaConfig=null) {
    metaConfig = setSomethingToHide("command", commands, metaConfig=metaConfig)
    logging("setCommandsToHide(${commands})", 1)
    return metaConfig
}

Map clearCommandsToHide(Map metaConfig=null) {
    metaConfig = clearTypeToHide("command", metaConfig=metaConfig)
    logging("clearCommandsToHide(metaConfig=${metaConfig})", 1)
    return metaConfig
}

Map setStateVariablesToHide(List stateVariables, Map metaConfig=null) {
    metaConfig = setSomethingToHide("stateVariable", stateVariables, metaConfig=metaConfig)
    logging("setStateVariablesToHide(${stateVariables})", 1)
    return metaConfig
}

Map clearStateVariablesToHide(Map metaConfig=null) {
    metaConfig = clearTypeToHide("stateVariable", metaConfig=metaConfig)
    logging("clearStateVariablesToHide(metaConfig=${metaConfig})", 1)
    return metaConfig
}

Map setCurrentStatesToHide(List currentStates, Map metaConfig=null) {
    metaConfig = setSomethingToHide("currentState", currentStates, metaConfig=metaConfig)
    logging("setCurrentStatesToHide(${currentStates})", 1)
    return metaConfig
}

Map clearCurrentStatesToHide(Map metaConfig=null) {
    metaConfig = clearTypeToHide("currentState", metaConfig=metaConfig)
    logging("clearCurrentStatesToHide(metaConfig=${metaConfig})", 1)
    return metaConfig
}

Map setDatasToHide(List datas, Map metaConfig=null) {
    metaConfig = setSomethingToHide("data", datas, metaConfig=metaConfig)
    logging("setDatasToHide(${datas})", 1)
    return metaConfig
}

Map clearDatasToHide(Map metaConfig=null) {
    metaConfig = clearTypeToHide("data", metaConfig=metaConfig)
    logging("clearDatasToHide(metaConfig=${metaConfig})", 1)
    return metaConfig
}

Map setPreferencesToHide(List preferences, Map metaConfig=null) {
    metaConfig = setSomethingToHide("preference", preferences, metaConfig=metaConfig)
    logging("setPreferencesToHide(${preferences})", 1)
    return metaConfig
}

Map clearPreferencesToHide(Map metaConfig=null) {
    metaConfig = clearTypeToHide("preference", metaConfig=metaConfig)
    logging("clearPreferencesToHide(metaConfig=${metaConfig})", 1)
    return metaConfig
}

def metaDataExporter() {
    List filteredPrefs = getPreferences()['sections']['input'].name[0]
    if(filteredPrefs != []) updateDataValue('preferences', "${filteredPrefs}".replaceAll("\\s",""))
}

String getDriverCSSWrapper() {
    Map metaConfig = getMetaConfig()
    boolean disableCSS = isCSSDisabled(metaConfig=metaConfig)
    String defaultCSS = '''
    /* This is part of the CSS for replacing a Command Title */
    div.mdl-card__title div.mdl-grid div.mdl-grid .mdl-cell p::after {
        visibility: visible;
        position: absolute;
        left: 50%;
        transform: translate(-50%, 0%);
        width: calc(100% - 20px);
        padding-left: 5px;
        padding-right: 5px;
        margin-top: 0px;
    }
    /* This is general CSS Styling for the Driver page */
    h3, h4, .property-label {
        font-weight: bold;
    }
    .preference-title {
        font-weight: bold;
    }
    .preference-description {
        font-style: italic;
    }
    '''
    String r = "<style>"
    
    if(disableCSS == false) {
        r += "$defaultCSS "
        try{
            r += " ${getCSSForHidingLastPreference()} "
            
            if(disableCSS == false) {
                if(metaConfig.containsKey("hide")) {
                    if(metaConfig["hide"].containsKey("command")) {
                        r += getCSSForCommandsToHide(metaConfig["hide"]["command"])
                    }
                    if(metaConfig["hide"].containsKey("stateVariable")) {
                        r += getCSSForStateVariablesToHide(metaConfig["hide"]["stateVariable"])
                    }
                    if(metaConfig["hide"].containsKey("currentState")) {
                        r += getCSSForCurrentStatesToHide(metaConfig["hide"]["currentState"])
                    }
                    if(metaConfig["hide"].containsKey("data")) {
                        r += getCSSForDatasToHide(metaConfig["hide"]["data"])
                    }
                    if(metaConfig["hide"].containsKey("preference")) {
                        r += getCSSForPreferencesToHide(metaConfig["hide"]["preference"])
                    }
                }
                if(metaConfig.containsKey("stateComment")) {
                    r += "div#stateComment:after { content: \"${metaConfig["stateComment"]}\" }"
                }
                r += " ${getDriverCSS()} "
            }
        }catch(MissingMethodException e) {
            if(!e.toString().contains("getDriverCSS()")) {
                log.warn "getDriverCSS() Error: $e"
            }
        } catch(e) {
            log.warn "getDriverCSS() Error: $e"
        }
    }
    r += " </style>"
    return r
}

Integer getCommandIndex(String cmd) {
    List commands = device.getSupportedCommands().unique()
    Integer i = commands.findIndexOf{ "$it" == cmd}+1
    return i
}

String getCSSForCommandHiding(String cmdToHide) {
    Integer i = getCommandIndex(cmdToHide)
    String r = ""
    if(i > 0) {
        r = "div.mdl-card__title div.mdl-grid div.mdl-grid .mdl-cell:nth-of-type($i){display: none;}"
    }
    return r
}

String getCSSForCommandsToHide(List commands) {
    String r = ""
    commands.each {
        r += getCSSForCommandHiding(it)
    }
    return r
}

String getCSSToChangeCommandTitle(String cmd, String newTitle) {
    Integer i = getCommandIndex(cmd)
    String r = ""
    if(i > 0) {
        r += "div.mdl-card__title div.mdl-grid div.mdl-grid .mdl-cell:nth-of-type($i) p {visibility: hidden;}"
        r += "div.mdl-card__title div.mdl-grid div.mdl-grid .mdl-cell:nth-of-type($i) p::after {content: '$newTitle';}"
    }
    return r
}

Integer getStateVariableIndex(String stateVariable) {
    def stateVariables = state.keySet()
    Integer i = stateVariables.findIndexOf{ "$it" == stateVariable}+1
    return i
}

String getCSSForStateVariableHiding(String stateVariableToHide) {
    Integer i = getStateVariableIndex(stateVariableToHide)
    String r = ""
    if(i > 0) {
        r = "ul#statev li.property-value:nth-of-type($i){display: none;}"
    }
    return r
}

String getCSSForStateVariablesToHide(List stateVariables) {
    String r = ""
    stateVariables.each {
        r += getCSSForStateVariableHiding(it)
    }
    return r
}

String getCSSForCurrentStatesToHide(List currentStates) {
    String r = ""
    currentStates.each {
        r += "ul#cstate li#cstate-$it {display: none;}"
    }
    return r
}

Integer getDataIndex(String data) {
    def datas = device.getData().keySet()
    Integer i = datas.findIndexOf{ "$it" == data}+1
    return i
}

String getCSSForDataHiding(String dataToHide) {
    Integer i = getDataIndex(dataToHide)
    String r = ""
    if(i > 0) {
        r = "table.property-list tr li.property-value:nth-of-type($i) {display: none;}"
    }
    return r
}

String  getCSSForDatasToHide(List datas) {
    String r = ""
    datas.each {
        r += getCSSForDataHiding(it)
    }
    return r
}

Integer getPreferenceIndex(String preference, boolean returnMax=false) {
    def filteredPrefs = getPreferences()['sections']['input'].name[0]
    if(filteredPrefs == [] || filteredPrefs == null) {
        d = getDataValue('preferences')
        if(d != null && d.length() > 2) {
            try{
                filteredPrefs = d[1..d.length()-2].tokenize(',')
            } catch(e) {
            }
        }
        
    }
    Integer i = 0
    if(returnMax == true) {
        i = filteredPrefs.size()
    } else {
        i = filteredPrefs.findIndexOf{ "$it" == preference}+1
    }
    return i
}

String getCSSForPreferenceHiding(String preferenceToHide, Integer overrideIndex=0) {
    Integer i = 0
    if(overrideIndex == 0) {
        i = getPreferenceIndex(preferenceToHide)
    } else {
        i = overrideIndex
    }
    String r = ""
    if(i > 0) {
        r = "form[action*=\"preference\"] div.mdl-grid div.mdl-cell:nth-of-type($i) {display: none;} "
    }else if(i == -1) {
        r = "form[action*=\"preference\"] div.mdl-grid div.mdl-cell:nth-last-child(2) {display: none;} "
    }
    return r
}

String getCSSForPreferencesToHide(List preferences) {
    String r = ""
    preferences.each {
        r += getCSSForPreferenceHiding(it)
    }
    return r
}

String getCSSForHidingLastPreference() {
    return getCSSForPreferenceHiding(null, overrideIndex=-1)
}
// END:  getHelperFunctions('driver-metadata')

// BEGIN:getHelperFunctions('styling')
String styling_addTitleDiv(title) {
    return '<div class="preference-title">' + title + '</div>'
}

String styling_addDescriptionDiv(description) {
    return '<div class="preference-description">' + description + '</div>'
}

String styling_makeTextBold(s) {
    if(isDriver()) {
        return "<b>$s</b>"
    } else {
        return "$s"
    }
}

String styling_makeTextItalic(s) {
    if(isDriver()) {
        return "<i>$s</i>"
    } else {
        return "$s"
    }
}

String styling_getDefaultCSS(boolean includeTags=true) {
    String defaultCSS = '''
    /* This is part of the CSS for replacing a Command Title */
    div.mdl-card__title div.mdl-grid div.mdl-grid .mdl-cell p::after {
        visibility: visible;
        position: absolute;
        left: 50%;
        transform: translate(-50%, 0%);
        width: calc(100% - 20px);
        padding-left: 5px;
        padding-right: 5px;
        margin-top: 0px;
    }
    /* This is general CSS Styling for the Driver page */
    h3, h4, .property-label {
        font-weight: bold;
    }
    .preference-title {
        font-weight: bold;
    }
    .preference-description {
        font-style: italic;
    }
    
    '''
    if(includeTags == true) {
        return "<style>$defaultCSS </style>"
    } else {
        return defaultCSS
    }
}

String styling_getLogo() {
    String logoCSS = '''
    #ohla_logo {
      display: block;
      width: 200px;
      height: 50px;
      position: absolute;
      top: 10px;
      right: 10px;
    }

    @media screen and (max-device-width:450px), screen and (max-width:450px) {
      #ohla_logo {
        width: 120px;
        top: 55px;
      }
    }
    
    '''
    return "<div id='ohla_logo'><a href='https://oh-lalabs.com/l/" + getDeviceInfoByName('filename') + "' target='_blank'><svg version='1.0' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 189 41' style='enable-background:new 0 0 189 41;' xml:space='preserve'><style type='text/css'>.st0{fill:#0066FF;}</style><g><g><g><g><path d='M38.5,29.7c-2.8,2.2-4.3,3.6-7.1,5.7c-0.5,0.4-1.8,0.5-2.5,0.6c-1.6,0.1-1.9-0.1-1.5-1c0.6-1.2,1.5-2.7,2.6-4.2c1-1.4,2.8-4,4.8-6.4c1.8-2.3,3.7-4.7,6.2-7.6c2.2-2.4,5-5.2,7.4-7.1c1.3-1.1,2.8-1.6,4-1.6c1.3,0,1.8,0.3,1.8,0.5c-1.8,1.1-7,6.5-10.9,11.1c-3.8,4.6-8.1,10.3-10,13.2l0.1,0.1c1.3-1.1,3.3-2.8,5.6-4.5c2-1.4,5.1-3.6,7.8-3.6c1.7,0,2.6,0.7,2.5,1.9c-0.1,1.1-1.3,2.6-2.3,3.8c-0.9,1.2-1.6,2-2,2.6c-0.4,0.8-0.5,1.2-0.2,1.4c0.3,0.2,1,0,1.6-0.3c0.9-0.4,1.9-1.1,2.7-1.8c0.7-0.5,1.6-1.3,2.3-1.9c0.3-0.3,1.1-1,1.8-1c0.6,0,0.6,0.4,1,0.6c0.4,0.2,0.2,0.3,0,0.5c-1.1-1.2-2.1,0-2.5,0.4c-0.9,0.8-2.2,1.9-3.1,2.6c-1.3,1-3.8,2.6-6,2.6c-2.5,0.1-2.9-1.2-2.7-2.3c0.1-1.2,0.9-2.2,2.3-3.8c0.9-1.1,1.9-2.2,2.6-3.1c0.3-0.3,0.3-0.7-0.1-0.8c-0.4-0.2-1.1,0.1-1.8,0.4C41.8,27.2,40.2,28.3,38.5,29.7z'/><path d='M65.2,31.5c0,0.3-0.1,0.4-0.2,0.4c-0.1,0-0.2-0.1-0.3-0.2c-0.6-0.8-1.3-0.9-2.8-0.9c-1.8,0-2.9,0.6-5.2,0.6c-0.8,0-2-0.3-2.4-0.9c-0.1-0.1-0.1-0.3-0.1-0.4c0.1-0.1,0.2,0,0.4,0.1c0.4,0.1,0.8,0.1,1.1,0.1c1.3-0.1,2.2-0.7,3.3-1.4c0.9-0.6,2-1,3-1C64.5,27.9,65.3,29.6,65.2,31.5z'/><path d='M36.4,12.9c-0.3-2.3-2.3-3.2-4.7-3.5c-6.7-0.6-15.3,2.5-21.3,8.2c-3.3,3.1-7.2,8.8-6,13.9c0.9,4.1,4.9,5.1,7.8,5.2c5.9,0.2,13.6-4.1,17.9-9.3c2.7-3.3,4-7.4,4-9.8c0-1.8-0.6-3.3-1.9-4c-2.4-1.4-6,0.2-6.6,2.9c-0.4,1.9,0.3,3.3,1.8,3.6c1.1,0.2,1.9-0.4,2.2-1.4c0.2-1-1-1.7-0.7-3c0.3-1.4,1.2-2.1,2.3-1.8c0,0,0,0,0,0c0.2,0,0.4,0.1,0.5,0.2c0.6,0.3,1.1,1,0.9,2.1c-0.3,3-2.3,6.7-5.3,10.5c-5.2,6.3-9.9,9.7-14.4,9.6C10.6,36,9.2,34.9,8.8,33c-0.9-4,2.3-9.8,5.9-13.6c3.5-3.7,10.8-9.3,17.1-9.1c1.9,0,3.7,1,4,2.7c0.1,1.1-0.3,2.1-0.8,2.8c-0.4,0.6-0.6,0.6-0.5,0.8c0.1,0.1,0.6-0.1,0.9-0.5C36,15.3,36.5,14.1,36.4,12.9z'/><path d='M102.1,9.6c-2.1,0-4.3,0.4-5.9,0.9c-0.3,0.1-0.9,0.3-1.2,0.4c-2.8,1-7.1,3.5-10.5,8.5c-0.1,0.1-0.3,0.3-0.4,0.5c-0.1,0.2-0.2,0.3-0.4,0.5c-1.3,0.1-2.5,0.3-3.7,0.5c-3.4,0.5-5.8,1.9-5.6,3.8c0.1,1.5,1.1,2.5,2.5,2.4c0.9-0.1,1.5-0.7,1.4-1.5c-0.1-0.9-1.2-1.1-1.3-2.3c-0.1-1,1.2-1.8,3.1-2c1.1-0.1,2.3-0.2,3.4-0.3c-2.4,3.6-4,7.8-6.9,10.7c-0.8-0.1-1.8-0.2-3.4-0.1c-3.8,0.1-9.3,1.5-9.4,3.6c-0.1,0.9,1.2,1.5,3.8,1.4c3.8-0.1,6.6-1.8,8.5-3.2c1,0.2,3.5,1.3,5.7,2.1c2.2,0.8,4.2,1.3,6.3,1.3c3.4,0,5.7-1.3,8.8-4.1c0.3-0.3,0.6-0.6,1-0.9c1.7-1.4,2.3-3.2,1.5-4.8c-0.7-1.4-1.9-1.9-3-1.4c-0.8,0.4-1.1,1.2-0.7,2c0.4,0.7,1.5,0.6,2,1.6c0.3,0.5,0.3,1,0.1,1.6c-0.2,0.4-0.7,0.8-1.3,1.2c-2.6,1.8-5.1,2.3-8,2.3c-1.1,0-3.5-0.4-5.2-0.8c-2.6-0.7-4.6-1.2-5.5-1.3c2.4-1.6,7.2-5.1,11.2-11.1c0.6,0,1.3,0.1,1.3,0.1c1.6,0.1,3.3,0.1,5-0.1c5-0.6,12.9-2.5,13.5-6.7C109.2,11.2,105.9,9.5,102.1,9.6z M68.6,35.7c-1.2,0.1-2-0.3-2-1c-0.1-1.3,2.1-2.3,4.1-2.3c1.6-0.1,3.1,0.2,4.5,0.6C73.5,34.5,70.7,35.7,68.6,35.7z M106.6,13.5c-0.6,3.8-7.6,6-11.1,6.4c-1.7,0.2-3.1,0.3-4.6,0.3c-0.5,0-0.9,0-1.4,0c0,0,0,0,0,0c2.3-3.3,4.7-6.2,6.9-7.7c0.3-0.2,0.7-0.5,0.9-0.6c1.3-0.9,3.4-1.6,5.1-1.6C104.8,10.1,106.8,11.2,106.6,13.5z'/><path d='M122.6,31.1c-1-1-2.7-0.9-4.5,0.2c-0.4,0.2-0.9,0.5-1.7,1.2c-1,0.8-2.1,1.6-2.7,1.8c-0.5,0.3-1.2,0.4-1.5,0.2c-0.3-0.2-0.3-0.7,0.1-1.3c0.4-0.8,1.3-2,2.1-3.1c1.3-1.6,2.3-2.8,3.4-4.2c0.6-0.8,0.4-1-1.4-1c-2,0-2.4,0.3-4,2c0.2-0.9-0.2-2.1-2-2.1c-3.7-0.1-7.6,1.9-9.6,3.5c-2,1.6-3.7,3.5-3.7,5.5c-0.1,1.9,1.4,2.5,3.4,2.5c3,0,6-2.3,7.7-3.8l0.1,0.1c-0.4,0.6-0.8,1.5-0.7,2.3c0.2,0.9,1,1.4,2.5,1.4c2,0,3.8-0.9,6-2.6c1-0.8,1.5-1.3,2.1-1.6c0.6-0.2,1.2-0.1,1.6,0.2c0.9,0.9,0.4,2,1.1,2.6c0.6,0.6,1.5,0.5,2.2-0.2C124,33.7,123.8,32.3,122.6,31.1z M107.4,32.2c-1.7,1.6-4.1,3-5.3,3c-0.4,0-0.8-0.2-0.8-0.6c-0.1-0.8,1.1-2.6,3.5-5c2.3-2.3,4.7-4.1,5.9-4.1c0.7,0,1.2,0.3,1.1,1.1C111.8,27.6,109.4,30.4,107.4,32.2z'/></g><g><path d='M17.2,9.5c-0.3-1-0.5-2.2-0.8-3.4c0-0.2-0.3-0.4-0.5-0.4c-0.1,0-0.2,0-0.3,0c-0.5,0-0.7,0.4-0.5,1c0.4,1,0.8,2,1,2.8c0.8,2,1.3,3.4,1.7,4.5c0.2-0.1,0.4-0.1,0.6-0.2C18.1,12.7,17.6,11.3,17.2,9.5z'/><g><g><path d='M4.1,8.1c0,0,0.8,0.4,2,0.2C7.3,8,9.4,6.7,11.9,6.6c2.5,0,3.5,1.7,3.7,2.4c0.2,0.7-0.4,1.1-0.4,1.1s0.8,0.9-0.7,1.8C8.6,14.4,4.1,8.1,4.1,8.1z'/><path d='M11.9,6.6c-2.2,0-4.1,1.1-5.4,1.5C7.7,8,9.8,6.9,12.2,7.1c2.1,0.2,3.1,1.7,3.4,2.5c0.1-0.2,0.1-0.3,0-0.6C15.4,8.4,14.3,6.6,11.9,6.6z'/><path d='M14.6,11.2C9.4,13.8,5,9,4.2,8.1c0,0,0,0,0,0s4.7,6.8,10.3,3.9c0.6-0.4,0.9-0.8,0.9-1.1c0,0-0.1-0.1-0.1-0.1C15,10.9,14.8,11.1,14.6,11.2z'/></g><path d='M9.7,9.4c0,0,3.3,0.2,5.4,1.1c2.1,0.9,3.1,2.9,3.1,2.9s-0.7-2.6-2.6-3.4C13.6,9.1,9.7,9.4,9.7,9.4z'/></g><g><g><path d='M22.7,4.6c0,0-0.3,0.4-0.9,0.6c-0.7,0.2-2,0.1-3.3,0.7c-1.2,0.6-1.3,1.8-1.2,2.1c0.1,0.4,0.5,0.5,0.5,0.5s-0.2,0.6,0.8,0.7C22.1,8.9,22.7,4.6,22.7,4.6z'/><path d='M18.5,5.9c1.1-0.6,2.3-0.5,3-0.6c-0.7,0.2-2,0.2-3.1,1c-1,0.7-1.1,1.6-1,2.1c-0.1-0.1-0.1-0.2-0.1-0.3C17.2,7.7,17.3,6.5,18.5,5.9z'/><path d='M18.4,8.9c3.2-0.1,4.1-3.6,4.3-4.2c0,0,0,0,0,0s-0.5,4.5-4,4.6c-0.4,0-0.6-0.1-0.7-0.3c0,0,0-0.1,0-0.1C18.1,8.8,18.2,8.8,18.4,8.9z'/></g><path d='M20,6.9c0,0-1.2,0.7-2,1.7c-0.8,0.9-0.8,2.2-0.8,2.2s-0.3-1.5,0.5-2.4C18.3,7.5,20,6.9,20,6.9z'/></g></g></g><g><path d='M112.2,20.8v-3.6h-1.1v-0.7h2.9v0.7h-1.1v3.6H112.2z'/><path d='M114.5,20.8v-4.3h1.1l0.7,3l0.7-3h1.1v4.3h-0.7v-3.4l-0.7,3.4h-0.7l-0.7-3.4v3.4H114.5z'/></g></g></g><g><path class='st0' d='M132.3,9.6v22.8h6.9v3.8h-11V9.6H132.3z'/><path class='st0' d='M150.2,36.2l-0.7-4.8h-5.1l-0.7,4.8h-3.8l4.2-26.6h6.1l4.2,26.6H150.2z M149,27.8l-2-13.4l-2,13.4H149z'/><path class='st0' d='M168.9,15.7v0.9c0,2.7-0.8,4.5-2.7,5.3c2.2,0.9,3.1,2.9,3.1,5.7v2.2c0,4.1-2.2,6.3-6.3,6.3h-6.6V9.6h6.3C167,9.6,168.9,11.7,168.9,15.7z M160.6,20.5h1.6c1.6,0,2.5-0.7,2.5-2.8v-1.5c0-1.9-0.6-2.7-2.1-2.7h-2V20.5z M160.6,32.4h2.4c1.4,0,2.2-0.6,2.2-2.6v-2.3c0-2.5-0.8-3.2-2.7-3.2h-1.9V32.4z'/><path class='st0' d='M177.4,9.3c4.1,0,6.1,2.4,6.1,6.7v0.8h-3.9v-1.1c0-1.9-0.8-2.6-2.1-2.6c-1.3,0-2.1,0.7-2.1,2.6c0,5.5,8.2,6.5,8.2,14.1c0,4.2-2.1,6.7-6.2,6.7s-6.2-2.4-6.2-6.7v-1.6h3.9v1.9c0,1.9,0.8,2.6,2.2,2.6c1.3,0,2.2-0.7,2.2-2.6c0-5.5-8.2-6.5-8.2-14.1C171.3,11.8,173.3,9.3,177.4,9.3z'/></g></svg></a></div><style>$logoCSS </style>"
}
// END:  getHelperFunctions('styling')

// BEGIN:getHelperFunctions('driver-default')
String getDEGREE() { return String.valueOf((char)(176)) }

void refresh(String cmd) {
    deviceCommand(cmd)
}

def installedDefault() {
	logging("installedDefault()", 100)
    
    try {
        tasmota_installedPreConfigure()
    } catch (MissingMethodException e) {
    }
    try {
        installedAdditional()
    } catch (MissingMethodException e) {
    }
}

def configureDefault() {
    logging("configureDefault()", 100)
    try {
        return configureAdditional()
    } catch (MissingMethodException e) {
    }
    try {
        getDriverVersion()
    } catch (MissingMethodException e) {
    }
}

void configureDelayed() {
    runIn(10, "configure")
    runIn(30, "refresh")
}

void configurePresence() {
    prepareCounters()
    if(presenceEnable == null || presenceEnable == true) {
        Random rnd = new Random()
        schedule("${rnd.nextInt(59)} ${rnd.nextInt(59)} 1/3 * * ? *", 'checkPresence')
        checkPresence(false)
    } else {
        sendEvent(name: "presence", value: "not present", descriptionText: "Presence Checking Disabled" )
        unschedule('checkPresence')
    }
}

void stopSchedules() {
    unschedule()
    log.info("Stopped ALL Device Schedules!")
}

void prepareCounters() {
    if(device.currentValue('restoredCounter') == null) sendEvent(name: "restoredCounter", value: 0, descriptionText: "Initialized to 0" )
    if(device.currentValue('notPresentCounter') == null) sendEvent(name: "notPresentCounter", value: 0, descriptionText: "Initialized to 0" )
    if(device.currentValue('presence') == null) sendEvent(name: "presence", value: "unknown", descriptionText: "Initialized as Unknown" )
}

boolean isValidDate(String dateFormat, String dateString) {
    try {
        Date.parse(dateFormat, dateString)
    } catch (e) {
        return false
    }
    return true
}

Integer retrieveMinimumMinutesToRepeat(Integer minimumMinutesToRepeat=55) {
    Integer mmr = null
    if(state.forcedMinutes == null || state.forcedMinutes == 0) {
        mmr = minimumMinutesToRepeat
    } else {
        mmr = state.forcedMinutes - 1 < 1 ? 1 : state.forcedMinutes.intValue() - 1
    }
    return mmr
}

boolean sendlastCheckinEvent(Integer minimumMinutesToRepeat=55) {
    boolean r = false
    Integer mmr = retrieveMinimumMinutesToRepeat(minimumMinutesToRepeat=minimumMinutesToRepeat)
    if (lastCheckinEnable == true || lastCheckinEnable == null) {
        String lastCheckinVal = device.currentValue('lastCheckin')
        if(lastCheckinVal == null || isValidDate('yyyy-MM-dd HH:mm:ss', lastCheckinVal) == false || now() >= Date.parse('yyyy-MM-dd HH:mm:ss', lastCheckinVal).getTime() + (mmr * 60 * 1000)) {
            r = true
		    sendEvent(name: "lastCheckin", value: new Date().format('yyyy-MM-dd HH:mm:ss'))
            logging("Updated lastCheckin", 1)
        } else {
             
        }
	}
    if (lastCheckinEpochEnable == true) {
		if(device.currentValue('lastCheckinEpoch') == null || now() >= device.currentValue('lastCheckinEpoch').toLong() + (mmr * 60 * 1000)) {
            r = true
		    sendEvent(name: "lastCheckinEpoch", value: now())
            logging("Updated lastCheckinEpoch", 1)
        } else {
             
        }
	}
    if(r == true) setAsPresent()
    return r
}

Long secondsSinceLastCheckinEvent() {
    Long r = null
    if (lastCheckinEnable == true || lastCheckinEnable == null) {
        String lastCheckinVal = device.currentValue('lastCheckin')
        if(lastCheckinVal == null || isValidDate('yyyy-MM-dd HH:mm:ss', lastCheckinVal) == false) {
            logging("No VALID lastCheckin event available! This should be resolved by itself within 1 or 2 hours and is perfectly NORMAL as long as the same device don't get this multiple times per day...", 100)
            r = -1
        } else {
            r = (now() - Date.parse('yyyy-MM-dd HH:mm:ss', lastCheckinVal).getTime()) / 1000
        }
	}
    if (lastCheckinEpochEnable == true) {
		if(device.currentValue('lastCheckinEpoch') == null) {
		    logging("No VALID lastCheckin event available! This should be resolved by itself within 1 or 2 hours and is perfectly NORMAL as long as the same device don't get this multiple times per day...", 100)
            r = r == null ? -1 : r
        } else {
            r = (now() - device.currentValue('lastCheckinEpoch').toLong()) / 1000
        }
	}
    return r
}

boolean hasCorrectCheckinEvents(Integer maximumMinutesBetweenEvents=90, boolean displayWarnings=true) {
    Long secondsSinceLastCheckin = secondsSinceLastCheckinEvent()
    if(secondsSinceLastCheckin != null && secondsSinceLastCheckin > maximumMinutesBetweenEvents * 60) {
        if(displayWarnings == true && (presenceWarningEnable == null || presenceWarningEnable == true)) log.warn("One or several EXPECTED checkin events have been missed! Something MIGHT be wrong with the mesh for this device. Minutes since last checkin: ${Math.round(secondsSinceLastCheckin / 60)} (maximum expected $maximumMinutesBetweenEvents)")
        return false
    }
    return true
}

boolean checkPresence(boolean displayWarnings=true) {
    boolean isPresent = false
    Long lastCheckinTime = null
    String lastCheckinVal = device.currentValue('lastCheckin')
    if ((lastCheckinEnable == true || lastCheckinEnable == null) && isValidDate('yyyy-MM-dd HH:mm:ss', lastCheckinVal) == true) {
        lastCheckinTime = Date.parse('yyyy-MM-dd HH:mm:ss', lastCheckinVal).getTime()
    } else if (lastCheckinEpochEnable == true && device.currentValue('lastCheckinEpoch') != null) {
        lastCheckinTime = device.currentValue('lastCheckinEpoch').toLong()
    }
    if(lastCheckinTime != null && lastCheckinTime >= now() - (3 * 60 * 60 * 1000)) {
        setAsPresent()
        isPresent = true
    } else {
        sendEvent(name: "presence", value: "not present")
        if(displayWarnings == true) {
            Integer numNotPresent = device.currentValue('notPresentCounter')
            numNotPresent = numNotPresent == null ? 1 : numNotPresent + 1
            sendEvent(name: "notPresentCounter", value: numNotPresent )
            if(presenceWarningEnable == null || presenceWarningEnable == true) {
                log.warn("No event seen from the device for over 3 hours! Something is not right... (consecutive events: $numNotPresent)")
            }
        }
    }
    return isPresent
}

void setAsPresent() {
    if(device.currentValue('presence') == "not present") {
        Integer numRestored = device.currentValue('restoredCounter')
        numRestored = numRestored == null ? 1 : numRestored + 1
        sendEvent(name: "restoredCounter", value: numRestored )
        sendEvent(name: "notPresentCounter", value: 0 )
    }
    sendEvent(name: "presence", value: "present")
}

void resetNotPresentCounter() {
    logging("resetNotPresentCounter()", 100)
    sendEvent(name: "notPresentCounter", value: 0, descriptionText: "Reset notPresentCounter to 0" )
}

void resetRestoredCounter() {
    logging("resetRestoredCounter()", 100)
    sendEvent(name: "restoredCounter", value: 0, descriptionText: "Reset restoredCounter to 0" )
}
// END:  getHelperFunctions('driver-default')

// BEGIN:getHelperFunctions('childDevices')
Integer channelNumber(String dni) {
    Integer ch = dni.split("-")[-1] as Integer
    return ch
}

void childOn(String dni) {
    onOffCmd(1, channelNumber(dni))
}

void childOff(String dni) {
    onOffCmd(0, channelNumber(dni))
}

void childSendState(String currentSwitchNumber, String state) {
    com.hubitat.app.ChildDeviceWrapper childDevice = childDevices.find{it.deviceNetworkId.endsWith("-${currentSwitchNumber}")}
    if (childDevice) {
        logging("childDevice.sendEvent ${currentSwitchNumber} ${state}",1)
        childDevice.sendEvent(name: "switch", value: state, type: type)
    } else {
        logging("childDevice.sendEvent ${currentSwitchNumber} is missing!",1)
    }
}

boolean areAllChildrenSwitchedOn(Integer skip = 0) {
    List<com.hubitat.app.ChildDeviceWrapper> children = getChildDevices()
    boolean status = true
    Integer i = 1
    children.each {child->
        if (i!=skip) {
  		    if(child.currentState("switch")?.value == "off") {
                status = false
            }
        }
        i++
    }
    return status
}

void sendParseEventToChildren(data) {
    List<com.hubitat.app.ChildDeviceWrapper> children = getChildDevices()
    children.each {child->
        child.parseParentData(data)
    }
}

void createChildDevices() {
    Integer numSwitchesI = numSwitches.toInteger()
    logging("createChildDevices: creating $numSwitchesI device(s)",1)
    
    for (i in 1..numSwitchesI) {
         
        try {
        addChildDevice("${getDeviceInfoByName("namespace")}", "${getChildDriverName()}", "$device.id-$i", [name: "${getFilteredDeviceDriverName()} #$i", label: "${getFilteredDeviceDisplayName()} $i", isComponent: true])
                } catch (com.hubitat.app.exception.UnknownDeviceTypeException e) {
                    log.error "'${getChildDriverName()}' driver can't be found! Did you forget to install the child driver?"
                }
         
    }
}

void recreateChildDevices() {
    Integer numSwitchesI = numSwitches.toInteger()
    logging("recreateChildDevices: recreating $numSwitchesI device(s)",1)
    com.hubitat.app.ChildDeviceWrapper childDevice = null

    for (i in 1..numSwitchesI) {
        childDevice = childDevices.find{it.deviceNetworkId.endsWith("-$i")}
        if (childDevice) {
            childDevice.setName("${getDeviceInfoByName('name')} #$i")
            childDevice.setDeviceNetworkId("$device.id-$i")
            logging(childDevice.getData(), 10)
        } else {
             
            try {
            addChildDevice("${getDeviceInfoByName("namespace")}", "${getChildDriverName()}", "$device.id-$i", [name: "${getFilteredDeviceDriverName()} #$i", label: "${getFilteredDeviceDisplayName()} $i", isComponent: true])
                    } catch (com.hubitat.app.exception.UnknownDeviceTypeException e) {
                        log.error "'${getChildDriverName()}' driver can't be found! Did you forget to install the child driver?"
                    }
             
        }
    }
    if (numSwitchesI < 4) {
        for (i in 1..4) {
            if (i > numSwitchesI) {
                childDevice = childDevices.find{it.deviceNetworkId.endsWith("-$i")}
                if (childDevice) {
                    logging("Removing child #$i!", 10)
                    deleteChildDevice(childDevice.deviceNetworkId)
                }
            }
        }
    }
}

void deleteChildren() {
	logging("deleteChildren()", 100)
	List<com.hubitat.app.ChildDeviceWrapper> children = getChildDevices()
    
    children.each {child->
  		deleteChildDevice(child.deviceNetworkId)
    }
}
// END:  getHelperFunctions('childDevices')

// BEGIN:getHelperFunctions('tasmota')
def generalInitialize() {
    logging("generalInitialize()", 100)
	unschedule()
    setLogsOffTask()
    refresh()
}

void parse(hubitat.scheduling.AsyncResponse asyncResponse, data) {
    if(asyncResponse != null) {
        try{
            logging("tasmota: parse(asyncResponse.getJson() = \"${asyncResponse.getJson()}\")", 1)
            parseResult(asyncResponse.getJson())
        } catch(MissingMethodException e1) {
            log.error e1
        } catch(e1) {
            try{
                logging("parse(asyncResponse.data = \"${asyncResponse.data}\", data = \"${data}\") e1=$e1", 1)
            } catch(e2) {
                logging("parse(asyncResponse.data = null, data = \"${data}\") Is the device online? e2=$e2", 1)
            }
        }
    } else {
        logging("tasmota: parse(asyncResponse.data = null, data = \"${data}\")", 1)
    }
}

void reboot() {
	logging("tasmota: reboot()", 1)
    tasmota_getAction(tasmota_getCommandString("Restart", "1"))
}

void sendCommand(String command, callback="tasmota_sendCommandParse") {
    logging("tasmota: sendCommand(command=$command)", 1)
    sendCommand(command, null)
}

void sendCommand(String command, String argument, callback="tasmota_sendCommandParse") {
    String descriptionText = "${command}${argument != null ? " " + argument : ""}"
    logging("tasmota: sendCommand($descriptionText)", 1)
    if(callback == "tasmota_sendCommandParse") {
        sendEvent(name: "commandSent", value: command, descriptionText: descriptionText, isStateChange: true)
    }
    tasmota_getAction(tasmota_getCommandString(command, argument), callback=callback)
}

void updatePresence(String presence) {
}

void tasmota_installedPreConfigure() {
    logging("tasmota_installedPreConfigure()", 1)
    logging("Password: ${decrypt(getDataValue('password'))}", 1)
    String pass = decrypt(getDataValue('password'))
    if(pass != null && pass != "" && pass != "[installed]") {
        device.updateSetting("password", [value: pass, type: "password"])
    }
    device.updateSetting("deviceConfig", [type: "enum", value:getDataValue('deviceConfig')])
}

void tasmota_updatedDefault() {
    log.warn("tasmota_updatedDefault() should NOT be used!")
    logging("tasmota_updatedDefault()", 10)
    updateNeededSettings()
    
    try {
    } catch (MissingMethodException e) {
    }
}

void tasmota_refreshChildren() {
    logging("tasmota_refreshChildren()", 1)
    tasmota_getAction(tasmota_getCommandString("Status", "0"), callback="tasmota_parseConfigureChildDevices")
}

void tasmota_refreshChildrenAgain() {
    logging("tasmota_refreshChildrenAgain()", 1)
    tasmota_refreshChildren()
}

Map tasmota_refresh(Map metaConfig=null) {
	logging("tasmota_refresh(metaConfig=$metaConfig)", 100)
    state = [:]

    tasmota_getAction(tasmota_getCommandString("Status", "0"), callback="tasmota_parseConfigureChildDevices")
    getDriverVersion()

    updateDataValue('namespace', getDeviceInfoByName('namespace'))

    metaConfig = clearThingsToHide()
    metaConfig = setCommandsToHide([], metaConfig=metaConfig)
    metaConfig = setStateVariablesToHide(['settings', 'colorMode', 'red', 'green', 'blue', 
        'mired', 'level', 'saturation', 'mode', 'hue'], metaConfig=metaConfig)
    
    metaConfig = setCurrentStatesToHide(['needUpdate'], metaConfig=metaConfig)
    metaConfig = setDatasToHide(['namespace', 'appReturn', 'password'], metaConfig=metaConfig)
    metaConfig = setPreferencesToHide([], metaConfig=metaConfig)
    try {
    } catch (MissingMethodException e1) {
        try {
        } catch (MissingMethodException e2) {
        }
    }
    return metaConfig
}

void tasmota_runInstallCommands(List installCommands) {
    logging("tasmota_runInstallCommands(installCommands=$installCommands)", 1)
    List backlogs = []
    List rule1 = []
    List rule2 = []
    List rule3 = []
    installCommands.each {cmd->
        if(cmd[0].toLowerCase() == "rule1") {
            rule1.add([command: cmd[0], value:cmd[1]])
        } else if(cmd[0].toLowerCase() == "rule2") {
            rule2.add([command: cmd[0], value:cmd[1]])
        } else if(cmd[0].toLowerCase() == "rule3") {
            rule3.add([command: cmd[0], value:cmd[1]])
        } else {
            backlogs.add([command: cmd[0], value:cmd[1]])
        }
    }

    pauseExecution(100)
    while(backlogs.size() > 0) {
        tasmota_getAction(tasmota_getMultiCommandString(backlogs.take(10)))
        backlogs = backlogs.drop(10)
        if(backlogs.size() > 0) pauseExecution(1000)
    }

    [rule1, rule2, rule3].each {
        it.each {rule->
            tasmota_getAction(tasmota_getCommandString(rule["command"], rule["value"]))
            pauseExecution(100)
        }
    }
}

void tasmota_updatePresence(String presence) {
    logging("tasmota_updatePresence(presence=$presence) DISABLED", 1)
     
}

Map tasmota_parseDescriptionAsMap(description) {
	description.split(",").inject([:]) { map, param ->
		def nameAndValue = param.split(":")
        
        if (nameAndValue.length == 2) { 
            map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
        } else {
            map += [(nameAndValue[0].trim()):""]
        }
	}
}

void tasmota_getAction(String uri, String callback="parse") { 
    logging("Using tasmota_getAction for '${uri}'...", 1)
    tasmota_httpGetAction(uri, callback=callback)
}

void tasmota_parseConfigureChildDevices(hubitat.scheduling.AsyncResponse asyncResponse, data) {
    if(asyncResponse != null) {
        try{
            logging("tasmota_parseConfigureChildDevices(asyncResponse.getJson() 2= \"${asyncResponse.getJson()}\", data = \"${data}\")", 1)
            tasmota_configureChildDevices(asyncResponse, data)
        } catch(MissingMethodException e1) {
            log.error e1
        } catch(e1) {
            try{
                logging("tasmota_parseConfigureChildDevices(asyncResponse.data = \"${asyncResponse.data}\", data = \"${data}\") e1=$e1", 1)
            } catch(e2) {
                logging("tasmota_parseConfigureChildDevices(asyncResponse.data = null, data = \"${data}\") Is the device online? e2=$e2", 1)
            }
        }
    } else {
        logging("parse(asyncResponse.data = null, data = \"${data}\")", 1)
    }
}

void tasmota_configureChildDevices(hubitat.scheduling.AsyncResponse asyncResponse, data) {
    Map statusMap = asyncResponse.getJson()
    logging("tasmota_configureChildDevices() statusMap=$statusMap", 1)

    Map deviceInfo = [:]
    deviceInfo["hasEnergy"] = false
    deviceInfo["numTemperature"] = 0
    deviceInfo["numHumidity"] = 0
    deviceInfo["numPressure"] = 0
    deviceInfo["numDistance"] = 0
    deviceInfo["numSensorGroups"] = 0
    deviceInfo["sensorMap"] = [:]
    if(statusMap.containsKey("StatusSNS")) {
        sns = statusMap["StatusSNS"]
        deviceInfo["hasEnergy"] = sns.containsKey("ENERGY")
        deviceInfo["isShutter"] = sns.containsKey("Shutter1")
        deviceInfo["sensorMap"] = map_getKeysWithMapAndId(sns)
        deviceInfo["sensorMap"].remove("ENERGY")
        deviceInfo["sensorMap"].remove("Shutter1")
        deviceInfo["sensorMap"].remove("Shutter2")
        deviceInfo["sensorMap"].remove("Shutter3")
        deviceInfo["sensorMap"].remove("Shutter4")
        deviceInfo["numSensorGroups"] = deviceInfo["sensorMap"].size()
        deviceInfo["numTemperature"] = map_numOfKeyInSubMap(sns, "Temperature")
        deviceInfo["numHumidity"] = map_numOfKeyInSubMap(sns, "Humidity")
        deviceInfo["numPressure"] = map_numOfKeyInSubMap(sns, "Pressure")
        deviceInfo["numDistance"] = map_numOfKeyInSubMap(sns, "Distance")
    }

    deviceInfo["numSwitch"] = 0
    deviceInfo["isDimmer"] = false
    deviceInfo["isAddressable"] = false
    deviceInfo["isRGB"] = false
    deviceInfo["hasCT"] = false
    deviceInfo["hasFanControl"] = false
    if(statusMap["StatusSTS"] != null) {
        sts = statusMap["StatusSTS"]
        deviceInfo["isDimmer"] = sts.containsKey("Dimmer")
        deviceInfo["isAddressable"] = sts.containsKey("Width")
        if(sts.containsKey("Color")) deviceInfo["isRGB"] = sts["Color"].length() >= 6
        deviceInfo["hasCT"] = sts.containsKey("CT")
        deviceInfo["hasFanControl"] = sts.containsKey("FanSpeed")

        if(sts["POWER"] != null) {
            deviceInfo["numSwitch"] = 1
        } else {
            i = 1
            while(sts["POWER$i"] != null) {
                i += 1
            }
            deviceInfo["numSwitch"] = i - 1
        }
        if(deviceInfo["isShutter"] == true && deviceInfo["numSwitch"] >= 2) {
            deviceInfo["numSwitch"] -= 2
        }
    }
    logging("Device info found: $deviceInfo", 100)

    List driverName = ["Tasmota - Universal Plug/Outlet (Child)", "Generic Component Switch"]
    String namespace = "tasmota"
    if(deviceInfo["numSwitch"] > 0) {
         
        if(deviceInfo["hasEnergy"]  == true && (deviceInfo["isAddressable"] == false && deviceInfo["isRGB"] == false && deviceInfo["hasCT"] == false)) {
            if(deviceInfo["isDimmer"]) {
                driverName = ["Tasmota - Universal Metering Dimmer (Child)", "Generic Component Dimmer"]
            } else {
                driverName = ["Tasmota - Universal Metering Plug/Outlet (Child)", 
                              "Tasmota - Universal Metering Bulb/Light (Child)",
                              "Generic Component Metering Switch"]
            }
        } else {
            if(deviceInfo["hasEnergy"] == true) {
                log.warn "This device reports Metering Capability AND has RGB, Color Temperature or is Addressable. Metering values will be ignored... This is NOT supported and may result in errors, please report it to the developer to find a solution."
            }
            if((deviceInfo["isDimmer"] == true || deviceInfo["isAddressable"] == true || 
                deviceInfo["isRGB"] == true || deviceInfo["hasCT"] == true)) {
                if(deviceInfo["isAddressable"] == false && deviceInfo["isRGB"] == false && deviceInfo["hasCT"] == false) {
                    driverName = ["Tasmota - Universal Dimmer (Child)", "Generic Component Dimmer"]
                } else if(deviceInfo["isAddressable"] == false && deviceInfo["isRGB"] == false && deviceInfo["hasCT"] == true) {
                    driverName = ["Tasmota - Universal CT/RGB/RGB+CW+WW (Child)", "Generic Component CT"]
                } else if(deviceInfo["isRGB"] == true && deviceInfo["hasCT"] == false) {
                    driverName = ["Tasmota - Universal CT/RGB/RGB+CW+WW (Child)", "Generic Component RGB"]
                } else {
                    driverName = ["Tasmota - Universal CT/RGB/RGB+CW+WW (Child)", "Generic Component RGBW"]
                }
            }
        }
        
        for(i in 1..deviceInfo["numSwitch"]) {
            namespace = "tasmota"
            String childId = "POWER$i"
            String childName = tasmota_getChildDeviceNameRoot(keepType=true) + " ${tasmota_getMinimizedDriverName(driverName[0])} ($childId)"
            String childLabel = "${tasmota_getMinimizedDriverName(device.getLabel())} ($i)"
            logging("createChildDevice: POWER$i", 1)
            tasmota_createChildDevice(namespace, driverName, childId, childName, childLabel)
            
            driverName = ["Tasmota - Universal Plug/Outlet (Child)", "Generic Component Switch"]
        }
    }
    
    if(deviceInfo["hasFanControl"] == true) {
         
        namespace = "tasmota"
        driverName = ["Tasmota - Universal Fan Control (Child)"]
        String childId = "FAN"
        String childName = tasmota_getChildDeviceNameRoot(keepType=true) + " ${tasmota_getMinimizedDriverName(driverName[0])} ($childId)"
        String childLabel = "${tasmota_getMinimizedDriverName(device.getLabel())} ($childId)"
        tasmota_createChildDevice(namespace, driverName, childId, childName, childLabel)
    }

    if(deviceInfo["isShutter"] == true) {
        logging("isShutter", 100)
        namespace = "tasmota"
        driverName = ["Tasmota - Universal Curtain (Child)"]
        String childId = "SHUTTER"
        String childName = tasmota_getChildDeviceNameRoot(keepType=true) + " ${tasmota_getMinimizedDriverName(driverName[0])} ($childId)"
        String childLabel = "${tasmota_getMinimizedDriverName(device.getLabel())} ($childId)"
        tasmota_createChildDevice(namespace, driverName, childId, childName, childLabel)
    }

    deviceInfo["sensorMap"].each {
         
        namespace = "tasmota"
        driverName = ["Tasmota - Universal Multi Sensor (Child)"]
        String childId = "${it.key}"
        String childName = tasmota_getChildDeviceNameRoot(keepType=true) + " ${tasmota_getMinimizedDriverName(driverName[0])} ($childId)"
        String childLabel = "${tasmota_getMinimizedDriverName(device.getLabel())} ($childId)"
        tasmota_createChildDevice(namespace, driverName, childId, childName, childLabel)
    }
    deviceInfo = null
    parseResult(statusMap)
    statusMap = null
}

String tasmota_getChildDeviceNameRoot(boolean keepType=false) {
    String childDeviceNameRoot = getDeviceInfoByName('name')
    String parentDeviceLabel = device.getLabel()
    if(parentDeviceLabel != null) childDeviceNameRoot = parentDeviceLabel
    if(childDeviceNameRoot.toLowerCase().endsWith(' (parent)')) {
        childDeviceNameRoot = childDeviceNameRoot.substring(0, childDeviceNameRoot.length()-9)
    } else if(childDeviceNameRoot.toLowerCase().endsWith(' parent')) {
        childDeviceNameRoot = childDeviceNameRoot.substring(0, childDeviceNameRoot.length()-7)
    }
    if(keepType == false && childDeviceNameRoot.toLowerCase().startsWith('tasmota - ')) {
        childDeviceNameRoot = childDeviceNameRoot.substring(10, childDeviceNameRoot.length())
    }
    return childDeviceNameRoot
}

String tasmota_getMinimizedDriverName(String driverName) {
    logging("tasmota_getMinimizedDriverName(driverName=$driverName)", 1)
    if(driverName == null) driverName = "Device"
    if(driverName.toLowerCase().endsWith(' (child)')) {
        driverName = driverName.substring(0, driverName.length()-8)
    } else if(driverName.toLowerCase().endsWith(' child')) {
        driverName = driverName.substring(0, driverName.length()-6)
    }
    if(driverName.toLowerCase().endsWith(' (parent)')) {
        driverName = driverName.substring(0, driverName.length()-9)
    } else if(driverName.toLowerCase().endsWith(' parent')) {
        driverName = driverName.substring(0, driverName.length()-7)
    }
    driverName = driverName.replaceAll("(?i) \\(parent\\)", "").replaceAll("(?i) parent", "").replaceAll("(?i)parent", "")
    logging("driverName: $driverName", 1)

    driverName = driverName.replaceFirst("\\(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\)", "").replaceFirst("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}", "");

    if(driverName.toLowerCase().startsWith('tasmota - ')) {
        driverName = driverName.substring(10, driverName.length())
    }
    if(driverName.toLowerCase().startsWith('universal ')) {
        driverName = driverName.substring(10, driverName.length())
    }
    driverName = driverName.replaceAll("Generic Component ", "")
    driverName = driverName.trim()
    if(driverName == '') driverName = "Device"
    
    logging("tasmota_getMinimizedDriverName(driverName=$driverName) end", 1)
    return driverName
}

com.hubitat.app.ChildDeviceWrapper tasmota_getChildDeviceByActionType(String actionType) {
    return childDevices.find{it.deviceNetworkId.endsWith("-$actionType")}
}

private void tasmota_createChildDevice(String namespace, List driverName, String childId, String childName, String childLabel) {
    logging("tasmota_createChildDevice(namespace=$namespace, driverName=$driverName, childId=$childId, childName=$childName, childLabel=$childLabel)", 1)
    com.hubitat.app.ChildDeviceWrapper childDevice = childDevices.find{it.deviceNetworkId.endsWith("-$childId")}
    if(!childDevice && childId.toLowerCase().startsWith("power")) {
        logging("Looking for $childId, ending in ${childId.substring(5)}", 1)
        childDevice = childDevices.find{it.deviceNetworkId.endsWith("-${childId.substring(5)}")}
        if(childDevice) {
            logging("Setting new Network ID for $childId to '$device.id-$childId'", 1)
            childDevice.setDeviceNetworkId("$device.id-$childId")
        }
    }
    if (childDevice != null) {
        childDevice.setName(childName)
        logging("childDevice.getData(): ${childDevice.getData()}", 1)
    } else {
         
        Integer s = childName.size()
        for(i in 0..s) {
            def currentNamespace = namespace
            if(driverName[i].toLowerCase().startsWith('generic component')) {
                currentNamespace = "hubitat"
            }
            try {
                addChildDevice(currentNamespace, driverName[i], "$device.id-$childId", [name: childName, label: childLabel, isComponent: false])
                logging("Created child device '$childLabel' using driver '${driverName[i]}'...", 100)
            } catch (com.hubitat.app.exception.UnknownDeviceTypeException e) {
                if(i == s) {
                    log.error "'${driverName[i]}' driver can't be found! Did you forget to install the child driver?"
                } else {
                    log.warn "'${driverName[i]}' driver can't be found! Trying another driver: ${driverName[i+1]}..."
                }
            }
        }
    }
}

private void tasmota_updateChildDeviceSetting(String settingName, String value) {
    getChildDevices().each { cDev ->
        cDev.clearSetting(settingName)
        cDev.removeSetting(settingName)
        cDev.updateSetting(settingName, value)
    }
}

private String tasmota_determineDeviceNetworkId(String macOrIP, boolean isIP = false) {
    String myDNI
    if (isIP == false) {
        myDNI = macOrIP
    } else {
         
        myDNI = tasmota_convertIPtoHex(macOrIP)
    }
     
    return myDNI
}

void tasmota_prepareDNI() {
    if (useIPAsID) {
        String hexIPAddress = tasmota_determineDeviceNetworkId(ipAddress, true)
        if(hexIPAddress != null && state.dni != hexIPAddress) {
            state.dni = hexIPAddress
            tasmota_updateDNI()
        }
    } else if (state.mac != null && state.dni != state.mac) { 
        state.dni = tasmota_determineDeviceNetworkId(state.mac)
        tasmota_updateDNI()
    }
}

private void tasmota_updateDNI() {
    if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) {
         
        device.deviceNetworkId = state.dni
    }
}

Integer tasmota_getTelePeriodValue() {
    return (telePeriod != null && telePeriod.isInteger() ? telePeriod.toInteger() : 300)
}

private String tasmota_getHostAddress() {
    Integer port = 80
    if (getDeviceDataByName("port") != null) {
        port = getDeviceDataByName("port").toInteger()
    }
    if (override == true && ipAddress != null){
        return "${ipAddress}:$port"
    } else if(device.currentValue("ip") != null) {
        return "${device.currentValue("ip")}:$port"
    } else if(getDeviceDataByName("ip") != null) {
        return "${getDeviceDataByName("ip")}:$port"
    } else {
        log.warn "tasmota_getHostAddress() failed and ran out of fallbacks! If this happens, contact the developer, this is an \"impossible\" scenario!"
	    return "127.0.0.1:$port"
    }
}

private String tasmota_convertIPtoHex(String ipAddress) {
    String hex = null
    if(ipAddress != null) {
        hex = ipAddress.tokenize( '.' ).collect {  String.format( '%02X', it.toInteger() ) }.join()
         
    } else {
        hex = null
        if (useIPAsID) {
            logging('ERROR: To use IP as Network ID "Device IP Address" needs to be set and "Override IP" needs to be enabled! If this error persists, consult the release thread in the Hubitat Forum.')
        }
    }
    return hex
}

private String tasmota_getFirstTwoIPBytes(String ipAddress) {
    String ipStart = null
    if(ipAddress != null) {
        ipStart = ipAddress.tokenize( '.' ).take(2).join('.') + '.'
         
    } else {
        ipStart = ''
    }
    return ipStart
}

void tasmota_sync(String ip, Integer port = null) {
    String existingIp = getDataValue("ip")
    String existingPort = getDataValue("port")
    logging("Running tasmota_sync()", 1)
    if (ip != null && ip != existingIp) {
        updateDataValue("ip", ip)
        sendEvent(name: 'ip', value: ip, isStateChange: false)
        sendEvent(name: "ipLink", value: "<a target=\"device\" href=\"http://$ip\">$ip</a>", isStateChange: false)
        logging("IP set to ${ip}", 1)
    }
    if (port && port != existingPort) {
        updateDataValue("port", port)
        logging("Port set to ${port}", 1)
    }
}

Integer tasmota_dBmToQuality(Integer dBm) {
     
    return dBm
}

private void tasmota_httpGetAction(String uri, String callback="parse") { 
  tasmota_updateDNI()
  
  Map headers = tasmota_getHeader()
  logging("tasmota_httpGetAction for 'http://${tasmota_getHostAddress()}$uri'...", 1)
  try {
     
    asynchttpGet(
        callback,
        [uri: "http://${tasmota_getHostAddress()}$uri",
        headers: headers]
    )
  } catch (e) {
    log.error "Error in tasmota_httpGetAction(uri): $e ('$uri')"
  }
}

private hubitat.device.HubAction tasmota_postAction(String uri, String data) { 
  tasmota_updateDNI()

  Map headers = tasmota_getHeader()

  hubitat.device.HubAction hubAction = null
  try {
    hubAction = new hubitat.device.HubAction(
    method: "POST",
    path: uri,
    headers: headers,
    body: data
  )
  } catch (e) {
    log.error "Error in tasmota_postAction(uri, data): $e ('$uri', '$data')"
  }
  return hubAction    
}

void tasmota_sendCommandParse(hubitat.scheduling.AsyncResponse asyncResponse, data) {
    if(asyncResponse != null) {
        try{
            Map r = asyncResponse.getJson()
            logging("tasmota_sendCommandParse(asyncResponse.getJson() = \"${r}\")", 1)
            sendEvent(name: "commandResult", value: asyncResponse.getData(), isStateChange: true)
            parseResult(r)
        } catch(MissingMethodException e1) {
            log.error e1
        } catch(e1) {
            try{
                logging("tasmota_sendCommandParse(asyncResponse.data = \"${asyncResponse.data}\", data = \"${data}\") e1=$e1", 1)
            } catch(e2) {
                logging("tasmota_sendCommandParse(asyncResponse.data = null, data = \"${data}\") Is the device online? e2=$e2", 1)
            }
        }
    } else {
        logging("tasmota_sendCommandParse(asyncResponse.data = null, data = \"${data}\")", 1)
    }
}

String tasmota_getCommandString(String command, String value) {
    String uri = "/cm?"
    if (password != null) {
        uri += "user=admin&password=${tasmota_urlEscape(password)}&"
    }
	if (value != null && value != "") {
		uri += "cmnd=${tasmota_urlEscape(command)}%20${tasmota_urlEscape(value)}"
	}
	else {
		uri += "cmnd=${tasmota_urlEscape(command)}"
	}
    return uri
}

String tasmota_getMultiCommandString(List<Map> commands) {
    String uri = "/cm?"
    if (password != null) {
        uri += "user=admin&password=${password}&"
    }
    uri += "cmnd=backlog%20"
    if(commands.size() > 30) {
        log.warn "Backlog only supports 30 commands, the last ${commands.size() - 30} will be ignored!"
    }
    commands.each {cmd->
        if(cmd.containsKey("value")) {
          uri += "${tasmota_urlEscape(cmd['command'])}%20${tasmota_urlEscape(cmd['value'])}%3B%20"
        } else {
          uri += "${tasmota_urlEscape(cmd['command'])}%3B%20"
        }
    }
    return uri
}

private String tasmota_urlEscape(String url) {
    return(URLEncoder.encode(url).replace("+", "%20").replace("#", "%23"))
}

private String tasmota_convertPortToHex(Integer port) {
	String hexport = port.toString().format( '%04X', port.toInteger() )
    return hexport
}

private String tasmota_encodeCredentials(String username, String password) {
	String userpassascii = "${username}:${password}"
    String userpass = "Basic " + userpassascii.bytes.encodeBase64().toString()
    return userpass
}

private Map tasmota_getHeader(String userpass = null) {
    Map headers = [:]
    headers.put("Host", tasmota_getHostAddress())
    headers.put("Content-Type", "application/x-www-form-urlencoded")
    if (userpass != null)
       headers.put("Authorization", userpass)
    return headers
}
// END:  getHelperFunctions('tasmota')

// BEGIN:getHelperFunctions('map')
boolean map_containsKeyInSubMap(aMap, key) {
    boolean hasKey = false
    aMap.find {
        try{
            hasKey = it.value.containsKey(key)
        } catch(e) {

        }
        hasKey == true
    }
    return hasKey
}

Integer map_numOfKeyInSubMap(aMap, String key) {
    Integer numKeys = 0
    aMap.each {
        try{
            if(it.value.containsKey(key)) numKeys += 1
        } catch(e) {
        }
    }
    return numKeys
}

Integer map_numOfKeysIsMap(aMap) {
    Integer numKeys = 0
    aMap.each {
        if(it.value instanceof java.util.Map) numKeys += 1
    }
    return numKeys
}

TreeMap map_getKeysWithMapAndId(aMap) {
    def foundMaps = [:] as TreeMap
    aMap.each {
        if(it.value instanceof java.util.Map) {
            foundMaps[it.key] = it.value
        }
    }
    return foundMaps
}
// END:  getHelperFunctions('map')

// BEGIN:getHelperFunctions('rgbw')
void rgbw_setColor(value) {
    logging("rgbw_setColor('${value}')", 10)
	if (value != null && value instanceof Map) {
        def h = value.containsKey("hue") ? value.hue : 0
        def s = value.containsKey("saturation") ? value.saturation : 0
        def b = value.containsKey("level") ? value.level : 0
        tasmota_rgbw_setHSB(h, s, b)
    } else {
        logging("rgbw_setColor('${value}') called with an INVALID argument!", 10)
    }
}

void rgbw_setHue(h) {
    logging("rgbw_setHue('${h}')", 10)
    tasmota_rgbw_setHSB(h, null, null)
}

void rgbw_setSaturation(s) {
    logging("rgbw_setSaturation('${s}')", 10)
    tasmota_rgbw_setHSB(null, s, null)
}

void rgbw_setLevel(b) {
    logging("rgbw_setLevel('${b}')", 10)
    rgbw_setLevel(b, 0)
}

def rgbw_rgbToHSB(red, green, blue) {
    BigDecimal r = red / 255f
    BigDecimal g = green / 255f
    BigDecimal b = blue / 255f
    BigDecimal max = [r, g, b].max()
    BigDecimal min = [r, g, b].min()
    BigDecimal delta = max - min
    def hue = 0
    def saturation = 0
    if (max == min) {
        hue = 0
    } else if (max == r) {
        def h1 = (g - b) / delta / 6
        def h2 = h1.asType(Integer)
        if (h1 < 0) {
            hue = Math.round(360 * (1 + h1 - h2))
        } else {
            hue = Math.round(360 * (h1 - h2))
        }
        logging("rgbToHSB: red max=${max} min=${min} delta=${delta} h1=${h1} h2=${h2} hue=${hue}", 1)
    } else if (max == g) {
        hue = 60 * ((b - r) / delta + 2)
        logging("rgbToHSB: green hue=${hue}", 1)
    } else {
        hue = 60 * ((r - g) / (max - min) + 4)
        logging("rgbToHSB: blue hue=${hue}", 1)
    }
    
    hue = Math.round((hue) / 3.6)

    if (max == 0) {
        saturation = 0
    } else {
        saturation = delta / max * 100
    }
    
    def level = max * 100
    
    return [
        "hue": hue.asType(Integer),
        "saturation": saturation.asType(Integer),
        "level": level.asType(Integer),
    ]
}

String rgbw_getColorNameFromTemperature(Integer colorTemperature){
    if (!colorTemperature) return "Undefined"
    String colorName = "Undefined"
    if (colorTemperature <= 2000) colorName = "Sodium"
    else if (colorTemperature <= 2100) colorName = "Starlight"
    else if (colorTemperature < 2400) colorName = "Sunrise"
    else if (colorTemperature < 2800) colorName = "Incandescent"
    else if (colorTemperature < 3300) colorName = "Soft White"
    else if (colorTemperature < 3500) colorName = "Warm White"
    else if (colorTemperature < 4150) colorName = "Moonlight"
    else if (colorTemperature <= 5000) colorName = "Horizon"
    else if (colorTemperature < 5500) colorName = "Daylight"
    else if (colorTemperature < 6000) colorName = "Electronic"
    else if (colorTemperature <= 6500) colorName = "Skylight"
    else if (colorTemperature < 20000) colorName = "Polar"
    return colorName
}

String rgbw_getColorNameFromHueSaturation(Integer hue, Integer saturation=null){
    if (!hue) hue = 0
    String colorName = "Undefined"
    switch (hue * 3.6 as Integer){
        case 0..15: colorName = "Red"
            break
        case 16..45: colorName = "Orange"
            break
        case 46..75: colorName = "Yellow"
            break
        case 76..105: colorName = "Chartreuse"
            break
        case 106..135: colorName = "Green"
            break
        case 136..165: colorName = "Spring"
            break
        case 166..195: colorName = "Cyan"
            break
        case 196..225: colorName = "Azure"
            break
        case 226..255: colorName = "Blue"
            break
        case 256..285: colorName = "Violet"
            break
        case 286..315: colorName = "Magenta"
            break
        case 316..345: colorName = "Rose"
            break
        case 346..360: colorName = "Red"
            break
    }
    if (saturation == 0) colorName = "White"
    return colorName
}

void rgbw_white() {
    logging("rgbw_white()", 10)
    tasmota_rgbw_whiteForPlatform()
}

void rgbw_red() {
    logging("rgbw_red()", 10)
    tasmota_rgbw_setRGB(255, 0, 0)
}

void rgbw_green() {
    logging("rgbw_green()", 10)
    tasmota_rgbw_setRGB(0, 255, 0)
}

void rgbw_blue() {
    logging("rgbw_blue()", 10)
    tasmota_rgbw_setRGB(0, 0, 255)
}

void rgbw_yellow() {
    logging("rgbw_yellow()", 10)
    tasmota_rgbw_setRGB(255, 255, 0)
}

void rgbw_cyan() {
    logging("rgbw_cyan()", 10)
    tasmota_rgbw_setRGB(0, 255, 255)
}

void rgbw_pink() {
    logging("rgbw_pink()", 10)
    tasmota_rgbw_setRGB(255, 0, 255)
}
// END:  getHelperFunctions('rgbw')

// BEGIN:getHelperFunctions('tasmota-rgbw')
void tasmota_rgbw_setColorTemperature(value) {
    logging("tasmota_rgbw_setColorTemperature('${value}')", 10)
    if(device.currentValue('colorTemperature') != value ) sendEvent(name: "colorTemperature", value: value)
    Integer t = value != null ?  (value as Integer) : 0
    if(t > 6536) t = 6536
    if(t < 2000) t = 2000
    t = Math.round(1000000/t)
    if(t < 153) t = 153
    if(t > 500) t = 500
    state.mired = t
    state.hue = 0
    state.saturation = 0
    state.colorMode = "CT"
    logging("tasmota_rgbw_setColorTemperature('${t}') ADJUSTED to Mired", 10)
    tasmota_getAction(tasmota_getCommandStringWithModeReset("CT", "${t}"))
}

void tasmota_rgbw_setHSB(h, s, b) {
    logging("tasmota_rgbw_setHSB('${h}','${s}','${b}')", 10)
    tasmota_rgbw_setHSB(h, s, b, true)
}

void tasmota_rgbw_setHSB(h, s, b, callWhite) {
    logging("tasmota_rgbw_setHSB('${h}','${s}','${b}', callWhite=${String.valueOf(callWhite)})", 10)
    boolean adjusted = False
    if(h == null || h == 'NaN') {
        h = state != null && state.containsKey("hue") ? state.hue : 0
        adjusted = True
    }
    if(s == null || s == 'NaN') {
        s = state != null && state.containsKey("saturation") ? state.saturation : 0
        adjusted = True
    }
    if(b == null || b == 'NaN') {
        b = state != null && state.containsKey("level") ? state.level : 0
        adjusted = True
    }
    if(adjusted) {
        logging("ADJUSTED tasmota_rgbw_setHSB('${h}','${s}','${b}'", 1)
    }
    Integer adjustedH = Math.round(h*3.6)
    if( adjustedH > 360 ) { adjustedH = 360 }
    if( b < 0 ) b = 0
    if( b > 100 ) b = 100
    String hsbcmd = "${adjustedH},${s},${b}"
    logging("hsbcmd = ${hsbcmd}", 1)
    state.hue = h
    state.saturation = s
    state.level = b
    state.colorMode = "RGB"
    if (hsbcmd.startsWith("0,0,")) {
        rgbw_white()
    } else {
        if(useAlternateColorCommand == true) {
            def rgbval = hubitat.helper.ColorUtils.hsvToRGB([h, s, b])
            tasmota_rgbw_setRGB(rgbval[0], rgbval[1], rgbval[2])
        } else {
            tasmota_getAction(tasmota_getCommandStringWithModeReset("HsbColor", hsbcmd))
        }
    }
}

void tasmota_rgbw_setRGB(r, g, b) {   
    logging("tasmota_rgbw_setRGB('${r}','${g}','${b}')", 10)
    boolean adjusted = False
    if(r == null || r == 'NaN') {
        r = 0
        adjusted = True
    }
    if(g == null || g == 'NaN') {
        g = 0
        adjusted = True
    }
    if(b == null || b == 'NaN') {
        b = 0
        adjusted = True
    }
    if(adjusted) {
        logging("ADJUSTED tasmota_rgbw_setRGB('${r}','${g}','${b}')", 1)
    }
    String rgbcmd = hubitat.helper.ColorUtils.rgbToHEX([r, g, b])
    logging("rgbcmd = ${rgbcmd}", 1)
    state.red = r
    state.green = g
    state.blue = b
    def hsbColor = rgbw_rgbToHSB(r, g, b)
    logging("hsbColor from RGB: ${hsbColor}", 1)
    state.colorMode = "RGB"
    state.hue = hsbColor['hue']
    state.saturation = hsbColor['saturation']
    state.level = hsbColor['level']
    tasmota_getAction(tasmota_getCommandStringWithModeReset("Color1", rgbcmd))
}

void tasmota_rgbw_setLevel(l) {
    tasmota_rgbw_setLevel(l, 0)
}

void tasmota_rgbw_setLevel(l, duration) {
    if (duration == 0) {
        if (false && state.colorMode == "RGB") {
            tasmota_rgbw_setHSB(null, null, l)
        } else {
            state.level = l
            tasmota_getAction(tasmota_getCommandString("Dimmer", "${l}"))
        }
    } else if (duration > 0) {
        if (false && state.colorMode == "RGB") {
            tasmota_rgbw_setHSB(null, null, l)
        } else {
            if (duration > 5400) {
                log.warn "Maximum supported dimming duration is 5400 seconds due to current implementation method used."
                duration = 5400
            } 
            Integer cLevel = state.level
            
            Integer levelDistance = l - cLevel
            Integer direction = 1
            if(levelDistance < 0) {
                direction = -1
                levelDistance = levelDistance * -1
            }
            Integer steps = 13
            Integer increment = Math.round(((levelDistance as Float)  / steps) as Float)
            if(increment <= 1 && Math.abs(levelDistance) < steps) {
                steps = Math.abs(levelDistance)
            }
            List fadeCommands = []
            fadeCommands.add([command: "Fade", value: "1"])
            fadeCommands.add([command: "Speed", value: "20"])
            if(steps > 0) {

                duration = ((duration as Float) - (2 * steps * 0.2) + 1) as Float
                BigDecimal stepTime = round2((duration / steps) as Float, 1)
                Integer stepTimeTasmota = Math.round((stepTime as Float) * 10)
                BigDecimal lastStepTime = round2((stepTime + (duration - (stepTime * steps)) as Float), 1)
                Integer lastStepTimeTasmota = Math.round((lastStepTime as Float) * 10)
                
                Integer cmdLevel = cLevel
                
                (1..steps).each{
                    cmdLevel += (increment * direction)
                    if(direction == 1 && (cmdLevel > l || it == steps)) cmdLevel = l
                    if(direction == -1 && (cmdLevel < l || it == steps)) cmdLevel = l
                    if(it != steps) {
                        fadeCommands.add([command: "Delay", value: "$stepTimeTasmota"])
                    } else {
                        fadeCommands.add([command: "Delay", value: "$lastStepTimeTasmota"])
                    }
                    fadeCommands.add([command: "Dimmer", value: "$cmdLevel"])
                }
            } else {
                fadeCommands.add([command: "Dimmer", value: "$l"])
            }
            fadeCommands.add([command: "Fade", value: "0"])
            logging("fadeCommands: '" + fadeCommands + "', cmdData=${[cLevel:cLevel, levelDistance:levelDistance, direction:direction, steps:steps, increment:increment, stepTime:stepTime, lastStepTime:lastStepTime]}", 1)
            tasmota_getAction(tasmota_getMultiCommandString(fadeCommands))
        }
   }
}

void tasmota_rgbw_stopLevelChange() {
    tasmota_getAction(tasmota_getCommandString("Fade", "0"))
    tasmota_getAction(tasmota_getCommandString("Backlog", null))
    tasmota_rgbw_modeSingleColor(1)
}

void tasmota_rgbw_startLevelChange(String direction) {
    Integer cLevel = state.level
    Integer delay = 30
    tasmota_rgbw_modeSingleColor(1)
    if(direction == "up") {
        if(cLevel != null) {
            delay = Math.round(((delay / 100) * (100-cLevel)) as Float)
        }
        tasmota_rgbw_setLevel(100, delay)
    } else {
        if(cLevel != null) {
            delay = Math.round(((delay / 100) * (cLevel)) as Float)
        }
        tasmota_rgbw_setLevel(0, delay)
    }
}

void tasmota_rgbw_whiteForPlatform() {
    logging("tasmota_rgbw_whiteForPlatform()", 10)
    Integer l = state.level
    if (l < 10) l = 10
    l = Math.round(l * 2.55).toInteger()
    if (l > 255) l = 255
    def lHex = l.toHexString(l)
    String hexCmd = "#${lHex}${lHex}${lHex}${lHex}${lHex}"
    logging("hexCmd='${hexCmd}'", 1)
    state.hue = 0
    state.saturation = 0
    state.red = l
    state.green = l
    state.blue = l
    state.colorMode = "CT"
    tasmota_getAction(tasmota_getCommandStringWithModeReset("Color1", hexCmd))
}

void tasmota_rgbw_modeSet(Integer mode, BigDecimal speed=3) {
    logging("tasmota_rgbw_modeSet('${mode}')", 10)
    tasmota_getAction(tasmota_getMultiCommandString([[command:"Speed", value:"$speed"], [command:"Scheme", value:"${mode}"]]))
}

void tasmota_rgbw_modeNext(BigDecimal speed=3) {
    logging("tasmota_rgbw_modeNext()", 10)
    if (state.mode < 4) {
        state.mode = state.mode + 1
    } else {
        state.mode = 0
    }
    tasmota_rgbw_modeSet(state.mode, speed)
}

void tasmota_rgbw_modePrevious(BigDecimal speed=3) {
    if (state.mode > 0) {
        state.mode = state.mode - 1
    } else {
        state.mode = 4
    }
    tasmota_rgbw_modeSet(state.mode, speed)
}

void tasmota_rgbw_modeSingleColor(BigDecimal speed=3) {
    state.mode = 0
    tasmota_rgbw_modeSet(state.mode, speed)
}

void tasmota_rgbw_modeCycleUpColors(BigDecimal speed=3) {
    state.mode = 2
    tasmota_rgbw_modeSet(state.mode, speed)
}

void tasmota_rgbw_modeCycleDownColors(BigDecimal speed=3) {
    state.mode = 3
    tasmota_rgbw_modeSet(state.mode, speed)
}

void tasmota_rgbw_modeRandomColors(BigDecimal speed=3) {
    state.mode = 4
    tasmota_rgbw_modeSet(state.mode, speed)
}

void tasmota_rgbw_modeWakeUp(BigDecimal wakeUpDuration) {
    Integer level = device.currentValue('level')
    Integer nlevel = level > 10 ? level : 10
    logging("tasmota_rgbw_modeWakeUp(wakeUpDuration ${wakeUpDuration}, current level: ${nlevel})", 1)
    tasmota_rgbw_modeWakeUp(wakeUpDuration, nlevel)
}

void tasmota_rgbw_modeWakeUp(BigDecimal wakeUpDuration, BigDecimal level) {
    logging("tasmota_rgbw_modeWakeUp(wakeUpDuration ${wakeUpDuration}, level: ${level})", 1)
    state.mode = 1
    wakeUpDuration = wakeUpDuration < 1 ? 1 : wakeUpDuration > 3000 ? 3000 : wakeUpDuration
    level = level < 1 ? 1 : level > 100 ? 100 : level
    state.level = level
    tasmota_getAction(tasmota_getMultiCommandString([[command: "WakeupDuration", value: "${wakeUpDuration}"],
                                    [command: "Wakeup", value: "${level}"]]))
}

void tasmota_rgbw_setColorByRGBString(String colorRGB) {
    logging("tasmota_rgbw_setColorByRGBString(colorRGB ${colorRGB})", 100)
    tasmota_getAction(tasmota_getCommandStringWithModeReset("Color1", colorRGB.take(11)))
}

void tasmota_rgbw_setPixelColor(String colorRGB, BigDecimal pixel) {
    logging("tasmota_rgbw_setPixelColor(colorRGB ${colorRGB}, pixel: ${pixel})", 1)
    if(pixel < 1) pixel = 1
    if(pixel > 512) pixel = 512
    tasmota_getAction(tasmota_getCommandStringWithModeReset("Led$pixel", colorRGB.take(7)))
}

void tasmota_rgbw_setAddressablePixels(BigDecimal pixels) {
    logging("tasmota_rgbw_setAddressablePixels(pixels: ${pixels})", 100)
    if(pixels < 1) pixels = 1
    if(pixels > 512) pixels = 512
    tasmota_getAction(tasmota_getCommandString("Pixels", "$pixels"))
}

void tasmota_rgbw_setAddressableRotation(BigDecimal pixels) {
    logging("tasmota_rgbw_setAddressableRotation(pixels: ${pixels})", 100)
    if(pixels < 1) pixels = 1
    if(pixels > 512) pixels = 512
    tasmota_getAction(tasmota_getCommandString("Rotation", "$pixels"))
}

void tasmota_rgbw_setEffectWidth(BigDecimal pixels) {
    logging("tasmota_rgbw_setEffectWidth(pixels: ${pixels})", 100)
    if(pixels < 0) pixels = 0
    if(pixels > 4) pixels = 4
    tasmota_getAction(tasmota_getCommandString("Width1", "$pixels"))
}

String tasmota_getCommandStringWithModeReset(String command, String value) {
    if(useAlternateColorCommand == true && command == "Color1") {
        if(value.startsWith("#") == true) value = value.substring(1)
        return tasmota_getCommandString("Var1", "$value")
    } else {
        return tasmota_getMultiCommandString([[command: "Scheme", value: "0"], [command: "Fade", value: "0"], 
                                  [command: command, value: value]])
    }
}
// END:  getHelperFunctions('tasmota-rgbw')
