/** * Copyright 2023-2024 Bloodtick, kkossev * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * * ver. 1.0.0 2022-10-12 kkossev - inital version * ver. 1.0.1 2022-10-13 kkossev - motion child devices are created for each zone; singleThreaded: true * ver. 1.0.2 2022-10-16 kkossev - (dev. branch) added illuminance threshold (default 5 lux); zone child devices logs use the device name * * TODO: debug logs auto off * TODO: change mode movement zoneState to ENUM */ @SuppressWarnings('unused') public static String version() {return "1.0.2"} public static String timeStamp() {return "10/16/2024 10:29 PM"} import groovy.transform.Field import com.hubitat.app.DeviceWrapper import com.hubitat.app.ChildDeviceWrapper @Field static Boolean _DEBUG = false @Field static Integer DEFAULT_ILLUMINANCE_THRESHOLD = 5 metadata { definition(name: "Replica Aqara FP2", namespace: "replica", author: "kkossev", importUrl:"https://raw.githubusercontent.com/kkossev/Hubitat/refs/heads/development/Drivers/Replica/replicaAqaraFP2.groovy", singleThreaded: true) { capability "Actuator" capability "Configuration" capability "MotionSensor" capability "Refresh" capability "IlluminanceMeasurement" attribute "healthStatus", "enum", ["offline", "online"] attribute "mode", "string" attribute "movement", "string" attribute "zoneState", "string" // ST: "multipleZonePresence" "zoneState" "enum", ["inactive", "approaching", "movingAway", "entering", "leaving", "enteringLeft", "enteringRight", "leavingLeft", "leavingRight" ] command "inactive" command "active" command "setIlluminance", [[name: "illuminance*", type: "NUMBER", description: "Set Illuminance in lux"]] command "setModeValue", [[name: "mode*", type: "STRING", description: "Set Mode"]] command "setMovementValue", [[name: "mode*", type: "STRING", description: "Set Movement State"]] command "setZoneStateValue", [[name: "zone*", type: "STRING", description: "Set Zone State"]] if (_DEBUG) { command "deleteAllChildren" } } preferences { input(name:"deviceInfoDisable", type: "bool", title: "Disable Info logging:", defaultValue: false) input(name:"deviceDebugEnable", type: "bool", title: "Enable Debug logging:", defaultValue: false) input('illuminanceThreshold', 'number', title: 'Lux threshold', description: 'Minimum change in the lux which will trigger an event', range: '0..999', defaultValue: DEFAULT_ILLUMINANCE_THRESHOLD) } } def installed() { initialize() } def updated() { initialize() } def initialize() { updateDataValue("triggers", groovy.json.JsonOutput.toJson(getReplicaTriggers())) updateDataValue("commands", groovy.json.JsonOutput.toJson(getReplicaCommands())) refresh() } def configure() { logInfo "${device.displayName} configured default rules" initialize() updateDataValue("rules", getReplicaRules()) sendCommand("configure") } // Methods documented here will show up in the Replica Command Configuration. These should be mostly setter in nature. Map getReplicaCommands() { return ([ "setMotionValue":[[name:"motion*",type:"ENUM"]], "setMotionActive":[], "setMotionInactive":[], "setIlluminanceValue":[[name:"illuminance*",type:"NUMBER"]], "setHealthStatusValue":[[name:"healthStatus*",type:"ENUM"]], "setModeValue":[[name:"mode*",type:"ENUM"]], "setMovementValue":[[name:"movement*",type:"STRING"]], "setZoneStateValue":[[name:"zoneState*",type:"STRING"]] ]) } def setIlluminanceValue(value) { Integer lastIllum = device.currentValue('illuminance') ?: 0 Integer delta = Math.abs(lastIllum - (value as Integer)) Integer threshold = (settings?.illuminanceThreshold ?: DEFAULT_ILLUMINANCE_THRESHOLD) as int if (delta < threshold) { logDebug "skipped illuminance ${value}, less than delta ${threshold} (lastIllum=${lastIllum})" return } String descriptionText = "${device.displayName} illuminance is $value lux" sendEvent(name: "illuminance", value: value, unit: "lx", descriptionText: descriptionText) logInfo descriptionText } def setMotionValue(value) { String descriptionText = "${device.displayName} motion is $value" sendEvent(name: "motion", value: value, descriptionText: descriptionText) logInfo descriptionText } def setModeValue(value) { String descriptionText = "${device.displayName} mode is $value" sendEvent(name: "mode", value: value, descriptionText: descriptionText) logInfo descriptionText } def setMovementValue(value) { String descriptionText = "${device.displayName} movement is $value" sendEvent(name: "movement", value: value, descriptionText: descriptionText) logInfo descriptionText } def setZoneStateValue(value) { //String descriptionText = "${device.displayName} zoneState is $value" //logDebug descriptionText parseZoneString(value) // ArrayList } def parseZoneString(ArrayList value) { logDebug "parseZoneString: ${value}" value.each { zone -> // Zone: [id:5, name:Microwave Zone, state:not present] ChildDeviceWrapper dw = getDw(zone.id as int) if (!dw) { logWarn "creating child device for zone ${zone.id} ${zone.name}" createChildDevice(zone.id as int, zone.name) dw = getDw(zone.id as int) } if (dw == null) { log.error "No child device was created for zone ${zone.id}" ; return } String currentValue = dw.currentValue("motion") ?: 'unknown' String childDeviceName = dw.device.displayName ?: zone.name Map event = [:] event.name = "motion" event.value = zone.state == "present" ? "active" : "inactive" event.descriptionText = "${childDeviceName} zone ${zone.id} state is ${zone.state}" if (currentValue == event.value) { logDebug "ignoring child device ${dw.device.displayName} zone ${zone.id} duplicated state ${event.value}" return // Skip to the next iteration } logDebug "Sending event ${event}" dw.parse([event]) } } String getChildDeviceId(int zoneId) { return "${device.id}-${String.format('%02d', zoneId)}" } ChildDeviceWrapper getDw(int zoneId) { String id = getChildDeviceId(zoneId) return getChildDevice(id) } void createChildDevice(int zoneId, String zoneName) { if (zoneId == 0 || zoneId >20) { return } if (zoneName == null || zoneName == "") { zoneName = "Zone ${zoneId}" } String childId = getChildDeviceId(zoneId) DeviceWrapper existingChild = getChildDevices()?.find { it.deviceNetworkId == childId } if (existingChild) { logWarn "${device.displayName} Child device ${childId} already exists (${existingChild})" return } log.info "${device.displayName} Creating device ${childId} zoneName ${zoneName}" addChildDevice('hubitat', 'Generic Component Motion Sensor', childId, [isComponent: false, name: "${device.displayName} zone ${zoneId}", label: zoneName]) } void deleteAllChildren() { logDebug 'Parent deleteChildren' getChildDevices().each { child -> log.info "${device.displayName} Deleting ${child.deviceNetworkId}" deleteChildDevice(child.deviceNetworkId) } } def setMotionActive() { setMotionValue("active") } def setMotionInactive() { setMotionValue("inactive") } def setHealthStatusValue(value) { sendEvent(name: "healthStatus", value: value, descriptionText: "${device.displayName} healthStatus set to $value") } // Methods documented here will show up in the Replica Trigger Configuration. These should be all of the native capability commands Map getReplicaTriggers() { return ([ "inactive":[] , "active":[], "setIlluminance":[[name:"illuminance*",type:"NUMBER"]], "refresh":[]]) } private def sendCommand(String name, def value=null, String unit=null, data=[:]) { data.version=version() parent?.deviceTriggerHandler(device, [name:name, value:value, unit:unit, data:data, now:now()]) } def inactive() { sendCommand("inactive") } def active() { sendCommand("active") } def setIlluminance(lux) { sendCommand("setIlluminance", lux, "lx") } void refresh() { sendCommand("refresh") } String getReplicaRules() { return """{"components":[{"command":{"label":"command: setHealthStatusValue(healthStatus*)","name":"setHealthStatusValue","parameters":[{"name":"healthStatus*","type":"ENUM"}],"type":"command"},"mute":true,"trigger":{"additionalProperties":false,"attribute":"healthStatus","capability":"healthCheck","label":"attribute: healthStatus.*","properties":{"value":{"title":"HealthState","type":"string"}},"required":["value"],"type":"attribute"},"type":"smartTrigger"},{"command":{"capability":"refresh","label":"command: refresh()","name":"refresh","type":"command"},"trigger":{"label":"command: refresh()","name":"refresh","type":"command"},"type":"hubitatTrigger"},{"command":{"label":"command: setIlluminanceValue(illuminance*)","name":"setIlluminanceValue","parameters":[{"name":"illuminance*","type":"NUMBER"}],"type":"command"},"trigger":{"additionalProperties":false,"attribute":"illuminance","capability":"illuminanceMeasurement","label":"attribute: illuminance.*","properties":{"unit":{"default":"lux","enum":["lux"],"type":"string"},"value":{"maximum":100000,"minimum":0,"type":"number"}},"required":["value"],"type":"attribute"},"type":"smartTrigger"},{"command":{"label":"command: setMotionActive()","name":"setMotionActive","type":"command"},"trigger":{"additionalProperties":false,"attribute":"presence","capability":"presenceSensor","dataType":"ENUM","label":"attribute: presence.present","properties":{"value":{"title":"PresenceState","type":"string"}},"required":["value"],"type":"attribute","value":"present"},"type":"smartTrigger"},{"command":{"label":"command: setMotionInactive()","name":"setMotionInactive","type":"command"},"trigger":{"additionalProperties":false,"attribute":"presence","capability":"presenceSensor","dataType":"ENUM","label":"attribute: presence.not present","properties":{"value":{"title":"PresenceState","type":"string"}},"required":["value"],"type":"attribute","value":"not present"},"type":"smartTrigger"},{"command":{"label":"command: setModeValue(mode*)","name":"setModeValue","parameters":[{"name":"mode*","type":"ENUM"}],"type":"command"},"trigger":{"additionalProperties":false,"attribute":"mode","capability":"stse.deviceMode","label":"attribute: mode.*","properties":{"value":{"type":"string"}},"required":["value"],"type":"attribute"},"type":"smartTrigger"},{"command":{"label":"command: setMovementValue(movement*)","name":"setMovementValue","parameters":[{"name":"movement*","type":"STRING"}],"type":"command"},"trigger":{"additionalProperties":false,"attribute":"movement","capability":"movementSensor","label":"attribute: movement.*","properties":{"value":{"title":"MovementType","type":"string"}},"required":["value"],"type":"attribute"},"type":"smartTrigger"},{"command":{"label":"command: setZoneStateValue(zoneState*)","name":"setZoneStateValue","parameters":[{"name":"zoneState*","type":"STRING"}],"type":"command"},"trigger":{"additionalProperties":false,"attribute":"zoneState","capability":"multipleZonePresence","label":"attribute: zoneState.*","properties":{"value":{"items":{"additionalProperties":false,"properties":{"id":{"maxLength":255,"title":"String","type":"string"},"name":{"maxLength":255,"title":"String","type":"string"},"state":{"enum":["present","not present"],"title":"PresenceState","type":"string"}},"required":["id","name","state"],"title":"zoneState","type":"object"},"type":"array"}},"required":["value"],"type":"attribute"},"type":"smartTrigger"}],"version":1} }""" } private logInfo(msg) { if(settings?.deviceInfoDisable != true) { log.info "${msg}" } } private logDebug(msg) { if(settings?.deviceDebugEnable == true) { log.debug "${msg}" } } private logTrace(msg) { if(settings?.deviceTraceEnable == true) { log.trace "${msg}" } } private logWarn(msg) { log.warn "${msg}" } private logError(msg) { log.error "${msg}" }