diff --git a/CODEOWNERS b/CODEOWNERS index b2c0d013be..2e8a828982 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -234,6 +234,7 @@ esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje +esphome/components/qwiic_pir/* @kahrendt esphome/components/radon_eye_ble/* @jeffeb3 esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet diff --git a/esphome/components/qwiic_pir/__init__.py b/esphome/components/qwiic_pir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/qwiic_pir/binary_sensor.py b/esphome/components/qwiic_pir/binary_sensor.py new file mode 100644 index 0000000000..360f8b506a --- /dev/null +++ b/esphome/components/qwiic_pir/binary_sensor.py @@ -0,0 +1,67 @@ +from esphome import core +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, binary_sensor +from esphome.const import ( + CONF_DEBOUNCE, + DEVICE_CLASS_MOTION, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@kahrendt"] + +qwiic_pir_ns = cg.esphome_ns.namespace("qwiic_pir") + +DebounceMode = qwiic_pir_ns.enum("DebounceMode") +DEBOUNCE_MODE_OPTIONS = { + "RAW": DebounceMode.RAW_DEBOUNCE_MODE, + "NATIVE": DebounceMode.NATIVE_DEBOUNCE_MODE, + "HYBRID": DebounceMode.HYBRID_DEBOUNCE_MODE, +} + +CONF_DEBOUNCE_MODE = "debounce_mode" + +QwiicPIRComponent = qwiic_pir_ns.class_( + "QwiicPIRComponent", cg.Component, i2c.I2CDevice, binary_sensor.BinarySensor +) + + +def validate_no_debounce_unless_native(config): + if CONF_DEBOUNCE in config: + if config[CONF_DEBOUNCE_MODE] != "NATIVE": + raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE") + return config + + +CONFIG_SCHEMA = cv.All( + binary_sensor.binary_sensor_schema( + QwiicPIRComponent, + device_class=DEVICE_CLASS_MOTION, + ) + .extend( + { + cv.Optional(CONF_DEBOUNCE): cv.All( + cv.time_period, + cv.Range(max=core.TimePeriod(milliseconds=65535)), + ), + cv.Optional(CONF_DEBOUNCE_MODE, default="HYBRID"): cv.enum( + DEBOUNCE_MODE_OPTIONS, upper=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x12)), + validate_no_debounce_unless_native, +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if debounce_time_setting := config.get(CONF_DEBOUNCE): + cg.add(var.set_debounce_time(debounce_time_setting.total_milliseconds)) + else: + cg.add(var.set_debounce_time(1)) # default to 1 ms if not configured + cg.add(var.set_debounce_mode(config[CONF_DEBOUNCE_MODE])) diff --git a/esphome/components/qwiic_pir/qwiic_pir.cpp b/esphome/components/qwiic_pir/qwiic_pir.cpp new file mode 100644 index 0000000000..c267554c45 --- /dev/null +++ b/esphome/components/qwiic_pir/qwiic_pir.cpp @@ -0,0 +1,137 @@ +#include "qwiic_pir.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace qwiic_pir { + +static const char *const TAG = "qwiic_pir"; + +void QwiicPIRComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up Qwiic PIR..."); + + // Verify I2C communcation by reading and verifying the chip ID + uint8_t chip_id; + + if (!this->read_byte(QWIIC_PIR_CHIP_ID, &chip_id)) { + ESP_LOGE(TAG, "Failed to read the chip's ID"); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + if (chip_id != QWIIC_PIR_DEVICE_ID) { + ESP_LOGE(TAG, "Unknown chip ID, is this a Qwiic PIR?"); + + this->error_code_ = ERROR_WRONG_CHIP_ID; + this->mark_failed(); + + return; + } + + if (!this->write_byte_16(QWIIC_PIR_DEBOUNCE_TIME, this->debounce_time_)) { + ESP_LOGE(TAG, "Failed to configure debounce time."); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + if (this->debounce_mode_ == NATIVE_DEBOUNCE_MODE) { + // Publish the starting raw state of the PIR sensor + // If NATIVE mode, the binary_sensor state would be unknown until a motion event + if (!this->read_byte(QWIIC_PIR_EVENT_STATUS, &this->event_register_.reg)) { + ESP_LOGE(TAG, "Failed to read initial sensor state."); + + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + + return; + } + + this->publish_state(this->event_register_.raw_reading); + } +} + +void QwiicPIRComponent::loop() { + // Read Event Register + if (!this->read_byte(QWIIC_PIR_EVENT_STATUS, &this->event_register_.reg)) { + ESP_LOGW(TAG, "Failed to communicate with sensor"); + + return; + } + + if (this->debounce_mode_ == HYBRID_DEBOUNCE_MODE) { + // Use a combination of the raw sensor reading and the device's event detection to determine state + // - The device is hardcoded to use a debounce time of 1 ms in this mode + // - Any event, even if it is object_removed, implies motion was active since the last loop, so publish true + // - Use ESPHome's built-in filters for debouncing + this->publish_state(this->event_register_.raw_reading || this->event_register_.event_available); + + if (this->event_register_.event_available) { + this->clear_events_(); + } + } else if (this->debounce_mode_ == NATIVE_DEBOUNCE_MODE) { + // Uses the device's firmware to debounce the signal + // - Follows the logic of SparkFun's example implementation: + // https://github.com/sparkfun/SparkFun_Qwiic_PIR_Arduino_Library/blob/master/examples/Example2_PrintPIRStatus/Example2_PrintPIRStatus.ino + // (accessed July 2023) + // - Is unreliable at detecting an object being removed, especially at debounce rates even slightly large + if (this->event_register_.event_available) { + // If an object is detected, publish true + if (this->event_register_.object_detected) + this->publish_state(true); + + // If an object has been removed, publish false + if (this->event_register_.object_removed) + this->publish_state(false); + + this->clear_events_(); + } + } else if (this->debounce_mode_ == RAW_DEBOUNCE_MODE) { + // Publishes the raw PIR sensor reading with no further logic + // - May miss a very short motion detection if the ESP's loop time is slow + this->publish_state(this->event_register_.raw_reading); + } +} + +void QwiicPIRComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Qwiic PIR:"); + + if (this->debounce_mode_ == RAW_DEBOUNCE_MODE) { + ESP_LOGCONFIG(TAG, " Debounce Mode: RAW"); + } else if (this->debounce_mode_ == NATIVE_DEBOUNCE_MODE) { + ESP_LOGCONFIG(TAG, " Debounce Mode: NATIVE"); + ESP_LOGCONFIG(TAG, " Debounce Time: %ums", this->debounce_time_); + } else if (this->debounce_mode_ == HYBRID_DEBOUNCE_MODE) { + ESP_LOGCONFIG(TAG, " Debounce Mode: HYBRID"); + } + + switch (this->error_code_) { + case NONE: + break; + case ERROR_COMMUNICATION_FAILED: + ESP_LOGE(TAG, " Communication with Qwiic PIR failed!"); + break; + case ERROR_WRONG_CHIP_ID: + ESP_LOGE(TAG, " Qwiic PIR has wrong chip ID - please verify you are using a Qwiic PIR"); + break; + default: + ESP_LOGE(TAG, " Qwiic PIR error code %d", (int) this->error_code_); + break; + } + + LOG_I2C_DEVICE(this); + LOG_BINARY_SENSOR(" ", "Qwiic PIR Binary Sensor", this); +} + +void QwiicPIRComponent::clear_events_() { + // Clear event status register + if (!this->write_byte(QWIIC_PIR_EVENT_STATUS, 0x00)) + ESP_LOGW(TAG, "Failed to clear events on sensor"); +} + +} // namespace qwiic_pir +} // namespace esphome diff --git a/esphome/components/qwiic_pir/qwiic_pir.h b/esphome/components/qwiic_pir/qwiic_pir.h new file mode 100644 index 0000000000..d58d67734f --- /dev/null +++ b/esphome/components/qwiic_pir/qwiic_pir.h @@ -0,0 +1,70 @@ +/* + * Adds support for Qwiic PIR motion sensors that communicate over an I2C bus. + * These sensors use Sharp PIR motion sensors to detect motion. A firmware running on an ATTiny84 translates the digital + * output to I2C communications. + * ATTiny84 firmware: https://github.com/sparkfun/Qwiic_PIR (acccessed July 2023) + * SparkFun's Arduino library: https://github.com/sparkfun/SparkFun_Qwiic_PIR_Arduino_Library (accessed July 2023) + */ + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace qwiic_pir { + +// Qwiic PIR I2C Register Addresses +enum { + QWIIC_PIR_CHIP_ID = 0x00, + QWIIC_PIR_EVENT_STATUS = 0x03, + QWIIC_PIR_DEBOUNCE_TIME = 0x05, // uint16_t debounce time in milliseconds +}; + +enum DebounceMode { + RAW_DEBOUNCE_MODE, + NATIVE_DEBOUNCE_MODE, + HYBRID_DEBOUNCE_MODE, +}; + +static const uint8_t QWIIC_PIR_DEVICE_ID = 0x72; + +class QwiicPIRComponent : public Component, public i2c::I2CDevice, public binary_sensor::BinarySensor { + public: + void setup() override; + void loop() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_debounce_time(uint16_t debounce_time) { this->debounce_time_ = debounce_time; } + void set_debounce_mode(DebounceMode mode) { this->debounce_mode_ = mode; } + + protected: + uint16_t debounce_time_{}; + + DebounceMode debounce_mode_{}; + + enum ErrorCode { + NONE = 0, + ERROR_COMMUNICATION_FAILED, + ERROR_WRONG_CHIP_ID, + } error_code_{NONE}; + + union { + struct { + bool raw_reading : 1; // raw state of PIR sensor + bool event_available : 1; // a debounced object has been detected or removed + bool object_removed : 1; // a debounced object is no longer detected + bool object_detected : 1; // a debounced object has been detected + bool : 4; + }; + uint8_t reg; + } event_register_ = {.reg = 0}; + + void clear_events_(); +}; + +} // namespace qwiic_pir +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 0edc0bbaa7..0d95dd6cb8 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1813,6 +1813,9 @@ binary_sensor: name: still out_pin_presence_status: name: out pin presence status + - platform: qwiic_pir + i2c_id: i2c_bus + name: "Qwiic PIR Motion Sensor" pca9685: frequency: 500