diff --git a/drivers/athom-human-presence-sensor.groovy b/drivers/athom-human-presence-sensor.groovy new file mode 100644 index 0000000..e681390 --- /dev/null +++ b/drivers/athom-human-presence-sensor.groovy @@ -0,0 +1,278 @@ +/** + * MIT License + * Copyright 2022 Jonathan Bradshaw (jb@nrgup.net) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +metadata { + definition( + name: 'Athom Human Presence Sensor', + namespace: 'esphome', + author: 'Joshua Glemza', + singleThreaded: true, + importUrl: 'https://github.com/jglemza/hubitat/raw/main/drivers/athom-human-presence-sensor.groovy') { + + capability 'Configuration' + capability 'IlluminanceMeasurement' + capability 'MotionSensor' + capability 'Sensor' + capability 'Refresh' + capability 'Initialize' + + attribute 'distance', 'number' + attribute 'mmwave', 'enum', [ 'active', 'not active' ] + attribute 'pir', 'enum', [ 'active', 'not active' ] + + // attribute populated by ESPHome API Library automatically + attribute 'networkStatus', 'enum', [ 'connecting', 'online', 'offline' ] + + command 'restart' + } + + preferences { + input name: 'ipAddress', // required setting for API library + type: 'text', + title: 'Device IP Address', + required: true + + input name: 'password', // optional setting for API library + type: 'text', + title: 'Device Password', + description: '(if required)', + required: false + + input name: 'txtEnable', type: 'bool', title: 'Enable descriptionText logging', defaultValue: true, description: \ + 'Enables command logging.' + + input name: 'logEnable', type: 'bool', title: 'Enable debug logging', defaultValue: false, description: \ + 'Turns on debug logging for 30 minutes.' + } +} + +void configure() { + +} + +void initialize() { + // API library command to open socket to device, it will automatically reconnect if needed + openSocket() + + if (logEnable) { + runIn(1800, 'logsOff') + } +} + +void installed() { + log.info "${device} driver installed" +} + +void logsOff() { + espHomeSubscribeLogs(LOG_LEVEL_INFO, false) // disable device logging + device.updateSetting('logEnable', false) + log.info "${device} debug logging disabled" +} + +void refresh() { + log.info "${device} refresh" + state.clear() + state.entities = [:] + state.requireRefresh = true + espHomeDeviceInfoRequest() +} + +void restart() { + log.info "${device} restart" + espHomeButtonCommand([ key: state.entities['restart'] as Long ]) +} + +void updated() { + log.info "${device} driver configuration updated" + initialize() + runIn(5, 'configure') +} + +void uninstalled() { + closeSocket('driver uninstalled') // make sure the socket is closed when uninstalling + log.info "${device} driver uninstalled" +} + +// the parse method is invoked by the API library when messages are received +void parse(final Map message) { + if (logEnable) { log.debug "ESPHome received: ${message}" } + + switch (message.type) { + case 'device': + // Device information + break + + case 'entity': + parseKeys(message) + break + + case 'state': + parseState(message) + } +} + +void parseKeys(final Map message) { + if (logEnable) { log.debug "ESPHome entity: ${message}" } + if (state.entities == null) { state.entities = [:] } + final long key = message.key as long + + switch (message.objectId) { + case 'light_sensor': + // Illuminance Sensor + state.entities['illuminance'] = key + break + case 'occupancy': + // Occupancy Sensor + state.entities['occupancy'] = key + break + case 'pir_sensor': + // Passive Infrared Sensor + state.entities['pir'] = key + break + case 'status_led': + // ESP32 Status LED + state.entities['status_led'] = key + break + case 'farthest_detection': + // Millimeter wave radar sensor distance + state.entities['mmwave_distance'] = key + break + case 'fading_time': + // Millimeter wave radar sensor off latency + state.entities['mmwave_off_latency'] = key + break + case 'detection_delay': + // Millimeter wave radar sensor on latency + state.entities['mmwave_on_latency'] = key + break + case 'trigger_sensitivity': + // Millimeter wave radar sensor sensitivity + state.entities['mmwave_sensitivity'] = key + break + case 'mmwave_sensor': + // Millimeter wave radar sensor switch + state.entities['mmwave_switch'] = key + break + case 'uart_presence_output': + case 'uart_target_output': + // ignore + break + default: + log.warn "ESPHome entity not supported: ${message}" + break + } +} + +void parseState(final Map message) { + if (logEnable) { log.debug "ESPHome state: ${message}" } + if (message.key == null) { return } + final long key = message.key as long + + switch (key) { + case state.entities['illuminance']: + // Illuminance Sensor + if (message.hasState) { + updateAttribute('illuminance', message.state as Integer, 'lx') + } + break + case state.entities['mmwave']: + // Millimeter wave radar sensor + if (message.hasState) { + updateAttribute('mmwave', message.state ? 'active' : 'inactive') + } + break + case state.entities['occupancy']: + // Combined Millimeter wave radar and PIR + if (message.hasState) { + updateAttribute('motion', message.state ? 'active' : 'inactive') + } + break + case state.entities['pir']: + // PIR sensor + if (message.hasState) { + updateAttribute('pir', message.state ? 'active' : 'inactive') + } + break + case state.entities['status_led']: + // ESP32 Status LED + if (message.hasState) { + log.info "ESP32 Status LED: ${message.state}" + device.updateSetting('statusLedEnable', message.state) + } + break + case state.entities['mmwave_distance']: + // Millimeter wave radar sensor distance + if (message.hasState) { + log.info "Millimeter wave radar sensor distance: ${message.state}" + device.updateSetting('mmwaveDistance', message.state) + } + break + case state.entities['mmwave_off_latency']: + // Millimeter wave radar sensor off latency + if (message.hasState) { + log.info "Millimeter wave radar sensor off latency: ${message.state}" + device.updateSetting('mmwaveOffLatency', message.state) + } + break + case state.entities['mmwave_on_latency']: + // Millimeter wave radar sensor on latency + if (message.hasState) { + log.info "Millimeter wave radar sensor on latency: ${message.state}" + device.updateSetting('mmwaveOnLatency', message.state) + } + break + case state.entities['mmwave_sensitivity']: + // Millimeter wave radar sensor sensitivity + if (message.hasState) { + log.info "Millimeter wave radar sensor sensitivity: ${message.state}" + device.updateSetting('mmwaveSensitivity', message.state) + } + break + } +} + +/** + * Check if the specified value is null or empty + * @param value value to check + * @return true if the value is null or empty, false otherwise + */ +private static boolean isNullOrEmpty(final Object value) { + return value == null || (value as String).trim().isEmpty() +} + +/** + * Update the specified device attribute with the specified value and log if changed + * @param attribute name of the attribute + * @param value value of the attribute + * @param unit unit of the attribute + * @param type type of the attribute + */ +private void updateAttribute(final String attribute, final Object value, final String unit = null, final String type = null) { + final String descriptionText = "${attribute} was set to ${value}${unit ?: ''}" + if (device.currentValue(attribute) != value && settings.txtEnable) { + log.info descriptionText + } + sendEvent(name: attribute, value: value, unit: unit, type: type, descriptionText: descriptionText) +} + +// Put this line at the end of the driver to include the ESPHome API library helper +#include esphome.espHomeApiHelper