diff --git a/CODEOWNERS b/CODEOWNERS index 815f10ce0b..d48294bad3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -85,6 +85,7 @@ esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/* @jesserockz +esphome/components/emc2101/* @ellull esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz @@ -122,6 +123,7 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode esphome/components/homeassistant/* @OttoWinter esphome/components/honeywellabp/* @RubyBailey +esphome/components/honeywellabp2_i2c/* @jpfaff esphome/components/host/* @esphome/core esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hte501/* @Stock-M @@ -233,6 +235,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/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 17f67002a3..8404e07894 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -104,7 +104,8 @@ void CurrentBasedCover::loop() { ESP_LOGD(TAG, "'%s' - Close position reached. Took %.1fs.", this->name_.c_str(), dur); this->direction_idle_(COVER_CLOSED); } - } else if (now - this->start_dir_time_ > this->max_duration_) { + } + if (now - this->start_dir_time_ > this->max_duration_) { ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str()); this->direction_idle_(); } diff --git a/esphome/components/emc2101/__init__.py b/esphome/components/emc2101/__init__.py new file mode 100644 index 0000000000..7a7b31cf14 --- /dev/null +++ b/esphome/components/emc2101/__init__.py @@ -0,0 +1,81 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_INVERTED, CONF_RESOLUTION + +CODEOWNERS = ["@ellull"] + +DEPENDENCIES = ["i2c"] + +CONF_PWM = "pwm" +CONF_DIVIDER = "divider" +CONF_DAC = "dac" +CONF_CONVERSION_RATE = "conversion_rate" + +CONF_EMC2101_ID = "emc2101_id" + +emc2101_ns = cg.esphome_ns.namespace("emc2101") + +Emc2101DACConversionRate = emc2101_ns.enum("Emc2101DACConversionRate") +CONVERSIONS_PER_SECOND = { + "1/16": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_16S, + "1/8": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_8S, + "1/4": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_4S, + "1/2": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_2S, + "1": Emc2101DACConversionRate.Emc2101_DAC_1_EVERY_SECOND, + "2": Emc2101DACConversionRate.Emc2101_DAC_2_EVERY_SECOND, + "4": Emc2101DACConversionRate.Emc2101_DAC_4_EVERY_SECOND, + "8": Emc2101DACConversionRate.Emc2101_DAC_8_EVERY_SECOND, + "16": Emc2101DACConversionRate.Emc2101_DAC_16_EVERY_SECOND, + "32": Emc2101DACConversionRate.Emc2101_DAC_32_EVERY_SECOND, +} + +Emc2101Component = emc2101_ns.class_("Emc2101Component", cg.Component, i2c.I2CDevice) + +EMC2101_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_EMC2101_ID): cv.use_id(Emc2101Component), + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Emc2101Component), + cv.Optional(CONF_PWM): cv.Schema( + { + cv.Optional(CONF_RESOLUTION, default=23): cv.int_range( + min=0, max=31 + ), + cv.Optional(CONF_DIVIDER, default=1): cv.uint8_t, + } + ), + cv.Optional(CONF_DAC): cv.Schema( + { + cv.Optional(CONF_CONVERSION_RATE, default="16"): cv.enum( + CONVERSIONS_PER_SECOND + ), + } + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x4C)), + cv.has_exactly_one_key(CONF_PWM, CONF_DAC), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if pwm_config := config.get(CONF_PWM): + cg.add(var.set_dac_mode(False)) + cg.add(var.set_pwm_resolution(pwm_config[CONF_RESOLUTION])) + cg.add(var.set_pwm_divider(pwm_config[CONF_DIVIDER])) + if dac_config := config.get(CONF_DAC): + cg.add(var.set_dac_mode(True)) + cg.add(var.set_dac_conversion_rate(dac_config[CONF_CONVERSION_RATE])) + cg.add(var.set_inverted(config[CONF_INVERTED])) diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp new file mode 100644 index 0000000000..2f45b2e27a --- /dev/null +++ b/esphome/components/emc2101/emc2101.cpp @@ -0,0 +1,169 @@ +// Implementation based on: +// - Adafruit_EMC2101: https://github.com/adafruit/Adafruit_EMC2101 +// - Official Datasheet: https://ww1.microchip.com/downloads/en/DeviceDoc/2101.pdf + +#include "esphome/core/log.h" +#include "emc2101.h" + +namespace esphome { +namespace emc2101 { + +static const char *const TAG = "EMC2101"; + +static const uint8_t EMC2101_CHIP_ID = 0x16; // EMC2101 default device id from part id +static const uint8_t EMC2101_ALT_CHIP_ID = 0x28; // EMC2101 alternate device id from part id + +// EMC2101 registers from the datasheet. We only define what we use. +static const uint8_t EMC2101_REGISTER_INTERNAL_TEMP = 0x00; // The internal temperature register +static const uint8_t EMC2101_REGISTER_EXTERNAL_TEMP_MSB = 0x01; // high byte for the external temperature reading +static const uint8_t EMC2101_REGISTER_DAC_CONV_RATE = 0x04; // DAC convesion rate config +static const uint8_t EMC2101_REGISTER_EXTERNAL_TEMP_LSB = 0x10; // low byte for the external temperature reading +static const uint8_t EMC2101_REGISTER_CONFIG = 0x03; // configuration register +static const uint8_t EMC2101_REGISTER_TACH_LSB = 0x46; // Tach RPM data low byte +static const uint8_t EMC2101_REGISTER_TACH_MSB = 0x47; // Tach RPM data high byte +static const uint8_t EMC2101_REGISTER_FAN_CONFIG = 0x4A; // General fan config register +static const uint8_t EMC2101_REGISTER_FAN_SETTING = 0x4C; // Fan speed for non-LUT settings +static const uint8_t EMC2101_REGISTER_PWM_FREQ = 0x4D; // PWM frequency setting +static const uint8_t EMC2101_REGISTER_PWM_DIV = 0x4E; // PWM frequency divisor +static const uint8_t EMC2101_REGISTER_WHOAMI = 0xFD; // Chip ID register + +// EMC2101 configuration bits from the datasheet. We only define what we use. + +// Determines the funcionallity of the ALERT/TACH pin. +// 0 (default): The ALERT/TECH pin will function as an open drain, active low interrupt. +// 1: The ALERT/TECH pin will function as a high impedance TACH input. This may require an +// external pull-up resistor to set the proper signaling levels. +static const uint8_t EMC2101_ALT_TCH_BIT = 1 << 2; + +// Determines the FAN output mode. +// 0 (default): PWM output enabled at FAN pin. +// 1: DAC output enabled at FAN ping. +static const uint8_t EMC2101_DAC_BIT = 1 << 4; + +// Overrides the CLK_SEL bit and uses the Frequency Divide Register to determine +// the base PWM frequency. It is recommended that this bit be set for maximum PWM resolution. +// 0 (default): The base clock frequency for the PWM is determined by the CLK_SEL bit. +// 1 (recommended): The base clock that is used to determine the PWM frequency is set by the +// Frequency Divide Register +static const uint8_t EMC2101_CLK_OVR_BIT = 1 << 2; + +// Sets the polarity of the Fan output driver. +// 0 (default): The polarity of the Fan output driver is non-inverted. A '00h' setting will +// correspond to a 0% duty cycle or a minimum DAC output voltage. +// 1: The polarity of the Fan output driver is inverted. A '00h' setting will correspond to a +// 100% duty cycle or a maximum DAC output voltage. +static const uint8_t EMC2101_POLARITY_BIT = 1 << 4; + +float Emc2101Component::get_setup_priority() const { return setup_priority::HARDWARE; } + +void Emc2101Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up Emc2101 sensor..."); + + // make sure we're talking to the right chip + uint8_t chip_id = reg(EMC2101_REGISTER_WHOAMI).get(); + if ((chip_id != EMC2101_CHIP_ID) && (chip_id != EMC2101_ALT_CHIP_ID)) { + ESP_LOGE(TAG, "Wrong chip ID %02X", chip_id); + this->mark_failed(); + return; + } + + // Configure EMC2101 + i2c::I2CRegister config = reg(EMC2101_REGISTER_CONFIG); + config |= EMC2101_ALT_TCH_BIT; + if (this->dac_mode_) { + config |= EMC2101_DAC_BIT; + } + if (this->inverted_) { + config |= EMC2101_POLARITY_BIT; + } + + if (this->dac_mode_) { // DAC mode configurations + // set DAC conversion rate + reg(EMC2101_REGISTER_DAC_CONV_RATE) = this->dac_conversion_rate_; + } else { // PWM mode configurations + // set PWM divider + reg(EMC2101_REGISTER_FAN_CONFIG) |= EMC2101_CLK_OVR_BIT; + reg(EMC2101_REGISTER_PWM_DIV) = this->pwm_divider_; + + // set PWM resolution + reg(EMC2101_REGISTER_PWM_FREQ) = this->pwm_resolution_; + } +} + +void Emc2101Component::dump_config() { + ESP_LOGCONFIG(TAG, "Emc2101 component:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + } + ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM"); + if (this->dac_mode_) { + ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_); + } else { + ESP_LOGCONFIG(TAG, " PWM Resolution: %02X", this->pwm_resolution_); + ESP_LOGCONFIG(TAG, " PWM Divider: %02X", this->pwm_divider_); + } + ESP_LOGCONFIG(TAG, " Inverted: %s", YESNO(this->inverted_)); +} + +void Emc2101Component::set_duty_cycle(float value) { + uint8_t duty_cycle = remap(value, 0.0f, 1.0f, (uint8_t) 0, this->max_output_value_); + ESP_LOGD(TAG, "Setting duty fan setting to %02X", duty_cycle); + if (!this->write_byte(EMC2101_REGISTER_FAN_SETTING, duty_cycle)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return; + } +} + +float Emc2101Component::get_duty_cycle() { + uint8_t duty_cycle; + if (!this->read_byte(EMC2101_REGISTER_FAN_SETTING, &duty_cycle)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + return remap(duty_cycle, (uint8_t) 0, this->max_output_value_, 0.0f, 1.0f); +} + +float Emc2101Component::get_internal_temperature() { + uint8_t temperature; + if (!this->read_byte(EMC2101_REGISTER_INTERNAL_TEMP, &temperature)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + return temperature; +} + +float Emc2101Component::get_external_temperature() { + // Read **MSB** first to match 'Data Read Interlock' behavior from 6.1 of datasheet + uint8_t lsb, msb; + if (!this->read_byte(EMC2101_REGISTER_EXTERNAL_TEMP_MSB, &msb) || + !this->read_byte(EMC2101_REGISTER_EXTERNAL_TEMP_LSB, &lsb)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + + // join msb and lsb (5 least significant bits are not used) + uint16_t raw = (msb << 8 | lsb) >> 5; + return raw * 0.125; +} + +float Emc2101Component::get_speed() { + // Read **LSB** first to match 'Data Read Interlock' behavior from 6.1 of datasheet + uint8_t lsb, msb; + if (!this->read_byte(EMC2101_REGISTER_TACH_LSB, &lsb) || !this->read_byte(EMC2101_REGISTER_TACH_MSB, &msb)) { + ESP_LOGE(TAG, "Communication with EMC2101 failed!"); + this->status_set_warning(); + return NAN; + } + + // calculate RPMs + uint16_t tach = msb << 8 | lsb; + return tach == 0xFFFF ? 0.0f : 5400000.0f / tach; +} + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/emc2101.h b/esphome/components/emc2101/emc2101.h new file mode 100644 index 0000000000..0f4bc560dd --- /dev/null +++ b/esphome/components/emc2101/emc2101.h @@ -0,0 +1,115 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace emc2101 { + +/** Enum listing all DAC conversion rates for the EMC2101. + * + * Specific values of the enum constants are register values taken from the EMC2101 datasheet. + */ +enum Emc2101DACConversionRate { + EMC2101_DAC_1_EVERY_16_S, + EMC2101_DAC_1_EVERY_8_S, + EMC2101_DAC_1_EVERY_4_S, + EMC2101_DAC_1_EVERY_2_S, + EMC2101_DAC_1_EVERY_SECOND, + EMC2101_DAC_2_EVERY_SECOND, + EMC2101_DAC_4_EVERY_SECOND, + EMC2101_DAC_8_EVERY_SECOND, + EMC2101_DAC_16_EVERY_SECOND, + EMC2101_DAC_32_EVERY_SECOND, +}; + +/// This class includes support for the EMC2101 i2c fan controller. +/// The device has an output (PWM or DAC) and several sensors and this +/// class is for the EMC2101 configuration. +class Emc2101Component : public Component, public i2c::I2CDevice { + public: + /** Sets the mode of the output. + * + * @param dac_mode false for PWM output and true for DAC mode. + */ + void set_dac_mode(bool dac_mode) { + this->dac_mode_ = dac_mode; + this->max_output_value_ = 63; + } + + /** Sets the PWM resolution. + * + * @param resolution the PWM resolution. + */ + void set_pwm_resolution(uint8_t resolution) { + this->pwm_resolution_ = resolution; + this->max_output_value_ = 2 * resolution; + } + + /** Sets the PWM divider used to derive the PWM frequency. + * + * @param divider The PWM divider. + */ + void set_pwm_divider(uint8_t divider) { this->pwm_divider_ = divider; } + + /** Sets the DAC conversion rate (how many conversions per second). + * + * @param conversion_rate The DAC conversion rate. + */ + void set_dac_conversion_rate(Emc2101DACConversionRate conversion_rate) { + this->dac_conversion_rate_ = conversion_rate; + } + + /** Inverts the polarity of the Fan output. + * + * @param inverted Invert or not the Fan output. + */ + void set_inverted(bool inverted) { this->inverted_ = inverted; } + + /** Sets the Fan output duty cycle + * + * @param value The duty cycle value, from 0.0f to 1.0f. + */ + void set_duty_cycle(float value); + + /** Gets the Fan output duty cycle + * + * @return The duty cycle percentage from 0.0f to 1.0f. + */ + float get_duty_cycle(); + + /** Gets the internal temperature sensor reading. + * + * @return The temperature in degrees celsius. + */ + float get_internal_temperature(); + + /** Gets the external temperature sensor reading. + * + * @return The temperature in degrees celsius. + */ + float get_external_temperature(); + + /** Gets the tachometer speed sensor reading. + * + * @return The fan speed in RPMs. + */ + float get_speed(); + + /** Used by ESPHome framework. */ + void setup() override; + /** Used by ESPHome framework. */ + void dump_config() override; + /** Used by ESPHome framework. */ + float get_setup_priority() const override; + + bool dac_mode_{false}; + bool inverted_{false}; + uint8_t max_output_value_; + uint8_t pwm_resolution_; + uint8_t pwm_divider_; + Emc2101DACConversionRate dac_conversion_rate_; +}; + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/output/__init__.py b/esphome/components/emc2101/output/__init__.py new file mode 100644 index 0000000000..461ef73de1 --- /dev/null +++ b/esphome/components/emc2101/output/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_ID +from .. import EMC2101_COMPONENT_SCHEMA, CONF_EMC2101_ID, emc2101_ns + +DEPENDENCIES = ["emc2101"] + +EMC2101Output = emc2101_ns.class_("EMC2101Output", output.FloatOutput) + +CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(EMC2101Output), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_EMC2101_ID]) + var = cg.new_Pvariable(config[CONF_ID], paren) + await output.register_output(var, config) diff --git a/esphome/components/emc2101/output/emc2101_output.cpp b/esphome/components/emc2101/output/emc2101_output.cpp new file mode 100644 index 0000000000..2ed506cd99 --- /dev/null +++ b/esphome/components/emc2101/output/emc2101_output.cpp @@ -0,0 +1,9 @@ +#include "emc2101_output.h" + +namespace esphome { +namespace emc2101 { + +void EMC2101Output::write_state(float state) { this->parent_->set_duty_cycle(state); } + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/output/emc2101_output.h b/esphome/components/emc2101/output/emc2101_output.h new file mode 100644 index 0000000000..232df6ff5f --- /dev/null +++ b/esphome/components/emc2101/output/emc2101_output.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../emc2101.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace emc2101 { + +/// This class allows to control the EMC2101 output. +class EMC2101Output : public output::FloatOutput { + public: + EMC2101Output(Emc2101Component *parent) : parent_(parent) {} + + protected: + /** Used by ESPHome framework. */ + void write_state(float state) override; + + Emc2101Component *parent_; +}; + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/sensor/__init__.py b/esphome/components/emc2101/sensor/__init__.py new file mode 100644 index 0000000000..03d3d0314e --- /dev/null +++ b/esphome/components/emc2101/sensor/__init__.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_SPEED, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_REVOLUTIONS_PER_MINUTE, + ICON_PERCENT, +) +from .. import EMC2101_COMPONENT_SCHEMA, CONF_EMC2101_ID, emc2101_ns + +DEPENDENCIES = ["emc2101"] + +CONF_INTERNAL_TEMPERATURE = "internal_temperature" +CONF_EXTERNAL_TEMPERATURE = "external_temperature" +CONF_DUTY_CYCLE = "duty_cycle" + +EMC2101Sensor = emc2101_ns.class_("EMC2101Sensor", cg.PollingComponent) + +CONFIG_SCHEMA = EMC2101_COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EMC2101Sensor), + cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:fan", + ), + cv.Optional(CONF_DUTY_CYCLE): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_PERCENT, + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_EMC2101_ID]) + var = cg.new_Pvariable(config[CONF_ID], paren) + await cg.register_component(var, config) + + if CONF_INTERNAL_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_INTERNAL_TEMPERATURE]) + cg.add(var.set_internal_temperature_sensor(sens)) + + if CONF_EXTERNAL_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_EXTERNAL_TEMPERATURE]) + cg.add(var.set_external_temperature_sensor(sens)) + + if CONF_SPEED in config: + sens = await sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_DUTY_CYCLE in config: + sens = await sensor.new_sensor(config[CONF_DUTY_CYCLE]) + cg.add(var.set_duty_cycle_sensor(sens)) diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.cpp b/esphome/components/emc2101/sensor/emc2101_sensor.cpp new file mode 100644 index 0000000000..2a199f48e9 --- /dev/null +++ b/esphome/components/emc2101/sensor/emc2101_sensor.cpp @@ -0,0 +1,43 @@ +#include "emc2101_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace emc2101 { + +static const char *const TAG = "EMC2101.sensor"; + +float EMC2101Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void EMC2101Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "Emc2101 sensor:"); + LOG_SENSOR(" ", "Internal temperature", this->internal_temperature_sensor_); + LOG_SENSOR(" ", "External temperature", this->external_temperature_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Duty cycle", this->duty_cycle_sensor_); +} + +void EMC2101Sensor::update() { + if (this->internal_temperature_sensor_ != nullptr) { + float internal_temperature = this->parent_->get_internal_temperature(); + this->internal_temperature_sensor_->publish_state(internal_temperature); + } + + if (this->external_temperature_sensor_ != nullptr) { + float external_temperature = this->parent_->get_external_temperature(); + this->external_temperature_sensor_->publish_state(external_temperature); + } + + if (this->speed_sensor_ != nullptr) { + float speed = this->parent_->get_speed(); + this->speed_sensor_->publish_state(speed); + } + + if (this->duty_cycle_sensor_ != nullptr) { + float duty_cycle = this->parent_->get_duty_cycle(); + this->duty_cycle_sensor_->publish_state(duty_cycle * 100.0f); + } +} + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.h b/esphome/components/emc2101/sensor/emc2101_sensor.h new file mode 100644 index 0000000000..3e8dcebc8e --- /dev/null +++ b/esphome/components/emc2101/sensor/emc2101_sensor.h @@ -0,0 +1,39 @@ +#pragma once + +#include "../emc2101.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace emc2101 { + +/// This class exposes the EMC2101 sensors. +class EMC2101Sensor : public PollingComponent { + public: + EMC2101Sensor(Emc2101Component *parent) : parent_(parent) {} + /** Used by ESPHome framework. */ + void dump_config() override; + /** Used by ESPHome framework. */ + void update() override; + /** Used by ESPHome framework. */ + float get_setup_priority() const override; + + /** Used by ESPHome framework. */ + void set_internal_temperature_sensor(sensor::Sensor *sensor) { this->internal_temperature_sensor_ = sensor; } + /** Used by ESPHome framework. */ + void set_external_temperature_sensor(sensor::Sensor *sensor) { this->external_temperature_sensor_ = sensor; } + /** Used by ESPHome framework. */ + void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; } + /** Used by ESPHome framework. */ + void set_duty_cycle_sensor(sensor::Sensor *sensor) { this->duty_cycle_sensor_ = sensor; } + + protected: + Emc2101Component *parent_; + sensor::Sensor *internal_temperature_sensor_{nullptr}; + sensor::Sensor *external_temperature_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *duty_cycle_sensor_{nullptr}; +}; + +} // namespace emc2101 +} // namespace esphome diff --git a/esphome/components/honeywellabp2_i2c/__init__.py b/esphome/components/honeywellabp2_i2c/__init__.py new file mode 100644 index 0000000000..e748df3c98 --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/__init__.py @@ -0,0 +1,2 @@ +"""Support for Honeywell ABP2""" +CODEOWNERS = ["@jpfaff"] diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp new file mode 100644 index 0000000000..e2910032cc --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp @@ -0,0 +1,108 @@ +#include "honeywellabp2.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace honeywellabp2_i2c { + +static const uint8_t STATUS_BIT_POWER = 6; +static const uint8_t STATUS_BIT_BUSY = 5; +static const uint8_t STATUS_BIT_ERROR = 2; +static const uint8_t STATUS_MATH_SAT = 0; + +static const char *const TAG = "honeywellabp2"; + +void HONEYWELLABP2Sensor::read_sensor_data() { + if (this->read(raw_data_, 7) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with ABP2 failed!"); + this->mark_failed(); + return; + } + float press_counts = encode_uint24(raw_data_[1], raw_data_[2], raw_data_[3]); // calculate digital pressure counts + float temp_counts = encode_uint24(raw_data_[4], raw_data_[5], raw_data_[6]); // calculate digital temperature counts + + this->last_pressure_ = (((press_counts - this->min_count_) / (this->max_count_ - this->min_count_)) * + (this->max_pressure_ - this->min_pressure_)) + + this->min_pressure_; + this->last_temperature_ = (temp_counts * 200 / 16777215) - 50; +} + +void HONEYWELLABP2Sensor::start_measurement() { + if (this->write(i2c_cmd_, 3) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with ABP2 failed!"); + this->mark_failed(); + return; + } + this->measurement_running_ = true; +} + +bool HONEYWELLABP2Sensor::is_measurement_ready() { + if (this->read(raw_data_, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with ABP2 failed!"); + this->mark_failed(); + return false; + } + if ((raw_data_[0] & (0x1 << STATUS_BIT_BUSY)) > 0) { + return false; + } + this->measurement_running_ = false; + return true; +} + +void HONEYWELLABP2Sensor::measurement_timeout() { + ESP_LOGE(TAG, "Timeout!"); + this->measurement_running_ = false; + this->mark_failed(); +} + +float HONEYWELLABP2Sensor::get_pressure() { return this->last_pressure_; } + +float HONEYWELLABP2Sensor::get_temperature() { return this->last_temperature_; } + +void HONEYWELLABP2Sensor::loop() { + if (this->measurement_running_) { + if (this->is_measurement_ready()) { + this->cancel_timeout("meas_timeout"); + + this->read_sensor_data(); + if (pressure_sensor_ != nullptr) { + this->pressure_sensor_->publish_state(this->get_pressure()); + } + if (temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(this->get_temperature()); + } + } + } +} + +void HONEYWELLABP2Sensor::update() { + ESP_LOGV(TAG, "Update Honeywell ABP2 Sensor"); + + this->start_measurement(); + this->set_timeout("meas_timeout", 50, [this] { this->measurement_timeout(); }); +} + +void HONEYWELLABP2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, " Min Pressure Range: %0.1f", this->min_pressure_); + ESP_LOGCONFIG(TAG, " Max Pressure Range: %0.1f", this->max_pressure_); + if (this->transfer_function_ == ABP2_TRANS_FUNC_A) { + ESP_LOGCONFIG(TAG, " Transfer function A"); + } else { + ESP_LOGCONFIG(TAG, " Transfer function B"); + } + LOG_UPDATE_INTERVAL(this); +} + +void HONEYWELLABP2Sensor::set_transfer_function(ABP2TRANFERFUNCTION transfer_function) { + this->transfer_function_ = transfer_function; + if (this->transfer_function_ == ABP2_TRANS_FUNC_B) { + this->max_count_ = this->max_count_b_; + this->min_count_ = this->min_count_b_; + } else { + this->max_count_ = this->max_count_a_; + this->min_count_ = this->min_count_a_; + } +} + +} // namespace honeywellabp2_i2c +} // namespace esphome diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h new file mode 100644 index 0000000000..bc81524ac2 --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -0,0 +1,60 @@ +// for Honeywell ABP sensor +// adapting code from https://github.com/vwls/Honeywell_pressure_sensors +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace honeywellabp2_i2c { + +enum ABP2TRANFERFUNCTION { ABP2_TRANS_FUNC_A = 0, ABP2_TRANS_FUNC_B = 1 }; + +class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { + public: + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }; + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }; + void loop() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + void dump_config() override; + + void read_sensor_data(); + void start_measurement(); + bool is_measurement_ready(); + void measurement_timeout(); + + float get_pressure(); + float get_temperature(); + + void set_min_pressure(float min_pressure) { this->min_pressure_ = min_pressure; }; + void set_max_pressure(float max_pressure) { this->max_pressure_ = max_pressure; }; + void set_transfer_function(ABP2TRANFERFUNCTION transfer_function); + + protected: + float min_pressure_ = 0.0; + float max_pressure_ = 0.0; + ABP2TRANFERFUNCTION transfer_function_ = ABP2_TRANS_FUNC_A; + + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + + const float max_count_a_ = 15099494.4; // (90% of 2^24 counts or 0xE66666) + const float min_count_a_ = 1677721.6; // (10% of 2^24 counts or 0x19999A) + const float max_count_b_ = 11744051.2; // (70% of 2^24 counts or 0xB33333) + const float min_count_b_ = 5033164.8; // (30% of 2^24 counts or 0x4CCCCC) + + float max_count_; + float min_count_; + bool measurement_running_ = false; + + uint8_t raw_data_[7]; // holds output data + uint8_t i2c_cmd_[3] = {0xAA, 0x00, 0x00}; // command to be sent + float last_pressure_; + float last_temperature_; +}; + +} // namespace honeywellabp2_i2c +} // namespace esphome diff --git a/esphome/components/honeywellabp2_i2c/sensor.py b/esphome/components/honeywellabp2_i2c/sensor.py new file mode 100644 index 0000000000..c38a380127 --- /dev/null +++ b/esphome/components/honeywellabp2_i2c/sensor.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] + +honeywellabp2_ns = cg.esphome_ns.namespace("honeywellabp2_i2c") + +CONF_MIN_PRESSURE = "min_pressure" +CONF_MAX_PRESSURE = "max_pressure" +TRANSFER_FUNCTION = "transfer_function" +ABP2TRANFERFUNCTION = honeywellabp2_ns.enum("ABP2TRANFERFUNCTION") +TRANS_FUNC_OPTIONS = { + "A": ABP2TRANFERFUNCTION.ABP2_TRANS_FUNC_A, + "B": ABP2TRANFERFUNCTION.ABP2_TRANS_FUNC_B, +} + +HONEYWELLABP2Sensor = honeywellabp2_ns.class_( + "HONEYWELLABP2Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HONEYWELLABP2Sensor), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement="Pa", + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Required(CONF_MIN_PRESSURE): cv.float_, + cv.Required(CONF_MAX_PRESSURE): cv.float_, + cv.Required(TRANSFER_FUNCTION): cv.enum(TRANS_FUNC_OPTIONS), + } + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_min_pressure(pressure_config[CONF_MIN_PRESSURE])) + cg.add(var.set_max_pressure(pressure_config[CONF_MAX_PRESSURE])) + cg.add(var.set_transfer_function(pressure_config[TRANSFER_FUNCTION])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 98483e8ae9..56a59c54d1 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -139,17 +139,22 @@ def _process_base_package(config: dict) -> dict: ) from e return packages - packages = {} + packages = None + error = "" try: packages = get_packages(files) - except cv.Invalid: - if revert is not None: - revert() - packages = get_packages(files) - finally: - if packages is None: - raise cv.Invalid("Failed to load packages") + except cv.Invalid as e: + error = e + try: + if revert is not None: + revert() + packages = get_packages(files) + except cv.Invalid as er: + error = er + + if packages is None: + raise cv.Invalid(f"Failed to load packages. {error}") return {"packages": packages} 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/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 8b4dcda9ef..42951d6089 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -1,5 +1,6 @@ #include "sen5x.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -135,9 +136,12 @@ void SEN5XComponent::setup() { ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); if (this->voc_sensor_ && this->store_baseline_) { - // Hash with compilation time + uint32_t combined_serial = + encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]); + // Hash with compilation time and serial number // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); + // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict + uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(combined_serial)); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->voc_baselines_storage_)) { diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index bd7306ac28..b3bf533695 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( CONF_FROM, CONF_ICON, CONF_ID, + CONF_IGNORE_OUT_OF_RANGE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -688,6 +689,7 @@ CLAMP_SCHEMA = cv.All( { cv.Optional(CONF_MIN_VALUE, default="NaN"): cv.float_, cv.Optional(CONF_MAX_VALUE, default="NaN"): cv.float_, + cv.Optional(CONF_IGNORE_OUT_OF_RANGE, default=False): cv.boolean, } ), validate_clamp, @@ -700,6 +702,7 @@ async def clamp_filter_to_code(config, filter_id): filter_id, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE], + config[CONF_IGNORE_OUT_OF_RANGE], ) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index af67a60754..d1cb8d1c4b 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -434,13 +434,25 @@ optional CalibratePolynomialFilter::new_value(float value) { return res; } -ClampFilter::ClampFilter(float min, float max) : min_(min), max_(max) {} +ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range) + : min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {} optional ClampFilter::new_value(float value) { if (std::isfinite(value)) { - if (std::isfinite(this->min_) && value < this->min_) - return this->min_; - if (std::isfinite(this->max_) && value > this->max_) - return this->max_; + if (std::isfinite(this->min_) && value < this->min_) { + if (this->ignore_out_of_range_) { + return {}; + } else { + return this->min_; + } + } + + if (std::isfinite(this->max_) && value > this->max_) { + if (this->ignore_out_of_range_) { + return {}; + } else { + return this->max_; + } + } } return value; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index d4239837b6..fa78f2fa46 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -411,12 +411,13 @@ class CalibratePolynomialFilter : public Filter { class ClampFilter : public Filter { public: - ClampFilter(float min, float max); + ClampFilter(float min, float max, bool ignore_out_of_range); optional new_value(float value) override; protected: float min_{NAN}; float max_{NAN}; + bool ignore_out_of_range_; }; class RoundFilter : public Filter { diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 0910a32a35..261604b992 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -73,9 +73,10 @@ void SGP30Component::setup() { return; } - // Hash with compilation time + // Hash with compilation time and serial number // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); + // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict + uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_)); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->baselines_storage_)) { diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 52f9adc808..a48372aab7 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -61,9 +61,10 @@ void SGP4xComponent::setup() { ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); if (this->store_baseline_) { - // Hash with compilation time + // Hash with compilation time and serial number // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); + // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict + uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_)); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->voc_baselines_storage_)) { diff --git a/esphome/const.py b/esphome/const.py index 9e0bd4da0a..47eedc24b7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -335,6 +335,7 @@ CONF_IDLE_LEVEL = "idle_level" CONF_IDLE_TIME = "idle_time" CONF_IF = "if" CONF_IGNORE_EFUSE_MAC_CRC = "ignore_efuse_mac_crc" +CONF_IGNORE_OUT_OF_RANGE = "ignore_out_of_range" CONF_IGNORE_STRAPPING_WARNING = "ignore_strapping_warning" CONF_IIR_FILTER = "iir_filter" CONF_ILLUMINANCE = "illuminance" diff --git a/tests/test1.yaml b/tests/test1.yaml index 49c44c2cb9..7ee1baaba5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -750,6 +750,16 @@ sensor: temperature: name: Honeywell temperature cs_pin: GPIO5 + - platform: honeywellabp2_i2c + pressure: + name: Honeywell2 pressure + min_pressure: 0 + max_pressure: 16000 + transfer_function: A + temperature: + name: Honeywell temperature + i2c_id: i2c_bus + address: 0x28 - platform: hte501 temperature: name: Office Temperature 2 @@ -1821,6 +1831,9 @@ binary_sensor: - platform: optolink name: Disturbance address: 0x0A82 + - platform: qwiic_pir + i2c_id: i2c_bus + name: "Qwiic PIR Motion Sensor" pca9685: frequency: 500